[automerger skipped] DO NOT MERGE Revert "Verify URI permissions for EXTRA_REMOTE_INPUT_HISTORY_ITEMS." am: 28428b7379 am: 85c45c918b -s ours am: 0b8ee56a4b -s ours am: 2ab6f65039 -s ours am: 065d2f9faa -s ours am: b4d0ca25ea -s ours am: c673e8548a -s ours am: 1b1a864099 -s ours am: 4d06de1d47 -s ours

am skip reason: subject contains skip directive

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

Change-Id: I5007a2f7910080bd2afc28d8d8330ab5707db235
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/apct-tests/perftests/surfaceflinger/AndroidTest.xml b/apct-tests/perftests/surfaceflinger/AndroidTest.xml
index 11d1106..58cf58b 100644
--- a/apct-tests/perftests/surfaceflinger/AndroidTest.xml
+++ b/apct-tests/perftests/surfaceflinger/AndroidTest.xml
@@ -58,8 +58,8 @@
         <option name="instrumentation-arg" key="report" value="true" />
         <option name="instrumentation-arg" key="arguments" value="-g" />
         <option name="instrumentation-arg" key="events_to_record" value="instructions,cpu-cycles,raw-l3d-cache-refill,sched:sched_waking" />
-        <option name="instrumentation-arg" key="processes_to_record" value="surfaceflinger" />
-        <option name="instrumentation-arg" key="symbols_to_report" value="&quot;commit;android::SurfaceFlinger::commit(;composite;android::SurfaceFlinger::composite(&quot;" />
+        <option name="instrumentation-arg" key="processes_to_record" value="surfaceflinger,android.perftests.surfaceflinger" />
+        <option name="instrumentation-arg" key="symbols_to_report" value="&quot;commit;android::SurfaceFlinger::commit(;composite;android::SurfaceFlinger::composite(;outbound;android::SurfaceComposerClient::Transaction::apply(;inbound;android::BnTransactionCompletedListener::onTransact(&quot;"/>
 
         <!-- should match profiling-iterations -->
         <option name="instrumentation-arg" key="test_iterations" value="525" />
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 91099cb..1841ea9 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2962,6 +2962,10 @@
     method @Deprecated public boolean isBound();
   }
 
+  public class NotificationRankingUpdate implements android.os.Parcelable {
+    method public final boolean isFdNotNullAndClosed();
+  }
+
 }
 
 package android.service.quickaccesswallet {
diff --git a/core/java/Android.bp b/core/java/Android.bp
index cb54d88..70cb597 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -515,6 +515,17 @@
     ],
 }
 
+// common protolog sources without classes that rely on Android SDK
+filegroup {
+    name: "protolog-common-no-android-src",
+    srcs: [
+        ":protolog-common-src",
+    ],
+    exclude_srcs: [
+        "com/android/internal/protolog/common/ProtoLog.java",
+    ],
+}
+
 java_library {
     name: "protolog-lib",
     platform_apis: true,
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 70a2e53..08543ee 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1084,6 +1084,16 @@
         }
 
         /**
+         * Update the forced status bar appearance.
+         * @hide
+         */
+        @Override
+        public void updateStatusBarAppearance(int appearance) {
+            mTaskDescription.setStatusBarAppearance(appearance);
+            setTaskDescription(mTaskDescription);
+        }
+
+        /**
          * Update the forced navigation bar color.
          * @hide
          */
@@ -3882,7 +3892,9 @@
      * it will set up the dispatch to call {@link #onKeyUp} where the action
      * will be performed; for earlier applications, it will perform the
      * action immediately in on-down, as those versions of the platform
-     * behaved.
+     * behaved. This implementation will also take care of {@link KeyEvent#KEYCODE_ESCAPE}
+     * by finishing the activity if it would be closed by touching outside
+     * of it.
      *
      * <p>Other additional default key handling may be performed
      * if configured with {@link #setDefaultKeyMode}.
@@ -3904,6 +3916,11 @@
             return true;
         }
 
+        if (keyCode == KeyEvent.KEYCODE_ESCAPE && mWindow.shouldCloseOnTouchOutside()) {
+            event.startTracking();
+            return true;
+        }
+
         if (mDefaultKeyMode == DEFAULT_KEYS_DISABLE) {
             return false;
         } else if (mDefaultKeyMode == DEFAULT_KEYS_SHORTCUT) {
@@ -3999,6 +4016,15 @@
                 return true;
             }
         }
+
+        if (keyCode == KeyEvent.KEYCODE_ESCAPE
+                && mWindow.shouldCloseOnTouchOutside()
+                && event.isTracking()
+                && !event.isCanceled()) {
+            finish();
+            return true;
+        }
+
         return false;
     }
 
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index b5ee895..ff0f437 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -81,6 +81,7 @@
 import android.util.DisplayMetrics;
 import android.util.Singleton;
 import android.util.Size;
+import android.view.WindowInsetsController.Appearance;
 import android.window.TaskSnapshot;
 
 import com.android.internal.app.LocalePicker;
@@ -1553,6 +1554,8 @@
         private int mColorBackgroundFloating;
         private int mStatusBarColor;
         private int mNavigationBarColor;
+        @Appearance
+        private int mStatusBarAppearance;
         private boolean mEnsureStatusBarContrastWhenTransparent;
         private boolean mEnsureNavigationBarContrastWhenTransparent;
         private int mResizeMode;
@@ -1653,8 +1656,8 @@
                 final Icon icon = mIconRes == Resources.ID_NULL ? null :
                         Icon.createWithResource(ActivityThread.currentPackageName(), mIconRes);
                 return new TaskDescription(mLabel, icon, mPrimaryColor, mBackgroundColor,
-                        mStatusBarColor, mNavigationBarColor, false, false, RESIZE_MODE_RESIZEABLE,
-                        -1, -1, 0);
+                        mStatusBarColor, mNavigationBarColor, 0, false, false,
+                        RESIZE_MODE_RESIZEABLE, -1, -1, 0);
             }
         }
 
@@ -1672,7 +1675,7 @@
         @Deprecated
         public TaskDescription(String label, @DrawableRes int iconRes, int colorPrimary) {
             this(label, Icon.createWithResource(ActivityThread.currentPackageName(), iconRes),
-                    colorPrimary, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
+                    colorPrimary, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
             if ((colorPrimary != 0) && (Color.alpha(colorPrimary) != 255)) {
                 throw new RuntimeException("A TaskDescription's primary color should be opaque");
             }
@@ -1690,7 +1693,7 @@
         @Deprecated
         public TaskDescription(String label, @DrawableRes int iconRes) {
             this(label, Icon.createWithResource(ActivityThread.currentPackageName(), iconRes),
-                    0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
+                    0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
         }
 
         /**
@@ -1702,7 +1705,7 @@
          */
         @Deprecated
         public TaskDescription(String label) {
-            this(label, null, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
+            this(label, null, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
         }
 
         /**
@@ -1712,7 +1715,7 @@
          */
         @Deprecated
         public TaskDescription() {
-            this(null, null, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
+            this(null, null, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
         }
 
         /**
@@ -1728,7 +1731,7 @@
         @Deprecated
         public TaskDescription(String label, Bitmap icon, int colorPrimary) {
             this(label, icon != null ? Icon.createWithBitmap(icon) : null, colorPrimary, 0, 0, 0,
-                    false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
+                    0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
             if ((colorPrimary != 0) && (Color.alpha(colorPrimary) != 255)) {
                 throw new RuntimeException("A TaskDescription's primary color should be opaque");
             }
@@ -1744,14 +1747,15 @@
          */
         @Deprecated
         public TaskDescription(String label, Bitmap icon) {
-            this(label, icon != null ? Icon.createWithBitmap(icon) : null, 0, 0, 0, 0, false, false,
-                    RESIZE_MODE_RESIZEABLE, -1, -1, 0);
+            this(label, icon != null ? Icon.createWithBitmap(icon) : null, 0, 0, 0, 0, 0, false,
+                    false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
         }
 
         /** @hide */
         public TaskDescription(@Nullable String label, @Nullable Icon icon,
                 int colorPrimary, int colorBackground,
                 int statusBarColor, int navigationBarColor,
+                @Appearance int statusBarAppearance,
                 boolean ensureStatusBarContrastWhenTransparent,
                 boolean ensureNavigationBarContrastWhenTransparent, int resizeMode, int minWidth,
                 int minHeight, int colorBackgroundFloating) {
@@ -1761,6 +1765,7 @@
             mColorBackground = colorBackground;
             mStatusBarColor = statusBarColor;
             mNavigationBarColor = navigationBarColor;
+            mStatusBarAppearance = statusBarAppearance;
             mEnsureStatusBarContrastWhenTransparent = ensureStatusBarContrastWhenTransparent;
             mEnsureNavigationBarContrastWhenTransparent =
                     ensureNavigationBarContrastWhenTransparent;
@@ -1789,6 +1794,7 @@
             mColorBackground = other.mColorBackground;
             mStatusBarColor = other.mStatusBarColor;
             mNavigationBarColor = other.mNavigationBarColor;
+            mStatusBarAppearance = other.mStatusBarAppearance;
             mEnsureStatusBarContrastWhenTransparent = other.mEnsureStatusBarContrastWhenTransparent;
             mEnsureNavigationBarContrastWhenTransparent =
                     other.mEnsureNavigationBarContrastWhenTransparent;
@@ -1818,6 +1824,9 @@
             if (other.mNavigationBarColor != 0) {
                 mNavigationBarColor = other.mNavigationBarColor;
             }
+            if (other.mStatusBarAppearance != 0) {
+                mStatusBarAppearance = other.mStatusBarAppearance;
+            }
 
             mEnsureStatusBarContrastWhenTransparent = other.mEnsureStatusBarContrastWhenTransparent;
             mEnsureNavigationBarContrastWhenTransparent =
@@ -2089,6 +2098,14 @@
         /**
          * @hide
          */
+        @Appearance
+        public int getStatusBarAppearance() {
+            return mStatusBarAppearance;
+        }
+
+        /**
+         * @hide
+         */
         public void setEnsureStatusBarContrastWhenTransparent(
                 boolean ensureStatusBarContrastWhenTransparent) {
             mEnsureStatusBarContrastWhenTransparent = ensureStatusBarContrastWhenTransparent;
@@ -2097,6 +2114,13 @@
         /**
          * @hide
          */
+        public void setStatusBarAppearance(@Appearance int statusBarAppearance) {
+            mStatusBarAppearance = statusBarAppearance;
+        }
+
+        /**
+         * @hide
+         */
         public boolean getEnsureNavigationBarContrastWhenTransparent() {
             return mEnsureNavigationBarContrastWhenTransparent;
         }
@@ -2218,6 +2242,7 @@
             dest.writeInt(mColorBackground);
             dest.writeInt(mStatusBarColor);
             dest.writeInt(mNavigationBarColor);
+            dest.writeInt(mStatusBarAppearance);
             dest.writeBoolean(mEnsureStatusBarContrastWhenTransparent);
             dest.writeBoolean(mEnsureNavigationBarContrastWhenTransparent);
             dest.writeInt(mResizeMode);
@@ -2241,6 +2266,7 @@
             mColorBackground = source.readInt();
             mStatusBarColor = source.readInt();
             mNavigationBarColor = source.readInt();
+            mStatusBarAppearance = source.readInt();
             mEnsureStatusBarContrastWhenTransparent = source.readBoolean();
             mEnsureNavigationBarContrastWhenTransparent = source.readBoolean();
             mResizeMode = source.readInt();
@@ -2289,6 +2315,7 @@
                     && mColorBackground == other.mColorBackground
                     && mStatusBarColor == other.mStatusBarColor
                     && mNavigationBarColor == other.mNavigationBarColor
+                    && mStatusBarAppearance == other.mStatusBarAppearance
                     && mEnsureStatusBarContrastWhenTransparent
                             == other.mEnsureStatusBarContrastWhenTransparent
                     && mEnsureNavigationBarContrastWhenTransparent
diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java
index 411d157..4851279 100644
--- a/core/java/android/app/Dialog.java
+++ b/core/java/android/app/Dialog.java
@@ -694,12 +694,22 @@
      */
     @Override
     public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
-        if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE)
-                && event.isTracking()
-                && !event.isCanceled()
-                && !WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) {
-            onBackPressed();
-            return true;
+        if (event.isTracking() && !event.isCanceled()) {
+            switch (keyCode) {
+                case KeyEvent.KEYCODE_BACK:
+                    if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) {
+                        onBackPressed();
+                        return true;
+                    }
+                    break;
+                case KeyEvent.KEYCODE_ESCAPE:
+                    if (mCancelable) {
+                        cancel();
+                    } else {
+                        dismiss();
+                    }
+                    return true;
+            }
         }
         return false;
     }
diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl
index 4e2b6fa..d189bab 100644
--- a/core/java/android/app/IActivityTaskManager.aidl
+++ b/core/java/android/app/IActivityTaskManager.aidl
@@ -24,7 +24,6 @@
 import android.app.IApplicationThread;
 import android.app.IActivityClientController;
 import android.app.IActivityController;
-import android.app.IAppTask;
 import android.app.IAssistDataReceiver;
 import android.app.IInstrumentationWatcher;
 import android.app.IProcessObserver;
@@ -107,14 +106,6 @@
             in ProfilerInfo profilerInfo, in Bundle options, int userId);
     boolean startNextMatchingActivity(in IBinder callingActivity,
             in Intent intent, in Bundle options);
-
-    /**
-    *  The DreamActivity has to be started in a special way that does not involve the PackageParser.
-    *  The DreamActivity is a framework component inserted in the dream application process. Hence,
-    *  it is not declared in the application's manifest and cannot be parsed. startDreamActivity
-    *  creates the activity and starts it without reaching out to the PackageParser.
-    */
-    boolean startDreamActivity(in Intent intent);
     int startActivityIntentSender(in IApplicationThread caller,
             in IIntentSender target, in IBinder whitelistToken, in Intent fillInIntent,
             in String resolvedType, in IBinder resultTo, in String resultWho, int requestCode,
diff --git a/core/java/android/app/IUiAutomationConnection.aidl b/core/java/android/app/IUiAutomationConnection.aidl
index fbb0748..63cae63 100644
--- a/core/java/android/app/IUiAutomationConnection.aidl
+++ b/core/java/android/app/IUiAutomationConnection.aidl
@@ -24,6 +24,8 @@
 import android.view.WindowContentFrameStats;
 import android.view.WindowAnimationFrameStats;
 import android.os.ParcelFileDescriptor;
+import android.window.ScreenCapture.ScreenCaptureListener;
+import android.window.ScreenCapture.LayerCaptureArgs;
 
 import java.util.List;
 
@@ -43,8 +45,8 @@
     void injectInputEventToInputFilter(in InputEvent event);
     void syncInputTransactions(boolean waitForAnimations);
     boolean setRotation(int rotation);
-    Bitmap takeScreenshot(in Rect crop);
-    Bitmap takeSurfaceControlScreenshot(in SurfaceControl surfaceControl);
+    boolean takeScreenshot(in Rect crop, in ScreenCaptureListener listener);
+    boolean takeSurfaceControlScreenshot(in SurfaceControl surfaceControl, in ScreenCaptureListener listener);
     boolean clearWindowContentFrameStats(int windowId);
     WindowContentFrameStats getWindowContentFrameStats(int windowId);
     void clearWindowAnimationFrameStats();
diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java
index 09450f5..e7a2b61e 100644
--- a/core/java/android/app/KeyguardManager.java
+++ b/core/java/android/app/KeyguardManager.java
@@ -117,6 +117,26 @@
             "android.app.action.CONFIRM_REMOTE_DEVICE_CREDENTIAL";
 
     /**
+     * Intent used to prompt user for device credential for entering repair
+     * mode. If the credential is verified successfully, then the information
+     * needed to verify the credential again will be written to a location that
+     * is available to repair mode. This makes it possible for repair mode to
+     * require that the same credential be provided to exit repair mode.
+     * @hide
+     */
+    public static final String ACTION_PREPARE_REPAIR_MODE_DEVICE_CREDENTIAL =
+            "android.app.action.PREPARE_REPAIR_MODE_DEVICE_CREDENTIAL";
+
+    /**
+     * Intent used to prompt user for device credential that is written by
+     * {@link #ACTION_PREPARE_REPAIR_MODE_DEVICE_CREDENTIAL} for exiting
+     * repair mode.
+     * @hide
+     */
+    public static final String ACTION_CONFIRM_REPAIR_MODE_DEVICE_CREDENTIAL =
+            "android.app.action.CONFIRM_REPAIR_MODE_DEVICE_CREDENTIAL";
+
+    /**
      * A CharSequence dialog title to show to the user when used with a
      * {@link #ACTION_CONFIRM_DEVICE_CREDENTIAL}.
      * @hide
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 2eb6ca7..8d2394b 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -2833,12 +2833,14 @@
     }
 
     /**
-     * Note all {@link Uri} that are referenced internally, with the expectation
-     * that Uri permission grants will need to be issued to ensure the recipient
-     * of this object is able to render its contents.
-     *
-     * @hide
-     */
+    * Note all {@link Uri} that are referenced internally, with the expectation that Uri permission
+    * grants will need to be issued to ensure the recipient of this object is able to render its
+    * contents.
+    * See b/281044385 for more context and examples about what happens when this isn't done
+    * correctly.
+    *
+    * @hide
+    */
     public void visitUris(@NonNull Consumer<Uri> visitor) {
         if (publicVersion != null) {
             publicVersion.visitUris(visitor);
@@ -2882,13 +2884,13 @@
             ArrayList<Person> people = extras.getParcelableArrayList(EXTRA_PEOPLE_LIST, android.app.Person.class);
             if (people != null && !people.isEmpty()) {
                 for (Person p : people) {
-                    visitor.accept(p.getIconUri());
+                    p.visitUris(visitor);
                 }
             }
 
             final Person person = extras.getParcelable(EXTRA_MESSAGING_PERSON, Person.class);
             if (person != null) {
-                visitor.accept(person.getIconUri());
+                person.visitUris(visitor);
             }
 
             final RemoteInputHistoryItem[] history = extras.getParcelableArray(
@@ -2910,12 +2912,7 @@
             if (!ArrayUtils.isEmpty(messages)) {
                 for (MessagingStyle.Message message : MessagingStyle.Message
                         .getMessagesFromBundleArray(messages)) {
-                    visitor.accept(message.getDataUri());
-
-                    Person senderPerson = message.getSenderPerson();
-                    if (senderPerson != null) {
-                        visitor.accept(senderPerson.getIconUri());
-                    }
+                    message.visitUris(visitor);
                 }
             }
 
@@ -2924,12 +2921,7 @@
             if (!ArrayUtils.isEmpty(historic)) {
                 for (MessagingStyle.Message message : MessagingStyle.Message
                         .getMessagesFromBundleArray(historic)) {
-                    visitor.accept(message.getDataUri());
-
-                    Person senderPerson = message.getSenderPerson();
-                    if (senderPerson != null) {
-                        visitor.accept(senderPerson.getIconUri());
-                    }
+                    message.visitUris(visitor);
                 }
             }
 
@@ -2939,7 +2931,7 @@
         if (isStyle(CallStyle.class) & extras != null) {
             Person callPerson = extras.getParcelable(EXTRA_CALL_PERSON, Person.class);
             if (callPerson != null) {
-                visitor.accept(callPerson.getIconUri());
+                callPerson.visitUris(visitor);
             }
             visitIconUri(visitor, extras.getParcelable(EXTRA_VERIFICATION_ICON, Icon.class));
         }
@@ -8833,6 +8825,18 @@
             }
 
             /**
+             * See {@link Notification#visitUris(Consumer)}.
+             *
+             * @hide
+             */
+            public void visitUris(@NonNull Consumer<Uri> visitor) {
+                visitor.accept(getDataUri());
+                if (mSender != null) {
+                    mSender.visitUris(visitor);
+                }
+            }
+
+            /**
              * Returns a list of messages read from the given bundle list, e.g.
              * {@link #EXTRA_MESSAGES} or {@link #EXTRA_HISTORIC_MESSAGES}.
              */
diff --git a/core/java/android/app/Person.java b/core/java/android/app/Person.java
index 97a794d..18fc0ce 100644
--- a/core/java/android/app/Person.java
+++ b/core/java/android/app/Person.java
@@ -24,6 +24,7 @@
 import android.os.Parcelable;
 
 import java.util.Objects;
+import java.util.function.Consumer;
 
 /**
  * Provides an immutable reference to an entity that appears repeatedly on different surfaces of the
@@ -177,6 +178,19 @@
         dest.writeBoolean(mIsBot);
     }
 
+    /**
+     * Note all {@link Uri} that are referenced internally, with the expectation that Uri permission
+     * grants will need to be issued to ensure the recipient of this object is able to render its
+     * contents.
+     * See b/281044385 for more context and examples about what happens when this isn't done
+     * correctly.
+     *
+     * @hide
+     */
+    public void visitUris(@NonNull Consumer<Uri> visitor) {
+        visitor.accept(getIconUri());
+    }
+
     /** Builder for the immutable {@link Person} class. */
     public static class Builder {
         @Nullable private CharSequence mName;
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index c4e4995..2b5175c 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -242,6 +242,18 @@
     public boolean isLetterboxDoubleTapEnabled;
 
     /**
+     * Whether the user aspect ratio settings button is enabled
+     * @hide
+     */
+    public boolean topActivityEligibleForUserAspectRatioButton;
+
+    /**
+     * Hint about the letterbox state of the top activity.
+     * @hide
+     */
+    public boolean topActivityBoundsLetterboxed;
+
+    /**
      * Whether the update comes from a letterbox double-tap action from the user or not.
      * @hide
      */
@@ -460,7 +472,8 @@
     public boolean hasCompatUI() {
         return hasCameraCompatControl() || topActivityInSizeCompat
                 || topActivityEligibleForLetterboxEducation
-                || isLetterboxDoubleTapEnabled;
+                || isLetterboxDoubleTapEnabled
+                || topActivityEligibleForUserAspectRatioButton;
     }
 
     /**
@@ -510,6 +523,8 @@
                 && supportsMultiWindow == that.supportsMultiWindow
                 && displayAreaFeatureId == that.displayAreaFeatureId
                 && isFromLetterboxDoubleTap == that.isFromLetterboxDoubleTap
+                && topActivityEligibleForUserAspectRatioButton
+                    == that.topActivityEligibleForUserAspectRatioButton
                 && topActivityLetterboxVerticalPosition == that.topActivityLetterboxVerticalPosition
                 && topActivityLetterboxWidth == that.topActivityLetterboxWidth
                 && topActivityLetterboxHeight == that.topActivityLetterboxHeight
@@ -543,6 +558,8 @@
                 && taskId == that.taskId
                 && topActivityInSizeCompat == that.topActivityInSizeCompat
                 && isFromLetterboxDoubleTap == that.isFromLetterboxDoubleTap
+                && topActivityEligibleForUserAspectRatioButton
+                    == that.topActivityEligibleForUserAspectRatioButton
                 && topActivityEligibleForLetterboxEducation
                     == that.topActivityEligibleForLetterboxEducation
                 && topActivityLetterboxVerticalPosition == that.topActivityLetterboxVerticalPosition
@@ -606,6 +623,8 @@
         displayAreaFeatureId = source.readInt();
         cameraCompatControlState = source.readInt();
         isLetterboxDoubleTapEnabled = source.readBoolean();
+        topActivityEligibleForUserAspectRatioButton = source.readBoolean();
+        topActivityBoundsLetterboxed = source.readBoolean();
         isFromLetterboxDoubleTap = source.readBoolean();
         topActivityLetterboxVerticalPosition = source.readInt();
         topActivityLetterboxHorizontalPosition = source.readInt();
@@ -660,6 +679,8 @@
         dest.writeInt(displayAreaFeatureId);
         dest.writeInt(cameraCompatControlState);
         dest.writeBoolean(isLetterboxDoubleTapEnabled);
+        dest.writeBoolean(topActivityEligibleForUserAspectRatioButton);
+        dest.writeBoolean(topActivityBoundsLetterboxed);
         dest.writeBoolean(isFromLetterboxDoubleTap);
         dest.writeInt(topActivityLetterboxVerticalPosition);
         dest.writeInt(topActivityLetterboxHorizontalPosition);
@@ -701,8 +722,11 @@
                 + " topActivityInSizeCompat=" + topActivityInSizeCompat
                 + " topActivityEligibleForLetterboxEducation= "
                         + topActivityEligibleForLetterboxEducation
-                + " topActivityLetterboxed= " + isLetterboxDoubleTapEnabled
-                + " isFromDoubleTap= " + isFromLetterboxDoubleTap
+                + " isLetterboxDoubleTapEnabled= " + isLetterboxDoubleTapEnabled
+                + " topActivityEligibleForUserAspectRatioButton= "
+                + topActivityEligibleForUserAspectRatioButton
+                + " topActivityBoundsLetterboxed= " + topActivityBoundsLetterboxed
+                + " isFromLetterboxDoubleTap= " + isFromLetterboxDoubleTap
                 + " topActivityLetterboxVerticalPosition= " + topActivityLetterboxVerticalPosition
                 + " topActivityLetterboxHorizontalPosition= "
                         + topActivityLetterboxHorizontalPosition
diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java
index 247d5bc..c3d26d4 100644
--- a/core/java/android/app/UiAutomation.java
+++ b/core/java/android/app/UiAutomation.java
@@ -37,6 +37,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.hardware.HardwareBuffer;
 import android.hardware.display.DisplayManagerGlobal;
 import android.os.Build;
 import android.os.Handler;
@@ -71,6 +72,8 @@
 import android.view.accessibility.AccessibilityWindowInfo;
 import android.view.accessibility.IAccessibilityInteractionConnection;
 import android.view.inputmethod.EditorInfo;
+import android.window.ScreenCapture;
+import android.window.ScreenCapture.ScreenshotHardwareBuffer;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -1160,17 +1163,12 @@
         Point displaySize = new Point();
         display.getRealSize(displaySize);
 
-        int rotation = display.getRotation();
-
         // Take the screenshot
-        Bitmap screenShot = null;
+        ScreenCapture.SynchronousScreenCaptureListener syncScreenCapture =
+                ScreenCapture.createSyncCaptureListener();
         try {
-            // Calling out without a lock held.
-            screenShot = mUiAutomationConnection.takeScreenshot(
-                    new Rect(0, 0, displaySize.x, displaySize.y));
-            if (screenShot == null) {
-                Log.e(LOG_TAG, "mUiAutomationConnection.takeScreenshot() returned null for display "
-                        + mDisplayId);
+            if (!mUiAutomationConnection.takeScreenshot(
+                    new Rect(0, 0, displaySize.x, displaySize.y), syncScreenCapture)) {
                 return null;
             }
         } catch (RemoteException re) {
@@ -1178,10 +1176,23 @@
             return null;
         }
 
-        // Optimization
-        screenShot.setHasAlpha(false);
+        final ScreenshotHardwareBuffer screenshotBuffer =
+                syncScreenCapture.getBuffer();
+        Bitmap screenShot = screenshotBuffer.asBitmap();
+        if (screenShot == null) {
+            Log.e(LOG_TAG, "mUiAutomationConnection.takeScreenshot() returned null for display "
+                    + mDisplayId);
+            return null;
+        }
+        Bitmap swBitmap;
+        try (HardwareBuffer buffer = screenshotBuffer.getHardwareBuffer()) {
+            swBitmap = screenShot.copy(Bitmap.Config.ARGB_8888, false);
+        }
+        screenShot.recycle();
 
-        return screenShot;
+        // Optimization
+        swBitmap.setHasAlpha(false);
+        return swBitmap;
     }
 
     /**
@@ -1218,12 +1229,27 @@
         // Apply a sync transaction to ensure SurfaceFlinger is flushed before capturing a
         // screenshot.
         new SurfaceControl.Transaction().apply(true);
+        ScreenCapture.SynchronousScreenCaptureListener syncScreenCapture =
+                ScreenCapture.createSyncCaptureListener();
         try {
-            return mUiAutomationConnection.takeSurfaceControlScreenshot(sc);
+            if (!mUiAutomationConnection.takeSurfaceControlScreenshot(sc, syncScreenCapture)) {
+                return null;
+            }
+
         } catch (RemoteException re) {
             Log.e(LOG_TAG, "Error while taking screenshot!", re);
             return null;
         }
+        ScreenCapture.ScreenshotHardwareBuffer captureBuffer =
+                syncScreenCapture.getBuffer();
+        Bitmap screenShot = captureBuffer.asBitmap();
+        Bitmap swBitmap;
+        try (HardwareBuffer buffer = captureBuffer.getHardwareBuffer()) {
+            swBitmap = screenShot.copy(Bitmap.Config.ARGB_8888, false);
+        }
+
+        screenShot.recycle();
+        return swBitmap;
     }
 
     /**
diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java
index 34f0964..52949d6 100644
--- a/core/java/android/app/UiAutomationConnection.java
+++ b/core/java/android/app/UiAutomationConnection.java
@@ -25,7 +25,6 @@
 import android.annotation.UserIdInt;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
-import android.graphics.Bitmap;
 import android.graphics.Rect;
 import android.hardware.input.InputManager;
 import android.hardware.input.InputManagerGlobal;
@@ -51,8 +50,6 @@
 import android.view.accessibility.IAccessibilityManager;
 import android.window.ScreenCapture;
 import android.window.ScreenCapture.CaptureArgs;
-import android.window.ScreenCapture.ScreenshotHardwareBuffer;
-import android.window.ScreenCapture.SynchronousScreenCaptureListener;
 
 import libcore.io.IoUtils;
 
@@ -224,56 +221,54 @@
     }
 
     @Override
-    public Bitmap takeScreenshot(Rect crop) {
+    public boolean takeScreenshot(Rect crop, ScreenCapture.ScreenCaptureListener listener) {
         synchronized (mLock) {
             throwIfCalledByNotTrustedUidLocked();
             throwIfShutdownLocked();
             throwIfNotConnectedLocked();
         }
+
         final long identity = Binder.clearCallingIdentity();
         try {
             final CaptureArgs captureArgs = new CaptureArgs.Builder<>()
                     .setSourceCrop(crop)
                     .build();
-            SynchronousScreenCaptureListener syncScreenCapture =
-                    ScreenCapture.createSyncCaptureListener();
-            mWindowManager.captureDisplay(DEFAULT_DISPLAY, captureArgs,
-                    syncScreenCapture);
-            final ScreenshotHardwareBuffer screenshotBuffer =
-                    syncScreenCapture.getBuffer();
-            return screenshotBuffer == null ? null : screenshotBuffer.asBitmap();
+            mWindowManager.captureDisplay(DEFAULT_DISPLAY, captureArgs, listener);
         } catch (RemoteException re) {
             re.rethrowAsRuntimeException();
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
-        return null;
+
+        return true;
     }
 
     @Nullable
     @Override
-    public Bitmap takeSurfaceControlScreenshot(@NonNull SurfaceControl surfaceControl) {
+    public boolean takeSurfaceControlScreenshot(@NonNull SurfaceControl surfaceControl,
+            ScreenCapture.ScreenCaptureListener listener) {
         synchronized (mLock) {
             throwIfCalledByNotTrustedUidLocked();
             throwIfShutdownLocked();
             throwIfNotConnectedLocked();
         }
 
-        ScreenCapture.ScreenshotHardwareBuffer captureBuffer;
         final long identity = Binder.clearCallingIdentity();
         try {
-            captureBuffer = ScreenCapture.captureLayers(
+            ScreenCapture.LayerCaptureArgs args =
                     new ScreenCapture.LayerCaptureArgs.Builder(surfaceControl)
-                            .setChildrenOnly(false)
-                            .build());
+                    .setChildrenOnly(false)
+                    .build();
+            int status = ScreenCapture.captureLayers(args, listener);
+
+            if (status != 0) {
+                return false;
+            }
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
 
-        if (captureBuffer == null) {
-            return null;
-        }
-        return captureBuffer.asBitmap();
+        return true;
     }
 
     @Override
diff --git a/core/java/android/app/WallpaperColors.java b/core/java/android/app/WallpaperColors.java
index b710644..5f5a7df 100644
--- a/core/java/android/app/WallpaperColors.java
+++ b/core/java/android/app/WallpaperColors.java
@@ -28,6 +28,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.SystemProperties;
+import android.os.Trace;
 import android.util.Log;
 import android.util.MathUtils;
 import android.util.Size;
@@ -144,6 +145,7 @@
             throw new IllegalArgumentException("Drawable cannot be null");
         }
 
+        Trace.beginSection("WallpaperColors#fromDrawable");
         Rect initialBounds = drawable.copyBounds();
         int width = drawable.getIntrinsicWidth();
         int height = drawable.getIntrinsicHeight();
@@ -165,6 +167,7 @@
         bitmap.recycle();
 
         drawable.setBounds(initialBounds);
+        Trace.endSection();
         return colors;
     }
 
@@ -195,7 +198,7 @@
     public static WallpaperColors fromBitmap(@NonNull Bitmap bitmap,
             @FloatRange (from = 0f, to = 1f) float dimAmount) {
         Objects.requireNonNull(bitmap, "Bitmap can't be null");
-
+        Trace.beginSection("WallpaperColors#fromBitmap");
         final int bitmapArea = bitmap.getWidth() * bitmap.getHeight();
         boolean shouldRecycle = false;
         if (bitmapArea > MAX_WALLPAPER_EXTRACTION_AREA) {
@@ -247,6 +250,7 @@
             bitmap.recycle();
         }
 
+        Trace.endSection();
         return new WallpaperColors(populationByColor, HINT_FROM_BITMAP | hints);
     }
 
@@ -462,7 +466,7 @@
      * Gets the most visually representative color of the wallpaper.
      * "Visually representative" means easily noticeable in the image,
      * probably happening at high frequency.
-     *
+     *fromBitmap
      * @return A color.
      */
     public @NonNull Color getPrimaryColor() {
@@ -545,6 +549,7 @@
             return 0;
         }
 
+        Trace.beginSection("WallpaperColors#calculateDarkHints");
         dimAmount = MathUtils.saturate(dimAmount);
         int[] pixels = new int[source.getWidth() * source.getHeight()];
         double totalLuminance = 0;
@@ -607,6 +612,7 @@
                     " maxD: " + maxDarkPixels + " numPixels: " + pixels.length);
         }
 
+        Trace.endSection();
         return hints;
     }
 
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 6131abd..723c564 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -853,7 +853,7 @@
     private static boolean isLockscreenLiveWallpaperEnabledHelper() {
         if (sGlobals == null) {
             sIsLockscreenLiveWallpaperEnabled = SystemProperties.getBoolean(
-                    "persist.wm.debug.lockscreen_live_wallpaper", false);
+                    "persist.wm.debug.lockscreen_live_wallpaper", true);
         }
         if (sIsLockscreenLiveWallpaperEnabled == null) {
             try {
@@ -2429,19 +2429,38 @@
     }
 
     /**
-     * Reset all wallpaper to the factory default.
+     * Reset all wallpaper to the factory default. As opposed to {@link #clear()}, if the device
+     * is configured to have a live wallpaper by default, apply it.
      *
      * <p>This method requires the caller to hold the permission
      * {@link android.Manifest.permission#SET_WALLPAPER}.
      */
     @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
     public void clearWallpaper() {
+        if (isLockscreenLiveWallpaperEnabled()) {
+            clearWallpaper(FLAG_LOCK | FLAG_SYSTEM, mContext.getUserId());
+            return;
+        }
         clearWallpaper(FLAG_LOCK, mContext.getUserId());
         clearWallpaper(FLAG_SYSTEM, mContext.getUserId());
     }
 
     /**
-     * Clear the wallpaper for a specific user.  The caller must hold the
+     * Clear the wallpaper for a specific user.
+     * <ul>
+     *     <li> When called with {@code which=}{@link #FLAG_LOCK}, clear the lockscreen wallpaper.
+     *     The home screen wallpaper will become visible on the lock screen. </li>
+     *
+     *     <li> When called with {@code which=}{@link #FLAG_SYSTEM}, revert the home screen
+     *     wallpaper to default. The lockscreen wallpaper will be unchanged: if the previous
+     *     wallpaper was shared between home and lock screen, it will become lock screen only. </li>
+     *
+     *     <li> When called with {@code which=}({@link #FLAG_LOCK} | {@link #FLAG_SYSTEM}), put the
+     *     default wallpaper on both home and lock screen, removing any user defined wallpaper.</li>
+     * </ul>
+     * </p>
+     *
+     * The caller must hold the
      * INTERACT_ACROSS_USERS_FULL permission to clear another user's
      * wallpaper, and must hold the SET_WALLPAPER permission in all
      * circumstances.
@@ -2746,8 +2765,9 @@
 
     /**
      * Remove any currently set system wallpaper, reverting to the system's built-in
-     * wallpaper. On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED}
-     * is broadcast.
+     * wallpaper. As opposed to {@link #clearWallpaper()}, this method always set a static wallpaper
+     * with the default image, even if the device is configured to have a live wallpaper by default.
+     * On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED} is broadcast.
      *
      * <p>This method requires the caller to hold the permission
      * {@link android.Manifest.permission#SET_WALLPAPER}.
@@ -2762,9 +2782,14 @@
 
     /**
      * Remove one or more currently set wallpapers, reverting to the system default
-     * display for each one.  If {@link #FLAG_SYSTEM} is set in the {@code which}
-     * parameter, the intent {@link Intent#ACTION_WALLPAPER_CHANGED} will be broadcast
-     * upon success.
+     * display for each one. On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED}
+     * is broadcast.
+     * <ul>
+     *     <li> If {@link #FLAG_SYSTEM} is set in the {@code which} parameter, put the default
+     *     wallpaper on both home and lock screen, removing any user defined wallpaper. </li>
+     *     <li> When called with {@code which=}{@link #FLAG_LOCK}, clear the lockscreen wallpaper.
+     *     The home screen wallpaper will become visible on the lock screen. </li>
+     * </ul>
      *
      * @param which A bitwise combination of {@link #FLAG_SYSTEM} or
      *   {@link #FLAG_LOCK}
@@ -2774,6 +2799,7 @@
     public void clear(@SetWallpaperFlags int which) throws IOException {
         if ((which & FLAG_SYSTEM) != 0) {
             clear();
+            if (isLockscreenLiveWallpaperEnabled()) return;
         }
         if ((which & FLAG_LOCK) != 0) {
             clearWallpaper(FLAG_LOCK, mContext.getUserId());
@@ -2886,22 +2912,63 @@
             }
         }
 
-        // Check if the package exists
-        if (cn != null) {
-            try {
-                final PackageManager packageManager = context.getPackageManager();
-                packageManager.getPackageInfo(cn.getPackageName(),
-                        PackageManager.MATCH_DIRECT_BOOT_AWARE
-                                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
-            } catch (PackageManager.NameNotFoundException e) {
-                cn = null;
-            }
+        if (!isComponentExist(context, cn)) {
+            cn = null;
         }
 
         return cn;
     }
 
     /**
+     * Return {@link ComponentName} of the CMF default wallpaper, or
+     * {@link #getDefaultWallpaperComponent(Context)} if none is defined.
+     *
+     * @hide
+     */
+    public static ComponentName getCmfDefaultWallpaperComponent(Context context) {
+        ComponentName cn = null;
+        String[] cmfWallpaperMap = context.getResources().getStringArray(
+                com.android.internal.R.array.default_wallpaper_component_per_device_color);
+        if (cmfWallpaperMap == null || cmfWallpaperMap.length == 0) {
+            Log.d(TAG, "No CMF wallpaper config");
+            return getDefaultWallpaperComponent(context);
+        }
+
+        for (String entry : cmfWallpaperMap) {
+            String[] cmfWallpaper;
+            if (!TextUtils.isEmpty(entry)) {
+                cmfWallpaper = entry.split(",");
+                if (cmfWallpaper != null && cmfWallpaper.length == 2 && VALUE_CMF_COLOR.equals(
+                        cmfWallpaper[0]) && !TextUtils.isEmpty(cmfWallpaper[1])) {
+                    cn = ComponentName.unflattenFromString(cmfWallpaper[1]);
+                    break;
+                }
+            }
+        }
+
+        if (!isComponentExist(context, cn)) {
+            cn = null;
+        }
+
+        return cn;
+    }
+
+    private static boolean isComponentExist(Context context, ComponentName cn) {
+        if (cn == null) {
+            return false;
+        }
+        try {
+            final PackageManager packageManager = context.getPackageManager();
+            packageManager.getPackageInfo(cn.getPackageName(),
+                    PackageManager.MATCH_DIRECT_BOOT_AWARE
+                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
      * Register a callback for lock wallpaper observation. Only the OS may use this.
      *
      * @return true on success; false on error.
diff --git a/core/java/android/app/admin/DevicePolicyManagerInternal.java b/core/java/android/app/admin/DevicePolicyManagerInternal.java
index 0e78275..8dd50f0 100644
--- a/core/java/android/app/admin/DevicePolicyManagerInternal.java
+++ b/core/java/android/app/admin/DevicePolicyManagerInternal.java
@@ -250,6 +250,16 @@
     public abstract ComponentName getProfileOwnerAsUser(@UserIdInt int userId);
 
     /**
+     * Returns the device owner component for the device, or {@code null} if there is not one.
+     *
+     * @deprecated added temporarily to support Android Role permission granting.
+     * Please contact Android Enterprise Device Policy team before calling this function.
+     */
+    @Deprecated
+    @Nullable
+    public abstract ComponentName getDeviceOwnerComponent(boolean callingUserOnly);
+
+    /**
      * Returns the user id of the device owner, or {@link UserHandle#USER_NULL} if there is not one.
      */
     @UserIdInt
diff --git a/core/java/android/app/servertransaction/TransactionExecutorHelper.java b/core/java/android/app/servertransaction/TransactionExecutorHelper.java
index 5311b09..baf2a47 100644
--- a/core/java/android/app/servertransaction/TransactionExecutorHelper.java
+++ b/core/java/android/app/servertransaction/TransactionExecutorHelper.java
@@ -193,8 +193,8 @@
         switch (prevState) {
             // TODO(lifecycler): Extend to support all possible states.
             case ON_START:
-                lifecycleItem = StartActivityItem.obtain(null /* activityOptions */);
-                break;
+                // Fall through to return the PAUSE item to ensure the activity is properly
+                // resumed while relaunching.
             case ON_PAUSE:
                 lifecycleItem = PauseActivityItem.obtain();
                 break;
diff --git a/core/java/android/content/ContentCaptureOptions.java b/core/java/android/content/ContentCaptureOptions.java
index 856bde8..36e0529 100644
--- a/core/java/android/content/ContentCaptureOptions.java
+++ b/core/java/android/content/ContentCaptureOptions.java
@@ -76,6 +76,20 @@
     public final boolean disableFlushForViewTreeAppearing;
 
     /**
+     * Is the content capture receiver enabled.
+     *
+     * @hide
+     */
+    public final boolean enableReceiver;
+
+    /**
+     * Options for the content protection flow.
+     *
+     * @hide
+     */
+    @NonNull public final ContentProtectionOptions contentProtectionOptions;
+
+    /**
      * List of activities explicitly allowlisted for content capture (or {@code null} if allowlisted
      * for all acitivites in the package).
      */
@@ -94,52 +108,99 @@
      * for contexts belonging to the content capture service app.
      */
     public ContentCaptureOptions(int loggingLevel) {
-        this(/* lite= */ true, loggingLevel, /* maxBufferSize= */ 0,
-                /* idleFlushingFrequencyMs= */ 0, /* textChangeFlushingFrequencyMs= */ 0,
-                /* logHistorySize= */ 0, /* disableFlushForViewTreeAppearing= */ false,
+        this(
+                /* lite= */ true,
+                loggingLevel,
+                /* maxBufferSize= */ 0,
+                /* idleFlushingFrequencyMs= */ 0,
+                /* textChangeFlushingFrequencyMs= */ 0,
+                /* logHistorySize= */ 0,
+                /* disableFlushForViewTreeAppearing= */ false,
+                /* enableReceiver= */ false,
+                new ContentProtectionOptions(
+                        /* enableReceiver= */ false,
+                        /* bufferSize= */ 0),
                 /* whitelistedComponents= */ null);
     }
 
-    /**
-     * Default constructor.
-     */
-    public ContentCaptureOptions(int loggingLevel, int maxBufferSize, int idleFlushingFrequencyMs,
-            int textChangeFlushingFrequencyMs, int logHistorySize,
-            @SuppressLint({"ConcreteCollection", "NullableCollection"})
-            @Nullable ArraySet<ComponentName> whitelistedComponents) {
-        this(/* lite= */ false, loggingLevel, maxBufferSize, idleFlushingFrequencyMs,
-                textChangeFlushingFrequencyMs, logHistorySize,
+    /** Default constructor. */
+    public ContentCaptureOptions(
+            int loggingLevel,
+            int maxBufferSize,
+            int idleFlushingFrequencyMs,
+            int textChangeFlushingFrequencyMs,
+            int logHistorySize,
+            @SuppressLint({"ConcreteCollection", "NullableCollection"}) @Nullable
+                    ArraySet<ComponentName> whitelistedComponents) {
+        this(
+                /* lite= */ false,
+                loggingLevel,
+                maxBufferSize,
+                idleFlushingFrequencyMs,
+                textChangeFlushingFrequencyMs,
+                logHistorySize,
                 ContentCaptureManager.DEFAULT_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING,
+                ContentCaptureManager.DEFAULT_ENABLE_CONTENT_CAPTURE_RECEIVER,
+                new ContentProtectionOptions(
+                        ContentCaptureManager.DEFAULT_ENABLE_CONTENT_PROTECTION_RECEIVER,
+                        ContentCaptureManager.DEFAULT_CONTENT_PROTECTION_BUFFER_SIZE),
                 whitelistedComponents);
     }
 
     /** @hide */
-    public ContentCaptureOptions(int loggingLevel, int maxBufferSize, int idleFlushingFrequencyMs,
-            int textChangeFlushingFrequencyMs, int logHistorySize,
+    public ContentCaptureOptions(
+            int loggingLevel,
+            int maxBufferSize,
+            int idleFlushingFrequencyMs,
+            int textChangeFlushingFrequencyMs,
+            int logHistorySize,
             boolean disableFlushForViewTreeAppearing,
-            @SuppressLint({"ConcreteCollection", "NullableCollection"})
-            @Nullable ArraySet<ComponentName> whitelistedComponents) {
-        this(/* lite= */ false, loggingLevel, maxBufferSize, idleFlushingFrequencyMs,
-                textChangeFlushingFrequencyMs, logHistorySize, disableFlushForViewTreeAppearing,
+            boolean enableReceiver,
+            @NonNull ContentProtectionOptions contentProtectionOptions,
+            @SuppressLint({"ConcreteCollection", "NullableCollection"}) @Nullable
+                    ArraySet<ComponentName> whitelistedComponents) {
+        this(
+                /* lite= */ false,
+                loggingLevel,
+                maxBufferSize,
+                idleFlushingFrequencyMs,
+                textChangeFlushingFrequencyMs,
+                logHistorySize,
+                disableFlushForViewTreeAppearing,
+                enableReceiver,
+                contentProtectionOptions,
                 whitelistedComponents);
     }
 
     /** @hide */
     @VisibleForTesting
     public ContentCaptureOptions(@Nullable ArraySet<ComponentName> whitelistedComponents) {
-        this(ContentCaptureManager.LOGGING_LEVEL_VERBOSE,
+        this(
+                ContentCaptureManager.LOGGING_LEVEL_VERBOSE,
                 ContentCaptureManager.DEFAULT_MAX_BUFFER_SIZE,
                 ContentCaptureManager.DEFAULT_IDLE_FLUSHING_FREQUENCY_MS,
                 ContentCaptureManager.DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS,
                 ContentCaptureManager.DEFAULT_LOG_HISTORY_SIZE,
                 ContentCaptureManager.DEFAULT_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING,
+                ContentCaptureManager.DEFAULT_ENABLE_CONTENT_CAPTURE_RECEIVER,
+                new ContentProtectionOptions(
+                        ContentCaptureManager.DEFAULT_ENABLE_CONTENT_PROTECTION_RECEIVER,
+                        ContentCaptureManager.DEFAULT_CONTENT_PROTECTION_BUFFER_SIZE),
                 whitelistedComponents);
     }
 
-    private ContentCaptureOptions(boolean lite, int loggingLevel, int maxBufferSize,
-            int idleFlushingFrequencyMs, int textChangeFlushingFrequencyMs, int logHistorySize,
+    private ContentCaptureOptions(
+            boolean lite,
+            int loggingLevel,
+            int maxBufferSize,
+            int idleFlushingFrequencyMs,
+            int textChangeFlushingFrequencyMs,
+            int logHistorySize,
             boolean disableFlushForViewTreeAppearing,
-            @Nullable ArraySet<ComponentName> whitelistedComponents) {
+            boolean enableReceiver,
+            @NonNull ContentProtectionOptions contentProtectionOptions,
+            @SuppressLint({"ConcreteCollection", "NullableCollection"}) @Nullable
+                    ArraySet<ComponentName> whitelistedComponents) {
         this.lite = lite;
         this.loggingLevel = loggingLevel;
         this.maxBufferSize = maxBufferSize;
@@ -147,6 +208,8 @@
         this.textChangeFlushingFrequencyMs = textChangeFlushingFrequencyMs;
         this.logHistorySize = logHistorySize;
         this.disableFlushForViewTreeAppearing = disableFlushForViewTreeAppearing;
+        this.enableReceiver = enableReceiver;
+        this.contentProtectionOptions = contentProtectionOptions;
         this.whitelistedComponents = whitelistedComponents;
     }
 
@@ -191,12 +254,22 @@
             return "ContentCaptureOptions [loggingLevel=" + loggingLevel + " (lite)]";
         }
         final StringBuilder string = new StringBuilder("ContentCaptureOptions [");
-        string.append("loggingLevel=").append(loggingLevel)
-            .append(", maxBufferSize=").append(maxBufferSize)
-            .append(", idleFlushingFrequencyMs=").append(idleFlushingFrequencyMs)
-            .append(", textChangeFlushingFrequencyMs=").append(textChangeFlushingFrequencyMs)
-            .append(", logHistorySize=").append(logHistorySize)
-            .append(", disableFlushForViewTreeAppearing=").append(disableFlushForViewTreeAppearing);
+        string.append("loggingLevel=")
+                .append(loggingLevel)
+                .append(", maxBufferSize=")
+                .append(maxBufferSize)
+                .append(", idleFlushingFrequencyMs=")
+                .append(idleFlushingFrequencyMs)
+                .append(", textChangeFlushingFrequencyMs=")
+                .append(textChangeFlushingFrequencyMs)
+                .append(", logHistorySize=")
+                .append(logHistorySize)
+                .append(", disableFlushForViewTreeAppearing=")
+                .append(disableFlushForViewTreeAppearing)
+                .append(", enableReceiver=")
+                .append(enableReceiver)
+                .append(", contentProtectionOptions=")
+                .append(contentProtectionOptions);
         if (whitelistedComponents != null) {
             string.append(", whitelisted=").append(whitelistedComponents);
         }
@@ -210,11 +283,21 @@
             pw.print(", lite");
             return;
         }
-        pw.print(", bufferSize="); pw.print(maxBufferSize);
-        pw.print(", idle="); pw.print(idleFlushingFrequencyMs);
-        pw.print(", textIdle="); pw.print(textChangeFlushingFrequencyMs);
-        pw.print(", logSize="); pw.print(logHistorySize);
-        pw.print(", disableFlushForViewTreeAppearing="); pw.print(disableFlushForViewTreeAppearing);
+        pw.print(", bufferSize=");
+        pw.print(maxBufferSize);
+        pw.print(", idle=");
+        pw.print(idleFlushingFrequencyMs);
+        pw.print(", textIdle=");
+        pw.print(textChangeFlushingFrequencyMs);
+        pw.print(", logSize=");
+        pw.print(logHistorySize);
+        pw.print(", disableFlushForViewTreeAppearing=");
+        pw.print(disableFlushForViewTreeAppearing);
+        pw.print(", enableReceiver=");
+        pw.print(enableReceiver);
+        pw.print(", contentProtectionOptions=[");
+        contentProtectionOptions.dumpShort(pw);
+        pw.print("]");
         if (whitelistedComponents != null) {
             pw.print(", whitelisted="); pw.print(whitelistedComponents);
         }
@@ -236,6 +319,8 @@
         parcel.writeInt(textChangeFlushingFrequencyMs);
         parcel.writeInt(logHistorySize);
         parcel.writeBoolean(disableFlushForViewTreeAppearing);
+        parcel.writeBoolean(enableReceiver);
+        contentProtectionOptions.writeToParcel(parcel);
         parcel.writeArraySet(whitelistedComponents);
     }
 
@@ -254,12 +339,22 @@
                     final int textChangeFlushingFrequencyMs = parcel.readInt();
                     final int logHistorySize = parcel.readInt();
                     final boolean disableFlushForViewTreeAppearing = parcel.readBoolean();
+                    final boolean enableReceiver = parcel.readBoolean();
+                    final ContentProtectionOptions contentProtectionOptions =
+                            ContentProtectionOptions.createFromParcel(parcel);
                     @SuppressWarnings("unchecked")
                     final ArraySet<ComponentName> whitelistedComponents =
                             (ArraySet<ComponentName>) parcel.readArraySet(null);
-                    return new ContentCaptureOptions(loggingLevel, maxBufferSize,
-                            idleFlushingFrequencyMs, textChangeFlushingFrequencyMs, logHistorySize,
-                            disableFlushForViewTreeAppearing, whitelistedComponents);
+                    return new ContentCaptureOptions(
+                            loggingLevel,
+                            maxBufferSize,
+                            idleFlushingFrequencyMs,
+                            textChangeFlushingFrequencyMs,
+                            logHistorySize,
+                            disableFlushForViewTreeAppearing,
+                            enableReceiver,
+                            contentProtectionOptions,
+                            whitelistedComponents);
                 }
 
                 @Override
@@ -267,4 +362,62 @@
                     return new ContentCaptureOptions[size];
                 }
     };
+
+    /**
+     * Content protection options for a given package.
+     *
+     * <p>Does not implement {@code Parcelable} since it is an inner class without a matching AIDL.
+     *
+     * @hide
+     */
+    public static class ContentProtectionOptions {
+
+        /**
+         * Is the content protection receiver enabled.
+         *
+         * @hide
+         */
+        public final boolean enableReceiver;
+
+        /**
+         * Size of the in-memory ring buffer for the content protection flow.
+         *
+         * @hide
+         */
+        public final int bufferSize;
+
+        public ContentProtectionOptions(boolean enableReceiver, int bufferSize) {
+            this.enableReceiver = enableReceiver;
+            this.bufferSize = bufferSize;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder stringBuilder = new StringBuilder("ContentProtectionOptions [");
+            stringBuilder
+                    .append("enableReceiver=")
+                    .append(enableReceiver)
+                    .append(", bufferSize=")
+                    .append(bufferSize);
+            return stringBuilder.append(']').toString();
+        }
+
+        private void dumpShort(@NonNull PrintWriter pw) {
+            pw.print("enableReceiver=");
+            pw.print(enableReceiver);
+            pw.print(", bufferSize=");
+            pw.print(bufferSize);
+        }
+
+        private void writeToParcel(Parcel parcel) {
+            parcel.writeBoolean(enableReceiver);
+            parcel.writeInt(bufferSize);
+        }
+
+        private static ContentProtectionOptions createFromParcel(Parcel parcel) {
+            boolean enableReceiver = parcel.readBoolean();
+            int bufferSize = parcel.readInt();
+            return new ContentProtectionOptions(enableReceiver, bufferSize);
+        }
+    }
 }
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 47a5db8..3fdd023 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -770,6 +770,11 @@
 
     void setSplashScreenTheme(String packageName, String themeName, int userId);
 
+    int getUserMinAspectRatio(String packageName, int userId);
+
+    @EnforcePermission("INSTALL_PACKAGES")
+    void setUserMinAspectRatio(String packageName, int userId, int aspectRatio);
+
     List<String> getMimeGroup(String packageName, String group);
 
     boolean isAutoRevokeWhitelisted(String packageName);
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 8fafb18..66aadac 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2342,6 +2342,64 @@
      */
     public static final int INSTALL_FAILED_SHARED_LIBRARY_BAD_CERTIFICATE_DIGEST = -130;
 
+    /**
+     * App minimum aspect ratio set by the user which will override app-defined aspect ratio.
+     *
+     * @hide
+     */
+    @IntDef(prefix = { "USER_MIN_ASPECT_RATIO_" }, value = {
+            USER_MIN_ASPECT_RATIO_UNSET,
+            USER_MIN_ASPECT_RATIO_SPLIT_SCREEN,
+            USER_MIN_ASPECT_RATIO_DISPLAY_SIZE,
+            USER_MIN_ASPECT_RATIO_4_3,
+            USER_MIN_ASPECT_RATIO_16_9,
+            USER_MIN_ASPECT_RATIO_3_2,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface UserMinAspectRatio {}
+
+    /**
+     * No aspect ratio override has been set by user.
+     *
+     * @hide
+     */
+    public static final int USER_MIN_ASPECT_RATIO_UNSET = 0;
+
+    /**
+     * Aspect ratio override code: user forces app to split screen aspect ratio. This is adjusted to
+     * half of the screen without the split screen divider.
+     *
+     * @hide
+     */
+    public static final int USER_MIN_ASPECT_RATIO_SPLIT_SCREEN = 1;
+
+    /**
+     * Aspect ratio override code: user forces app to the aspect ratio of the device display size.
+     * This will be the portrait aspect ratio of the device if the app is portrait or the landscape
+     * aspect ratio of the device if the app is landscape.
+     *
+     * @hide
+     */
+    public static final int USER_MIN_ASPECT_RATIO_DISPLAY_SIZE = 2;
+
+    /**
+     * Aspect ratio override code: user forces app to 4:3 min aspect ratio
+     * @hide
+     */
+    public static final int USER_MIN_ASPECT_RATIO_4_3 = 3;
+
+    /**
+     * Aspect ratio override code: user forces app to 16:9 min aspect ratio
+     * @hide
+     */
+    public static final int USER_MIN_ASPECT_RATIO_16_9 = 4;
+
+    /**
+     * Aspect ratio override code: user forces app to 3:2 min aspect ratio
+     * @hide
+     */
+    public static final int USER_MIN_ASPECT_RATIO_3_2 = 5;
+
     /** @hide */
     @IntDef(flag = true, prefix = { "DELETE_" }, value = {
             DELETE_KEEP_DATA,
diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
index d48e20e..6baf91d7 100644
--- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
@@ -34,6 +34,7 @@
 import android.hardware.camera2.impl.CameraMetadataNative;
 import android.hardware.camera2.params.ExtensionSessionConfiguration;
 import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Binder;
 import android.os.ConditionVariable;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -48,7 +49,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -346,28 +346,29 @@
             }
         }
 
-        public long registerClient(Context ctx) {
+        public boolean registerClient(Context ctx, IBinder token) {
             synchronized (mLock) {
                 connectToProxyLocked(ctx);
-                if (mProxy != null) {
-                    try {
-                        return mProxy.registerClient();
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "Failed to initialize extension! Extension service does "
-                                + " not respond!");
-                        return -1;
-                    }
-                } else {
-                    return -1;
+                if (mProxy == null) {
+                    return false;
                 }
+
+                try {
+                    return mProxy.registerClient(token);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Failed to initialize extension! Extension service does "
+                            + " not respond!");
+                }
+
+                return false;
             }
         }
 
-        public void unregisterClient(long clientId) {
+        public void unregisterClient(IBinder token) {
             synchronized (mLock) {
                 if (mProxy != null) {
                     try {
-                        mProxy.unregisterClient(clientId);
+                        mProxy.unregisterClient(token);
                     } catch (RemoteException e) {
                         Log.e(TAG, "Failed to de-initialize extension! Extension service does"
                                 + " not respond!");
@@ -438,15 +439,15 @@
     /**
      * @hide
      */
-    public static long registerClient(Context ctx) {
-        return CameraExtensionManagerGlobal.get().registerClient(ctx);
+    public static boolean registerClient(Context ctx, IBinder token) {
+        return CameraExtensionManagerGlobal.get().registerClient(ctx, token);
     }
 
     /**
      * @hide
      */
-    public static void unregisterClient(long clientId) {
-        CameraExtensionManagerGlobal.get().unregisterClient(clientId);
+    public static void unregisterClient(IBinder token) {
+        CameraExtensionManagerGlobal.get().unregisterClient(token);
     }
 
     /**
@@ -564,8 +565,9 @@
      */
     public @NonNull List<Integer> getSupportedExtensions() {
         ArrayList<Integer> ret = new ArrayList<>();
-        long clientId = registerClient(mContext);
-        if (clientId < 0) {
+        final IBinder token = new Binder(TAG + "#getSupportedExtensions:" + mCameraId);
+        boolean success = registerClient(mContext, token);
+        if (!success) {
             return Collections.unmodifiableList(ret);
         }
 
@@ -576,7 +578,7 @@
                 }
             }
         } finally {
-            unregisterClient(clientId);
+            unregisterClient(token);
         }
 
         return Collections.unmodifiableList(ret);
@@ -599,8 +601,9 @@
      * supported device-specific extension
      */
     public boolean isPostviewAvailable(@Extension int extension) {
-        long clientId = registerClient(mContext);
-        if (clientId < 0) {
+        final IBinder token = new Binder(TAG + "#isPostviewAvailable:" + mCameraId);
+        boolean success = registerClient(mContext, token);
+        if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
 
@@ -623,7 +626,7 @@
             Log.e(TAG, "Failed to query the extension for postview availability! Extension "
                     + "service does not respond!");
         } finally {
-            unregisterClient(clientId);
+            unregisterClient(token);
         }
 
         return false;
@@ -656,9 +659,9 @@
     @NonNull
     public List<Size> getPostviewSupportedSizes(@Extension int extension,
             @NonNull Size captureSize, int format) {
-
-        long clientId = registerClient(mContext);
-        if (clientId < 0) {
+        final IBinder token = new Binder(TAG + "#getPostviewSupportedSizes:" + mCameraId);
+        boolean success = registerClient(mContext, token);
+        if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
 
@@ -719,7 +722,7 @@
                     + "service does not respond!");
             return Collections.emptyList();
         } finally {
-            unregisterClient(clientId);
+            unregisterClient(token);
         }
     }
 
@@ -756,8 +759,9 @@
         // TODO: Revisit this code once the Extension preview processor output format
         //       ambiguity is resolved in b/169799538.
 
-        long clientId = registerClient(mContext);
-        if (clientId < 0) {
+        final IBinder token = new Binder(TAG + "#getExtensionSupportedSizes:" + mCameraId);
+        boolean success = registerClient(mContext, token);
+        if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
 
@@ -787,7 +791,7 @@
                     + " not respond!");
             return new ArrayList<>();
         } finally {
-            unregisterClient(clientId);
+            unregisterClient(token);
         }
     }
 
@@ -814,8 +818,9 @@
     public @NonNull
     List<Size> getExtensionSupportedSizes(@Extension int extension, int format) {
         try {
-            long clientId = registerClient(mContext);
-            if (clientId < 0) {
+            final IBinder token = new Binder(TAG + "#getExtensionSupportedSizes:" + mCameraId);
+            boolean success = registerClient(mContext, token);
+            if (!success) {
                 throw new IllegalArgumentException("Unsupported extensions");
             }
 
@@ -867,7 +872,7 @@
                     }
                 }
             } finally {
-                unregisterClient(clientId);
+                unregisterClient(token);
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to query the extension supported sizes! Extension service does"
@@ -888,7 +893,6 @@
      * @param format            device-specific extension output format
      * @return the range of estimated minimal and maximal capture latency in milliseconds
      * or null if no capture latency info can be provided
-     *
      * @throws IllegalArgumentException in case of format different from {@link ImageFormat#JPEG} /
      *                                  {@link ImageFormat#YUV_420_888}; or unsupported extension.
      */
@@ -903,8 +907,9 @@
                 throw new IllegalArgumentException("Unsupported format: " + format);
         }
 
-        long clientId = registerClient(mContext);
-        if (clientId < 0) {
+        final IBinder token = new Binder(TAG + "#getEstimatedCaptureLatencyRangeMillis:" + mCameraId);
+        boolean success = registerClient(mContext, token);
+        if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
 
@@ -952,7 +957,7 @@
             Log.e(TAG, "Failed to query the extension capture latency! Extension service does"
                     + " not respond!");
         } finally {
-            unregisterClient(clientId);
+            unregisterClient(token);
         }
 
         return null;
@@ -968,8 +973,9 @@
      * @throws IllegalArgumentException in case of an unsupported extension.
      */
     public boolean isCaptureProcessProgressAvailable(@Extension int extension) {
-        long clientId = registerClient(mContext);
-        if (clientId < 0) {
+        final IBinder token = new Binder(TAG + "#isCaptureProcessProgressAvailable:" + mCameraId);
+        boolean success = registerClient(mContext, token);
+        if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
 
@@ -992,7 +998,7 @@
             Log.e(TAG, "Failed to query the extension progress callbacks! Extension service does"
                     + " not respond!");
         } finally {
-            unregisterClient(clientId);
+            unregisterClient(token);
         }
 
         return false;
@@ -1013,8 +1019,9 @@
      */
     @NonNull
     public Set<CaptureRequest.Key> getAvailableCaptureRequestKeys(@Extension int extension) {
-        long clientId = registerClient(mContext);
-        if (clientId < 0) {
+        final IBinder token = new Binder(TAG + "#getAvailableCaptureRequestKeys:" + mCameraId);
+        boolean success = registerClient(mContext, token);
+        if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
 
@@ -1033,10 +1040,11 @@
             } else {
                 Pair<IPreviewExtenderImpl, IImageCaptureExtenderImpl> extenders =
                         initializeExtension(extension);
-                extenders.second.onInit(mCameraId, mCharacteristicsMapNative.get(mCameraId));
+                extenders.second.onInit(token, mCameraId,
+                        mCharacteristicsMapNative.get(mCameraId));
                 extenders.second.init(mCameraId, mCharacteristicsMapNative.get(mCameraId));
                 captureRequestMeta = extenders.second.getAvailableCaptureRequestKeys();
-                extenders.second.onDeInit();
+                extenders.second.onDeInit(token);
             }
 
             if (captureRequestMeta != null) {
@@ -1067,7 +1075,7 @@
         } catch (RemoteException e) {
             throw new IllegalStateException("Failed to query the available capture request keys!");
         } finally {
-            unregisterClient(clientId);
+            unregisterClient(token);
         }
 
         return Collections.unmodifiableSet(ret);
@@ -1092,8 +1100,9 @@
      */
     @NonNull
     public Set<CaptureResult.Key> getAvailableCaptureResultKeys(@Extension int extension) {
-        long clientId = registerClient(mContext);
-        if (clientId < 0) {
+        final IBinder token = new Binder(TAG + "#getAvailableCaptureResultKeys:" + mCameraId);
+        boolean success = registerClient(mContext, token);
+        if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
 
@@ -1111,10 +1120,11 @@
             } else {
                 Pair<IPreviewExtenderImpl, IImageCaptureExtenderImpl> extenders =
                         initializeExtension(extension);
-                extenders.second.onInit(mCameraId, mCharacteristicsMapNative.get(mCameraId));
+                extenders.second.onInit(token, mCameraId,
+                        mCharacteristicsMapNative.get(mCameraId));
                 extenders.second.init(mCameraId, mCharacteristicsMapNative.get(mCameraId));
                 captureResultMeta = extenders.second.getAvailableCaptureResultKeys();
-                extenders.second.onDeInit();
+                extenders.second.onDeInit(token);
             }
 
             if (captureResultMeta != null) {
@@ -1126,7 +1136,7 @@
                 }
                 CameraCharacteristics resultChars = new CameraCharacteristics(captureResultMeta);
                 Object crKey = CaptureResult.Key.class;
-                Class<CaptureResult.Key<?>> crKeyTyped = (Class<CaptureResult.Key<?>>)crKey;
+                Class<CaptureResult.Key<?>> crKeyTyped = (Class<CaptureResult.Key<?>>) crKey;
 
                 ret.addAll(resultChars.getAvailableKeyList(CaptureResult.class, crKeyTyped,
                         resultKeys, /*includeSynthetic*/ true));
@@ -1145,7 +1155,7 @@
         } catch (RemoteException e) {
             throw new IllegalStateException("Failed to query the available capture result keys!");
         } finally {
-            unregisterClient(clientId);
+            unregisterClient(token);
         }
 
         return Collections.unmodifiableSet(ret);
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 85f8ca6..a098362 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -67,6 +67,7 @@
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Executor;
@@ -178,22 +179,20 @@
             boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state);
 
             mFoldedDeviceState = folded;
-            ArrayList<WeakReference<DeviceStateListener>> invalidListeners = new ArrayList<>();
-            for (WeakReference<DeviceStateListener> listener : mDeviceStateListeners) {
-                DeviceStateListener callback = listener.get();
+            Iterator<WeakReference<DeviceStateListener>> it = mDeviceStateListeners.iterator();
+            while(it.hasNext()) {
+                DeviceStateListener callback = it.next().get();
                 if (callback != null) {
                     callback.onDeviceStateChanged(folded);
                 } else {
-                    invalidListeners.add(listener);
+                    it.remove();
                 }
             }
-            if (!invalidListeners.isEmpty()) {
-                mDeviceStateListeners.removeAll(invalidListeners);
-            }
         }
 
         public synchronized void addDeviceStateListener(DeviceStateListener listener) {
             listener.onDeviceStateChanged(mFoldedDeviceState);
+            mDeviceStateListeners.removeIf(l -> l.get() == null);
             mDeviceStateListeners.add(new WeakReference<>(listener));
         }
 
diff --git a/core/java/android/hardware/camera2/extension/ICameraExtensionsProxyService.aidl b/core/java/android/hardware/camera2/extension/ICameraExtensionsProxyService.aidl
index b52c6500..3b7d801 100644
--- a/core/java/android/hardware/camera2/extension/ICameraExtensionsProxyService.aidl
+++ b/core/java/android/hardware/camera2/extension/ICameraExtensionsProxyService.aidl
@@ -20,11 +20,13 @@
 import android.hardware.camera2.extension.IImageCaptureExtenderImpl;
 import android.hardware.camera2.extension.IInitializeSessionCallback;
 
+import android.os.IBinder;
+
 /** @hide */
 interface ICameraExtensionsProxyService
 {
-    long registerClient();
-    void unregisterClient(long clientId);
+    boolean registerClient(in IBinder token);
+    void unregisterClient(in IBinder token);
     boolean advancedExtensionsSupported();
     void initializeSession(in IInitializeSessionCallback cb);
     void releaseSession();
diff --git a/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl b/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl
index 754f8f6..5a22418 100644
--- a/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl
+++ b/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl
@@ -24,11 +24,13 @@
 import android.hardware.camera2.extension.Size;
 import android.hardware.camera2.extension.SizeList;
 
+import android.os.IBinder;
+
 /** @hide */
 interface IImageCaptureExtenderImpl
 {
-    void onInit(in String cameraId, in CameraMetadataNative cameraCharacteristics);
-    void onDeInit();
+    void onInit(in IBinder token, in String cameraId, in CameraMetadataNative cameraCharacteristics);
+    void onDeInit(in IBinder token);
     @nullable CaptureStageImpl onPresetSession();
     @nullable CaptureStageImpl onEnableSession();
     @nullable CaptureStageImpl onDisableSession();
diff --git a/core/java/android/hardware/camera2/extension/IPreviewExtenderImpl.aidl b/core/java/android/hardware/camera2/extension/IPreviewExtenderImpl.aidl
index 01046d0..9ea8a74 100644
--- a/core/java/android/hardware/camera2/extension/IPreviewExtenderImpl.aidl
+++ b/core/java/android/hardware/camera2/extension/IPreviewExtenderImpl.aidl
@@ -22,11 +22,13 @@
 import android.hardware.camera2.extension.IRequestUpdateProcessorImpl;
 import android.hardware.camera2.extension.SizeList;
 
+import android.os.IBinder;
+
 /** @hide */
 interface IPreviewExtenderImpl
 {
-    void onInit(in String cameraId, in CameraMetadataNative cameraCharacteristics);
-    void onDeInit();
+    void onInit(in IBinder token, in String cameraId, in CameraMetadataNative cameraCharacteristics);
+    void onDeInit(in IBinder token);
     @nullable CaptureStageImpl onPresetSession();
     @nullable CaptureStageImpl onEnableSession();
     @nullable CaptureStageImpl onDisableSession();
diff --git a/core/java/android/hardware/camera2/extension/ISessionProcessorImpl.aidl b/core/java/android/hardware/camera2/extension/ISessionProcessorImpl.aidl
index 13b93a8..0581ec0 100644
--- a/core/java/android/hardware/camera2/extension/ISessionProcessorImpl.aidl
+++ b/core/java/android/hardware/camera2/extension/ISessionProcessorImpl.aidl
@@ -25,13 +25,15 @@
 import android.hardware.camera2.extension.LatencyRange;
 import android.hardware.camera2.extension.OutputSurface;
 
+import android.os.IBinder;
+
 /** @hide */
 interface ISessionProcessorImpl
 {
-    CameraSessionConfig initSession(in String cameraId,
+    CameraSessionConfig initSession(in IBinder token, in String cameraId,
             in Map<String, CameraMetadataNative> charsMap, in OutputSurface previewSurface,
             in OutputSurface imageCaptureSurface, in OutputSurface postviewSurface);
-    void deInitSession();
+    void deInitSession(in IBinder token);
     void onCaptureSessionStart(IRequestProcessorImpl requestProcessor);
     void onCaptureSessionEnd();
     int startRepeating(in ICaptureCallback callback);
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index 65d4b43..ae700a0 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -60,6 +60,7 @@
 import android.os.Binder;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
 import android.util.Size;
@@ -79,7 +80,6 @@
     private final Executor mExecutor;
     private CameraDevice mCameraDevice;
     private final Map<String, CameraMetadataNative> mCharacteristicsMap;
-    private final long mExtensionClientId;
     private final Handler mHandler;
     private final HandlerThread mHandlerThread;
     private final CameraExtensionSession.StateCallback mCallbacks;
@@ -90,6 +90,7 @@
     private final HashMap<Integer, ImageReader> mReaderMap = new HashMap<>();
     private RequestProcessor mRequestProcessor = new RequestProcessor();
     private final int mSessionId;
+    private final IBinder mToken;
 
     private Surface mClientRepeatingRequestSurface;
     private Surface mClientCaptureSurface;
@@ -114,8 +115,9 @@
             @NonNull Map<String, CameraCharacteristics> characteristicsMap,
             @NonNull Context ctx, @NonNull ExtensionSessionConfiguration config, int sessionId)
             throws CameraAccessException, RemoteException {
-        long clientId = CameraExtensionCharacteristics.registerClient(ctx);
-        if (clientId < 0) {
+        final IBinder token = new Binder(TAG + " : " + sessionId);
+        boolean success = CameraExtensionCharacteristics.registerClient(ctx, token);
+        if (!success) {
             throw new UnsupportedOperationException("Unsupported extension!");
         }
 
@@ -202,11 +204,10 @@
         IAdvancedExtenderImpl extender = CameraExtensionCharacteristics.initializeAdvancedExtension(
                 config.getExtension());
         extender.init(cameraId, characteristicsMapNative);
-
-        CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(clientId,
-                extender, cameraDevice, characteristicsMapNative, repeatingRequestSurface,
+        CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(extender,
+                cameraDevice, characteristicsMapNative, repeatingRequestSurface,
                 burstCaptureSurface, postviewSurface, config.getStateCallback(),
-                config.getExecutor(), sessionId);
+                config.getExecutor(), sessionId, token);
 
         ret.mStatsAggregator.setClientName(ctx.getOpPackageName());
         ret.mStatsAggregator.setExtensionType(config.getExtension());
@@ -216,15 +217,13 @@
         return ret;
     }
 
-    private CameraAdvancedExtensionSessionImpl(long extensionClientId,
-            @NonNull IAdvancedExtenderImpl extender,
+    private CameraAdvancedExtensionSessionImpl(@NonNull IAdvancedExtenderImpl extender,
             @NonNull CameraDeviceImpl cameraDevice,
             Map<String, CameraMetadataNative> characteristicsMap,
             @Nullable Surface repeatingRequestSurface, @Nullable Surface burstCaptureSurface,
             @Nullable Surface postviewSurface,
             @NonNull StateCallback callback, @NonNull Executor executor,
-            int sessionId) {
-        mExtensionClientId = extensionClientId;
+            int sessionId, @NonNull IBinder token) {
         mAdvancedExtender = extender;
         mCameraDevice = cameraDevice;
         mCharacteristicsMap = characteristicsMap;
@@ -240,6 +239,7 @@
         mSessionClosed = false;
         mInitializeHandler = new InitializeSessionHandler();
         mSessionId = sessionId;
+        mToken = token;
         mInterfaceLock = cameraDevice.mInterfaceLock;
 
         mStatsAggregator = new ExtensionSessionStatsAggregator(mCameraDevice.getId(),
@@ -260,7 +260,8 @@
         OutputSurface postviewSurface = initializeParcelable(mClientPostviewSurface);
 
         mSessionProcessor = mAdvancedExtender.getSessionProcessor();
-        CameraSessionConfig sessionConfig = mSessionProcessor.initSession(mCameraDevice.getId(),
+        CameraSessionConfig sessionConfig = mSessionProcessor.initSession(mToken,
+                mCameraDevice.getId(),
                 mCharacteristicsMap, previewSurface, captureSurface, postviewSurface);
         List<CameraOutputConfig> outputConfigs = sessionConfig.outputConfigs;
         ArrayList<OutputConfiguration> outputList = new ArrayList<>();
@@ -526,7 +527,11 @@
         synchronized (mInterfaceLock) {
             if (mInitialized) {
                 try {
-                    mCaptureSession.stopRepeating();
+                    try {
+                        mCaptureSession.stopRepeating();
+                    } catch (IllegalStateException e) {
+                        // OK: already be closed, nothing else to do
+                    }
                     mSessionProcessor.stopRepeating();
                     mSessionProcessor.onCaptureSessionEnd();
                     mSessionClosed = true;
@@ -565,7 +570,7 @@
                     if (!mSessionClosed) {
                         mSessionProcessor.onCaptureSessionEnd();
                     }
-                    mSessionProcessor.deInitSession();
+                    mSessionProcessor.deInitSession(mToken);
                 } catch (RemoteException e) {
                     Log.e(TAG, "Failed to de-initialize session processor, extension service"
                             + " does not respond!") ;
@@ -573,12 +578,10 @@
                 mSessionProcessor = null;
             }
 
-            if (mExtensionClientId >= 0) {
-                CameraExtensionCharacteristics.unregisterClient(mExtensionClientId);
-                if (mInitialized || (mCaptureSession != null)) {
-                    notifyClose = true;
-                    CameraExtensionCharacteristics.releaseSession();
-                }
+            CameraExtensionCharacteristics.unregisterClient(mToken);
+            if (mInitialized || (mCaptureSession != null)) {
+                notifyClose = true;
+                CameraExtensionCharacteristics.releaseSession();
             }
             mInitialized = false;
 
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index 9ebef0b..1db4808 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -56,6 +56,7 @@
 import android.os.Binder;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
 import android.util.LongSparseArray;
@@ -79,7 +80,6 @@
 
     private final Executor mExecutor;
     private final CameraDevice mCameraDevice;
-    private final long mExtensionClientId;
     private final IImageCaptureExtenderImpl mImageExtender;
     private final IPreviewExtenderImpl mPreviewExtender;
     private final Handler mHandler;
@@ -91,6 +91,7 @@
     private final Set<CaptureRequest.Key> mSupportedRequestKeys;
     private final Set<CaptureResult.Key> mSupportedResultKeys;
     private final ExtensionSessionStatsAggregator mStatsAggregator;
+    private final IBinder mToken;
     private boolean mCaptureResultsSupported;
 
     private CameraCaptureSession mCaptureSession = null;
@@ -111,6 +112,7 @@
     private int mPreviewProcessorType = IPreviewExtenderImpl.PROCESSOR_TYPE_NONE;
 
     private boolean mInitialized;
+    private boolean mSessionClosed;
     // Enable/Disable internal preview/(repeating request). Extensions expect
     // that preview/(repeating request) is enabled and active at any point in time.
     // In case the client doesn't explicitly enable repeating requests, the framework
@@ -135,8 +137,9 @@
             @NonNull ExtensionSessionConfiguration config,
             int sessionId)
             throws CameraAccessException, RemoteException {
-        long clientId = CameraExtensionCharacteristics.registerClient(ctx);
-        if (clientId < 0) {
+        final IBinder token = new Binder(TAG + " : " + sessionId);
+        boolean success = CameraExtensionCharacteristics.registerClient(ctx, token);
+        if (!success) {
             throw new UnsupportedOperationException("Unsupported extension!");
         }
 
@@ -224,15 +227,16 @@
         }
 
         extenders.first.init(cameraId, characteristicsMap.get(cameraId).getNativeMetadata());
-        extenders.first.onInit(cameraId, characteristicsMap.get(cameraId).getNativeMetadata());
+        extenders.first.onInit(token, cameraId,
+                characteristicsMap.get(cameraId).getNativeMetadata());
         extenders.second.init(cameraId, characteristicsMap.get(cameraId).getNativeMetadata());
-        extenders.second.onInit(cameraId, characteristicsMap.get(cameraId).getNativeMetadata());
+        extenders.second.onInit(token, cameraId,
+                characteristicsMap.get(cameraId).getNativeMetadata());
 
         CameraExtensionSessionImpl session = new CameraExtensionSessionImpl(
                 extenders.second,
                 extenders.first,
                 supportedPreviewSizes,
-                clientId,
                 cameraDevice,
                 repeatingRequestSurface,
                 burstCaptureSurface,
@@ -240,6 +244,7 @@
                 config.getStateCallback(),
                 config.getExecutor(),
                 sessionId,
+                token,
                 extensionChars.getAvailableCaptureRequestKeys(config.getExtension()),
                 extensionChars.getAvailableCaptureResultKeys(config.getExtension()));
 
@@ -254,7 +259,6 @@
     public CameraExtensionSessionImpl(@NonNull IImageCaptureExtenderImpl imageExtender,
             @NonNull IPreviewExtenderImpl previewExtender,
             @NonNull List<Size> previewSizes,
-            long extensionClientId,
             @NonNull android.hardware.camera2.impl.CameraDeviceImpl cameraDevice,
             @Nullable Surface repeatingRequestSurface,
             @Nullable Surface burstCaptureSurface,
@@ -262,9 +266,9 @@
             @NonNull StateCallback callback,
             @NonNull Executor executor,
             int sessionId,
+            @NonNull IBinder token,
             @NonNull Set<CaptureRequest.Key> requestKeys,
             @Nullable Set<CaptureResult.Key> resultKeys) {
-        mExtensionClientId = extensionClientId;
         mImageExtender = imageExtender;
         mPreviewExtender = previewExtender;
         mCameraDevice = cameraDevice;
@@ -278,8 +282,10 @@
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
         mInitialized = false;
+        mSessionClosed = false;
         mInitializeHandler = new InitializeSessionHandler();
         mSessionId = sessionId;
+        mToken = token;
         mSupportedRequestKeys = requestKeys;
         mSupportedResultKeys = resultKeys;
         mCaptureResultsSupported = !resultKeys.isEmpty();
@@ -775,7 +781,12 @@
         synchronized (mInterfaceLock) {
             if (mInitialized) {
                 mInternalRepeatingRequestEnabled = false;
-                mCaptureSession.stopRepeating();
+                try {
+                    mCaptureSession.stopRepeating();
+                } catch (IllegalStateException e) {
+                    // OK: already be closed, nothing else to do
+                    mSessionClosed = true;
+                }
 
                 ArrayList<CaptureStageImpl> captureStageList = new ArrayList<>();
                 try {
@@ -793,13 +804,14 @@
                     Log.e(TAG, "Failed to disable extension! Extension service does not "
                             + "respond!");
                 }
-                if (!captureStageList.isEmpty()) {
+                if (!captureStageList.isEmpty() && !mSessionClosed) {
                     CaptureRequest disableRequest = createRequest(mCameraDevice, captureStageList,
                             mCameraRepeatingSurface, CameraDevice.TEMPLATE_PREVIEW);
                     mCaptureSession.capture(disableRequest,
                             new CloseRequestHandler(mRepeatingRequestImageCallback), mHandler);
                 }
 
+                mSessionClosed = true;
                 mStatsAggregator.commit(/*isFinal*/true); // Commit stats before closing session
                 mCaptureSession.close();
             }
@@ -854,19 +866,22 @@
             mHandlerThread.quit();
 
             try {
-                mPreviewExtender.onDeInit();
-                mImageExtender.onDeInit();
+                if (!mSessionClosed) {
+                    // return value is omitted. nothing can do after session is closed.
+                    mPreviewExtender.onDisableSession();
+                    mImageExtender.onDisableSession();
+                }
+                mPreviewExtender.onDeInit(mToken);
+                mImageExtender.onDeInit(mToken);
             } catch (RemoteException e) {
                 Log.e(TAG, "Failed to release extensions! Extension service does not"
                         + " respond!");
             }
 
-            if (mExtensionClientId >= 0) {
-                CameraExtensionCharacteristics.unregisterClient(mExtensionClientId);
-                if (mInitialized || (mCaptureSession != null)) {
-                    notifyClose = true;
-                    CameraExtensionCharacteristics.releaseSession();
-                }
+            CameraExtensionCharacteristics.unregisterClient(mToken);
+            if (mInitialized || (mCaptureSession != null)) {
+                notifyClose = true;
+                CameraExtensionCharacteristics.releaseSession();
             }
             mInitialized = false;
 
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 76efce5..022f3c4 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -1762,6 +1762,24 @@
          * 123,1,critical,0.8,default;123,1,moderate,0.6,id_2;456,2,moderate,0.9,critical,0.7
          */
         String KEY_BRIGHTNESS_THROTTLING_DATA = "brightness_throttling_data";
+
+        /**
+         * Key for new power controller feature flag. If enabled new DisplayPowerController will
+         * be used.
+         * Read value via {@link android.provider.DeviceConfig#getBoolean(String, String, boolean)}
+         * with {@link android.provider.DeviceConfig#NAMESPACE_DISPLAY_MANAGER} as the namespace.
+         * @hide
+         */
+        String KEY_NEW_POWER_CONTROLLER = "use_newly_structured_display_power_controller";
+
+        /**
+         * Key for normal brightness mode controller feature flag.
+         * It enables NormalBrightnessModeController.
+         * Read value via {@link android.provider.DeviceConfig#getBoolean(String, String, boolean)}
+         * with {@link android.provider.DeviceConfig#NAMESPACE_DISPLAY_MANAGER} as the namespace.
+         * @hide
+         */
+        String KEY_USE_NORMAL_BRIGHTNESS_MODE_CONTROLLER = "use_normal_brightness_mode_controller";
     }
 
     /**
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index d6df033..94bff89 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -413,6 +413,15 @@
     public abstract HostUsiVersion getHostUsiVersion(int displayId);
 
     /**
+     * Get the ALS data for a particular display.
+     *
+     * @param displayId The id of the display.
+     * @return {@link AmbientLightSensorData}
+     */
+    @Nullable
+    public abstract AmbientLightSensorData getAmbientLightSensorData(int displayId);
+
+    /**
      * Get all available DisplayGroupIds.
      */
     public abstract IntArray getDisplayGroupIds();
@@ -669,4 +678,23 @@
             return "RefreshRateLimitation(" + type + ": " + range + ")";
         }
     }
+
+    /**
+     * Class to provide Ambient sensor data using the API
+     * {@link DisplayManagerInternal#getAmbientLightSensorData(int)}
+     */
+    public static final class AmbientLightSensorData {
+        public String sensorName;
+        public String sensorType;
+
+        public AmbientLightSensorData(String name, String type) {
+            sensorName = name;
+            sensorType = type;
+        }
+
+        @Override
+        public String toString() {
+            return "AmbientLightSensorData(" + sensorName + ", " + sensorType + ")";
+        }
+    }
 }
diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl
index 9d5073e..7080133 100644
--- a/core/java/android/hardware/face/IFaceService.aidl
+++ b/core/java/android/hardware/face/IFaceService.aidl
@@ -177,5 +177,5 @@
     // Internal operation used to clear face biometric scheduler.
     // Ensures that the scheduler is not stuck.
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
-    void scheduleWatchdog();
+    oneway void scheduleWatchdog();
 }
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index 6195443..f594377 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -980,23 +980,6 @@
     }
 
     /**
-     * @hide
-     */
-    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
-    public void setUdfpsOverlay(@NonNull IUdfpsOverlay controller) {
-        if (mService == null) {
-            Slog.w(TAG, "setUdfpsOverlay: no fingerprint service");
-            return;
-        }
-
-        try {
-            mService.setUdfpsOverlay(controller);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
      * Forwards BiometricStateListener to FingerprintService
      * @param listener new BiometricStateListener being added
      * @hide
diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
index ff2f313..e2840ec 100644
--- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl
+++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
@@ -27,7 +27,6 @@
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.hardware.fingerprint.ISidefpsController;
-import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
@@ -203,10 +202,6 @@
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     void setSidefpsController(in ISidefpsController controller);
 
-    // Sets the controller for managing the UDFPS overlay.
-    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
-    void setUdfpsOverlay(in IUdfpsOverlay controller);
-
     // Registers BiometricStateListener.
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     void registerBiometricStateListener(IBiometricStateListener listener);
@@ -218,5 +213,5 @@
     // Internal operation used to clear fingerprint biometric scheduler.
     // Ensures that the scheduler is not stuck.
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
-    void scheduleWatchdog();
+    oneway void scheduleWatchdog();
 }
diff --git a/core/java/android/hardware/fingerprint/IUdfpsOverlay.aidl b/core/java/android/hardware/fingerprint/IUdfpsOverlay.aidl
deleted file mode 100644
index c99fccc..0000000
--- a/core/java/android/hardware/fingerprint/IUdfpsOverlay.aidl
+++ /dev/null
@@ -1,29 +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 android.hardware.fingerprint;
-
-/**
- * Interface for interacting with the under-display fingerprint sensor (UDFPS) overlay.
- * @hide
- */
-oneway interface IUdfpsOverlay {
-    // Shows the overlay.
-    void show(long requestId, int sensorId, int reason);
-
-    // Hides the overlay.
-    void hide(int sensorId);
-}
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 17bbe14..33960c0 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -266,7 +266,7 @@
      */
     public static boolean useTouchpadTapToClick(@NonNull Context context) {
         return Settings.System.getIntForUser(context.getContentResolver(),
-                Settings.System.TOUCHPAD_TAP_TO_CLICK, 0, UserHandle.USER_CURRENT) == 1;
+                Settings.System.TOUCHPAD_TAP_TO_CLICK, 1, UserHandle.USER_CURRENT) == 1;
     }
 
     /**
diff --git a/core/java/android/hardware/input/KeyboardLayout.java b/core/java/android/hardware/input/KeyboardLayout.java
index 0311da4..4403251 100644
--- a/core/java/android/hardware/input/KeyboardLayout.java
+++ b/core/java/android/hardware/input/KeyboardLayout.java
@@ -73,7 +73,7 @@
     private final int mProductId;
 
     /** Currently supported Layout types in the KCM files */
-    private enum LayoutType {
+    public enum LayoutType {
         UNDEFINED(0, LAYOUT_TYPE_UNDEFINED),
         QWERTY(1, LAYOUT_TYPE_QWERTY),
         QWERTZ(2, LAYOUT_TYPE_QWERTZ),
@@ -88,9 +88,11 @@
         private final int mValue;
         private final String mName;
         private static final Map<Integer, LayoutType> VALUE_TO_ENUM_MAP = new HashMap<>();
+        private static final Map<String, LayoutType> NAME_TO_ENUM_MAP = new HashMap<>();
         static {
             for (LayoutType type : LayoutType.values()) {
                 VALUE_TO_ENUM_MAP.put(type.mValue, type);
+                NAME_TO_ENUM_MAP.put(type.mName, type);
             }
         }
 
@@ -110,6 +112,25 @@
         private String getName() {
             return mName;
         }
+
+        /**
+         * Returns enum value for provided layout type
+         * @param layoutName name of the layout type
+         * @return int value corresponding to the LayoutType enum that matches the layout name.
+         * (LayoutType.UNDEFINED if no match found)
+         */
+        public static int getLayoutTypeEnumValue(String layoutName) {
+            return NAME_TO_ENUM_MAP.getOrDefault(layoutName, UNDEFINED).getValue();
+        }
+
+        /**
+         * Returns name for provided layout type enum value
+         * @param enumValue value representation for LayoutType enum
+         * @return Layout name corresponding to the enum value (LAYOUT_TYPE_UNDEFINED if not found)
+         */
+        public static String getLayoutNameFromValue(int enumValue) {
+            return VALUE_TO_ENUM_MAP.getOrDefault(enumValue, UNDEFINED).getName();
+        }
     }
 
     @NonNull
diff --git a/core/java/android/hardware/lights/Light.java b/core/java/android/hardware/lights/Light.java
index 1df9b75..18d0b09 100644
--- a/core/java/android/hardware/lights/Light.java
+++ b/core/java/android/hardware/lights/Light.java
@@ -110,6 +110,8 @@
     private final int mOrdinal;
     private final int mType;
     private final int mCapabilities;
+    @Nullable
+    private final int[] mPreferredBrightnessLevels;
 
     /**
      * Creates a new light with the given data.
@@ -117,7 +119,7 @@
      * @hide
      */
     public Light(int id, int ordinal, int type) {
-        this(id, "Light", ordinal, type, 0);
+        this(id, "Light", ordinal, type, 0, null);
     }
 
     /**
@@ -126,11 +128,22 @@
      * @hide
      */
     public Light(int id, String name, int ordinal, int type, int capabilities) {
+        this(id, name, ordinal, type, capabilities, null);
+    }
+
+    /**
+     * Creates a new light with the given data.
+     *
+     * @hide
+     */
+    public Light(int id, String name, int ordinal, int type, int capabilities,
+            @Nullable int[] preferredBrightnessLevels) {
         mId = id;
         mName = name;
         mOrdinal = ordinal;
         mType = type;
         mCapabilities = capabilities;
+        mPreferredBrightnessLevels = preferredBrightnessLevels;
     }
 
     private Light(@NonNull Parcel in) {
@@ -139,6 +152,7 @@
         mOrdinal = in.readInt();
         mType = in.readInt();
         mCapabilities = in.readInt();
+        mPreferredBrightnessLevels = in.createIntArray();
     }
 
     /** Implement the Parcelable interface */
@@ -149,6 +163,7 @@
         dest.writeInt(mOrdinal);
         dest.writeInt(mType);
         dest.writeInt(mCapabilities);
+        dest.writeIntArray(mPreferredBrightnessLevels);
     }
 
     /** Implement the Parcelable interface */
@@ -252,4 +267,17 @@
         return (mCapabilities & LIGHT_CAPABILITY_COLOR_RGB) == LIGHT_CAPABILITY_COLOR_RGB;
     }
 
+    /**
+     * Returns preferred brightness levels for the light which will be used when user
+     * increase/decrease brightness levels for the light (currently only used for Keyboard
+     * backlight control using backlight up/down keys).
+     *
+     * The values in the preferred brightness level array are in the range [0, 255].
+     *
+     * @hide
+     */
+    @Nullable
+    public int[] getPreferredBrightnessLevels() {
+        return mPreferredBrightnessLevels;
+    }
 }
diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java
index 94971b8..7664bad 100644
--- a/core/java/android/os/GraphicsEnvironment.java
+++ b/core/java/android/os/GraphicsEnvironment.java
@@ -112,9 +112,19 @@
     private static final int ANGLE_GL_DRIVER_ALL_ANGLE_OFF = 0;
 
     // Values for ANGLE_GL_DRIVER_SELECTION_VALUES
-    private static final String ANGLE_GL_DRIVER_CHOICE_DEFAULT = "default";
-    private static final String ANGLE_GL_DRIVER_CHOICE_ANGLE = "angle";
-    private static final String ANGLE_GL_DRIVER_CHOICE_NATIVE = "native";
+    private enum AngleDriverChoice {
+        DEFAULT("default"),
+        ANGLE("angle"),
+        NATIVE("native");
+
+        public final String choice;
+
+        AngleDriverChoice(String choice) {
+            this.choice = choice;
+        }
+    }
+
+    private static final String PROPERTY_RO_ANGLE_SUPPORTED = "ro.gfx.angle.supported";
 
     private ClassLoader mClassLoader;
     private String mLibrarySearchPaths;
@@ -123,6 +133,7 @@
 
     private int mAngleOptInIndex = -1;
     private boolean mEnabledByGameMode = false;
+    private boolean mShouldUseAngle = false;
 
     /**
      * Set up GraphicsEnvironment
@@ -141,19 +152,16 @@
 
         // Setup ANGLE and pass down ANGLE details to the C++ code
         Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "setupAngle");
-        boolean useAngle = false;
         if (setupAngle(context, coreSettings, pm, packageName)) {
-            if (shouldUseAngle(context, coreSettings, packageName)) {
-                useAngle = true;
-                setGpuStats(ANGLE_DRIVER_NAME, ANGLE_DRIVER_VERSION_NAME, ANGLE_DRIVER_VERSION_CODE,
-                        0, packageName, getVulkanVersion(pm));
-            }
+            mShouldUseAngle = true;
+            setGpuStats(ANGLE_DRIVER_NAME, ANGLE_DRIVER_VERSION_NAME, ANGLE_DRIVER_VERSION_CODE,
+                    0, packageName, getVulkanVersion(pm));
         }
         Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
 
         Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "chooseDriver");
         if (!chooseDriver(context, coreSettings, pm, packageName, appInfoWithMetaData)) {
-            if (!useAngle) {
+            if (!mShouldUseAngle) {
                 setGpuStats(SYSTEM_DRIVER_NAME, SYSTEM_DRIVER_VERSION_NAME,
                         SYSTEM_DRIVER_VERSION_CODE,
                         SystemProperties.getLong(PROPERTY_GFX_DRIVER_BUILD_TIME, 0),
@@ -195,15 +203,16 @@
     }
 
     /**
-     * Query to determine if ANGLE should be used
+     * Query to determine the ANGLE driver choice.
      */
-    private boolean shouldUseAngle(Context context, Bundle coreSettings, String packageName) {
+    private AngleDriverChoice queryAngleChoice(Context context, Bundle coreSettings,
+                                               String packageName) {
         if (TextUtils.isEmpty(packageName)) {
             Log.v(TAG, "No package name specified; use the system driver");
-            return false;
+            return AngleDriverChoice.DEFAULT;
         }
 
-        return shouldUseAngleInternal(context, coreSettings, packageName);
+        return queryAngleChoiceInternal(context, coreSettings, packageName);
     }
 
     private int getVulkanVersion(PackageManager pm) {
@@ -424,10 +433,11 @@
      *    forces a choice;
      * 3) Use ANGLE if isAngleEnabledByGameMode() returns true;
      */
-    private boolean shouldUseAngleInternal(Context context, Bundle bundle, String packageName) {
+    private AngleDriverChoice queryAngleChoiceInternal(Context context, Bundle bundle,
+                                                       String packageName) {
         // Make sure we have a good package name
         if (TextUtils.isEmpty(packageName)) {
-            return false;
+            return AngleDriverChoice.DEFAULT;
         }
 
         // Check the semi-global switch (i.e. once system has booted enough) for whether ANGLE
@@ -442,7 +452,7 @@
         }
         if (allUseAngle == ANGLE_GL_DRIVER_ALL_ANGLE_ON) {
             Log.v(TAG, "Turn on ANGLE for all applications.");
-            return true;
+            return AngleDriverChoice.ANGLE;
         }
 
         // Get the per-application settings lists
@@ -465,7 +475,7 @@
                             + optInPackages.size() + ", "
                         + "number of values: "
                             + optInValues.size());
-            return mEnabledByGameMode;
+            return mEnabledByGameMode ? AngleDriverChoice.ANGLE : AngleDriverChoice.DEFAULT;
         }
 
         // See if this application is listed in the per-application settings list
@@ -473,7 +483,7 @@
 
         if (pkgIndex < 0) {
             Log.v(TAG, packageName + " is not listed in per-application setting");
-            return mEnabledByGameMode;
+            return mEnabledByGameMode ? AngleDriverChoice.ANGLE : AngleDriverChoice.DEFAULT;
         }
         mAngleOptInIndex = pkgIndex;
 
@@ -483,14 +493,14 @@
         Log.v(TAG,
                 "ANGLE Developer option for '" + packageName + "' "
                         + "set to: '" + optInValue + "'");
-        if (optInValue.equals(ANGLE_GL_DRIVER_CHOICE_ANGLE)) {
-            return true;
-        } else if (optInValue.equals(ANGLE_GL_DRIVER_CHOICE_NATIVE)) {
-            return false;
+        if (optInValue.equals(AngleDriverChoice.ANGLE.choice)) {
+            return AngleDriverChoice.ANGLE;
+        } else if (optInValue.equals(AngleDriverChoice.NATIVE.choice)) {
+            return AngleDriverChoice.NATIVE;
         } else {
             // The user either chose default or an invalid value; go with the default driver or what
             // the game mode indicates
-            return mEnabledByGameMode;
+            return mEnabledByGameMode ? AngleDriverChoice.ANGLE : AngleDriverChoice.DEFAULT;
         }
     }
 
@@ -503,10 +513,12 @@
         final List<ResolveInfo> resolveInfos =
                 pm.queryIntentActivities(intent, PackageManager.MATCH_SYSTEM_ONLY);
         if (resolveInfos.size() != 1) {
-            Log.e(TAG, "Invalid number of ANGLE packages. Required: 1, Found: "
+            Log.v(TAG, "Invalid number of ANGLE packages. Required: 1, Found: "
                     + resolveInfos.size());
-            for (ResolveInfo resolveInfo : resolveInfos) {
-                Log.e(TAG, "Found ANGLE package: " + resolveInfo.activityInfo.packageName);
+            if (DEBUG) {
+                for (ResolveInfo resolveInfo : resolveInfos) {
+                    Log.d(TAG, "Found ANGLE package: " + resolveInfo.activityInfo.packageName);
+                }
             }
             return "";
         }
@@ -541,26 +553,48 @@
     }
 
     /**
-     * Determine whether ANGLE should be used, set it up if so, and pass ANGLE details down to
-     * the C++ GraphicsEnv class.
+     * Determine whether ANGLE should be used, attempt to set up from apk first, if ANGLE can be
+     * set up from apk, pass ANGLE details down to the C++ GraphicsEnv class via
+     * GraphicsEnv::setAngleInfo(). If apk setup fails, attempt to set up to use system ANGLE.
+     * Return false if both fail.
      *
-     * If ANGLE will be used, GraphicsEnv::setAngleInfo() will be called to enable ANGLE to be
-     * properly used.
-     *
-     * @param context
-     * @param bundle
-     * @param pm
+     * @param context - Context of the application.
+     * @param bundle - Bundle of the application.
+     * @param packageManager - PackageManager of the application process.
      * @param packageName - package name of the application.
-     * @return true: ANGLE setup successfully
-     *         false: ANGLE not setup (not on allowlist, ANGLE not present, etc.)
+     * @return true: can set up to use ANGLE successfully.
+     *         false: can not set up to use ANGLE (not on allowlist, ANGLE not present, etc.)
      */
-    private boolean setupAngle(Context context, Bundle bundle, PackageManager pm,
+    private boolean setupAngle(Context context, Bundle bundle, PackageManager packageManager,
             String packageName) {
 
-        if (!shouldUseAngle(context, bundle, packageName)) {
+        final AngleDriverChoice angleDriverChoice = queryAngleChoice(context, bundle, packageName);
+        if (angleDriverChoice == AngleDriverChoice.DEFAULT) {
             return false;
         }
 
+        if (queryAngleChoice(context, bundle, packageName) == AngleDriverChoice.NATIVE) {
+            nativeSetAngleInfo("", true, packageName, null);
+            return false;
+        }
+
+        return setupAngleFromApk(context, bundle, packageManager, packageName)
+                || setupAngleFromSystem(context, bundle, packageName);
+    }
+
+    /**
+     * Attempt to set up ANGLE from the packaged apk, if the apk can be found, pass ANGLE details to
+     * the C++ GraphicsEnv class.
+     *
+     * @param context - Context of the application.
+     * @param bundle - Bundle of the application.
+     * @param packageManager - PackageManager of the application process.
+     * @param packageName - package name of the application.
+     * @return true: can set up to use ANGLE apk.
+     *         false: can not set up to use ANGLE apk (ANGLE apk not present, etc.)
+     */
+    private boolean setupAngleFromApk(Context context, Bundle bundle, PackageManager packageManager,
+            String packageName) {
         ApplicationInfo angleInfo = null;
 
         // If the developer has specified a debug package over ADB, attempt to find it
@@ -569,7 +603,7 @@
             Log.v(TAG, "ANGLE debug package enabled: " + anglePkgName);
             try {
                 // Note the debug package does not have to be pre-installed
-                angleInfo = pm.getApplicationInfo(anglePkgName, 0);
+                angleInfo = packageManager.getApplicationInfo(anglePkgName, 0);
             } catch (PackageManager.NameNotFoundException e) {
                 // If the debug package is specified but not found, abort.
                 Log.v(TAG, "ANGLE debug package '" + anglePkgName + "' not installed");
@@ -579,7 +613,7 @@
 
         // Otherwise, check to see if ANGLE is properly installed
         if (angleInfo == null) {
-            anglePkgName = getAnglePackageName(pm);
+            anglePkgName = getAnglePackageName(packageManager);
             if (TextUtils.isEmpty(anglePkgName)) {
                 Log.v(TAG, "Failed to find ANGLE package.");
                 return false;
@@ -588,7 +622,7 @@
             Log.v(TAG, "ANGLE package enabled: " + anglePkgName);
             try {
                 // Production ANGLE libraries must be pre-installed as a system app
-                angleInfo = pm.getApplicationInfo(anglePkgName,
+                angleInfo = packageManager.getApplicationInfo(anglePkgName,
                         PackageManager.MATCH_SYSTEM_ONLY);
             } catch (PackageManager.NameNotFoundException e) {
                 Log.v(TAG, "ANGLE package '" + anglePkgName + "' not installed");
@@ -609,15 +643,39 @@
             Log.d(TAG, "ANGLE package libs: " + paths);
         }
 
-        // If we make it to here, ANGLE will be used.  Call setAngleInfo() with the package name,
-        // and features to use.
+        // If we make it to here, ANGLE apk will be used.  Call nativeSetAngleInfo() with the
+        // application package name and ANGLE features to use.
         final String[] features = getAngleEglFeatures(context, bundle);
-        setAngleInfo(paths, packageName, ANGLE_GL_DRIVER_CHOICE_ANGLE, features);
+        nativeSetAngleInfo(paths, false, packageName, features);
 
         return true;
     }
 
     /**
+     * Attempt to set up ANGLE from system, if the apk can be found, pass ANGLE details to
+     * the C++ GraphicsEnv class.
+     *
+     * @param context - Context of the application.
+     * @param bundle - Bundle of the application.
+     * @param packageName - package name of the application.
+     * @return true: can set up to use system ANGLE.
+     *         false: can not set up to use system ANGLE because it doesn't exist.
+     */
+    private boolean setupAngleFromSystem(Context context, Bundle bundle, String packageName) {
+        final boolean systemAngleSupported = SystemProperties
+                                             .getBoolean(PROPERTY_RO_ANGLE_SUPPORTED, false);
+        if (!systemAngleSupported) {
+            return false;
+        }
+
+        // If we make it to here, system ANGLE will be used.  Call nativeSetAngleInfo() with
+        // the application package name and ANGLE features to use.
+        final String[] features = getAngleEglFeatures(context, bundle);
+        nativeSetAngleInfo("system", false, packageName, features);
+        return true;
+    }
+
+    /**
      * Determine if the "ANGLE In Use" dialog box should be shown.
      */
     private boolean shouldShowAngleInUseDialogBox(Context context) {
@@ -636,44 +694,37 @@
     }
 
     /**
-     * Determine if ANGLE will be used and setup the environment
-     */
-    private boolean setupAndUseAngle(Context context, String packageName) {
-        // Need to make sure we are evaluating ANGLE usage for the correct circumstances
-        if (!setupAngle(context, null, context.getPackageManager(), packageName)) {
-            Log.v(TAG, "Package '" + packageName + "' should not use ANGLE");
-            return false;
-        }
-
-        final boolean useAngle = getShouldUseAngle(packageName);
-        Log.v(TAG, "Package '" + packageName + "' should use ANGLE = '" + useAngle + "'");
-
-        return useAngle;
-    }
-
-    /**
-     * Show the ANGLE in Use Dialog Box
+     * Show the ANGLE in use dialog box.
+     * The ANGLE in use dialog box will show up as long as the application
+     * should use ANGLE. It does not mean the application has successfully
+     * loaded ANGLE because this check happens before the loading completes.
      * @param context
      */
     public void showAngleInUseDialogBox(Context context) {
-        final String packageName = context.getPackageName();
-
-        if (shouldShowAngleInUseDialogBox(context) && setupAndUseAngle(context, packageName)) {
-            final Intent intent = new Intent(ACTION_ANGLE_FOR_ANDROID_TOAST_MESSAGE);
-            String anglePkg = getAnglePackageName(context.getPackageManager());
-            intent.setPackage(anglePkg);
-
-            context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    Bundle results = getResultExtras(true);
-
-                    String toastMsg = results.getString(INTENT_KEY_A4A_TOAST_MESSAGE);
-                    final Toast toast = Toast.makeText(context, toastMsg, Toast.LENGTH_LONG);
-                    toast.show();
-                }
-            }, null, Activity.RESULT_OK, null, null);
+        if (!shouldShowAngleInUseDialogBox(context)) {
+            return;
         }
+
+        if (!mShouldUseAngle) {
+            return;
+        }
+
+        final Intent intent = new Intent(ACTION_ANGLE_FOR_ANDROID_TOAST_MESSAGE);
+        final String anglePkg = getAnglePackageName(context.getPackageManager());
+        if (anglePkg.isEmpty()) {
+            return;
+        }
+
+        context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                final Bundle results = getResultExtras(true);
+
+                final String toastMsg = results.getString(INTENT_KEY_A4A_TOAST_MESSAGE);
+                final Toast toast = Toast.makeText(context, toastMsg, Toast.LENGTH_LONG);
+                toast.show();
+            }
+        }, null, Activity.RESULT_OK, null, null);
     }
 
     private String[] getAngleEglFeatures(Context context, Bundle coreSettings) {
@@ -901,9 +952,8 @@
     private static native void setDriverPathAndSphalLibraries(String path, String sphalLibraries);
     private static native void setGpuStats(String driverPackageName, String driverVersionName,
             long driverVersionCode, long driverBuildTime, String appPackageName, int vulkanVersion);
-    private static native void setAngleInfo(String path, String appPackage,
-            String devOptIn, String[] features);
-    private static native boolean getShouldUseAngle(String packageName);
+    private static native void nativeSetAngleInfo(String path, boolean useNativeDriver,
+            String packageName, String[] features);
     private static native boolean setInjectLayersPrSetDumpable();
     private static native void nativeToggleAngleAsSystemDriver(boolean enabled);
 
diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING
index cc54266..5c4aa4a 100644
--- a/core/java/android/os/TEST_MAPPING
+++ b/core/java/android/os/TEST_MAPPING
@@ -52,8 +52,20 @@
       ],
       "name": "FrameworksServicesTests",
       "options": [
-        { "include-filter": "com.android.server.am.BatteryStatsServiceTest" },
-        { "include-filter": "com.android.server.power.stats.BatteryStatsTests" }
+        { "include-filter": "com.android.server.am.BatteryStatsServiceTest" }
+      ]
+    },
+    {
+      "file_patterns": [
+        "BatteryStats[^/]*\\.java",
+        "BatteryUsageStats[^/]*\\.java",
+        "PowerComponents\\.java",
+        "[^/]*BatteryConsumer[^/]*\\.java"
+      ],
+      "name": "FrameworksServicesTests",
+      "options": [
+        { "include-filter": "com.android.server.power.stats" },
+        { "exclude-filter": "com.android.server.power.stats.BatteryStatsTests" }
       ]
     },
     {
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 84f1880..2d21718 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -1764,10 +1764,19 @@
     /**
      * Specifies if a user is not allowed to use 2g networks.
      *
+     * <p> This is a security feature. 2g has no mutual authentication between a device and
+     * cellular base station and downgrading a device's connection to 2g is a common tactic for
+     * several types of privacy and security compromising attacks that could allow an adversary
+     * to intercept, inject, or modify cellular communications.
+     *
      * <p>This restriction can only be set by a device owner or a profile owner of an
      * organization-owned managed profile on the parent profile.
-     * In all cases, the setting applies globally on the device and will prevent the device from
-     * scanning for or connecting to 2g networks, except in the case of an emergency.
+     * In all cases, the setting applies globally on the device.
+     *
+     * <p> Cellular connectivity loss (where a device would have otherwise successfully
+     * connected to a 2g network) occurs if the device is in an area where only 2g networks are
+     * available. Emergency calls are an exception and are never impacted. The device will still
+     * scan for and connect to a 2g network for emergency calls.
      *
      * <p>Holders of the permission
      * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MOBILE_NETWORK}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index d695c0c..820f454 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7486,6 +7486,14 @@
         public static final String STYLUS_BUTTONS_ENABLED = "stylus_buttons_enabled";
 
         /**
+         * Preferred default user profile to use with the notes task button shortcut.
+         *
+         * @hide
+         */
+        @SuppressLint("NoSettingsProvider")
+        public static final String DEFAULT_NOTE_TASK_PROFILE = "default_note_task_profile";
+
+        /**
          * Host name and port for global http proxy. Uses ':' seperator for
          * between host and port.
          *
@@ -12605,6 +12613,26 @@
         public static final String MOBILE_DATA_ALWAYS_ON = "mobile_data_always_on";
 
         /**
+         * The duration in milliseconds of each action, separated by commas. Ex:
+         *
+         * "18000,18000,18000,18000,0"
+         *
+         * See com.android.internal.telephony.data.DataStallRecoveryManager for more info
+         * @hide
+         */
+        public static final String DSRM_DURATION_MILLIS = "dsrm_duration_millis";
+
+        /**
+         * The list of DSRM enabled actions, separated by commas. Ex:
+         *
+         * "true,true,false,true,true"
+         *
+         * See com.android.internal.telephony.data.DataStallRecoveryManager for more info
+         * @hide
+         */
+        public static final String DSRM_ENABLED_ACTIONS = "dsrm_enabled_actions";
+
+        /**
          * Whether the wifi data connection should remain active even when higher
          * priority networks like Ethernet are active, to keep both networks.
          * In the case where higher priority networks are connected, wifi will be
@@ -17979,6 +18007,15 @@
                 "review_permissions_notification_state";
 
         /**
+         * Whether repair mode is active on the device.
+         * <p>
+         * Set to 1 for true and 0 for false.
+         *
+         * @hide
+         */
+        public static final String REPAIR_MODE_ACTIVE = "repair_mode_active";
+
+        /**
          * Settings migrated from Wear OS settings provider.
          * @hide
          */
diff --git a/core/java/android/service/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java
index 737c95f..7fa0ac8 100644
--- a/core/java/android/service/contentcapture/ContentCaptureService.java
+++ b/core/java/android/service/contentcapture/ContentCaptureService.java
@@ -37,6 +37,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.ParcelFileDescriptor;
+import android.os.Process;
 import android.os.RemoteException;
 import android.util.Log;
 import android.util.Slog;
@@ -88,6 +89,18 @@
             "android.service.contentcapture.ContentCaptureService";
 
     /**
+     * The {@link Intent} that must be declared as handled by the protection service.
+     *
+     * <p>To be supported, the service must also require the {@link
+     * android.Manifest.permission#BIND_CONTENT_CAPTURE_SERVICE} permission so that other
+     * applications can not abuse it.
+     *
+     * @hide
+     */
+    public static final String PROTECTION_SERVICE_INTERFACE =
+            "android.service.contentcapture.ContentProtectionService";
+
+    /**
      * Name under which a ContentCaptureService component publishes information about itself.
      *
      * <p>This meta-data should reference an XML resource containing a
@@ -128,10 +141,9 @@
     private long mCallerMismatchTimeout = 1000;
     private long mLastCallerMismatchLog;
 
-    /**
-     * Binder that receives calls from the system server.
-     */
-    private final IContentCaptureService mServerInterface = new IContentCaptureService.Stub() {
+    /** Binder that receives calls from the system server in the content capture flow. */
+    private final IContentCaptureService mContentCaptureServerInterface =
+            new IContentCaptureService.Stub() {
 
         @Override
         public void onConnected(IBinder callback, boolean verbose, boolean debug) {
@@ -187,10 +199,24 @@
         }
     };
 
-    /**
-     * Binder that receives calls from the app.
-     */
-    private final IContentCaptureDirectManager mClientInterface =
+    /** Binder that receives calls from the system server in the content protection flow. */
+    private final IContentProtectionService mContentProtectionServerInterface =
+            new IContentProtectionService.Stub() {
+
+                @Override
+                public void onLoginDetected(
+                        @SuppressWarnings("rawtypes") ParceledListSlice events) {
+                    mHandler.sendMessage(
+                            obtainMessage(
+                                    ContentCaptureService::handleOnLoginDetected,
+                                    ContentCaptureService.this,
+                                    Binder.getCallingUid(),
+                                    events));
+                }
+            };
+
+    /** Binder that receives calls from the app in the content capture flow. */
+    private final IContentCaptureDirectManager mContentCaptureClientInterface =
             new IContentCaptureDirectManager.Stub() {
 
         @Override
@@ -220,9 +246,19 @@
     @Override
     public final IBinder onBind(Intent intent) {
         if (SERVICE_INTERFACE.equals(intent.getAction())) {
-            return mServerInterface.asBinder();
+            return mContentCaptureServerInterface.asBinder();
         }
-        Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
+        if (PROTECTION_SERVICE_INTERFACE.equals(intent.getAction())) {
+            return mContentProtectionServerInterface.asBinder();
+        }
+        Log.w(
+                TAG,
+                "Tried to bind to wrong intent (should be "
+                        + SERVICE_INTERFACE
+                        + " or "
+                        + PROTECTION_SERVICE_INTERFACE
+                        + "): "
+                        + intent);
         return null;
     }
 
@@ -456,7 +492,7 @@
         } else {
             stateFlags |= ContentCaptureSession.STATE_DISABLED;
         }
-        setClientState(clientReceiver, stateFlags, mClientInterface.asBinder());
+        setClientState(clientReceiver, stateFlags, mContentCaptureClientInterface.asBinder());
     }
 
     private void handleSendEvents(int uid,
@@ -524,6 +560,18 @@
         writeFlushMetrics(lastSessionId, activityComponent, metrics, options, reason);
     }
 
+    private void handleOnLoginDetected(
+            int uid, @NonNull ParceledListSlice<ContentCaptureEvent> parceledEvents) {
+        if (uid != Process.SYSTEM_UID) {
+            Log.e(TAG, "handleOnLoginDetected() not allowed for uid: " + uid);
+            return;
+        }
+        List<ContentCaptureEvent> events = parceledEvents.getList();
+        int sessionIdInt = events.isEmpty() ? NO_SESSION_ID : events.get(0).getSessionId();
+        ContentCaptureSessionId sessionId = new ContentCaptureSessionId(sessionIdInt);
+        events.forEach(event -> onContentCaptureEvent(sessionId, event));
+    }
+
     private void handleOnActivitySnapshot(int sessionId, @NonNull SnapshotData snapshotData) {
         onActivitySnapshot(new ContentCaptureSessionId(sessionId), snapshotData);
     }
diff --git a/core/java/android/service/contentcapture/ContentCaptureServiceInfo.java b/core/java/android/service/contentcapture/ContentCaptureServiceInfo.java
index fb60619..4aa5c00 100644
--- a/core/java/android/service/contentcapture/ContentCaptureServiceInfo.java
+++ b/core/java/android/service/contentcapture/ContentCaptureServiceInfo.java
@@ -48,7 +48,7 @@
  *
  * @hide
  */
-public final class ContentCaptureServiceInfo {
+public class ContentCaptureServiceInfo {
 
     private static final String TAG = ContentCaptureServiceInfo.class.getSimpleName();
     private static final String XML_TAG_SERVICE = "content-capture-service";
diff --git a/core/java/android/service/contentcapture/IContentProtectionService.aidl b/core/java/android/service/contentcapture/IContentProtectionService.aidl
new file mode 100644
index 0000000..4a13c3f
--- /dev/null
+++ b/core/java/android/service/contentcapture/IContentProtectionService.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.contentcapture;
+
+import android.content.pm.ParceledListSlice;
+import android.view.contentcapture.ContentCaptureEvent;
+
+/**
+ * Interface from the system server to the content protection service.
+ *
+ * @hide
+ */
+oneway interface IContentProtectionService {
+
+    void onLoginDetected(in ParceledListSlice events);
+}
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index cd57de5..9b19937 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -16,6 +16,8 @@
 
 package android.service.dreams;
 
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
 import android.annotation.IdRes;
 import android.annotation.LayoutRes;
 import android.annotation.NonNull;
@@ -24,7 +26,6 @@
 import android.annotation.SdkConstant.SdkConstantType;
 import android.annotation.TestApi;
 import android.app.Activity;
-import android.app.ActivityTaskManager;
 import android.app.AlarmManager;
 import android.app.Service;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -630,7 +631,7 @@
     }
 
     /**
-     * Marks this dream as windowless. Only available to doze dreams.
+     * Marks this dream as windowless. It should be called in {@link #onCreate} method.
      *
      * @hide
      *
@@ -640,7 +641,7 @@
     }
 
     /**
-     * Returns whether this dream is windowless. Only available to doze dreams.
+     * Returns whether this dream is windowless.
      *
      * @hide
      */
@@ -1230,8 +1231,10 @@
 
         mDreamToken = dreamToken;
         mCanDoze = canDoze;
-        if (mWindowless && !mCanDoze) {
-            throw new IllegalStateException("Only doze dreams can be windowless");
+        // This is not a security check to prevent malicious dreams but a guard rail to stop
+        // third-party dreams from being windowless and not working well as a result.
+        if (mWindowless && !mCanDoze && !isCallerSystemUi()) {
+            throw new IllegalStateException("Only doze or SystemUI dreams can be windowless.");
         }
 
         mDispatchAfterOnAttachedToWindow = () -> {
@@ -1264,9 +1267,7 @@
                     fetchDreamLabel(this, serviceInfo, isPreviewMode));
 
             try {
-                if (!ActivityTaskManager.getService().startDreamActivity(i)) {
-                    detach();
-                }
+                mDreamManager.startDreamActivity(i);
             } catch (SecurityException e) {
                 Log.w(mTag,
                         "Received SecurityException trying to start DreamActivity. "
@@ -1366,6 +1367,11 @@
         }
     }
 
+    private boolean isCallerSystemUi() {
+        return checkCallingOrSelfPermission(android.Manifest.permission.STATUS_BAR_SERVICE)
+                == PERMISSION_GRANTED;
+    }
+
     private int applyFlags(int oldFlags, int flags, int mask) {
         return (oldFlags&~mask) | (flags&mask);
     }
diff --git a/core/java/android/service/dreams/IDreamManager.aidl b/core/java/android/service/dreams/IDreamManager.aidl
index 609425c..dd8b3de 100644
--- a/core/java/android/service/dreams/IDreamManager.aidl
+++ b/core/java/android/service/dreams/IDreamManager.aidl
@@ -17,6 +17,7 @@
 package android.service.dreams;
 
 import android.content.ComponentName;
+import android.content.Intent;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.os.IBinder;
@@ -45,4 +46,5 @@
     void setDreamComponentsForUser(int userId, in ComponentName[] componentNames);
     void setSystemDreamComponent(in ComponentName componentName);
     void registerDreamOverlayService(in ComponentName componentName);
+    void startDreamActivity(in Intent intent);
 }
diff --git a/core/java/android/service/notification/NotificationRankingUpdate.java b/core/java/android/service/notification/NotificationRankingUpdate.java
index a853714..75640bd 100644
--- a/core/java/android/service/notification/NotificationRankingUpdate.java
+++ b/core/java/android/service/notification/NotificationRankingUpdate.java
@@ -16,32 +16,117 @@
 package android.service.notification;
 
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.SharedMemory;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags;
+
+import java.nio.ByteBuffer;
 
 /**
+ * Represents an update to notification rankings.
  * @hide
  */
+@SuppressLint({"ParcelNotFinal", "ParcelCreator"})
+@TestApi
 public class NotificationRankingUpdate implements Parcelable {
     private final NotificationListenerService.RankingMap mRankingMap;
 
+    // The ranking map is stored in shared memory when parceled, for sending across the binder.
+    // This is done because the ranking map can grow large if there are many notifications.
+    private SharedMemory mRankingMapFd = null;
+    private final String mSharedMemoryName = "NotificationRankingUpdatedSharedMemory";
+
+    /**
+     * @hide
+     */
     public NotificationRankingUpdate(NotificationListenerService.Ranking[] rankings) {
         mRankingMap = new NotificationListenerService.RankingMap(rankings);
     }
 
+    /**
+     * @hide
+     */
     public NotificationRankingUpdate(Parcel in) {
-        mRankingMap = in.readParcelable(getClass().getClassLoader(), android.service.notification.NotificationListenerService.RankingMap.class);
+        if (SystemUiSystemPropertiesFlags.getResolver().isEnabled(
+                SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM)) {
+            // Recover the ranking map from the SharedMemory and store it in mapParcel.
+            final Parcel mapParcel = Parcel.obtain();
+            ByteBuffer buffer = null;
+            try {
+                // The ranking map should be stored in shared memory when it is parceled, so we
+                // unwrap the SharedMemory object.
+                mRankingMapFd = in.readParcelable(getClass().getClassLoader(), SharedMemory.class);
+
+                // In the case that the ranking map can't be read, readParcelable may return null.
+                // In this case, we set mRankingMap to null;
+                if (mRankingMapFd == null) {
+                    mRankingMap = null;
+                    return;
+                }
+                // We only need read-only access to the shared memory region.
+                buffer = mRankingMapFd.mapReadOnly();
+                if (buffer == null) {
+                    mRankingMap = null;
+                    return;
+                }
+                byte[] payload = new byte[buffer.remaining()];
+                buffer.get(payload);
+                mapParcel.unmarshall(payload, 0, payload.length);
+                mapParcel.setDataPosition(0);
+
+                mRankingMap = mapParcel.readParcelable(getClass().getClassLoader(),
+                        android.service.notification.NotificationListenerService.RankingMap.class);
+            } catch (ErrnoException e) {
+                // TODO(b/284297289): remove throw when associated flag is moved to droidfood, to
+                // avoid crashes; change to Log.wtf.
+                throw new RuntimeException(e);
+            } finally {
+                mapParcel.recycle();
+                if (buffer != null) {
+                    mRankingMapFd.unmap(buffer);
+                }
+            }
+        } else {
+            mRankingMap = in.readParcelable(getClass().getClassLoader(),
+                    android.service.notification.NotificationListenerService.RankingMap.class);
+        }
     }
 
+    /**
+     * Confirms that the SharedMemory file descriptor is closed. Should only be used for testing.
+     * @hide
+     */
+    @TestApi
+    public final boolean isFdNotNullAndClosed() {
+        return mRankingMapFd != null && mRankingMapFd.getFd() == -1;
+    }
+
+    /**
+     * @hide
+     */
     public NotificationListenerService.RankingMap getRankingMap() {
         return mRankingMap;
     }
 
+    /**
+     * @hide
+     */
     @Override
     public int describeContents() {
         return 0;
     }
 
+    /**
+     * @hide
+     */
     @Override
     public boolean equals(@Nullable Object o) {
         if (this == o) return true;
@@ -51,11 +136,51 @@
         return mRankingMap.equals(other.mRankingMap);
     }
 
+    /**
+     * @hide
+     */
     @Override
-    public void writeToParcel(Parcel out, int flags) {
-        out.writeParcelable(mRankingMap, flags);
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        if (SystemUiSystemPropertiesFlags.getResolver().isEnabled(
+                SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM)) {
+            final Parcel mapParcel = Parcel.obtain();
+            try {
+                // Parcels the ranking map and measures its size.
+                mapParcel.writeParcelable(mRankingMap, flags);
+                int mapSize = mapParcel.dataSize();
+
+                // Creates a new SharedMemory object with enough space to hold the ranking map.
+                SharedMemory mRankingMapFd = SharedMemory.create(mSharedMemoryName, mapSize);
+                if (mRankingMapFd == null) {
+                    return;
+                }
+
+                // Gets a read/write buffer mapping the entire shared memory region.
+                final ByteBuffer buffer = mRankingMapFd.mapReadWrite();
+
+                // Puts the ranking map into the shared memory region buffer.
+                buffer.put(mapParcel.marshall(), 0, mapSize);
+
+                // Protects the region from being written to, by setting it to be read-only.
+                mRankingMapFd.setProtect(OsConstants.PROT_READ);
+
+                // Puts the SharedMemory object in the parcel.
+                out.writeParcelable(mRankingMapFd, flags);
+            } catch (ErrnoException e) {
+                // TODO(b/284297289): remove throw when associated flag is moved to droidfood, to
+                // avoid crashes; change to Log.wtf.
+                throw new RuntimeException(e);
+            } finally {
+                mapParcel.recycle();
+            }
+        } else {
+            out.writeParcelable(mRankingMap, flags);
+        }
     }
 
+    /**
+    * @hide
+    */
     public static final @android.annotation.NonNull Parcelable.Creator<NotificationRankingUpdate> CREATOR
             = new Parcelable.Creator<NotificationRankingUpdate>() {
         public NotificationRankingUpdate createFromParcel(Parcel parcel) {
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 6371da4..ab9cff0 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -25,7 +25,6 @@
 import android.graphics.text.LineBreakConfig;
 import android.graphics.text.LineBreaker;
 import android.os.Build;
-import android.os.SystemProperties;
 import android.text.style.LeadingMarginSpan;
 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
 import android.text.style.LineHeightSpan;
@@ -33,7 +32,6 @@
 import android.util.Log;
 import android.util.Pools.SynchronizedPool;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.GrowingArrayUtils;
 
@@ -75,13 +73,6 @@
      * default values.
      */
     public final static class Builder {
-        // The content length threshold to enable LINE_BREAK_WORD_STYLE_PHRASE.
-        private static final int DEFAULT_LINECOUNT_THRESHOLD_FOR_PHRASE = 3;
-
-        // The property of content length threshold to enable LINE_BREAK_WORD_STYLE_PHRASE.
-        private static final String PROPERTY_LINECOUNT_THRESHOLD_FOR_PHRASE =
-                "android.phrase.linecount.threshold";
-
         private Builder() {}
 
         /**
@@ -440,55 +431,11 @@
          */
         @NonNull
         public StaticLayout build() {
-            reviseLineBreakConfig();
             StaticLayout result = new StaticLayout(this);
             Builder.recycle(this);
             return result;
         }
 
-        private void reviseLineBreakConfig() {
-            boolean autoPhraseBreaking = mLineBreakConfig.getAutoPhraseBreaking();
-            int wordStyle = mLineBreakConfig.getLineBreakWordStyle();
-            if (autoPhraseBreaking) {
-                if (wordStyle != LineBreakConfig.LINE_BREAK_WORD_STYLE_PHRASE) {
-                    if (shouldEnablePhraseBreaking()) {
-                        mLineBreakConfig = LineBreakConfig.getLineBreakConfig(
-                                mLineBreakConfig.getLineBreakStyle(),
-                                LineBreakConfig.LINE_BREAK_WORD_STYLE_PHRASE,
-                                mLineBreakConfig.getAutoPhraseBreaking());
-                    }
-                }
-            }
-        }
-
-        private boolean shouldEnablePhraseBreaking() {
-            if (TextUtils.isEmpty(mText) || mWidth <= 0) {
-                return false;
-            }
-            int lineLimit = SystemProperties.getInt(
-                    PROPERTY_LINECOUNT_THRESHOLD_FOR_PHRASE,
-                    DEFAULT_LINECOUNT_THRESHOLD_FOR_PHRASE);
-            double desiredWidth = (double) Layout.getDesiredWidth(mText, mStart,
-                    mEnd, mPaint, mTextDir);
-            int lineCount = (int) Math.ceil(desiredWidth / mWidth);
-            if (lineCount > 0 && lineCount <= lineLimit) {
-                return true;
-            }
-            return false;
-        }
-
-        /**
-         * Get the line break word style.
-         *
-         * @return The current line break word style.
-         *
-         * @hide
-         */
-        @VisibleForTesting
-        public int getLineBreakWordStyle() {
-            return mLineBreakConfig.getLineBreakWordStyle();
-        }
-
         private CharSequence mText;
         private int mStart;
         private int mEnd;
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index ff7d8bb..0071a0d 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -86,9 +86,6 @@
     public static final String SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST =
             "settings_need_connected_ble_device_for_broadcast";
 
-    /** @hide */
-    public static final String SETTINGS_AUTO_TEXT_WRAPPING = "settings_auto_text_wrapping";
-
     /**
      * Enable new language and keyboard settings UI
      * @hide
@@ -225,7 +222,6 @@
         DEFAULT_FLAGS.put(SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS, "true");
         DEFAULT_FLAGS.put(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME, "true");
         DEFAULT_FLAGS.put(SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, "true");
-        DEFAULT_FLAGS.put(SETTINGS_AUTO_TEXT_WRAPPING, "false");
         DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_UI, "true");
         DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY, "true");
         DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD, "true");
@@ -253,7 +249,6 @@
         PERSISTENT_FLAGS.add(SETTINGS_SUPPORT_LARGE_SCREEN);
         PERSISTENT_FLAGS.add(SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS);
         PERSISTENT_FLAGS.add(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME);
-        PERSISTENT_FLAGS.add(SETTINGS_AUTO_TEXT_WRAPPING);
         PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_UI);
         PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY);
         PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_TRACKPAD);
diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java
index dc06671..1ed5d3f 100644
--- a/core/java/android/view/AttachedSurfaceControl.java
+++ b/core/java/android/view/AttachedSurfaceControl.java
@@ -23,6 +23,9 @@
 import android.hardware.HardwareBuffer;
 import android.window.SurfaceSyncGroup;
 
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
 /**
  * Provides an interface to the root-Surface of a View Hierarchy or Window. This
  * is used in combination with the {@link android.view.SurfaceControl} API to enable
@@ -167,4 +170,40 @@
      */
     default void setChildBoundingInsets(@NonNull Rect insets) {
     }
+
+    /**
+     * Add a trusted presentation listener on the SurfaceControl associated with this window.
+     *
+     * @param t          Transaction that the trusted presentation listener is added on. This should
+     *                   be applied by the caller.
+     * @param thresholds The {@link SurfaceControl.TrustedPresentationThresholds} that will specify
+     *                   when the to invoke the callback.
+     * @param executor   The {@link Executor} where the callback will be invoked on.
+     * @param listener   The {@link Consumer} that will receive the callbacks when entered or
+     *                   exited the threshold.
+     *
+     * @see SurfaceControl.Transaction#setTrustedPresentationCallback(SurfaceControl,
+     * SurfaceControl.TrustedPresentationThresholds, Executor, Consumer)
+     *
+     * @hide b/287076178 un-hide with API bump
+     */
+    default void addTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t,
+            @NonNull SurfaceControl.TrustedPresentationThresholds thresholds,
+            @NonNull Executor executor, @NonNull Consumer<Boolean> listener) {
+    }
+
+    /**
+     * Remove a trusted presentation listener on the SurfaceControl associated with this window.
+     *
+     * @param t          Transaction that the trusted presentation listener removed on. This should
+     *                   be applied by the caller.
+     * @param listener   The {@link Consumer} that was previously registered with
+     *                   addTrustedPresentationCallback that should be removed.
+     *
+     * @see SurfaceControl.Transaction#clearTrustedPresentationCallback(SurfaceControl)
+     * @hide b/287076178 un-hide with API bump
+     */
+    default void removeTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t,
+            @NonNull Consumer<Boolean> listener) {
+    }
 }
diff --git a/core/java/android/view/ISurfaceControlViewHost.aidl b/core/java/android/view/ISurfaceControlViewHost.aidl
index 15008ae..fd4b329 100644
--- a/core/java/android/view/ISurfaceControlViewHost.aidl
+++ b/core/java/android/view/ISurfaceControlViewHost.aidl
@@ -19,6 +19,7 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.view.InsetsState;
+import android.view.ISurfaceControlViewHostParent;
 import android.window.ISurfaceSyncGroup;
 
 /**
@@ -34,4 +35,9 @@
     oneway void onDispatchDetachedFromWindow();
     oneway void onInsetsChanged(in InsetsState state, in Rect insetFrame);
     ISurfaceSyncGroup getSurfaceSyncGroup();
+    /**
+     * Attaches the parent interface so the embedded content can communicate back to the parent.
+     * If null is passed in, it will remove the parent interface and no more updates will be sent.
+     */
+    oneway void attachParentInterface(in @nullable ISurfaceControlViewHostParent parentInterface);
 }
diff --git a/core/java/android/view/ISurfaceControlViewHostParent.aidl b/core/java/android/view/ISurfaceControlViewHostParent.aidl
new file mode 100644
index 0000000..f42e001
--- /dev/null
+++ b/core/java/android/view/ISurfaceControlViewHostParent.aidl
@@ -0,0 +1,27 @@
+/*
+** 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.
+*/
+
+package android.view;
+
+import android.view.WindowManager;
+
+/**
+ * API from embedded content in SurfaceControlViewHost to parent containing the embedded.
+ * {@hide}
+ */
+oneway interface ISurfaceControlViewHostParent {
+    void updateParams(in WindowManager.LayoutParams[] childAttrs);
+}
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index 209729b..6d96bb94 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -471,10 +471,23 @@
      * Requests Keyboard Shortcuts from the displayed window.
      *
      * @param receiver The receiver to deliver the results to.
+     * @param deviceId The deviceId of KeyEvent by which this request is triggered, or -1 if it's
+     *                 not triggered by a KeyEvent.
+     * @see #requestImeKeyboardShortcuts(IResultReceiver, int)
      */
     void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId);
 
     /**
+     * Requests Keyboard Shortcuts from currently selected IME.
+     *
+     * @param receiver The receiver to deliver the results to.
+     * @param deviceId The deviceId of KeyEvent by which this request is triggered, or -1 if it's
+     *                 not triggered by a KeyEvent.
+     * @see #requestAppKeyboardShortcuts(IResultReceiver, int)
+     */
+    void requestImeKeyboardShortcuts(IResultReceiver receiver, int deviceId);
+
+    /**
      * Retrieves the current stable insets from the primary display.
      */
     @UnsupportedAppUsage
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 5019b85..c1eacb5 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -797,7 +797,7 @@
             }
 
             WindowInsets insets = state.calculateInsets(mFrame, mState /* ignoringVisibilityState*/,
-                    mLastInsets.isRound(), mLastInsets.shouldAlwaysConsumeSystemBars(),
+                    mLastInsets.isRound(), false /* alwaysConsumeSystemBars */,
                     mLastLegacySoftInputMode, mLastLegacyWindowFlags, mLastLegacySystemUiFlags,
                     mWindowType, mLastWindowingMode, null /* idSideMap */);
             mHost.dispatchWindowInsetsAnimationProgress(insets,
@@ -841,7 +841,6 @@
         return mLastDispatchedState;
     }
 
-    @VisibleForTesting
     public boolean onStateChanged(InsetsState state) {
         boolean stateChanged = false;
         if (!CAPTION_ON_SHELL) {
diff --git a/core/java/android/view/InsetsFrameProvider.java b/core/java/android/view/InsetsFrameProvider.java
index 37b6c77..83bdb08 100644
--- a/core/java/android/view/InsetsFrameProvider.java
+++ b/core/java/android/view/InsetsFrameProvider.java
@@ -164,6 +164,10 @@
         return mFlags;
     }
 
+    public boolean hasFlags(@Flags int mask) {
+        return (mFlags & mask) == mask;
+    }
+
     public InsetsFrameProvider setInsetsSize(Insets insetsSize) {
         mInsetsSize = insetsSize;
         return this;
diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java
index 9fc42ff..6441186 100644
--- a/core/java/android/view/InsetsSource.java
+++ b/core/java/android/view/InsetsSource.java
@@ -58,9 +58,26 @@
      */
     public static final int FLAG_SUPPRESS_SCRIM = 1;
 
+    /**
+     * Controls whether the insets frame will be used to move {@link RoundedCorner} inward with the
+     * insets frame size when calculating the rounded corner insets to other windows.
+     *
+     * For example, task bar will draw fake rounded corners above itself, so we need to move the
+     * rounded corner up by the task bar insets size to make other windows see a rounded corner
+     * above the task bar.
+     */
+    public static final int FLAG_INSETS_ROUNDED_CORNER = 1 << 1;
+
+    /**
+     * Controls whether the insets provided by this source should be forcibly consumed.
+     */
+    public static final int FLAG_FORCE_CONSUMING = 1 << 2;
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag = true, prefix = "FLAG_", value = {
             FLAG_SUPPRESS_SCRIM,
+            FLAG_INSETS_ROUNDED_CORNER,
+            FLAG_FORCE_CONSUMING,
     })
     public @interface Flags {}
 
@@ -78,7 +95,6 @@
     private @Nullable Rect mVisibleFrame;
 
     private boolean mVisible;
-    private boolean mInsetsRoundedCornerFrame;
 
     private final Rect mTmpFrame = new Rect();
 
@@ -98,7 +114,6 @@
                 ? new Rect(other.mVisibleFrame)
                 : null;
         mFlags = other.mFlags;
-        mInsetsRoundedCornerFrame = other.mInsetsRoundedCornerFrame;
     }
 
     public void set(InsetsSource other) {
@@ -108,7 +123,6 @@
                 ? new Rect(other.mVisibleFrame)
                 : null;
         mFlags = other.mFlags;
-        mInsetsRoundedCornerFrame = other.mInsetsRoundedCornerFrame;
     }
 
     public InsetsSource setFrame(int left, int top, int right, int bottom) {
@@ -136,6 +150,11 @@
         return this;
     }
 
+    public InsetsSource setFlags(@Flags int flags, @Flags int mask) {
+        mFlags = (mFlags & ~mask) | (flags & mask);
+        return this;
+    }
+
     public int getId() {
         return mId;
     }
@@ -160,20 +179,15 @@
         return mFlags;
     }
 
+    public boolean hasFlags(int flags) {
+        return (mFlags & flags) == flags;
+    }
+
     boolean isUserControllable() {
         // If mVisibleFrame is null, it will be the same area as mFrame.
         return mVisibleFrame == null || !mVisibleFrame.isEmpty();
     }
 
-    public boolean insetsRoundedCornerFrame() {
-        return mInsetsRoundedCornerFrame;
-    }
-
-    public InsetsSource setInsetsRoundedCornerFrame(boolean insetsRoundedCornerFrame) {
-        mInsetsRoundedCornerFrame = insetsRoundedCornerFrame;
-        return this;
-    }
-
     /**
      * Calculates the insets this source will cause to a client window.
      *
@@ -317,6 +331,12 @@
         if ((flags & FLAG_SUPPRESS_SCRIM) != 0) {
             joiner.add("SUPPRESS_SCRIM");
         }
+        if ((flags & FLAG_INSETS_ROUNDED_CORNER) != 0) {
+            joiner.add("INSETS_ROUNDED_CORNER");
+        }
+        if ((flags & FLAG_FORCE_CONSUMING) != 0) {
+            joiner.add("FORCE_CONSUMING");
+        }
         return joiner.toString();
     }
 
@@ -347,7 +367,6 @@
         }
         pw.print(" visible="); pw.print(mVisible);
         pw.print(" flags="); pw.print(flagsToString(mFlags));
-        pw.print(" insetsRoundedCornerFrame="); pw.print(mInsetsRoundedCornerFrame);
         pw.println();
     }
 
@@ -372,14 +391,12 @@
         if (mFlags != that.mFlags) return false;
         if (excludeInvisibleImeFrames && !mVisible && mType == WindowInsets.Type.ime()) return true;
         if (!Objects.equals(mVisibleFrame, that.mVisibleFrame)) return false;
-        if (mInsetsRoundedCornerFrame != that.mInsetsRoundedCornerFrame) return false;
         return mFrame.equals(that.mFrame);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags,
-                mInsetsRoundedCornerFrame);
+        return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags);
     }
 
     public InsetsSource(Parcel in) {
@@ -393,7 +410,6 @@
         }
         mVisible = in.readBoolean();
         mFlags = in.readInt();
-        mInsetsRoundedCornerFrame = in.readBoolean();
     }
 
     @Override
@@ -414,7 +430,6 @@
         }
         dest.writeBoolean(mVisible);
         dest.writeInt(mFlags);
-        dest.writeBoolean(mInsetsRoundedCornerFrame);
     }
 
     @Override
@@ -424,7 +439,6 @@
                 + " mFrame=" + mFrame.toShortString()
                 + " mVisible=" + mVisible
                 + " mFlags=[" + flagsToString(mFlags) + "]"
-                + (mInsetsRoundedCornerFrame ? " insetsRoundedCornerFrame" : "")
                 + "}";
     }
 
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index b55d25a..c13b9ab 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -16,6 +16,8 @@
 
 package android.view;
 
+import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
+import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
 import static android.view.InsetsStateProto.DISPLAY_CUTOUT;
 import static android.view.InsetsStateProto.DISPLAY_FRAME;
 import static android.view.InsetsStateProto.SOURCES;
@@ -143,12 +145,18 @@
         boolean[] typeVisibilityMap = new boolean[Type.SIZE];
         final Rect relativeFrame = new Rect(frame);
         final Rect relativeFrameMax = new Rect(frame);
+        @InsetsType int forceConsumingTypes = 0;
         @InsetsType int suppressScrimTypes = 0;
         for (int i = mSources.size() - 1; i >= 0; i--) {
             final InsetsSource source = mSources.valueAt(i);
+            final @InsetsType int type = source.getType();
+
+            if ((source.getFlags() & InsetsSource.FLAG_FORCE_CONSUMING) != 0) {
+                forceConsumingTypes |= type;
+            }
 
             if ((source.getFlags() & InsetsSource.FLAG_SUPPRESS_SCRIM) != 0) {
-                suppressScrimTypes |= source.getType();
+                suppressScrimTypes |= type;
             }
 
             processSource(source, relativeFrame, false /* ignoreVisibility */, typeInsetsMap,
@@ -156,7 +164,7 @@
 
             // IME won't be reported in max insets as the size depends on the EditorInfo of the IME
             // target.
-            if (source.getType() != WindowInsets.Type.ime()) {
+            if (type != WindowInsets.Type.ime()) {
                 InsetsSource ignoringVisibilitySource = ignoringVisibilityState != null
                         ? ignoringVisibilityState.peekSource(source.getId())
                         : source;
@@ -177,13 +185,13 @@
         if ((legacyWindowFlags & FLAG_FULLSCREEN) != 0) {
             compatInsetsTypes &= ~statusBars();
         }
-        if (clearsCompatInsets(windowType, legacyWindowFlags, windowingMode)
-                && !alwaysConsumeSystemBars) {
-            compatInsetsTypes = 0;
+        if (clearsCompatInsets(windowType, legacyWindowFlags, windowingMode)) {
+            // Clear all types but forceConsumingTypes.
+            compatInsetsTypes &= forceConsumingTypes;
         }
 
         return new WindowInsets(typeInsetsMap, typeMaxInsetsMap, typeVisibilityMap, isScreenRound,
-                alwaysConsumeSystemBars, suppressScrimTypes, calculateRelativeCutout(frame),
+                forceConsumingTypes, suppressScrimTypes, calculateRelativeCutout(frame),
                 calculateRelativeRoundedCorners(frame),
                 calculateRelativePrivacyIndicatorBounds(frame),
                 calculateRelativeDisplayShape(frame),
@@ -220,7 +228,7 @@
         final Rect roundedCornerFrame = new Rect(mRoundedCornerFrame);
         for (int i = mSources.size() - 1; i >= 0; i--) {
             final InsetsSource source = mSources.valueAt(i);
-            if (source.insetsRoundedCornerFrame()) {
+            if (source.hasFlags(FLAG_INSETS_ROUNDED_CORNER)) {
                 final Insets insets = source.calculateInsets(roundedCornerFrame, false);
                 roundedCornerFrame.inset(insets);
             }
@@ -289,9 +297,8 @@
 
     public Insets calculateVisibleInsets(Rect frame, int windowType, int windowingMode,
             @SoftInputModeFlags int softInputMode, int windowFlags) {
-        if (clearsCompatInsets(windowType, windowFlags, windowingMode)) {
-            return Insets.NONE;
-        }
+        final boolean clearsCompatInsets = clearsCompatInsets(
+                windowType, windowFlags, windowingMode);
         final int softInputAdjustMode = softInputMode & SOFT_INPUT_MASK_ADJUST;
         final int visibleInsetsTypes = softInputAdjustMode != SOFT_INPUT_ADJUST_NOTHING
                 ? systemBars() | ime()
@@ -302,6 +309,9 @@
             if ((source.getType() & visibleInsetsTypes) == 0) {
                 continue;
             }
+            if (clearsCompatInsets && !source.hasFlags(FLAG_FORCE_CONSUMING)) {
+                continue;
+            }
             insets = Insets.max(source.calculateVisibleInsets(frame), insets);
         }
         return insets;
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index 1af8ca2..c703af5 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -1285,6 +1285,8 @@
      * </ul>
      * These values are relative to the state from the last event, not accumulated, so developers
      * should make sure to process this axis value for all batched historical events.
+     * <p>
+     * This axis is only set on the first pointer in a motion event.
      */
     public static final int AXIS_GESTURE_X_OFFSET = 48;
 
@@ -1304,6 +1306,8 @@
      * </ul>
      * These values are relative to the state from the last event, not accumulated, so developers
      * should make sure to process this axis value for all batched historical events.
+     * <p>
+     * This axis is only set on the first pointer in a motion event.
      */
     public static final int AXIS_GESTURE_SCROLL_X_DISTANCE = 50;
 
@@ -1324,14 +1328,29 @@
      * </ul>
      * These values are relative to the state from the last event, not accumulated, so developers
      * should make sure to process this axis value for all batched historical events.
+     * <p>
+     * This axis is only set on the first pointer in a motion event.
      */
     public static final int AXIS_GESTURE_PINCH_SCALE_FACTOR = 52;
 
+    /**
+     * Axis constant: the number of fingers being used in a multi-finger swipe gesture.
+     * <p>
+     * <ul>
+     * <li>For a touch pad, reports the number of fingers being used in a multi-finger swipe gesture
+     * (with CLASSIFICATION_MULTI_FINGER_SWIPE).
+     * </ul>
+     * <p>
+     * Since CLASSIFICATION_MULTI_FINGER_SWIPE is a hidden API, so is this axis. It is only set on
+     * the first pointer in a motion event.
+     * @hide
+     */
+    public static final int AXIS_GESTURE_SWIPE_FINGER_COUNT = 53;
+
     // NOTE: If you add a new axis here you must also add it to:
     //  frameworks/native/include/android/input.h
     //  frameworks/native/libs/input/InputEventLabels.cpp
-    //  platform/cts/tests/tests/view/src/android/view/cts/MotionEventTest.java
-    //    (testAxisFromToString)
+    //  cts/tests/tests/view/src/android/view/cts/MotionEventTest.java (testAxisFromToString)
 
     // Symbolic names of all axes.
     private static final SparseArray<String> AXIS_SYMBOLIC_NAMES = new SparseArray<String>();
@@ -1387,6 +1406,7 @@
         names.append(AXIS_GESTURE_SCROLL_X_DISTANCE, "AXIS_GESTURE_SCROLL_X_DISTANCE");
         names.append(AXIS_GESTURE_SCROLL_Y_DISTANCE, "AXIS_GESTURE_SCROLL_Y_DISTANCE");
         names.append(AXIS_GESTURE_PINCH_SCALE_FACTOR, "AXIS_GESTURE_PINCH_SCALE_FACTOR");
+        names.append(AXIS_GESTURE_SWIPE_FINGER_COUNT, "AXIS_GESTURE_SWIPE_FINGER_COUNT");
     }
 
     /**
diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java
index c8cf7d9..effc127 100644
--- a/core/java/android/view/SurfaceControlViewHost.java
+++ b/core/java/android/view/SurfaceControlViewHost.java
@@ -54,7 +54,7 @@
     private final static String TAG = "SurfaceControlViewHost";
     private final ViewRootImpl mViewRoot;
     private final CloseGuard mCloseGuard = CloseGuard.get();
-    private WindowlessWindowManager mWm;
+    private final WindowlessWindowManager mWm;
 
     private SurfaceControl mSurfaceControl;
     private IAccessibilityEmbeddedConnection mAccessibilityEmbeddedConnection;
@@ -67,9 +67,7 @@
                 return;
             }
             mViewRoot.mHandler.post(() -> {
-                if (mWm != null) {
-                    mWm.setConfiguration(configuration);
-                }
+                mWm.setConfiguration(configuration);
                 if (mViewRoot != null) {
                     mViewRoot.forceWmRelayout();
                 }
@@ -116,6 +114,11 @@
             }
             return null;
         }
+
+        @Override
+        public void attachParentInterface(@Nullable ISurfaceControlViewHostParent parentInterface) {
+            mViewRoot.mHandler.post(() -> mWm.setParentInterface(parentInterface));
+        }
     }
 
     private ISurfaceControlViewHost mRemoteInterface = new ISurfaceControlViewHostImpl();
@@ -148,10 +151,11 @@
         private SurfaceControl mSurfaceControl;
         private final IAccessibilityEmbeddedConnection mAccessibilityEmbeddedConnection;
         private final IBinder mInputToken;
+        @NonNull
         private final ISurfaceControlViewHost mRemoteInterface;
 
         SurfacePackage(SurfaceControl sc, IAccessibilityEmbeddedConnection connection,
-               IBinder inputToken, ISurfaceControlViewHost ri) {
+                IBinder inputToken, @NonNull ISurfaceControlViewHost ri) {
             mSurfaceControl = sc;
             mAccessibilityEmbeddedConnection = connection;
             mInputToken = inputToken;
@@ -213,6 +217,7 @@
         /**
          * @hide
          */
+        @NonNull
         public ISurfaceControlViewHost getRemoteInterface() {
             return mRemoteInterface;
         }
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 0e4cf89..1e268be 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
 import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_OVERLAY_SUBLAYER;
 import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_SUBLAYER;
 import static android.view.WindowManagerPolicyConstants.APPLICATION_PANEL_SUBLAYER;
@@ -40,6 +41,7 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
+import android.os.RemoteException;
 import android.os.SystemClock;
 import android.util.ArraySet;
 import android.util.AttributeSet;
@@ -54,6 +56,8 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Consumer;
@@ -302,6 +306,26 @@
     private SurfaceControl mBlastSurfaceControl;
     private BLASTBufferQueue mBlastBufferQueue;
 
+    private final ConcurrentLinkedQueue<WindowManager.LayoutParams> mEmbeddedWindowParams =
+            new ConcurrentLinkedQueue<>();
+
+    private final ISurfaceControlViewHostParent mSurfaceControlViewHostParent =
+            new ISurfaceControlViewHostParent.Stub() {
+        @Override
+        public void updateParams(WindowManager.LayoutParams[] childAttrs) {
+            mEmbeddedWindowParams.clear();
+            mEmbeddedWindowParams.addAll(Arrays.asList(childAttrs));
+
+            if (isAttachedToWindow()) {
+                runOnUiThread(() -> {
+                    if (mParent != null) {
+                        mParent.recomputeViewAttributes(SurfaceView.this);
+                    }
+                });
+            }
+        }
+    };
+
     public SurfaceView(Context context) {
         this(context, null);
     }
@@ -801,9 +825,18 @@
                 mBlastSurfaceControl = null;
             }
 
-            if (releaseSurfacePackage && mSurfacePackage != null) {
-                mSurfacePackage.release();
-                mSurfacePackage = null;
+            if (mSurfacePackage != null) {
+                try {
+                    mSurfacePackage.getRemoteInterface().attachParentInterface(null);
+                    mEmbeddedWindowParams.clear();
+                } catch (RemoteException e) {
+                    Log.d(TAG, "Failed to remove parent interface from SCVH. Likely SCVH is "
+                            + "already dead");
+                }
+                if (releaseSurfacePackage) {
+                    mSurfacePackage.release();
+                    mSurfacePackage = null;
+                }
             }
 
             applyTransactionOnVriDraw(transaction);
@@ -1854,6 +1887,12 @@
             applyTransactionOnVriDraw(transaction);
         }
         mSurfacePackage = p;
+        try {
+            mSurfacePackage.getRemoteInterface().attachParentInterface(
+                    mSurfaceControlViewHostParent);
+        } catch (RemoteException e) {
+            Log.d(TAG, "Failed to attach parent interface to SCVH. Likely SCVH is already dead.");
+        }
 
         if (isFocused()) {
             requestEmbeddedFocus(true);
@@ -2014,4 +2053,19 @@
             mBlastBufferQueue.mergeWithNextTransaction(transaction, frameNumber);
         }
     }
+
+    @Override
+    void performCollectViewAttributes(AttachInfo attachInfo, int visibility) {
+        super.performCollectViewAttributes(attachInfo, visibility);
+        if (mEmbeddedWindowParams.isEmpty()) {
+            return;
+        }
+
+        for (WindowManager.LayoutParams embeddedWindowAttr : mEmbeddedWindowParams) {
+            if ((embeddedWindowAttr.flags & FLAG_KEEP_SCREEN_ON) == FLAG_KEEP_SCREEN_ON) {
+                attachInfo.mKeepScreenOn = true;
+                break;
+            }
+        }
+    }
 }
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index d457847..01a99b9 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -2043,7 +2043,10 @@
         final float x = event.getXDispatchLocation(pointerIndex);
         final float y = event.getYDispatchLocation(pointerIndex);
         if (isOnScrollbarThumb(x, y) || isDraggingScrollBar()) {
-            return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_ARROW);
+            // Return null here so that it fallbacks to the default PointerIcon for the source
+            // device. For mouse, the default PointerIcon is PointerIcon.TYPE_ARROW.
+            // For stylus, the default PointerIcon is PointerIcon.TYPE_NULL.
+            return null;
         }
         // Check what the child under the pointer says about the pointer.
         final int childrenCount = mChildrenCount;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index a28be4b..bef28b2 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -1900,9 +1900,10 @@
                 && !Objects.equals(mTmpFrames.attachedFrame, attachedFrame);
         final boolean displayChanged = mDisplay.getDisplayId() != displayId;
         final boolean compatScaleChanged = mTmpFrames.compatScale != compatScale;
+        final boolean dragResizingChanged = mPendingDragResizing != dragResizing;
         if (msg == MSG_RESIZED && !frameChanged && !configChanged && !attachedFrameChanged
                 && !displayChanged && !forceNextWindowRelayout
-                && !compatScaleChanged) {
+                && !compatScaleChanged && !dragResizingChanged) {
             return;
         }
 
@@ -3819,7 +3820,7 @@
                 }
                 mPendingTransitions.clear();
             }
-            if (!performDraw() && mActiveSurfaceSyncGroup != null) {
+            if (!performDraw(mActiveSurfaceSyncGroup) && mActiveSurfaceSyncGroup != null) {
                 mActiveSurfaceSyncGroup.markSyncReady();
             }
         }
@@ -3854,7 +3855,15 @@
         mWmsRequestSyncGroupState = WMS_SYNC_PENDING;
         mWmsRequestSyncGroup = new SurfaceSyncGroup("wmsSync-" + mTag, t -> {
             mWmsRequestSyncGroupState = WMS_SYNC_MERGED;
-            reportDrawFinished(t, seqId);
+            // See b/286355097. If the current process is not system, then invoking finishDraw on
+            // any thread is fine since once it calls into system process, finishDrawing will run
+            // on a different thread. However, when the current process is system, the finishDraw in
+            // system server will be run on the current thread, which could result in a deadlock.
+            if (mWindowSession instanceof Binder) {
+                reportDrawFinished(t, seqId);
+            } else {
+                mHandler.postAtFrontOfQueue(() -> reportDrawFinished(t, seqId));
+            }
         });
         if (DEBUG_BLAST) {
             Log.d(mTag, "Setup new sync=" + mWmsRequestSyncGroup.getName());
@@ -4582,6 +4591,10 @@
         });
     }
 
+    /**
+     * These callbacks check if the draw failed for any reason and apply
+     * those transactions directly so they don't get stuck forever.
+     */
     private void registerCallbackForPendingTransactions() {
         Transaction t = new Transaction();
         t.merge(mPendingTransaction);
@@ -4610,7 +4623,7 @@
         });
     }
 
-    private boolean performDraw() {
+    private boolean performDraw(@Nullable SurfaceSyncGroup surfaceSyncGroup) {
         mLastPerformDrawSkippedReason = null;
         if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
             mLastPerformDrawSkippedReason = "screen_off";
@@ -4620,7 +4633,7 @@
             return false;
         }
 
-        final boolean fullRedrawNeeded = mFullRedrawNeeded || mActiveSurfaceSyncGroup != null;
+        final boolean fullRedrawNeeded = mFullRedrawNeeded || surfaceSyncGroup != null;
         mFullRedrawNeeded = false;
 
         mIsDrawing = true;
@@ -4628,22 +4641,12 @@
 
         addFrameCommitCallbackIfNeeded();
 
-        boolean usingAsyncReport = isHardwareEnabled() && mActiveSurfaceSyncGroup != null;
-        if (usingAsyncReport) {
-            registerCallbacksForSync(mSyncBuffer, mActiveSurfaceSyncGroup);
-        } else if (mHasPendingTransactions) {
-            // These callbacks are only needed if there's no sync involved and there were calls to
-            // applyTransactionOnDraw. These callbacks check if the draw failed for any reason and
-            // apply those transactions directly so they don't get stuck forever.
-            registerCallbackForPendingTransactions();
-        }
-        mHasPendingTransactions = false;
+        boolean usingAsyncReport;
 
         try {
-            boolean canUseAsync = draw(fullRedrawNeeded, usingAsyncReport && mSyncBuffer);
-            if (usingAsyncReport && !canUseAsync) {
+            usingAsyncReport = draw(fullRedrawNeeded, surfaceSyncGroup, mSyncBuffer);
+            if (mAttachInfo.mThreadedRenderer != null && !usingAsyncReport) {
                 mAttachInfo.mThreadedRenderer.setFrameCallback(null);
-                usingAsyncReport = false;
             }
         } finally {
             mIsDrawing = false;
@@ -4681,10 +4684,12 @@
             }
 
             if (mSurfaceHolder != null && mSurface.isValid()) {
-                final SurfaceSyncGroup surfaceSyncGroup = mActiveSurfaceSyncGroup;
-                SurfaceCallbackHelper sch = new SurfaceCallbackHelper(() ->
-                        mHandler.post(() -> surfaceSyncGroup.markSyncReady()));
-                mActiveSurfaceSyncGroup = null;
+                usingAsyncReport = true;
+                SurfaceCallbackHelper sch = new SurfaceCallbackHelper(() -> {
+                    if (surfaceSyncGroup != null) {
+                        surfaceSyncGroup.markSyncReady();
+                    }
+                });
 
                 SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
 
@@ -4695,8 +4700,9 @@
                 }
             }
         }
-        if (mActiveSurfaceSyncGroup != null && !usingAsyncReport) {
-            mActiveSurfaceSyncGroup.markSyncReady();
+
+        if (surfaceSyncGroup != null && !usingAsyncReport) {
+            surfaceSyncGroup.markSyncReady();
         }
         if (mPerformContentCapture) {
             performContentCaptureInitialReport();
@@ -4789,7 +4795,8 @@
         }
     }
 
-    private boolean draw(boolean fullRedrawNeeded, boolean forceDraw) {
+    private boolean draw(boolean fullRedrawNeeded,
+            @Nullable SurfaceSyncGroup activeSyncGroup, boolean syncBuffer) {
         Surface surface = mSurface;
         if (!surface.isValid()) {
             return false;
@@ -4933,9 +4940,19 @@
                     mAttachInfo.mThreadedRenderer.setTargetHdrSdrRatio(mRenderHdrSdrRatio);
                 }
 
-                if (forceDraw) {
-                    mAttachInfo.mThreadedRenderer.forceDrawNextFrame();
+                if (activeSyncGroup != null) {
+                    registerCallbacksForSync(syncBuffer, activeSyncGroup);
+                    if (syncBuffer) {
+                        mAttachInfo.mThreadedRenderer.forceDrawNextFrame();
+                    }
+                } else if (mHasPendingTransactions) {
+                    // Register a calback if there's no sync involved but there were calls to
+                    // applyTransactionOnDraw. If there is a sync involved, the sync callback will
+                    // handle merging the pending transaction.
+                    registerCallbackForPendingTransactions();
                 }
+                mHasPendingTransactions = false;
+
                 mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
             } else {
                 // If we get here with a disabled & requested hardware renderer, something went
@@ -11553,4 +11570,17 @@
         mChildBoundingInsetsChanged = true;
         scheduleTraversals();
     }
+
+    @Override
+    public void addTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t,
+            @NonNull SurfaceControl.TrustedPresentationThresholds thresholds,
+            @NonNull Executor executor, @NonNull Consumer<Boolean> listener) {
+        t.setTrustedPresentationCallback(getSurfaceControl(), thresholds, executor, listener);
+    }
+
+    @Override
+    public void removeTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t,
+            @NonNull Consumer<Boolean> listener) {
+        t.clearTrustedPresentationCallback(getSurfaceControl());
+    }
 }
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 21fe87f..2f04b0c 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -658,6 +658,12 @@
         void updateStatusBarColor(int color);
 
         /**
+         * Update the status bar appearance.
+         */
+
+        void updateStatusBarAppearance(int appearance);
+
+        /**
          * Update the navigation bar color to a forced one.
          */
         void updateNavigationBarColor(int color);
@@ -1039,6 +1045,9 @@
         if (mDecorCallback != null) {
             mDecorCallback.onSystemBarAppearanceChanged(appearance);
         }
+        if (mWindowControllerCallback != null) {
+            mWindowControllerCallback.updateStatusBarAppearance(appearance);
+        }
     }
 
     /** @hide */
@@ -1482,6 +1491,11 @@
     }
 
     /** @hide */
+    public boolean shouldCloseOnTouchOutside() {
+        return mCloseOnTouchOutside;
+    }
+
+    /** @hide */
     @SuppressWarnings("HiddenAbstractMethod")
     @UnsupportedAppUsage
     public abstract void alwaysReadCloseOnTouchAttr();
diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java
index 4acaea8..57a4161 100644
--- a/core/java/android/view/WindowInsets.java
+++ b/core/java/android/view/WindowInsets.java
@@ -84,13 +84,7 @@
     @Nullable private final PrivacyIndicatorBounds mPrivacyIndicatorBounds;
     @Nullable private final DisplayShape mDisplayShape;
 
-    /**
-     * In multi-window we force show the navigation bar. Because we don't want that the surface size
-     * changes in this mode, we instead have a flag whether the navigation bar size should always
-     * be consumed, so the app is treated like there is no virtual navigation bar at all.
-     */
-    private final boolean mAlwaysConsumeSystemBars;
-
+    private final @InsetsType int mForceConsumingTypes;
     private final @InsetsType int mSuppressScrimTypes;
     private final boolean mSystemWindowInsetsConsumed;
     private final boolean mStableInsetsConsumed;
@@ -117,7 +111,7 @@
 
     static {
         CONSUMED = new WindowInsets(createCompatTypeMap(null), createCompatTypeMap(null),
-                createCompatVisibilityMap(createCompatTypeMap(null)), false, false, 0, null,
+                createCompatVisibilityMap(createCompatTypeMap(null)), false, 0, 0, null,
                 null, null, null, systemBars(), false);
     }
 
@@ -137,7 +131,8 @@
             @Nullable Insets[] typeMaxInsetsMap,
             boolean[] typeVisibilityMap,
             boolean isRound,
-            boolean alwaysConsumeSystemBars, @InsetsType int suppressScrimTypes,
+            @InsetsType int forceConsumingTypes,
+            @InsetsType int suppressScrimTypes,
             DisplayCutout displayCutout,
             RoundedCorners roundedCorners,
             PrivacyIndicatorBounds privacyIndicatorBounds,
@@ -155,7 +150,7 @@
 
         mTypeVisibilityMap = typeVisibilityMap;
         mIsRound = isRound;
-        mAlwaysConsumeSystemBars = alwaysConsumeSystemBars;
+        mForceConsumingTypes = forceConsumingTypes;
         mSuppressScrimTypes = suppressScrimTypes;
         mCompatInsetsTypes = compatInsetsTypes;
         mCompatIgnoreVisibility = compatIgnoreVisibility;
@@ -178,7 +173,7 @@
         this(src.mSystemWindowInsetsConsumed ? null : src.mTypeInsetsMap,
                 src.mStableInsetsConsumed ? null : src.mTypeMaxInsetsMap,
                 src.mTypeVisibilityMap, src.mIsRound,
-                src.mAlwaysConsumeSystemBars, src.mSuppressScrimTypes,
+                src.mForceConsumingTypes, src.mSuppressScrimTypes,
                 displayCutoutCopyConstructorArgument(src),
                 src.mRoundedCorners,
                 src.mPrivacyIndicatorBounds,
@@ -235,7 +230,7 @@
     /** @hide */
     @UnsupportedAppUsage
     public WindowInsets(Rect systemWindowInsets) {
-        this(createCompatTypeMap(systemWindowInsets), null, new boolean[SIZE], false, false, 0,
+        this(createCompatTypeMap(systemWindowInsets), null, new boolean[SIZE], false, 0, 0,
                 null, null, null, null, systemBars(), false /* compatIgnoreVisibility */);
     }
 
@@ -556,7 +551,7 @@
         return new WindowInsets(mSystemWindowInsetsConsumed ? null : mTypeInsetsMap,
                 mStableInsetsConsumed ? null : mTypeMaxInsetsMap,
                 mTypeVisibilityMap,
-                mIsRound, mAlwaysConsumeSystemBars, mSuppressScrimTypes,
+                mIsRound, mForceConsumingTypes, mSuppressScrimTypes,
                 null /* displayCutout */, mRoundedCorners, mPrivacyIndicatorBounds, mDisplayShape,
                 mCompatInsetsTypes, mCompatIgnoreVisibility);
     }
@@ -607,7 +602,7 @@
     public WindowInsets consumeSystemWindowInsets() {
         return new WindowInsets(null, null,
                 mTypeVisibilityMap,
-                mIsRound, mAlwaysConsumeSystemBars, mSuppressScrimTypes,
+                mIsRound, mForceConsumingTypes, mSuppressScrimTypes,
                 // If the system window insets types contain displayCutout, we should also consume
                 // it.
                 (mCompatInsetsTypes & displayCutout()) != 0
@@ -895,8 +890,8 @@
     /**
      * @hide
      */
-    public boolean shouldAlwaysConsumeSystemBars() {
-        return mAlwaysConsumeSystemBars;
+    public @InsetsType int getForceConsumingTypes() {
+        return mForceConsumingTypes;
     }
 
     /**
@@ -930,6 +925,8 @@
         result.append("\n    ");
         result.append(mDisplayShape != null ? "displayShape=" + mDisplayShape : "");
         result.append("\n    ");
+        result.append("forceConsumingTypes=" + Type.toString(mForceConsumingTypes));
+        result.append("\n    ");
         result.append("suppressScrimTypes=" + Type.toString(mSuppressScrimTypes));
         result.append("\n    ");
         result.append("compatInsetsTypes=" + Type.toString(mCompatInsetsTypes));
@@ -1027,7 +1024,7 @@
                         ? null
                         : insetInsets(mTypeMaxInsetsMap, left, top, right, bottom),
                 mTypeVisibilityMap,
-                mIsRound, mAlwaysConsumeSystemBars, mSuppressScrimTypes,
+                mIsRound, mForceConsumingTypes, mSuppressScrimTypes,
                 mDisplayCutoutConsumed
                         ? null
                         : mDisplayCutout == null
@@ -1050,7 +1047,7 @@
         WindowInsets that = (WindowInsets) o;
 
         return mIsRound == that.mIsRound
-                && mAlwaysConsumeSystemBars == that.mAlwaysConsumeSystemBars
+                && mForceConsumingTypes == that.mForceConsumingTypes
                 && mSuppressScrimTypes == that.mSuppressScrimTypes
                 && mSystemWindowInsetsConsumed == that.mSystemWindowInsetsConsumed
                 && mStableInsetsConsumed == that.mStableInsetsConsumed
@@ -1068,7 +1065,7 @@
     public int hashCode() {
         return Objects.hash(Arrays.hashCode(mTypeInsetsMap), Arrays.hashCode(mTypeMaxInsetsMap),
                 Arrays.hashCode(mTypeVisibilityMap), mIsRound, mDisplayCutout, mRoundedCorners,
-                mAlwaysConsumeSystemBars, mSuppressScrimTypes, mSystemWindowInsetsConsumed,
+                mForceConsumingTypes, mSuppressScrimTypes, mSystemWindowInsetsConsumed,
                 mStableInsetsConsumed, mDisplayCutoutConsumed, mPrivacyIndicatorBounds,
                 mDisplayShape);
     }
@@ -1134,7 +1131,7 @@
         private DisplayShape mDisplayShape = DisplayShape.NONE;
 
         private boolean mIsRound;
-        private boolean mAlwaysConsumeSystemBars;
+        private @InsetsType int mForceConsumingTypes;
         private @InsetsType int mSuppressScrimTypes;
 
         private PrivacyIndicatorBounds mPrivacyIndicatorBounds = new PrivacyIndicatorBounds();
@@ -1162,7 +1159,7 @@
             mDisplayCutout = displayCutoutCopyConstructorArgument(insets);
             mRoundedCorners = insets.mRoundedCorners;
             mIsRound = insets.mIsRound;
-            mAlwaysConsumeSystemBars = insets.mAlwaysConsumeSystemBars;
+            mForceConsumingTypes = insets.mForceConsumingTypes;
             mSuppressScrimTypes = insets.mSuppressScrimTypes;
             mPrivacyIndicatorBounds = insets.mPrivacyIndicatorBounds;
             mDisplayShape = insets.mDisplayShape;
@@ -1433,7 +1430,15 @@
         /** @hide */
         @NonNull
         public Builder setAlwaysConsumeSystemBars(boolean alwaysConsumeSystemBars) {
-            mAlwaysConsumeSystemBars = alwaysConsumeSystemBars;
+            // TODO (b/277891341): Remove this and related usages. This has been replaced by
+            //                     #setForceConsumingTypes.
+            return this;
+        }
+
+        /** @hide */
+        @NonNull
+        public Builder setForceConsumingTypes(@InsetsType int forceConsumingTypes) {
+            mForceConsumingTypes = forceConsumingTypes;
             return this;
         }
 
@@ -1453,7 +1458,7 @@
         public WindowInsets build() {
             return new WindowInsets(mSystemInsetsConsumed ? null : mTypeInsetsMap,
                     mStableInsetsConsumed ? null : mTypeMaxInsetsMap, mTypeVisibilityMap,
-                    mIsRound, mAlwaysConsumeSystemBars, mSuppressScrimTypes, mDisplayCutout,
+                    mIsRound, mForceConsumingTypes, mSuppressScrimTypes, mDisplayCutout,
                     mRoundedCorners, mPrivacyIndicatorBounds, mDisplayShape, systemBars(),
                     false /* compatIgnoreVisibility */);
         }
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index a917815..d702367 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -1384,15 +1384,28 @@
             "android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED";
 
     /**
-     * Request for keyboard shortcuts to be retrieved asynchronously.
+     * Request for app's keyboard shortcuts to be retrieved asynchronously.
      *
      * @param receiver The callback to be triggered when the result is ready.
+     * @param deviceId The deviceId of KeyEvent by which this request is triggered, or -1 if it's
+     *                 not triggered by a KeyEvent.
      *
      * @hide
      */
     public void requestAppKeyboardShortcuts(final KeyboardShortcutsReceiver receiver, int deviceId);
 
     /**
+     * Request for ime's keyboard shortcuts to be retrieved asynchronously.
+     *
+     * @param receiver The callback to be triggered when the result is ready.
+     * @param deviceId The deviceId of KeyEvent by which this request is triggered, or -1 if it's
+     *                 not triggered by a KeyEvent.
+     *
+     * @hide
+     */
+    default void requestImeKeyboardShortcuts(KeyboardShortcutsReceiver receiver, int deviceId) {};
+
+    /**
      * Return the touch region for the current IME window, or an empty region if there is none.
      *
      * @return The region of the IME that is accepting touch inputs, or null if there is no IME, no
@@ -4294,17 +4307,6 @@
         public InsetsFrameProvider[] providedInsets;
 
         /**
-         * If specified, the frame that used to calculate relative {@link RoundedCorner} will be
-         * the window frame of this window minus the insets that this window provides.
-         *
-         * Task bar will draw fake rounded corners above itself, so we need this insets to calculate
-         * correct rounded corners for this window.
-         *
-         * @hide
-         */
-        public boolean insetsRoundedCornerFrame = false;
-
-        /**
          * {@link LayoutParams} to be applied to the window when layout with a assigned rotation.
          * This will make layout during rotation change smoothly.
          *
@@ -4760,7 +4762,6 @@
             out.writeBoolean(mFitInsetsIgnoringVisibility);
             out.writeBoolean(preferMinimalPostProcessing);
             out.writeInt(mBlurBehindRadius);
-            out.writeBoolean(insetsRoundedCornerFrame);
             out.writeBoolean(mWallpaperTouchEventsEnabled);
             out.writeTypedArray(providedInsets, 0 /* parcelableFlags */);
             checkNonRecursiveParams();
@@ -4832,7 +4833,6 @@
             mFitInsetsIgnoringVisibility = in.readBoolean();
             preferMinimalPostProcessing = in.readBoolean();
             mBlurBehindRadius = in.readInt();
-            insetsRoundedCornerFrame = in.readBoolean();
             mWallpaperTouchEventsEnabled = in.readBoolean();
             providedInsets = in.createTypedArray(InsetsFrameProvider.CREATOR);
             paramsForRotation = in.createTypedArray(LayoutParams.CREATOR);
@@ -5140,11 +5140,6 @@
                 changes |= LAYOUT_CHANGED;
             }
 
-            if (insetsRoundedCornerFrame != o.insetsRoundedCornerFrame) {
-                insetsRoundedCornerFrame = o.insetsRoundedCornerFrame;
-                changes |= LAYOUT_CHANGED;
-            }
-
             if (paramsForRotation != o.paramsForRotation) {
                 if ((changes & LAYOUT_CHANGED) == 0) {
                     if (paramsForRotation != null && o.paramsForRotation != null
@@ -5382,10 +5377,6 @@
                     sb.append(prefix).append("    ").append(providedInsets[i]);
                 }
             }
-            if (insetsRoundedCornerFrame) {
-                sb.append(" insetsRoundedCornerFrame=");
-                sb.append(insetsRoundedCornerFrame);
-            }
             if (paramsForRotation != null && paramsForRotation.length != 0) {
                 sb.append(System.lineSeparator());
                 sb.append(prefix).append("  paramsForRotation:");
diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
index df3e0bb..b57163c 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -215,14 +215,36 @@
             @Override
             public void send(int resultCode, Bundle resultData) throws RemoteException {
                 List<KeyboardShortcutGroup> result =
-                        resultData.getParcelableArrayList(PARCEL_KEY_SHORTCUTS_ARRAY, android.view.KeyboardShortcutGroup.class);
+                        resultData.getParcelableArrayList(PARCEL_KEY_SHORTCUTS_ARRAY,
+                                android.view.KeyboardShortcutGroup.class);
                 receiver.onKeyboardShortcutsReceived(result);
             }
         };
         try {
             WindowManagerGlobal.getWindowManagerService()
-                .requestAppKeyboardShortcuts(resultReceiver, deviceId);
+                    .requestAppKeyboardShortcuts(resultReceiver, deviceId);
         } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
+    public void requestImeKeyboardShortcuts(
+            final KeyboardShortcutsReceiver receiver, int deviceId) {
+        IResultReceiver resultReceiver = new IResultReceiver.Stub() {
+            @Override
+            public void send(int resultCode, Bundle resultData) throws RemoteException {
+                List<KeyboardShortcutGroup> result =
+                        resultData.getParcelableArrayList(PARCEL_KEY_SHORTCUTS_ARRAY,
+                                android.view.KeyboardShortcutGroup.class);
+                receiver.onKeyboardShortcutsReceived(result);
+            }
+        };
+        try {
+            WindowManagerGlobal.getWindowManagerService()
+                    .requestImeKeyboardShortcuts(resultReceiver, deviceId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
         }
     }
 
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index 96bfb2d..7d3d283 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -49,7 +49,8 @@
 
     private class State {
         SurfaceControl mSurfaceControl;
-        WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
+        final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
+        final WindowManager.LayoutParams mLastReportedParams = new WindowManager.LayoutParams();
         int mDisplayId;
         IBinder mInputChannelToken;
         Region mInputRegion;
@@ -94,6 +95,8 @@
     private final MergedConfiguration mTmpConfig = new MergedConfiguration();
     private final WindowlessWindowLayout mLayout = new WindowlessWindowLayout();
 
+    private ISurfaceControlViewHostParent mParentInterface;
+
     public WindowlessWindowManager(Configuration c, SurfaceControl rootSurface,
             IBinder hostInputToken) {
         mRootSurface = rootSurface;
@@ -244,6 +247,7 @@
         final int res = WindowManagerGlobal.ADD_OKAY | WindowManagerGlobal.ADD_FLAG_APP_VISIBLE |
                         WindowManagerGlobal.ADD_FLAG_USE_BLAST;
 
+        sendLayoutParamsToParent();
         // Include whether the window is in touch mode.
         return isInTouchModeInternal(displayId) ? res | WindowManagerGlobal.ADD_FLAG_IN_TOUCH_MODE
                 : res;
@@ -425,6 +429,7 @@
             outInsetsState.set(mInsetsState);
         }
 
+        sendLayoutParamsToParent();
         return 0;
     }
 
@@ -645,4 +650,45 @@
             " we shouldn't get here!");
         return false;
     }
+
+    void setParentInterface(@Nullable ISurfaceControlViewHostParent parentInterface) {
+        IBinder oldInterface = mParentInterface == null ? null : mParentInterface.asBinder();
+        IBinder newInterface = parentInterface == null ? null : parentInterface.asBinder();
+        // If the parent interface has changed, it needs to clear the last reported params so it
+        // will update the new interface with the params.
+        if (oldInterface != newInterface) {
+            clearLastReportedParams();
+        }
+        mParentInterface = parentInterface;
+        sendLayoutParamsToParent();
+    }
+
+    private void clearLastReportedParams() {
+        WindowManager.LayoutParams emptyParam = new WindowManager.LayoutParams();
+        for (State windowInfo : mStateForWindow.values()) {
+            windowInfo.mLastReportedParams.copyFrom(emptyParam);
+        }
+    }
+
+    private void sendLayoutParamsToParent() {
+        if (mParentInterface == null) {
+            return;
+        }
+        WindowManager.LayoutParams[] params =
+                new WindowManager.LayoutParams[mStateForWindow.size()];
+        int index = 0;
+        boolean hasChanges = false;
+        for (State windowInfo : mStateForWindow.values()) {
+            int changes = windowInfo.mLastReportedParams.copyFrom(windowInfo.mParams);
+            hasChanges |= (changes != 0);
+            params[index++] = windowInfo.mParams;
+        }
+
+        if (hasChanges) {
+            try {
+                mParentInterface.updateParams(params);
+            } catch (RemoteException e) {
+            }
+        }
+    }
 }
diff --git a/core/java/android/view/accessibility/IWindowMagnificationConnection.aidl b/core/java/android/view/accessibility/IWindowMagnificationConnection.aidl
index 8a30f8c..a11c6d0 100644
--- a/core/java/android/view/accessibility/IWindowMagnificationConnection.aidl
+++ b/core/java/android/view/accessibility/IWindowMagnificationConnection.aidl
@@ -115,4 +115,13 @@
      * @param callback the interface to be called.
      */
     void setConnectionCallback(in IWindowMagnificationConnectionCallback callback);
+
+    /**
+     * Notify System UI the magnification scale on the specified display for userId is changed.
+     *
+     * @param userId the user id.
+     * @param displayId the logical display id.
+     * @param scale magnification scale.
+     */
+    void onUserMagnificationScaleChanged(int userId, int displayId, float scale);
 }
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 668351b..c9afdc0 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -52,6 +52,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.RingBuffer;
 import com.android.internal.util.SyncResultReceiver;
 
 import java.io.PrintWriter;
@@ -352,6 +353,30 @@
     public static final String DEVICE_CONFIG_PROPERTY_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING =
             "disable_flush_for_view_tree_appearing";
 
+    /**
+     * Enables the content protection receiver.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_PROPERTY_ENABLE_CONTENT_PROTECTION_RECEIVER =
+            "enable_content_protection_receiver";
+
+    /**
+     * Sets the size of the app blocklist for the content protection flow.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_APPS_BLOCKLIST_SIZE =
+            "content_protection_apps_blocklist_size";
+
+    /**
+     * Sets the size of the in-memory ring buffer for the content protection flow.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_BUFFER_SIZE =
+            "content_protection_buffer_size";
+
     /** @hide */
     @TestApi
     public static final int LOGGING_LEVEL_OFF = 0;
@@ -384,6 +409,14 @@
     public static final int DEFAULT_LOG_HISTORY_SIZE = 10;
     /** @hide */
     public static final boolean DEFAULT_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING = false;
+    /** @hide */
+    public static final boolean DEFAULT_ENABLE_CONTENT_CAPTURE_RECEIVER = true;
+    /** @hide */
+    public static final boolean DEFAULT_ENABLE_CONTENT_PROTECTION_RECEIVER = false;
+    /** @hide */
+    public static final int DEFAULT_CONTENT_PROTECTION_APPS_BLOCKLIST_SIZE = 1000;
+    /** @hide */
+    public static final int DEFAULT_CONTENT_PROTECTION_BUFFER_SIZE = 150;
 
     private final Object mLock = new Object();
 
@@ -414,6 +447,9 @@
     @Nullable // set on-demand by addDumpable()
     private Dumper mDumpable;
 
+    // Created here in order to live across activity and session changes
+    @Nullable private final RingBuffer<ContentCaptureEvent> mContentProtectionEventBuffer;
+
     /** @hide */
     public interface ContentCaptureClient {
         /**
@@ -424,12 +460,15 @@
     }
 
     /** @hide */
-    static class StrippedContext {
-        final String mPackageName;
-        final String mContext;
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public static class StrippedContext {
+        @NonNull final String mPackageName;
+        @NonNull final String mContext;
         final @UserIdInt int mUserId;
 
-        private StrippedContext(Context context) {
+        /** @hide */
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+        public StrippedContext(@NonNull Context context) {
             mPackageName = context.getPackageName();
             mContext = context.toString();
             mUserId = context.getUserId();
@@ -440,6 +479,7 @@
             return mContext;
         }
 
+        @NonNull
         public String getPackageName() {
             return mPackageName;
         }
@@ -469,6 +509,16 @@
         mHandler = Handler.createAsync(Looper.getMainLooper());
 
         mDataShareAdapterResourceManager = new LocalDataShareAdapterResourceManager();
+
+        if (mOptions.contentProtectionOptions.enableReceiver
+                && mOptions.contentProtectionOptions.bufferSize > 0) {
+            mContentProtectionEventBuffer =
+                    new RingBuffer(
+                            ContentCaptureEvent.class,
+                            mOptions.contentProtectionOptions.bufferSize);
+        } else {
+            mContentProtectionEventBuffer = null;
+        }
     }
 
     /**
@@ -837,6 +887,13 @@
         activity.addDumpable(mDumpable);
     }
 
+    /** @hide */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    @Nullable
+    public RingBuffer<ContentCaptureEvent> getContentProtectionEventBuffer() {
+        return mContentProtectionEventBuffer;
+    }
+
     // NOTE: ContentCaptureManager cannot implement it directly as it would be exposed as public API
     private final class Dumper implements Dumpable {
         @Override
diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java
index 62044aa..dc3d323 100644
--- a/core/java/android/view/contentcapture/ContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/ContentCaptureSession.java
@@ -191,20 +191,22 @@
     static final long NOTIFY_NODES_DISAPPEAR_NOW_SENDS_TREE_EVENTS = 258825825L;
 
     /** @hide */
-    @IntDef(prefix = { "FLUSH_REASON_" }, value = {
-            FLUSH_REASON_FULL,
-            FLUSH_REASON_VIEW_ROOT_ENTERED,
-            FLUSH_REASON_SESSION_STARTED,
-            FLUSH_REASON_SESSION_FINISHED,
-            FLUSH_REASON_IDLE_TIMEOUT,
-            FLUSH_REASON_TEXT_CHANGE_TIMEOUT,
-            FLUSH_REASON_SESSION_CONNECTED,
-            FLUSH_REASON_FORCE_FLUSH,
-            FLUSH_REASON_VIEW_TREE_APPEARING,
-            FLUSH_REASON_VIEW_TREE_APPEARED
-    })
+    @IntDef(
+            prefix = {"FLUSH_REASON_"},
+            value = {
+                FLUSH_REASON_FULL,
+                FLUSH_REASON_VIEW_ROOT_ENTERED,
+                FLUSH_REASON_SESSION_STARTED,
+                FLUSH_REASON_SESSION_FINISHED,
+                FLUSH_REASON_IDLE_TIMEOUT,
+                FLUSH_REASON_TEXT_CHANGE_TIMEOUT,
+                FLUSH_REASON_SESSION_CONNECTED,
+                FLUSH_REASON_FORCE_FLUSH,
+                FLUSH_REASON_VIEW_TREE_APPEARING,
+                FLUSH_REASON_VIEW_TREE_APPEARED
+            })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface FlushReason{}
+    public @interface FlushReason {}
 
     private final Object mLock = new Object();
 
@@ -686,7 +688,7 @@
             case FLUSH_REASON_VIEW_TREE_APPEARED:
                 return "VIEW_TREE_APPEARED";
             default:
-                return "UNKOWN-" + reason;
+                return "UNKNOWN-" + reason;
         }
     }
 
diff --git a/core/java/android/view/contentcapture/IContentCaptureManager.aidl b/core/java/android/view/contentcapture/IContentCaptureManager.aidl
index a641110..1487997 100644
--- a/core/java/android/view/contentcapture/IContentCaptureManager.aidl
+++ b/core/java/android/view/contentcapture/IContentCaptureManager.aidl
@@ -17,6 +17,7 @@
 package android.view.contentcapture;
 
 import android.content.ComponentName;
+import android.content.pm.ParceledListSlice;
 import android.view.contentcapture.ContentCaptureContext;
 import android.view.contentcapture.ContentCaptureEvent;
 import android.view.contentcapture.DataRemovalRequest;
@@ -108,4 +109,9 @@
      */
     void registerContentCaptureOptionsCallback(String packageName,
                                                in IContentCaptureOptionsCallback callback);
+
+    /**
+     * Notifies the system server that a login was detected.
+     */
+    void onLoginDetected(in ParceledListSlice<ContentCaptureEvent> events);
 }
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index efd50e7..2241fd5 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -52,8 +52,10 @@
 import android.util.TimeUtils;
 import android.view.autofill.AutofillId;
 import android.view.contentcapture.ViewNode.ViewStructureImpl;
+import android.view.contentprotection.ContentProtectionEventProcessor;
 import android.view.inputmethod.BaseInputConnection;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.IResultReceiver;
 
 import java.io.PrintWriter;
@@ -118,9 +120,13 @@
     /**
      * Direct interface to the service binder object - it's used to send the events, including the
      * last ones (when the session is finished)
+     *
+     * @hide
      */
-    @NonNull
-    private IContentCaptureDirectManager mDirectServiceInterface;
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    @Nullable
+    public IContentCaptureDirectManager mDirectServiceInterface;
+
     @Nullable
     private DeathRecipient mDirectServiceVulture;
 
@@ -131,14 +137,19 @@
     @Nullable
     private IBinder mShareableActivityToken;
 
+    /** @hide */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
     @Nullable
-    private ComponentName mComponentName;
+    public ComponentName mComponentName;
 
     /**
      * List of events held to be sent as a batch.
+     *
+     * @hide
      */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
     @Nullable
-    private ArrayList<ContentCaptureEvent> mEvents;
+    public ArrayList<ContentCaptureEvent> mEvents;
 
     // Used just for debugging purposes (on dump)
     private long mNextFlush;
@@ -157,6 +168,11 @@
     @NonNull
     private final SessionStateReceiver mSessionStateReceiver;
 
+    /** @hide */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    @Nullable
+    public ContentProtectionEventProcessor mContentProtectionEventProcessor;
+
     private static class SessionStateReceiver extends IResultReceiver.Stub {
         private final WeakReference<MainContentCaptureSession> mMainSession;
 
@@ -194,8 +210,12 @@
         }
     }
 
-    protected MainContentCaptureSession(@NonNull ContentCaptureManager.StrippedContext context,
-            @NonNull ContentCaptureManager manager, @NonNull Handler handler,
+    /** @hide */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
+    public MainContentCaptureSession(
+            @NonNull ContentCaptureManager.StrippedContext context,
+            @NonNull ContentCaptureManager manager,
+            @NonNull Handler handler,
             @NonNull IContentCaptureManager systemServerInterface) {
         mContext = context;
         mManager = manager;
@@ -273,15 +293,16 @@
     }
 
     /**
-     * Callback from {@code system_server} after call to
-     * {@link IContentCaptureManager#startSession(IBinder, ComponentName, String, int,
-     * IResultReceiver)}.
+     * Callback from {@code system_server} after call to {@link
+     * IContentCaptureManager#startSession(IBinder, ComponentName, String, int, IResultReceiver)}.
      *
      * @param resultCode session state
      * @param binder handle to {@code IContentCaptureDirectManager}
+     * @hide
      */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
     @UiThread
-    private void onSessionStarted(int resultCode, @Nullable IBinder binder) {
+    public void onSessionStarted(int resultCode, @Nullable IBinder binder) {
         if (binder != null) {
             mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
             mDirectServiceVulture = () -> {
@@ -296,6 +317,20 @@
             }
         }
 
+        // Should not be possible for mComponentName to be null here but check anyway
+        if (mManager.mOptions.contentProtectionOptions.enableReceiver
+                && mManager.getContentProtectionEventBuffer() != null
+                && mComponentName != null) {
+            mContentProtectionEventProcessor =
+                    new ContentProtectionEventProcessor(
+                            mManager.getContentProtectionEventBuffer(),
+                            mHandler,
+                            mSystemServerInterface,
+                            mComponentName.getPackageName());
+        } else {
+            mContentProtectionEventProcessor = null;
+        }
+
         if ((resultCode & STATE_DISABLED) != 0) {
             resetSession(resultCode);
         } else {
@@ -311,8 +346,10 @@
         }
     }
 
+    /** @hide */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
     @UiThread
-    private void sendEvent(@NonNull ContentCaptureEvent event) {
+    public void sendEvent(@NonNull ContentCaptureEvent event) {
         sendEvent(event, /* forceFlush= */ false);
     }
 
@@ -337,6 +374,25 @@
             if (sVerbose) Log.v(TAG, "handleSendEvent(): ignoring when disabled");
             return;
         }
+
+        if (isContentProtectionReceiverEnabled()) {
+            sendContentProtectionEvent(event);
+        }
+        if (isContentCaptureReceiverEnabled()) {
+            sendContentCaptureEvent(event, forceFlush);
+        }
+    }
+
+    @UiThread
+    private void sendContentProtectionEvent(@NonNull ContentCaptureEvent event) {
+        if (mContentProtectionEventProcessor != null) {
+            mContentProtectionEventProcessor.processEvent(event);
+        }
+    }
+
+    @UiThread
+    private void sendContentCaptureEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
+        final int eventType = event.getType();
         final int maxBufferSize = mManager.mOptions.maxBufferSize;
         if (mEvents == null) {
             if (sVerbose) {
@@ -528,9 +584,11 @@
         flush(reason);
     }
 
+    /** @hide */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     @Override
     @UiThread
-    void flush(@FlushReason int reason) {
+    public void flush(@FlushReason int reason) {
         if (mEvents == null || mEvents.size() == 0) {
             if (sVerbose) {
                 Log.v(TAG, "Don't flush for empty event buffer.");
@@ -544,6 +602,10 @@
             return;
         }
 
+        if (!isContentCaptureReceiverEnabled()) {
+            return;
+        }
+
         if (mDirectServiceInterface == null) {
             if (sVerbose) {
                 Log.v(TAG, "handleForceFlush(" + getDebugState(reason) + "): hold your horses, "
@@ -607,8 +669,10 @@
         return new ParceledListSlice<>(events);
     }
 
+    /** hide */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
     @UiThread
-    private void destroySession() {
+    public void destroySession() {
         if (sDebug) {
             Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
                     + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
@@ -626,12 +690,15 @@
             mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
         }
         mDirectServiceInterface = null;
+        mContentProtectionEventProcessor = null;
     }
 
     // TODO(b/122454205): once we support multiple sessions, we might need to move some of these
     // clearings out.
+    /** @hide */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
     @UiThread
-    private void resetSession(int newState) {
+    public void resetSession(int newState) {
         if (sVerbose) {
             Log.v(TAG, "handleResetSession(" + getActivityName() + "): from "
                     + getStateAsString(mState) + " to " + getStateAsString(newState));
@@ -651,6 +718,7 @@
             }
         }
         mDirectServiceInterface = null;
+        mContentProtectionEventProcessor = null;
         mHandler.removeMessages(MSG_FLUSH);
     }
 
@@ -878,4 +946,14 @@
     private String getDebugState(@FlushReason int reason) {
         return getDebugState() + ", reason=" + getFlushReasonAsString(reason);
     }
+
+    @UiThread
+    private boolean isContentProtectionReceiverEnabled() {
+        return mManager.mOptions.contentProtectionOptions.enableReceiver;
+    }
+
+    @UiThread
+    private boolean isContentCaptureReceiverEnabled() {
+        return mManager.mOptions.enableReceiver;
+    }
 }
diff --git a/core/java/android/view/contentcapture/ViewNode.java b/core/java/android/view/contentcapture/ViewNode.java
index 044a31f..f218995 100644
--- a/core/java/android/view/contentcapture/ViewNode.java
+++ b/core/java/android/view/contentcapture/ViewNode.java
@@ -480,6 +480,11 @@
         return mLocaleList;
     }
 
+    /** @hide */
+    public void setTextIdEntry(@NonNull String textIdEntry) {
+        mTextIdEntry = textIdEntry;
+    }
+
     private void writeSelfToParcel(@NonNull Parcel parcel, int parcelFlags) {
         long nodeFlags = mFlags;
 
diff --git a/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java
new file mode 100644
index 0000000..b44abf3
--- /dev/null
+++ b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.contentprotection;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.content.pm.ParceledListSlice;
+import android.os.Handler;
+import android.text.InputType;
+import android.util.Log;
+import android.view.contentcapture.ContentCaptureEvent;
+import android.view.contentcapture.IContentCaptureManager;
+import android.view.contentcapture.ViewNode;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.RingBuffer;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Main entry point for processing {@link ContentCaptureEvent} for the content protection flow.
+ *
+ * @hide
+ */
+public class ContentProtectionEventProcessor {
+
+    private static final String TAG = "ContentProtectionEventProcessor";
+
+    private static final List<Integer> PASSWORD_FIELD_INPUT_TYPES =
+            Collections.unmodifiableList(
+                    Arrays.asList(
+                            InputType.TYPE_NUMBER_VARIATION_PASSWORD,
+                            InputType.TYPE_TEXT_VARIATION_PASSWORD,
+                            InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
+                            InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD));
+
+    private static final List<String> PASSWORD_TEXTS =
+            Collections.unmodifiableList(
+                    Arrays.asList("password", "pass word", "code", "pin", "credential"));
+
+    private static final List<String> ADDITIONAL_SUSPICIOUS_TEXTS =
+            Collections.unmodifiableList(
+                    Arrays.asList("user", "mail", "phone", "number", "login", "log in", "sign in"));
+
+    private static final Duration MIN_DURATION_BETWEEN_FLUSHING = Duration.ofSeconds(3);
+
+    private static final String ANDROID_CLASS_NAME_PREFIX = "android.";
+
+    private static final Set<Integer> EVENT_TYPES_TO_STORE =
+            Collections.unmodifiableSet(
+                    new HashSet<>(
+                            Arrays.asList(
+                                    ContentCaptureEvent.TYPE_VIEW_APPEARED,
+                                    ContentCaptureEvent.TYPE_VIEW_DISAPPEARED,
+                                    ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED)));
+
+    private static final int RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS = 150;
+
+    @NonNull private final RingBuffer<ContentCaptureEvent> mEventBuffer;
+
+    @NonNull private final Handler mHandler;
+
+    @NonNull private final IContentCaptureManager mContentCaptureManager;
+
+    @NonNull private final String mPackageName;
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public boolean mPasswordFieldDetected = false;
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public boolean mSuspiciousTextDetected = false;
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    @Nullable
+    public Instant mLastFlushTime;
+
+    private int mResetLoginRemainingEventsToProcess;
+
+    public ContentProtectionEventProcessor(
+            @NonNull RingBuffer<ContentCaptureEvent> eventBuffer,
+            @NonNull Handler handler,
+            @NonNull IContentCaptureManager contentCaptureManager,
+            @NonNull String packageName) {
+        mEventBuffer = eventBuffer;
+        mHandler = handler;
+        mContentCaptureManager = contentCaptureManager;
+        mPackageName = packageName;
+    }
+
+    /** Main entry point for {@link ContentCaptureEvent} processing. */
+    @UiThread
+    public void processEvent(@NonNull ContentCaptureEvent event) {
+        if (EVENT_TYPES_TO_STORE.contains(event.getType())) {
+            storeEvent(event);
+        }
+        if (event.getType() == ContentCaptureEvent.TYPE_VIEW_APPEARED) {
+            processViewAppearedEvent(event);
+        }
+    }
+
+    @UiThread
+    private void storeEvent(@NonNull ContentCaptureEvent event) {
+        // Ensure receiver gets the package name which might not be set
+        ViewNode viewNode = (event.getViewNode() != null) ? event.getViewNode() : new ViewNode();
+        viewNode.setTextIdEntry(mPackageName);
+        event.setViewNode(viewNode);
+        mEventBuffer.append(event);
+    }
+
+    @UiThread
+    private void processViewAppearedEvent(@NonNull ContentCaptureEvent event) {
+        mPasswordFieldDetected |= isPasswordField(event);
+        mSuspiciousTextDetected |= isSuspiciousText(event);
+        if (mPasswordFieldDetected && mSuspiciousTextDetected) {
+            loginDetected();
+        } else {
+            maybeResetLoginFlags();
+        }
+    }
+
+    @UiThread
+    private void loginDetected() {
+        if (mLastFlushTime == null
+                || Instant.now().isAfter(mLastFlushTime.plus(MIN_DURATION_BETWEEN_FLUSHING))) {
+            flush();
+        }
+        resetLoginFlags();
+    }
+
+    @UiThread
+    private void resetLoginFlags() {
+        mPasswordFieldDetected = false;
+        mSuspiciousTextDetected = false;
+        mResetLoginRemainingEventsToProcess = 0;
+    }
+
+    @UiThread
+    private void maybeResetLoginFlags() {
+        if (mPasswordFieldDetected || mSuspiciousTextDetected) {
+            if (mResetLoginRemainingEventsToProcess <= 0) {
+                mResetLoginRemainingEventsToProcess = RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS;
+            } else {
+                mResetLoginRemainingEventsToProcess--;
+                if (mResetLoginRemainingEventsToProcess <= 0) {
+                    resetLoginFlags();
+                }
+            }
+        }
+    }
+
+    @UiThread
+    private void flush() {
+        mLastFlushTime = Instant.now();
+
+        // Note the thread annotations, do not move clearEvents to mHandler
+        ParceledListSlice<ContentCaptureEvent> events = clearEvents();
+        mHandler.post(() -> handlerOnLoginDetected(events));
+    }
+
+    @UiThread
+    @NonNull
+    private ParceledListSlice<ContentCaptureEvent> clearEvents() {
+        List<ContentCaptureEvent> events = Arrays.asList(mEventBuffer.toArray());
+        mEventBuffer.clear();
+        return new ParceledListSlice<>(events);
+    }
+
+    private void handlerOnLoginDetected(@NonNull ParceledListSlice<ContentCaptureEvent> events) {
+        try {
+            mContentCaptureManager.onLoginDetected(events);
+        } catch (Exception ex) {
+            Log.e(TAG, "Failed to flush events for: " + mPackageName, ex);
+        }
+    }
+
+    private boolean isPasswordField(@NonNull ContentCaptureEvent event) {
+        return isPasswordField(event.getViewNode());
+    }
+
+    private boolean isPasswordField(@Nullable ViewNode viewNode) {
+        if (viewNode == null) {
+            return false;
+        }
+        return isAndroidPasswordField(viewNode) || isWebViewPasswordField(viewNode);
+    }
+
+    private boolean isAndroidPasswordField(@NonNull ViewNode viewNode) {
+        if (!isAndroidViewNode(viewNode)) {
+            return false;
+        }
+        int inputType = viewNode.getInputType();
+        return PASSWORD_FIELD_INPUT_TYPES.stream()
+                .anyMatch(passwordInputType -> (inputType & passwordInputType) != 0);
+    }
+
+    private boolean isWebViewPasswordField(@NonNull ViewNode viewNode) {
+        if (viewNode.getClassName() != null) {
+            return false;
+        }
+        return isPasswordText(ContentProtectionUtils.getViewNodeText(viewNode));
+    }
+
+    private boolean isAndroidViewNode(@NonNull ViewNode viewNode) {
+        String className = viewNode.getClassName();
+        return className != null && className.startsWith(ANDROID_CLASS_NAME_PREFIX);
+    }
+
+    private boolean isSuspiciousText(@NonNull ContentCaptureEvent event) {
+        return isSuspiciousText(ContentProtectionUtils.getEventText(event))
+                || isSuspiciousText(ContentProtectionUtils.getViewNodeText(event));
+    }
+
+    private boolean isSuspiciousText(@Nullable String text) {
+        if (text == null) {
+            return false;
+        }
+        if (isPasswordText(text)) {
+            return true;
+        }
+        String lowerCaseText = text.toLowerCase();
+        return ADDITIONAL_SUSPICIOUS_TEXTS.stream()
+                .anyMatch(suspiciousText -> lowerCaseText.contains(suspiciousText));
+    }
+
+    private boolean isPasswordText(@Nullable String text) {
+        if (text == null) {
+            return false;
+        }
+        String lowerCaseText = text.toLowerCase();
+        return PASSWORD_TEXTS.stream()
+                .anyMatch(passwordText -> lowerCaseText.contains(passwordText));
+    }
+}
diff --git a/core/java/android/view/contentprotection/ContentProtectionUtils.java b/core/java/android/view/contentprotection/ContentProtectionUtils.java
new file mode 100644
index 0000000..9abf6f1
--- /dev/null
+++ b/core/java/android/view/contentprotection/ContentProtectionUtils.java
@@ -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 android.view.contentprotection;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.view.contentcapture.ContentCaptureEvent;
+import android.view.contentcapture.ViewNode;
+
+/**
+ * Utilities for reading data from {@link ContentCaptureEvent} and {@link ViewNode}.
+ *
+ * @hide
+ */
+public final class ContentProtectionUtils {
+
+    /** Returns the text extracted directly from the {@link ContentCaptureEvent}, if set. */
+    @Nullable
+    public static String getEventText(@NonNull ContentCaptureEvent event) {
+        CharSequence text = event.getText();
+        if (text == null) {
+            return null;
+        }
+        return text.toString();
+    }
+
+    /** Returns the text extracted from the event's {@link ViewNode}, if set. */
+    @Nullable
+    public static String getViewNodeText(@NonNull ContentCaptureEvent event) {
+        ViewNode viewNode = event.getViewNode();
+        if (viewNode == null) {
+            return null;
+        }
+        return getViewNodeText(viewNode);
+    }
+
+    /** Returns the text extracted directly from the {@link ViewNode}, if set. */
+    @Nullable
+    public static String getViewNodeText(@NonNull ViewNode viewNode) {
+        CharSequence text = viewNode.getText();
+        if (text == null) {
+            return null;
+        }
+        return text.toString();
+    }
+}
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 3cac1e5..3f308e6 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -4361,15 +4361,14 @@
      * @param icProto {@link InputConnection} call data in proto format.
      * @hide
      */
-    @GuardedBy("mH")
     public void dumpDebug(ProtoOutputStream proto, @Nullable byte[] icProto) {
-        if (!isImeSessionAvailableLocked()) {
-            return;
-        }
-
-        proto.write(DISPLAY_ID, mDisplayId);
-        final long token = proto.start(INPUT_METHOD_MANAGER);
         synchronized (mH) {
+            if (!isImeSessionAvailableLocked()) {
+                return;
+            }
+
+            proto.write(DISPLAY_ID, mDisplayId);
+            final long token = proto.start(INPUT_METHOD_MANAGER);
             proto.write(CUR_ID, mCurBindState.mImeId);
             proto.write(FULLSCREEN_MODE, mFullscreenMode);
             proto.write(ACTIVE, mActive);
diff --git a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
index aa9225b..e9d7b9b 100644
--- a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
+++ b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
@@ -1118,7 +1118,7 @@
             @InputConnection.CursorUpdateFilter int cursorUpdateFilter, int imeDisplayId) {
         final InputConnection ic = getInputConnection();
         if (ic == null || !isActive()) {
-            Log.w(TAG, "requestCursorAnchorInfo on inactive InputConnection");
+            Log.w(TAG, "requestCursorUpdates on inactive InputConnection");
             return false;
         }
         if (mParentInputMethodManager.mRequestCursorUpdateDisplayIdCheck.get()
diff --git a/core/java/android/view/inputmethod/TextAppearanceInfo.java b/core/java/android/view/inputmethod/TextAppearanceInfo.java
index 05717dd..7eee33f 100644
--- a/core/java/android/view/inputmethod/TextAppearanceInfo.java
+++ b/core/java/android/view/inputmethod/TextAppearanceInfo.java
@@ -238,7 +238,10 @@
                 .setFontFeatureSettings(textPaint.getFontFeatureSettings())
                 .setFontVariationSettings(textPaint.getFontVariationSettings())
                 .setTextScaleX(textPaint.getTextScaleX())
-                .setTextColor(textPaint.getColor())
+                // When there is a hint text (text length is 0), the text color should be the normal
+                // text color rather than hint text color.
+                .setTextColor(text.length() == 0
+                        ? textView.getCurrentTextColor() : textPaint.getColor())
                 .setLinkTextColor(textPaint.linkColor)
                 .setAllCaps(textView.isAllCaps())
                 .setFallbackLineSpacing(textView.isFallbackLineSpacing())
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index adeb889..6ad1960 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -4703,7 +4703,7 @@
 
     @Override
     public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
-        if (mFastScroll != null) {
+        if (mFastScroll != null && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
             PointerIcon pointerIcon = mFastScroll.onResolvePointerIcon(event, pointerIndex);
             if (pointerIcon != null) {
                 return pointerIcon;
diff --git a/core/java/android/widget/Button.java b/core/java/android/widget/Button.java
index 634cbe3..405099d 100644
--- a/core/java/android/widget/Button.java
+++ b/core/java/android/widget/Button.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
+import android.view.InputDevice;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.PointerIcon;
@@ -173,7 +174,8 @@
 
     @Override
     public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
-        if (getPointerIcon() == null && isClickable() && isEnabled()) {
+        if (getPointerIcon() == null && isClickable() && isEnabled()
+                && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
             return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
         }
         return super.onResolvePointerIcon(event, pointerIndex);
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 2dbff58..3da9e96 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -8105,6 +8105,16 @@
         private final Paint mHighlightPaint;
         private final Path mHighlightPath;
 
+        /**
+         * Whether it is in the progress of updating transformation method. It's needed because
+         * {@link TextView#setTransformationMethod(TransformationMethod)} will eventually call
+         * {@link TextView#setText(CharSequence)}.
+         * Because it normally should exit insert mode when {@link TextView#setText(CharSequence)}
+         * is called externally, we need this boolean to distinguish whether setText is triggered
+         * by setTransformation or not.
+         */
+        private boolean mUpdatingTransformationMethod;
+
         InsertModeController(@NonNull TextView textView) {
             mTextView = Objects.requireNonNull(textView);
             mIsInsertModeActive = false;
@@ -8112,16 +8122,10 @@
             mHighlightPaint = new Paint();
             mHighlightPath = new Path();
 
-            // The highlight color is supposed to be 12% of the color primary40. We can't
-            // directly access Material 3 theme. But because Material 3 sets the colorPrimary to
-            // be primary40, here we hardcoded it to be 12% of colorPrimary.
-            final TypedValue typedValue = new TypedValue();
-            mTextView.getContext().getTheme()
-                    .resolveAttribute(R.attr.colorPrimary, typedValue, true);
-            final int colorPrimary = typedValue.data;
-            final int highlightColor = ColorUtils.setAlphaComponent(colorPrimary,
-                    (int) (0.12f * Color.alpha(colorPrimary)));
-            mHighlightPaint.setColor(highlightColor);
+            // Insert mode highlight color is 20% opacity of the default text color.
+            int color = mTextView.getTextColors().getDefaultColor();
+            color = ColorUtils.setAlphaComponent(color, (int) (0.2f * Color.alpha(color)));
+            mHighlightPaint.setColor(color);
         }
 
         /**
@@ -8143,7 +8147,7 @@
             final boolean isSingleLine = mTextView.isSingleLine();
             mInsertModeTransformationMethod = new InsertModeTransformationMethod(offset,
                     isSingleLine, oldTransformationMethod);
-            mTextView.setTransformationMethodInternal(mInsertModeTransformationMethod);
+            setTransformationMethod(mInsertModeTransformationMethod, true);
             Selection.setSelection((Spannable) mTextView.getText(), offset);
 
             mIsInsertModeActive = true;
@@ -8151,6 +8155,10 @@
         }
 
         void exitInsertMode() {
+            exitInsertMode(true);
+        }
+
+        void exitInsertMode(boolean updateText) {
             if (!mIsInsertModeActive) return;
             if (mInsertModeTransformationMethod == null
                     || mInsertModeTransformationMethod != mTextView.getTransformationMethod()) {
@@ -8163,7 +8171,7 @@
             final int selectionEnd = mTextView.getSelectionEnd();
             final TransformationMethod oldTransformationMethod =
                     mInsertModeTransformationMethod.getOldTransformationMethod();
-            mTextView.setTransformationMethodInternal(oldTransformationMethod);
+            setTransformationMethod(oldTransformationMethod, updateText);
             Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
             mIsInsertModeActive = false;
         }
@@ -8184,6 +8192,32 @@
         }
 
         /**
+         * Update the TransformationMethod on the {@link TextView}.
+         * @param method the new method to be set on the {@link TextView}/
+         * @param updateText whether to update the text during setTransformationMethod call.
+         */
+        private void setTransformationMethod(TransformationMethod method, boolean updateText) {
+            mUpdatingTransformationMethod = true;
+            mTextView.setTransformationMethodInternal(method, updateText);
+            mUpdatingTransformationMethod = false;
+        }
+
+        /**
+         * Notify the InsertMode controller that the {@link TextView} is about to set its text.
+         */
+        void beforeSetText() {
+            // TextView#setText is called because our call to
+            // TextView#setTransformationMethodInternal in enterInsertMode() or exitInsertMode().
+            // Do nothing in this case.
+            if (mUpdatingTransformationMethod) {
+                return;
+            }
+            // TextView#setText is called externally. Exit InsertMode but don't update text again
+            // when calling setTransformationMethod.
+            exitInsertMode(/* updateText */ false);
+        }
+
+        /**
          * Notify the {@link InsertModeController} before the TextView's
          * {@link TransformationMethod} is updated. If it's not in the insert mode,
          * the given method is directly returned. Otherwise, it will wrap the given transformation
@@ -8211,6 +8245,9 @@
         return mInsertModeController.enterInsertMode(offset);
     }
 
+    /**
+     * Exit insert mode if this editor is in insert mode.
+     */
     void exitInsertMode() {
         if (mInsertModeController == null) return;
         mInsertModeController.exitInsertMode();
@@ -8223,7 +8260,7 @@
      */
     void setTransformationMethod(TransformationMethod method) {
         if (mInsertModeController == null || !mInsertModeController.mIsInsertModeActive) {
-            mTextView.setTransformationMethodInternal(method);
+            mTextView.setTransformationMethodInternal(method, /* updateText */ true);
             return;
         }
 
@@ -8232,11 +8269,19 @@
         final int selectionStart = mTextView.getSelectionStart();
         final int selectionEnd = mTextView.getSelectionEnd();
         method = mInsertModeController.updateTransformationMethod(method);
-        mTextView.setTransformationMethodInternal(method);
+        mTextView.setTransformationMethodInternal(method, /* updateText */ true);
         Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
     }
 
     /**
+     * Notify that the Editor that the associated {@link TextView} is about to set its text.
+     */
+    void beforeSetText() {
+        if (mInsertModeController == null) return;
+        mInsertModeController.beforeSetText();
+    }
+
+    /**
      * Initializes the nodeInfo with smart actions.
      */
     void onInitializeSmartActionsAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
diff --git a/core/java/android/widget/ImageButton.java b/core/java/android/widget/ImageButton.java
index e1b0c91..b6c5396c 100644
--- a/core/java/android/widget/ImageButton.java
+++ b/core/java/android/widget/ImageButton.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
+import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.PointerIcon;
 import android.widget.RemoteViews.RemoteView;
@@ -99,7 +100,8 @@
 
     @Override
     public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
-        if (getPointerIcon() == null && isClickable() && isEnabled()) {
+        if (getPointerIcon() == null && isClickable() && isEnabled()
+                && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
             return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
         }
         return super.onResolvePointerIcon(event, pointerIndex);
diff --git a/core/java/android/widget/RadialTimePickerView.java b/core/java/android/widget/RadialTimePickerView.java
index f3600b0..edf0f48 100644
--- a/core/java/android/widget/RadialTimePickerView.java
+++ b/core/java/android/widget/RadialTimePickerView.java
@@ -38,6 +38,7 @@
 import android.util.StateSet;
 import android.util.TypedValue;
 import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.PointerIcon;
 import android.view.View;
@@ -1060,9 +1061,11 @@
         if (!isEnabled()) {
             return null;
         }
-        final int degrees = getDegreesFromXY(event.getX(), event.getY(), false);
-        if (degrees != -1) {
-            return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+        if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+            final int degrees = getDegreesFromXY(event.getX(), event.getY(), false);
+            if (degrees != -1) {
+                return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+            }
         }
         return super.onResolvePointerIcon(event, pointerIndex);
     }
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 7f96266..bd7f5a0 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -232,6 +232,7 @@
     private static final int NIGHT_MODE_REFLECTION_ACTION_TAG = 30;
     private static final int SET_REMOTE_COLLECTION_ITEMS_ADAPTER_TAG = 31;
     private static final int ATTRIBUTE_REFLECTION_ACTION_TAG = 32;
+    private static final int SET_REMOTE_ADAPTER_TAG = 33;
 
     /** @hide **/
     @IntDef(prefix = "MARGIN_", value = {
@@ -412,6 +413,13 @@
     /** Class cookies of the Parcel this instance was read from. */
     private Map<Class, Object> mClassCookies;
 
+    /**
+     * {@link LayoutInflater.Factory2} which will be passed into a {@link LayoutInflater} instance
+     * used by this class.
+     */
+    @Nullable
+    private LayoutInflater.Factory2 mLayoutInflaterFactory2;
+
     private static final InteractionHandler DEFAULT_INTERACTION_HANDLER =
             (view, pendingIntent, response) ->
                     startPendingIntent(view, pendingIntent, response.getLaunchOptions(view));
@@ -431,6 +439,29 @@
     }
 
     /**
+     * Sets {@link LayoutInflater.Factory2} to be passed into {@link LayoutInflater} used
+     * by this class instance. It has to be set before the views are inflated to have any effect.
+     *
+     * The factory callbacks will be called on the background thread so the implementation needs
+     * to be thread safe.
+     *
+     * @hide
+     */
+    public void setLayoutInflaterFactory(@Nullable LayoutInflater.Factory2 factory) {
+        mLayoutInflaterFactory2 = factory;
+    }
+
+    /**
+     * Returns currently set {@link LayoutInflater.Factory2}.
+     *
+     * @hide
+     */
+    @Nullable
+    public LayoutInflater.Factory2 getLayoutInflaterFactory() {
+        return mLayoutInflaterFactory2;
+    }
+
+    /**
      * Reduces all images and ensures that they are all below the given sizes.
      *
      * @param maxWidth the maximum width allowed
@@ -960,6 +991,11 @@
             return SET_REMOTE_VIEW_ADAPTER_LIST_TAG;
         }
 
+        @Override
+        public String getUniqueKey() {
+            return (SET_REMOTE_ADAPTER_TAG + "_" + viewId);
+        }
+
         int viewTypeCount;
         ArrayList<RemoteViews> list;
     }
@@ -1082,6 +1118,11 @@
         public int getActionTag() {
             return SET_REMOTE_COLLECTION_ITEMS_ADAPTER_TAG;
         }
+
+        @Override
+        public String getUniqueKey() {
+            return (SET_REMOTE_ADAPTER_TAG + "_" + viewId);
+        }
     }
 
     private class SetRemoteViewsAdapterIntent extends Action {
@@ -5648,6 +5689,9 @@
         // we don't add a filter to the static version returned by getSystemService.
         inflater = inflater.cloneInContext(inflationContext);
         inflater.setFilter(shouldUseStaticFilter() ? INFLATER_FILTER : this);
+        if (mLayoutInflaterFactory2 != null) {
+            inflater.setFactory2(mLayoutInflaterFactory2);
+        }
         View v = inflater.inflate(rv.getLayoutId(), parent, false);
         if (mViewId != View.NO_ID) {
             v.setId(mViewId);
diff --git a/core/java/android/widget/SimpleMonthView.java b/core/java/android/widget/SimpleMonthView.java
index 6c53a44..1317b51 100644
--- a/core/java/android/widget/SimpleMonthView.java
+++ b/core/java/android/widget/SimpleMonthView.java
@@ -39,6 +39,7 @@
 import android.util.IntArray;
 import android.util.MathUtils;
 import android.util.StateSet;
+import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.PointerIcon;
@@ -1041,12 +1042,15 @@
         if (!isEnabled()) {
             return null;
         }
-        // Add 0.5f to event coordinates to match the logic in onTouchEvent.
-        final int x = (int) (event.getX() + 0.5f);
-        final int y = (int) (event.getY() + 0.5f);
-        final int dayUnderPointer = getDayAtLocation(x, y);
-        if (dayUnderPointer >= 0) {
-            return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+
+        if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+            // Add 0.5f to event coordinates to match the logic in onTouchEvent.
+            final int x = (int) (event.getX() + 0.5f);
+            final int y = (int) (event.getY() + 0.5f);
+            final int dayUnderPointer = getDayAtLocation(x, y);
+            if (dayUnderPointer >= 0) {
+                return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+            }
         }
         return super.onResolvePointerIcon(event, pointerIndex);
     }
diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java
index ad431ef..ecc41a5 100644
--- a/core/java/android/widget/Spinner.java
+++ b/core/java/android/widget/Spinner.java
@@ -38,6 +38,7 @@
 import android.util.Log;
 import android.view.ContextThemeWrapper;
 import android.view.Gravity;
+import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.PointerIcon;
 import android.view.View;
@@ -935,7 +936,8 @@
 
     @Override
     public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
-        if (getPointerIcon() == null && isClickable() && isEnabled()) {
+        if (getPointerIcon() == null && isClickable() && isEnabled()
+                && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
             return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
         }
         return super.onResolvePointerIcon(event, pointerIndex);
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index db7d484..27d8a8f 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -156,7 +156,6 @@
 import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
-import android.util.FeatureFlagUtils;
 import android.util.IntArray;
 import android.util.Log;
 import android.util.SparseIntArray;
@@ -831,11 +830,6 @@
     private int mLineBreakStyle = DEFAULT_LINE_BREAK_STYLE;
     private int mLineBreakWordStyle = DEFAULT_LINE_BREAK_WORD_STYLE;
 
-    // The auto option for LINE_BREAK_WORD_STYLE_PHRASE may not be applied in recycled view due to
-    // one-way flag flipping. This is a tentative limitation during experiment and will not have the
-    // issue once this is finalized to LINE_BREAK_WORD_STYLE_PHRASE_AUTO option.
-    private boolean mUserSpeficiedLineBreakwordStyle = false;
-
     // This is used to reflect the current user preference for changing font weight and making text
     // more bold.
     private int mFontWeightAdjustment;
@@ -1546,9 +1540,6 @@
                     break;
 
                 case com.android.internal.R.styleable.TextView_lineBreakWordStyle:
-                    if (a.hasValue(attr)) {
-                        mUserSpeficiedLineBreakwordStyle = true;
-                    }
                     mLineBreakWordStyle = a.getInt(attr,
                             LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE);
                     break;
@@ -2795,11 +2786,22 @@
         if (mEditor != null) {
             mEditor.setTransformationMethod(method);
         } else {
-            setTransformationMethodInternal(method);
+            setTransformationMethodInternal(method, /* updateText */ true);
         }
     }
 
-    void setTransformationMethodInternal(@Nullable TransformationMethod method) {
+    /**
+     * Set the transformation that is applied to the text that this TextView is displaying,
+     * optionally call the setText.
+     * @param method the new transformation method to be set.
+     * @param updateText whether the call {@link #setText} which will update the TextView to display
+     *                   the new content. This method is helpful when updating
+     *                   {@link TransformationMethod} inside {@link #setText}. It should only be
+     *                   false if text will be updated immediately after this call, otherwise the
+     *                   TextView will enter an inconsistent state.
+     */
+    void setTransformationMethodInternal(@Nullable TransformationMethod method,
+            boolean updateText) {
         if (method == mTransformation) {
             // Avoid the setText() below if the transformation is
             // the same.
@@ -2821,7 +2823,9 @@
             mAllowTransformationLengthChange = false;
         }
 
-        setText(mText);
+        if (updateText) {
+            setText(mText);
+        }
 
         if (hasPasswordTransformationMethod()) {
             notifyViewAccessibilityStateChangedIfNeeded(
@@ -4337,7 +4341,6 @@
                     break;
                 case com.android.internal.R.styleable.TextAppearance_lineBreakWordStyle:
                     attributes.mHasLineBreakWordStyle = true;
-                    mUserSpeficiedLineBreakwordStyle = true;
                     attributes.mLineBreakWordStyle =
                             appearance.getInt(attr, attributes.mLineBreakWordStyle);
                     break;
@@ -5073,7 +5076,6 @@
      * @param lineBreakWordStyle The line-break word style for the text.
      */
     public void setLineBreakWordStyle(@LineBreakConfig.LineBreakWordStyle int lineBreakWordStyle) {
-        mUserSpeficiedLineBreakwordStyle = true;
         if (mLineBreakWordStyle != lineBreakWordStyle) {
             mLineBreakWordStyle = lineBreakWordStyle;
             if (mLayout != null) {
@@ -5109,12 +5111,8 @@
      * @see PrecomputedText
      */
     public @NonNull PrecomputedText.Params getTextMetricsParams() {
-        final boolean autoPhraseBreaking =
-                !mUserSpeficiedLineBreakwordStyle && FeatureFlagUtils.isEnabled(mContext,
-                        FeatureFlagUtils.SETTINGS_AUTO_TEXT_WRAPPING);
         return new PrecomputedText.Params(new TextPaint(mTextPaint),
-                LineBreakConfig.getLineBreakConfig(mLineBreakStyle, mLineBreakWordStyle,
-                        autoPhraseBreaking),
+                LineBreakConfig.getLineBreakConfig(mLineBreakStyle, mLineBreakWordStyle),
                 getTextDirectionHeuristic(),
                 mBreakStrategy, mHyphenationFrequency);
     }
@@ -5134,7 +5132,6 @@
         LineBreakConfig lineBreakConfig = params.getLineBreakConfig();
         mLineBreakStyle = lineBreakConfig.getLineBreakStyle();
         mLineBreakWordStyle = lineBreakConfig.getLineBreakWordStyle();
-        mUserSpeficiedLineBreakwordStyle = true;
         if (mLayout != null) {
             nullLayouts();
             requestLayout();
@@ -7000,6 +6997,9 @@
     @UnsupportedAppUsage
     private void setText(CharSequence text, BufferType type,
                          boolean notifyBefore, int oldlen) {
+        if (mEditor != null) {
+            mEditor.beforeSetText();
+        }
         mTextSetFromXmlOrResourceId = false;
         if (text == null) {
             text = "";
@@ -7061,13 +7061,10 @@
             if (mTextDir == null) {
                 mTextDir = getTextDirectionHeuristic();
             }
-            final boolean autoPhraseBreaking =
-                    !mUserSpeficiedLineBreakwordStyle && FeatureFlagUtils.isEnabled(mContext,
-                            FeatureFlagUtils.SETTINGS_AUTO_TEXT_WRAPPING);
             final @PrecomputedText.Params.CheckResultUsableResult int checkResult =
                     precomputed.getParams().checkResultUsable(getPaint(), mTextDir, mBreakStrategy,
                             mHyphenationFrequency, LineBreakConfig.getLineBreakConfig(
-                                    mLineBreakStyle, mLineBreakWordStyle, autoPhraseBreaking));
+                                    mLineBreakStyle, mLineBreakWordStyle));
             switch (checkResult) {
                 case PrecomputedText.Params.UNUSABLE:
                     throw new IllegalArgumentException(
@@ -9223,18 +9220,20 @@
 
     @Override
     public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
-        if (mSpannable != null && mLinksClickable) {
-            final float x = event.getX(pointerIndex);
-            final float y = event.getY(pointerIndex);
-            final int offset = getOffsetForPosition(x, y);
-            final ClickableSpan[] clickables = mSpannable.getSpans(offset, offset,
-                    ClickableSpan.class);
-            if (clickables.length > 0) {
-                return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_HAND);
+        if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+            if (mSpannable != null && mLinksClickable) {
+                final float x = event.getX(pointerIndex);
+                final float y = event.getY(pointerIndex);
+                final int offset = getOffsetForPosition(x, y);
+                final ClickableSpan[] clickables = mSpannable.getSpans(offset, offset,
+                        ClickableSpan.class);
+                if (clickables.length > 0) {
+                    return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_HAND);
+                }
             }
-        }
-        if (isTextSelectable() || isTextEditable()) {
-            return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_TEXT);
+            if (isTextSelectable() || isTextEditable()) {
+                return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_TEXT);
+            }
         }
         return super.onResolvePointerIcon(event, pointerIndex);
     }
@@ -10622,9 +10621,6 @@
             }
             // TODO: code duplication with makeSingleLayout()
             if (mHintLayout == null) {
-                final boolean autoPhraseBreaking =
-                        !mUserSpeficiedLineBreakwordStyle && FeatureFlagUtils.isEnabled(mContext,
-                                FeatureFlagUtils.SETTINGS_AUTO_TEXT_WRAPPING);
                 StaticLayout.Builder builder = StaticLayout.Builder.obtain(mHint, 0,
                         mHint.length(), mTextPaint, hintWidth)
                         .setAlignment(alignment)
@@ -10637,7 +10633,7 @@
                         .setJustificationMode(mJustificationMode)
                         .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE)
                         .setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
-                                mLineBreakStyle, mLineBreakWordStyle, autoPhraseBreaking));
+                                mLineBreakStyle, mLineBreakWordStyle));
                 if (shouldEllipsize) {
                     builder.setEllipsize(mEllipsize)
                             .setEllipsizedWidth(ellipsisWidth);
@@ -10741,9 +10737,6 @@
             }
         }
         if (result == null) {
-            final boolean autoPhraseBreaking =
-                    !mUserSpeficiedLineBreakwordStyle && FeatureFlagUtils.isEnabled(mContext,
-                            FeatureFlagUtils.SETTINGS_AUTO_TEXT_WRAPPING);
             StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
                     0, mTransformed.length(), mTextPaint, wantWidth)
                     .setAlignment(alignment)
@@ -10756,7 +10749,7 @@
                     .setJustificationMode(mJustificationMode)
                     .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE)
                     .setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
-                            mLineBreakStyle, mLineBreakWordStyle, autoPhraseBreaking));
+                            mLineBreakStyle, mLineBreakWordStyle));
             if (shouldEllipsize) {
                 builder.setEllipsize(effectiveEllipsize)
                         .setEllipsizedWidth(ellipsisWidth);
@@ -11114,9 +11107,6 @@
 
         final StaticLayout.Builder layoutBuilder = StaticLayout.Builder.obtain(
                 text, 0, text.length(),  mTempTextPaint, Math.round(availableSpace.right));
-        final boolean autoPhraseBreaking =
-                !mUserSpeficiedLineBreakwordStyle && FeatureFlagUtils.isEnabled(mContext,
-                        FeatureFlagUtils.SETTINGS_AUTO_TEXT_WRAPPING);
         layoutBuilder.setAlignment(getLayoutAlignment())
                 .setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier())
                 .setIncludePad(getIncludeFontPadding())
@@ -11127,7 +11117,7 @@
                 .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE)
                 .setTextDirection(getTextDirectionHeuristic())
                 .setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
-                        mLineBreakStyle, mLineBreakWordStyle, autoPhraseBreaking));
+                        mLineBreakStyle, mLineBreakWordStyle));
 
         final StaticLayout layout = layoutBuilder.build();
 
@@ -13809,13 +13799,14 @@
     }
 
     /**
-     * Helper method to set {@code rect} to the text content's non-clipped area in the view's
-     * coordinates.
+     * Helper method to set {@code rect} to this TextView's non-clipped area in its own coordinates.
+     * This method obtains the view's visible rectangle whereas the method
+     * {@link #getContentVisibleRect} returns the text layout's visible rectangle.
      *
      * @return true if at least part of the text content is visible; false if the text content is
      * completely clipped or translated out of the visible area.
      */
-    private boolean getContentVisibleRect(Rect rect) {
+    private boolean getViewVisibleRect(Rect rect) {
         if (!getLocalVisibleRect(rect)) {
             return false;
         }
@@ -13824,6 +13815,20 @@
         // view's coordinates. So we need to offset it with the negative scrolled amount to convert
         // it to view's coordinate.
         rect.offset(-getScrollX(), -getScrollY());
+        return true;
+    }
+
+    /**
+     * Helper method to set {@code rect} to the text content's non-clipped area in the view's
+     * coordinates.
+     *
+     * @return true if at least part of the text content is visible; false if the text content is
+     * completely clipped or translated out of the visible area.
+     */
+    private boolean getContentVisibleRect(Rect rect) {
+        if (!getViewVisibleRect(rect)) {
+            return false;
+        }
         // Clip the view's visible rect with the text layout's visible rect.
         return rect.intersect(getCompoundPaddingLeft(), getCompoundPaddingTop(),
                 getWidth() - getCompoundPaddingRight(), getHeight() - getCompoundPaddingBottom());
@@ -13953,14 +13958,25 @@
         builder.setMatrix(viewToScreenMatrix);
 
         if (includeEditorBounds) {
-            final RectF editorBounds = new RectF();
-            editorBounds.set(0 /* left */, 0 /* top */,
-                    getWidth(), getHeight());
-            final RectF handwritingBounds = new RectF(
-                    -getHandwritingBoundsOffsetLeft(),
-                    -getHandwritingBoundsOffsetTop(),
-                    getWidth() + getHandwritingBoundsOffsetRight(),
-                    getHeight() + getHandwritingBoundsOffsetBottom());
+            if (mTempRect == null) {
+                mTempRect = new Rect();
+            }
+            final Rect bounds = mTempRect;
+            final RectF editorBounds;
+            final RectF handwritingBounds;
+            if (getViewVisibleRect(bounds)) {
+                editorBounds = new RectF(bounds);
+                handwritingBounds = new RectF(editorBounds);
+                handwritingBounds.top -= getHandwritingBoundsOffsetTop();
+                handwritingBounds.left -= getHandwritingBoundsOffsetLeft();
+                handwritingBounds.bottom += getHandwritingBoundsOffsetBottom();
+                handwritingBounds.right += getHandwritingBoundsOffsetRight();
+            } else {
+                // The editor is not visible at all, return empty rectangles. We still need to
+                // return an EditorBoundsInfo because IME has subscribed the EditorBoundsInfo.
+                editorBounds = new RectF();
+                handwritingBounds = new RectF();
+            }
             EditorBoundsInfo.Builder boundsBuilder = new EditorBoundsInfo.Builder();
             EditorBoundsInfo editorBoundsInfo = boundsBuilder.setEditorBounds(editorBounds)
                     .setHandwritingBounds(handwritingBounds).build();
diff --git a/core/java/android/window/WindowInfosListenerForTest.java b/core/java/android/window/WindowInfosListenerForTest.java
index 25bf85c..7fc55a7 100644
--- a/core/java/android/window/WindowInfosListenerForTest.java
+++ b/core/java/android/window/WindowInfosListenerForTest.java
@@ -87,6 +87,14 @@
             this.isTrustedOverlay = (inputConfig & InputConfig.TRUSTED_OVERLAY) != 0;
             this.isVisible = (inputConfig & InputConfig.NOT_VISIBLE) == 0;
         }
+
+        @Override
+        public String toString() {
+            return name + ", frame=" + bounds
+                    + ", isVisible=" + isVisible
+                    + ", isTrustedOverlay=" + isTrustedOverlay
+                    + ", token=" + windowToken;
+        }
     }
 
     private static final String TAG = "WindowInfosListenerForTest";
diff --git a/core/java/android/window/WindowMetricsController.java b/core/java/android/window/WindowMetricsController.java
index 954f686..2858f0a 100644
--- a/core/java/android/window/WindowMetricsController.java
+++ b/core/java/android/window/WindowMetricsController.java
@@ -145,13 +145,13 @@
         for (int i = 0; i < possibleDisplayInfos.size(); i++) {
             currentDisplayInfo = possibleDisplayInfos.get(i);
 
-            // Calculate max bounds for this rotation and state.
-            Rect maxBounds = new Rect(0, 0, currentDisplayInfo.logicalWidth,
-                    currentDisplayInfo.logicalHeight);
+            // Calculate max bounds for natural rotation and state.
+            Rect maxBounds = new Rect(0, 0, currentDisplayInfo.getNaturalWidth(),
+                    currentDisplayInfo.getNaturalHeight());
 
-            // Calculate insets for the rotated max bounds.
+            // Calculate insets for the natural max bounds.
             final boolean isScreenRound = (currentDisplayInfo.flags & Display.FLAG_ROUND) != 0;
-            // Initialize insets based upon display rotation. Note any window-provided insets
+            // Initialize insets based on Surface.ROTATION_0. Note any window-provided insets
             // will not be set.
             windowInsets = getWindowInsetsFromServerForDisplay(
                     currentDisplayInfo.displayId, null /* token */,
diff --git a/core/java/android/window/WindowProviderService.java b/core/java/android/window/WindowProviderService.java
index f2ae973..611da3c 100644
--- a/core/java/android/window/WindowProviderService.java
+++ b/core/java/android/window/WindowProviderService.java
@@ -34,6 +34,7 @@
 import android.hardware.display.DisplayManager;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.util.Log;
 import android.view.Display;
 import android.view.WindowManager;
 import android.view.WindowManager.LayoutParams.WindowType;
@@ -52,6 +53,8 @@
 @UiContext
 public abstract class WindowProviderService extends Service implements WindowProvider {
 
+    private static final String TAG = WindowProviderService.class.getSimpleName();
+
     private final Bundle mOptions;
     private final WindowTokenClient mWindowToken = new WindowTokenClient();
     private final WindowContextController mController = new WindowContextController(mWindowToken);
@@ -194,8 +197,16 @@
     public final Context createServiceBaseContext(ActivityThread mainThread,
             LoadedApk packageInfo) {
         final Context context = super.createServiceBaseContext(mainThread, packageInfo);
-        final Display display = context.getSystemService(DisplayManager.class)
-                .getDisplay(getInitialDisplayId());
+        final DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+        final int initialDisplayId = getInitialDisplayId();
+        Display display = displayManager.getDisplay(initialDisplayId);
+        // Fallback to use the default display if the initial display to start WindowProviderService
+        // is detached.
+        if (display == null) {
+            Log.e(TAG, "Display with id " + initialDisplayId + " not found, falling back to "
+                    + "DEFAULT_DISPLAY");
+            display = displayManager.getDisplay(DEFAULT_DISPLAY);
+        }
         return context.createTokenContext(mWindowToken, display);
     }
 
diff --git a/core/java/com/android/internal/accessibility/common/MagnificationConstants.java b/core/java/com/android/internal/accessibility/common/MagnificationConstants.java
index 94c230b..2c49303 100644
--- a/core/java/com/android/internal/accessibility/common/MagnificationConstants.java
+++ b/core/java/com/android/internal/accessibility/common/MagnificationConstants.java
@@ -17,7 +17,7 @@
 package com.android.internal.accessibility.common;
 
 /**
- * Collection of common constants for accessibility shortcut.
+ * Collection of common constants for accessibility magnification.
  */
 public final class MagnificationConstants {
     private MagnificationConstants() {}
@@ -27,4 +27,10 @@
      * the min value, there will be no obvious magnification effect.
      */
     public static final float PERSISTED_SCALE_MIN_VALUE = 1.3f;
+
+    /** Minimum supported value for magnification scale. */
+    public static final float SCALE_MIN_VALUE = 1.0f;
+
+    /** Maximum supported value for magnification scale. */
+    public static final float SCALE_MAX_VALUE = 8.0f;
 }
diff --git a/core/java/com/android/internal/app/AssistUtils.java b/core/java/com/android/internal/app/AssistUtils.java
index 57cc38c..0ea8014 100644
--- a/core/java/com/android/internal/app/AssistUtils.java
+++ b/core/java/com/android/internal/app/AssistUtils.java
@@ -59,6 +59,8 @@
     public static final int INVOCATION_TYPE_POWER_BUTTON_LONG_PRESS = 6;
     /** value for INVOCATION_TYPE_KEY: press on physcial assistant button */
     public static final int INVOCATION_TYPE_ASSIST_BUTTON = 7;
+    /** value for INVOCATION_TYPE_KEY: long press on nav handle */
+    public static final int INVOCATION_TYPE_NAV_HANDLE_LONG_PRESS = 8;
 
     private final Context mContext;
     private final IVoiceInteractionManagerService mVoiceInteractionManagerService;
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index dd52de4..9ffccb3 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -574,6 +574,11 @@
      */
     public static final String COMBINED_BROADCAST_ENABLED = "combined_broadcast_enabled";
 
+    /**
+     * (boolean) Whether to allow cursor hover states for certain elements.
+     */
+    public static final String CURSOR_HOVER_STATES_ENABLED = "cursor_hover_states_enabled";
+
     private SystemUiDeviceConfigFlags() {
     }
 }
diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
index c9e7600..6b8ae94 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
@@ -85,6 +85,10 @@
         /** Gating the holding of WakeLocks until NLSes are told about a new notification. */
         public static final Flag WAKE_LOCK_FOR_POSTING_NOTIFICATION =
                 devFlag("persist.sysui.notification.wake_lock_for_posting_notification");
+
+        /** Gating storing NotificationRankingUpdate ranking map in shared memory. */
+        public static final Flag RANKING_UPDATE_ASHMEM = devFlag(
+                "persist.sysui.notification.ranking_update_ashmem");
     }
 
     //// == End of flags.  Everything below this line is the implementation. == ////
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index e530aec..92427ec 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -28,6 +28,7 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__IME_INSETS_ANIMATION;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_ALL_APPS_SCROLL;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_CLOSE_TO_HOME;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_CLOSE_TO_PIP;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_LAUNCH_FROM_ICON;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_LAUNCH_FROM_RECENTS;
@@ -68,6 +69,7 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_APP_LAUNCH_FROM_SETTINGS_BUTTON;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_CLEAR_ALL;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_DIALOG_OPEN;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_EXPAND_FROM_STATUS_BAR;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_HEADS_UP_APPEAR;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_HEADS_UP_DISAPPEAR;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_NOTIFICATION_ADD;
@@ -258,8 +260,17 @@
     public static final int CUJ_IME_INSETS_ANIMATION = 69;
     public static final int CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION = 70;
     public static final int CUJ_LAUNCHER_OPEN_SEARCH_RESULT = 71;
+    // 72 - 77 are reserved for b/281564325.
 
-    private static final int LAST_CUJ = CUJ_LAUNCHER_OPEN_SEARCH_RESULT;
+    /**
+     * In some cases when we do not have any end-target, we play a simple slide-down animation.
+     * eg: Open an app from Overview/Task switcher such that there is no home-screen icon.
+     * eg: Exit the app using back gesture.
+     */
+    public static final int CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK = 78;
+    public static final int CUJ_SHADE_EXPAND_FROM_STATUS_BAR = 79;
+
+    private static final int LAST_CUJ = CUJ_SHADE_EXPAND_FROM_STATUS_BAR;
     private static final int NO_STATSD_LOGGING = -1;
 
     // Used to convert CujType to InteractionType enum value for statsd logging.
@@ -340,6 +351,15 @@
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_IME_INSETS_ANIMATION] = UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__IME_INSETS_ANIMATION;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION] = UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_CLOCK_MOVE_ANIMATION;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_OPEN_SEARCH_RESULT] = UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_OPEN_SEARCH_RESULT;
+        // 72 - 77 are reserved for b/281564325.
+        CUJ_TO_STATSD_INTERACTION_TYPE[72] = NO_STATSD_LOGGING;
+        CUJ_TO_STATSD_INTERACTION_TYPE[73] = NO_STATSD_LOGGING;
+        CUJ_TO_STATSD_INTERACTION_TYPE[74] = NO_STATSD_LOGGING;
+        CUJ_TO_STATSD_INTERACTION_TYPE[75] = NO_STATSD_LOGGING;
+        CUJ_TO_STATSD_INTERACTION_TYPE[76] = NO_STATSD_LOGGING;
+        CUJ_TO_STATSD_INTERACTION_TYPE[77] = NO_STATSD_LOGGING;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK] = UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_SHADE_EXPAND_FROM_STATUS_BAR] = UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_EXPAND_FROM_STATUS_BAR;
     }
 
     private static class InstanceHolder {
@@ -439,6 +459,8 @@
             CUJ_IME_INSETS_ANIMATION,
             CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION,
             CUJ_LAUNCHER_OPEN_SEARCH_RESULT,
+            CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK,
+            CUJ_SHADE_EXPAND_FROM_STATUS_BAR,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
@@ -1050,6 +1072,10 @@
                 return "LOCKSCREEN_CLOCK_MOVE_ANIMATION";
             case CUJ_LAUNCHER_OPEN_SEARCH_RESULT:
                 return "LAUNCHER_OPEN_SEARCH_RESULT";
+            case CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK:
+                return "LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK";
+            case CUJ_SHADE_EXPAND_FROM_STATUS_BAR:
+                return "SHADE_EXPAND_FROM_STATUS_BAR";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index af1fdd7..bb86801 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -232,7 +232,7 @@
     private boolean mLastHasRightStableInset = false;
     private boolean mLastHasLeftStableInset = false;
     private int mLastWindowFlags = 0;
-    private boolean mLastShouldAlwaysConsumeSystemBars = false;
+    private @InsetsType int mLastForceConsumingTypes = 0;
     private @InsetsType int mLastSuppressScrimTypes = 0;
 
     private int mRootScrollY = 0;
@@ -1111,19 +1111,19 @@
                     : controller.getSystemBarsAppearance();
 
             if (insets != null) {
-                mLastShouldAlwaysConsumeSystemBars = insets.shouldAlwaysConsumeSystemBars();
+                mLastForceConsumingTypes = insets.getForceConsumingTypes();
 
-                final boolean clearsCompatInsets =
-                        clearsCompatInsets(attrs.type, attrs.flags,
-                                getResources().getConfiguration().windowConfiguration
-                                        .getWindowingMode())
-                        && !mLastShouldAlwaysConsumeSystemBars;
+                @InsetsType int compatInsetsTypes =
+                        WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout();
+                if (clearsCompatInsets(attrs.type, attrs.flags,
+                        getResources().getConfiguration().windowConfiguration.getWindowingMode())) {
+                    compatInsetsTypes &= mLastForceConsumingTypes;
+                }
                 final Insets stableBarInsets = insets.getInsetsIgnoringVisibility(
                         WindowInsets.Type.systemBars());
-                final Insets systemInsets = clearsCompatInsets
+                final Insets systemInsets = compatInsetsTypes == 0
                         ? Insets.NONE
-                        : Insets.min(insets.getInsets(WindowInsets.Type.systemBars()
-                                | WindowInsets.Type.displayCutout()), stableBarInsets);
+                        : Insets.min(insets.getInsets(compatInsetsTypes), stableBarInsets);
                 mLastTopInset = systemInsets.top;
                 mLastBottomInset = systemInsets.bottom;
                 mLastRightInset = systemInsets.right;
@@ -1208,7 +1208,8 @@
                         && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
                         && decorFitsSystemWindows
                         && !hideNavigation)
-                || (mLastShouldAlwaysConsumeSystemBars && hideNavigation);
+                || ((mLastForceConsumingTypes & WindowInsets.Type.navigationBars()) != 0
+                        && hideNavigation);
 
         boolean consumingNavBar =
                 ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
@@ -1224,13 +1225,15 @@
         boolean fullscreen = (sysUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) != 0
                 || (attrs.flags & FLAG_FULLSCREEN) != 0
                 || (requestedVisibleTypes & WindowInsets.Type.statusBars()) == 0;
-        boolean consumingStatusBar = (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0
-                && decorFitsSystemWindows
-                && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0
-                && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0
-                && mForceWindowDrawsBarBackgrounds
-                && mLastTopInset != 0
-                || (mLastShouldAlwaysConsumeSystemBars && fullscreen);
+        boolean consumingStatusBar =
+                ((sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0
+                        && decorFitsSystemWindows
+                        && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0
+                        && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0
+                        && mForceWindowDrawsBarBackgrounds
+                        && mLastTopInset != 0)
+                || ((mLastForceConsumingTypes & WindowInsets.Type.statusBars()) != 0
+                        && fullscreen);
 
         int consumedTop = consumingStatusBar ? mLastTopInset : 0;
         int consumedRight = consumingNavBar ? mLastRightInset : 0;
@@ -1434,9 +1437,9 @@
     private void updateColorViewInt(final ColorViewState state, int color, int dividerColor,
             int size, boolean verticalBar, boolean seascape, int sideMargin, boolean animate,
             boolean force, @InsetsType int requestedVisibleTypes) {
+        final @InsetsType int type = state.attributes.insetsType;
         state.present = state.attributes.isPresent(
-                (requestedVisibleTypes & state.attributes.insetsType) != 0
-                        || mLastShouldAlwaysConsumeSystemBars,
+                (requestedVisibleTypes & type) != 0 || (mLastForceConsumingTypes & type) != 0,
                 mWindow.getAttributes().flags, force);
         boolean show = state.attributes.isVisible(state.present, color,
                 mWindow.getAttributes().flags, force);
diff --git a/core/java/com/android/internal/protolog/common/ProtoLog.java b/core/java/com/android/internal/protolog/common/ProtoLog.java
index 93765cd..8870096 100644
--- a/core/java/com/android/internal/protolog/common/ProtoLog.java
+++ b/core/java/com/android/internal/protolog/common/ProtoLog.java
@@ -16,6 +16,8 @@
 
 package com.android.internal.protolog.common;
 
+import android.util.Log;
+
 /**
  * ProtoLog API - exposes static logging methods. Usage of this API is similar
  * to {@code android.utils.Log} class. Instead of plain text log messages each call consists of
@@ -53,6 +55,9 @@
             throw new UnsupportedOperationException(
                     "ProtoLog calls MUST be processed with ProtoLogTool");
         }
+        if (group.isLogToLogcat()) {
+            Log.d(group.getTag(), String.format(messageString, args));
+        }
     }
 
     /**
@@ -68,6 +73,9 @@
             throw new UnsupportedOperationException(
                     "ProtoLog calls MUST be processed with ProtoLogTool");
         }
+        if (group.isLogToLogcat()) {
+            Log.v(group.getTag(), String.format(messageString, args));
+        }
     }
 
     /**
@@ -83,6 +91,9 @@
             throw new UnsupportedOperationException(
                     "ProtoLog calls MUST be processed with ProtoLogTool");
         }
+        if (group.isLogToLogcat()) {
+            Log.i(group.getTag(), String.format(messageString, args));
+        }
     }
 
     /**
@@ -98,6 +109,9 @@
             throw new UnsupportedOperationException(
                     "ProtoLog calls MUST be processed with ProtoLogTool");
         }
+        if (group.isLogToLogcat()) {
+            Log.w(group.getTag(), String.format(messageString, args));
+        }
     }
 
     /**
@@ -113,6 +127,9 @@
             throw new UnsupportedOperationException(
                     "ProtoLog calls MUST be processed with ProtoLogTool");
         }
+        if (group.isLogToLogcat()) {
+            Log.e(group.getTag(), String.format(messageString, args));
+        }
     }
 
     /**
@@ -128,5 +145,8 @@
             throw new UnsupportedOperationException(
                     "ProtoLog calls MUST be processed with ProtoLogTool");
         }
+        if (group.isLogToLogcat()) {
+            Log.wtf(group.getTag(), String.format(messageString, args));
+        }
     }
 }
diff --git a/core/java/com/android/internal/statusbar/IAppClipsService.aidl b/core/java/com/android/internal/statusbar/IAppClipsService.aidl
index 013d0d3..d6ab8bc 100644
--- a/core/java/com/android/internal/statusbar/IAppClipsService.aidl
+++ b/core/java/com/android/internal/statusbar/IAppClipsService.aidl
@@ -23,4 +23,6 @@
  */
 interface IAppClipsService {
     boolean canLaunchCaptureContentActivityForNote(in int taskId);
-}
\ No newline at end of file
+
+    int canLaunchCaptureContentActivityForNoteInternal(in int taskId);
+}
diff --git a/core/java/com/android/internal/util/TraceBuffer.java b/core/java/com/android/internal/util/TraceBuffer.java
index fcc77bd4..c23e902 100644
--- a/core/java/com/android/internal/util/TraceBuffer.java
+++ b/core/java/com/android/internal/util/TraceBuffer.java
@@ -18,6 +18,7 @@
 
 import android.util.proto.ProtoOutputStream;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.File;
@@ -39,12 +40,13 @@
  * {@hide}
  */
 public class TraceBuffer<P, S extends P, T extends P> {
-    private final Object mBufferLock = new Object();
-
     private final ProtoProvider<P, S, T> mProtoProvider;
+    @GuardedBy("this")
     private final Queue<T> mBuffer = new ArrayDeque<>();
     private final Consumer mProtoDequeuedCallback;
+    @GuardedBy("this")
     private int mBufferUsedSize;
+    @GuardedBy("this")
     private int mBufferCapacity;
 
     /**
@@ -115,18 +117,18 @@
         resetBuffer();
     }
 
-    public int getAvailableSpace() {
+    public synchronized int getAvailableSpace() {
         return mBufferCapacity - mBufferUsedSize;
     }
 
     /**
      * Returns buffer size.
      */
-    public int size() {
+    public synchronized int size() {
         return mBuffer.size();
     }
 
-    public void setCapacity(int capacity) {
+    public synchronized void setCapacity(int capacity) {
         mBufferCapacity = capacity;
     }
 
@@ -137,22 +139,19 @@
      * @throws IllegalStateException if the element cannot be added because it is larger
      *                               than the buffer size.
      */
-    public void add(T proto) {
+    public synchronized void add(T proto) {
         int protoLength = mProtoProvider.getItemSize(proto);
         if (protoLength > mBufferCapacity) {
             throw new IllegalStateException("Trace object too large for the buffer. Buffer size:"
                     + mBufferCapacity + " Object size: " + protoLength);
         }
-        synchronized (mBufferLock) {
-            discardOldest(protoLength);
-            mBuffer.add(proto);
-            mBufferUsedSize += protoLength;
-            mBufferLock.notify();
-        }
+        discardOldest(protoLength);
+        mBuffer.add(proto);
+        mBufferUsedSize += protoLength;
     }
 
     @VisibleForTesting
-    public boolean contains(byte[] other) {
+    public synchronized boolean contains(byte[] other) {
         return mBuffer.stream()
                 .anyMatch(p -> Arrays.equals(mProtoProvider.getBytes(p), other));
     }
@@ -160,15 +159,13 @@
     /**
      * Writes the trace buffer to disk inside the encapsulatingProto.
      */
-    public void writeTraceToFile(File traceFile, S encapsulatingProto)
+    public synchronized void writeTraceToFile(File traceFile, S encapsulatingProto)
             throws IOException {
-        synchronized (mBufferLock) {
-            traceFile.delete();
-            try (OutputStream os = new FileOutputStream(traceFile)) {
-                traceFile.setReadable(true /* readable */, false /* ownerOnly */);
-                mProtoProvider.write(encapsulatingProto, mBuffer, os);
-                os.flush();
-            }
+        traceFile.delete();
+        try (OutputStream os = new FileOutputStream(traceFile)) {
+            traceFile.setReadable(true /* readable */, false /* ownerOnly */);
+            mProtoProvider.write(encapsulatingProto, mBuffer, os);
+            os.flush();
         }
     }
 
@@ -199,31 +196,27 @@
     /**
      * Removes all elements from the buffer
      */
-    public void resetBuffer() {
-        synchronized (mBufferLock) {
-            if (mProtoDequeuedCallback != null) {
-                for (T item : mBuffer) {
-                    mProtoDequeuedCallback.accept(item);
-                }
+    public synchronized void resetBuffer() {
+        if (mProtoDequeuedCallback != null) {
+            for (T item : mBuffer) {
+                mProtoDequeuedCallback.accept(item);
             }
-            mBuffer.clear();
-            mBufferUsedSize = 0;
         }
+        mBuffer.clear();
+        mBufferUsedSize = 0;
     }
 
     @VisibleForTesting
-    public int getBufferSize() {
+    public synchronized int getBufferSize() {
         return mBufferUsedSize;
     }
 
     /**
      * Returns the buffer status in human-readable form.
      */
-    public String getStatus() {
-        synchronized (mBufferLock) {
-            return "Buffer size: " + mBufferCapacity + " bytes" + "\n"
-                    + "Buffer usage: " + mBufferUsedSize + " bytes" + "\n"
-                    + "Elements in the buffer: " + mBuffer.size();
-        }
+    public synchronized String getStatus() {
+        return "Buffer size: " + mBufferCapacity + " bytes" + "\n"
+                + "Buffer usage: " + mBufferUsedSize + " bytes" + "\n"
+                + "Elements in the buffer: " + mBuffer.size();
     }
 }
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 92cfa67..361bdce 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -47,6 +47,7 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.storage.StorageManager;
@@ -160,9 +161,17 @@
      */
     public static final int VERIFY_FLAG_REQUEST_GK_PW_HANDLE = 1 << 0;
 
+    /**
+     * Flag provided to {@link #verifyCredential(LockscreenCredential, int, int)} . If set, the
+     * method writes the password data to the repair mode file after the credential is verified
+     * successfully.
+     */
+    public static final int VERIFY_FLAG_WRITE_REPAIR_MODE_PW = 1 << 1;
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag = true, value = {
-            VERIFY_FLAG_REQUEST_GK_PW_HANDLE
+            VERIFY_FLAG_REQUEST_GK_PW_HANDLE,
+            VERIFY_FLAG_WRITE_REPAIR_MODE_PW
     })
     public @interface VerifyFlag {}
 
@@ -171,6 +180,11 @@
      */
     public static final int USER_FRP = UserHandle.USER_NULL + 1;
 
+    /**
+     * Special user id for triggering the exiting repair mode verification flow.
+     */
+    public static final int USER_REPAIR_MODE = UserHandle.USER_NULL + 2;
+
     public final static String PATTERN_EVER_CHOSEN_KEY = "lockscreen.patterneverchosen";
     public final static String PASSWORD_TYPE_KEY = "lockscreen.password_type";
     @Deprecated
@@ -201,6 +215,8 @@
     public static final String CURRENT_LSKF_BASED_PROTECTOR_ID_KEY = "sp-handle";
     public static final String PASSWORD_HISTORY_DELIMITER = ",";
 
+    private static final String GSI_RUNNING_PROP = "ro.gsid.image_running";
+
     /**
      * drives the pin auto confirmation feature availability in code logic.
      */
@@ -389,7 +405,7 @@
 
     @UnsupportedAppUsage
     public void reportFailedPasswordAttempt(int userId) {
-        if (userId == USER_FRP && frpCredentialEnabled(mContext)) {
+        if (isSpecialUserId(mContext, userId, /* checkDeviceSupported= */ true)) {
             return;
         }
         getDevicePolicyManager().reportFailedPasswordAttempt(userId);
@@ -398,7 +414,7 @@
 
     @UnsupportedAppUsage
     public void reportSuccessfulPasswordAttempt(int userId) {
-        if (userId == USER_FRP && frpCredentialEnabled(mContext)) {
+        if (isSpecialUserId(mContext, userId, /* checkDeviceSupported= */ true)) {
             return;
         }
         getDevicePolicyManager().reportSuccessfulPasswordAttempt(userId);
@@ -406,21 +422,21 @@
     }
 
     public void reportPasswordLockout(int timeoutMs, int userId) {
-        if (userId == USER_FRP && frpCredentialEnabled(mContext)) {
+        if (isSpecialUserId(mContext, userId, /* checkDeviceSupported= */ true)) {
             return;
         }
         getTrustManager().reportUnlockLockout(timeoutMs, userId);
     }
 
     public int getCurrentFailedPasswordAttempts(int userId) {
-        if (userId == USER_FRP && frpCredentialEnabled(mContext)) {
+        if (isSpecialUserId(mContext, userId, /* checkDeviceSupported= */ true)) {
             return 0;
         }
         return getDevicePolicyManager().getCurrentFailedPasswordAttempts(userId);
     }
 
     public int getMaximumFailedPasswordsForWipe(int userId) {
-        if (userId == USER_FRP && frpCredentialEnabled(mContext)) {
+        if (isSpecialUserId(mContext, userId, /* checkDeviceSupported= */ true)) {
             return 0;
         }
         return getDevicePolicyManager().getMaximumFailedPasswordsForWipe(
@@ -757,6 +773,17 @@
         }
     }
 
+    /** Returns the credential type corresponding to the given PIN or password quality. */
+    public static int pinOrPasswordQualityToCredentialType(int quality) {
+        if (isQualityAlphabeticPassword(quality)) {
+            return CREDENTIAL_TYPE_PASSWORD;
+        }
+        if (isQualityNumericPin(quality)) {
+            return CREDENTIAL_TYPE_PIN;
+        }
+        throw new IllegalArgumentException("Quality is neither Pin nor password: " + quality);
+    }
+
     /**
      * Save a new lockscreen credential.
      *
@@ -991,7 +1018,7 @@
                 }
                 @Override
                 public boolean shouldBypassCache(Integer userHandle) {
-                    return userHandle == USER_FRP;
+                    return isSpecialUserId(userHandle);
                 }
             };
 
@@ -1105,9 +1132,10 @@
     @UnsupportedAppUsage
     public long setLockoutAttemptDeadline(int userId, int timeoutMs) {
         final long deadline = SystemClock.elapsedRealtime() + timeoutMs;
-        if (userId == USER_FRP) {
-            // For secure password storage (that is required for FRP), the underlying storage also
-            // enforces the deadline. Since we cannot store settings for the FRP user, don't.
+        if (isSpecialUserId(userId)) {
+            // For secure password storage (that is required for special users such as FRP), the
+            // underlying storage also enforces the deadline. Since we cannot store settings
+            // for special users, don't.
             return deadline;
         }
         mLockoutDeadlines.put(userId, deadline);
@@ -1847,6 +1875,64 @@
     }
 
     /**
+     * Return {@code true} if repair mode is supported by the device.
+     */
+    public static boolean isRepairModeSupported(Context context) {
+        return context.getResources().getBoolean(
+                com.android.internal.R.bool.config_repairModeSupported);
+    }
+
+    /**
+     * Return {@code true} if repair mode is active on the device.
+     */
+    public static boolean isRepairModeActive(Context context) {
+        return Settings.Global.getInt(context.getContentResolver(),
+                Settings.Global.REPAIR_MODE_ACTIVE, /* def= */ 0) > 0;
+    }
+
+    /**
+     * Return {@code true} if repair mode is supported by the device and the user has been granted
+     * admin privileges.
+     */
+    public static boolean canUserEnterRepairMode(Context context, UserInfo info) {
+        return info != null && info.isAdmin() && isRepairModeSupported(context);
+    }
+
+    /**
+     * Return {@code true} if GSI is running on the device.
+     */
+    public static boolean isGsiRunning() {
+        return SystemProperties.getInt(GSI_RUNNING_PROP, 0) > 0;
+    }
+
+    /**
+     * Return {@code true} if the given user id is a special user such as {@link #USER_FRP}.
+     */
+    public static boolean isSpecialUserId(int userId) {
+        return isSpecialUserId(/* context= */ null, userId, /* checkDeviceSupported= */ false);
+    }
+
+    /**
+     * Return {@code true} if the given user id is a special user for the verification flow.
+     *
+     * @param checkDeviceSupported {@code true} to check the specified user is supported
+     *                             by the device.
+     */
+    private static boolean isSpecialUserId(@Nullable Context context, int userId,
+            boolean checkDeviceSupported) {
+        switch (userId) {
+            case USER_FRP:
+                if (checkDeviceSupported) return frpCredentialEnabled(context);
+                return true;
+
+            case USER_REPAIR_MODE:
+                if (checkDeviceSupported) return isRepairModeSupported(context);
+                return true;
+        }
+        return false;
+    }
+
+    /**
      * Attempt to rederive the unified work challenge for the specified profile user and unlock the
      * user. If successful, this would allow the user to leave quiet mode automatically without
      * additional user authentication.
diff --git a/core/java/com/android/internal/widget/NotificationExpandButton.java b/core/java/com/android/internal/widget/NotificationExpandButton.java
index 07ee9b5..d4dd1e7 100644
--- a/core/java/com/android/internal/widget/NotificationExpandButton.java
+++ b/core/java/com/android/internal/widget/NotificationExpandButton.java
@@ -21,6 +21,8 @@
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
 import android.util.AttributeSet;
 import android.view.RemotableViewMethod;
 import android.view.View;
@@ -42,7 +44,7 @@
 @RemoteViews.RemoteView
 public class NotificationExpandButton extends FrameLayout {
 
-    private View mPillView;
+    private Drawable mPillDrawable;
     private TextView mNumberView;
     private ImageView mIconView;
     private boolean mExpanded;
@@ -73,7 +75,10 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mPillView = findViewById(R.id.expand_button_pill);
+
+        final View pillView = findViewById(R.id.expand_button_pill);
+        final LayerDrawable layeredPill = (LayerDrawable) pillView.getBackground();
+        mPillDrawable = layeredPill.findDrawableByLayerId(R.id.expand_button_pill_colorized_layer);
         mNumberView = findViewById(R.id.expand_button_number);
         mIconView = findViewById(R.id.expand_button_icon);
     }
@@ -156,7 +161,7 @@
     private void updateColors() {
         if (shouldShowNumber()) {
             if (mHighlightPillColor != 0) {
-                mPillView.setBackgroundTintList(ColorStateList.valueOf(mHighlightPillColor));
+                mPillDrawable.setTintList(ColorStateList.valueOf(mHighlightPillColor));
             }
             mIconView.setColorFilter(mHighlightTextColor);
             if (mHighlightTextColor != 0) {
@@ -164,7 +169,7 @@
             }
         } else {
             if (mDefaultPillColor != 0) {
-                mPillView.setBackgroundTintList(ColorStateList.valueOf(mDefaultPillColor));
+                mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor));
             }
             mIconView.setColorFilter(mDefaultTextColor);
             if (mDefaultTextColor != 0) {
diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java
index f55d15d..e65b4b6 100644
--- a/core/java/com/android/internal/widget/PointerLocationView.java
+++ b/core/java/com/android/internal/widget/PointerLocationView.java
@@ -163,6 +163,8 @@
     @UnsupportedAppUsage
     private boolean mPrintCoords = true;
 
+    private float mDensity;
+
     public PointerLocationView(Context c) {
         super(c);
         setFocusableInTouchMode(true);
@@ -365,11 +367,8 @@
                 drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.toolMajor,
                         ps.mCoords.toolMinor, ps.mCoords.orientation, mPaint);
 
-                // Draw the orientation arrow.
-                float arrowSize = ps.mCoords.toolMajor * 0.7f;
-                if (arrowSize < 20) {
-                    arrowSize = 20;
-                }
+                // Draw the orientation arrow, and ensure it has a minimum size of 24dp.
+                final float arrowSize = Math.max(ps.mCoords.toolMajor * 0.7f, 24 * mDensity);
                 mPaint.setARGB(255, pressureLevel, 255, 0);
                 float orientationVectorX = (float) (Math.sin(ps.mCoords.orientation)
                         * arrowSize);
@@ -398,7 +397,7 @@
                 canvas.drawCircle(
                         ps.mCoords.x + orientationVectorX * tiltScale,
                         ps.mCoords.y + orientationVectorY * tiltScale,
-                        3.0f, mPaint);
+                        3.0f * mDensity, mPaint);
 
                 // Draw the current bounding box
                 if (ps.mHasBoundingBox) {
@@ -1003,10 +1002,10 @@
 
     // Compute size by display density.
     private void configureDensityDependentFactors() {
-        final float density = getResources().getDisplayMetrics().density;
-        mTextPaint.setTextSize(10 * density);
-        mPaint.setStrokeWidth(1 * density);
-        mCurrentPointPaint.setStrokeWidth(1 * density);
-        mPathPaint.setStrokeWidth(1 * density);
+        mDensity = getResources().getDisplayMetrics().density;
+        mTextPaint.setTextSize(10 * mDensity);
+        mPaint.setStrokeWidth(1 * mDensity);
+        mCurrentPointPaint.setStrokeWidth(1 * mDensity);
+        mPathPaint.setStrokeWidth(1 * mDensity);
     }
 }
diff --git a/core/jni/android_hardware_input_InputWindowHandle.cpp b/core/jni/android_hardware_input_InputWindowHandle.cpp
index 416d991..eb5f297 100644
--- a/core/jni/android_hardware_input_InputWindowHandle.cpp
+++ b/core/jni/android_hardware_input_InputWindowHandle.cpp
@@ -163,10 +163,9 @@
 
     mInfo.touchOcclusionMode = static_cast<TouchOcclusionMode>(
             env->GetIntField(obj, gInputWindowHandleClassInfo.touchOcclusionMode));
-    mInfo.ownerPid = env->GetIntField(obj,
-            gInputWindowHandleClassInfo.ownerPid);
-    mInfo.ownerUid = env->GetIntField(obj,
-            gInputWindowHandleClassInfo.ownerUid);
+    mInfo.ownerPid = gui::Pid{env->GetIntField(obj, gInputWindowHandleClassInfo.ownerPid)};
+    mInfo.ownerUid = gui::Uid{
+            static_cast<uid_t>(env->GetIntField(obj, gInputWindowHandleClassInfo.ownerUid))};
     mInfo.packageName = getStringField(env, obj, gInputWindowHandleClassInfo.packageName, "<null>");
     mInfo.displayId = env->GetIntField(obj,
             gInputWindowHandleClassInfo.displayId);
@@ -308,8 +307,10 @@
 
     env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.touchOcclusionMode,
                      static_cast<int32_t>(windowInfo.touchOcclusionMode));
-    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.ownerPid, windowInfo.ownerPid);
-    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.ownerUid, windowInfo.ownerUid);
+    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.ownerPid,
+                     windowInfo.ownerPid.val());
+    env->SetIntField(inputWindowHandle, gInputWindowHandleClassInfo.ownerUid,
+                     windowInfo.ownerUid.val());
     ScopedLocalRef<jstring> packageName(env, env->NewStringUTF(windowInfo.packageName.data()));
     env->SetObjectField(inputWindowHandle, gInputWindowHandleClassInfo.packageName,
                         packageName.get());
diff --git a/core/jni/android_os_GraphicsEnvironment.cpp b/core/jni/android_os_GraphicsEnvironment.cpp
index 01dbceb..8fc30d1 100644
--- a/core/jni/android_os_GraphicsEnvironment.cpp
+++ b/core/jni/android_os_GraphicsEnvironment.cpp
@@ -16,11 +16,12 @@
 
 #define LOG_TAG "GraphicsEnvironment"
 
-#include <vector>
-
 #include <graphicsenv/GraphicsEnv.h>
 #include <nativehelper/ScopedUtfChars.h>
 #include <nativeloader/native_loader.h>
+
+#include <vector>
+
 #include "core_jni_helpers.h"
 
 namespace {
@@ -49,11 +50,10 @@
                                                     appPackageNameChars.c_str(), vulkanVersion);
 }
 
-void setAngleInfo_native(JNIEnv* env, jobject clazz, jstring path, jstring appName,
-                         jstring devOptIn, jobjectArray featuresObj) {
+void setAngleInfo_native(JNIEnv* env, jobject clazz, jstring path, jboolean useNativeDriver,
+                         jstring packageName, jobjectArray featuresObj) {
     ScopedUtfChars pathChars(env, path);
-    ScopedUtfChars appNameChars(env, appName);
-    ScopedUtfChars devOptInChars(env, devOptIn);
+    ScopedUtfChars packageNameChars(env, packageName);
 
     std::vector<std::string> features;
     if (featuresObj != nullptr) {
@@ -73,13 +73,8 @@
         }
     }
 
-    android::GraphicsEnv::getInstance().setAngleInfo(pathChars.c_str(), appNameChars.c_str(),
-                                                     devOptInChars.c_str(), features);
-}
-
-bool shouldUseAngle_native(JNIEnv* env, jobject clazz, jstring appName) {
-    ScopedUtfChars appNameChars(env, appName);
-    return android::GraphicsEnv::getInstance().shouldUseAngle(appNameChars.c_str());
+    android::GraphicsEnv::getInstance().setAngleInfo(pathChars.c_str(), useNativeDriver,
+                                                     packageNameChars.c_str(), features);
 }
 
 void setLayerPaths_native(JNIEnv* env, jobject clazz, jobject classLoader, jstring layerPaths) {
@@ -123,11 +118,8 @@
          reinterpret_cast<void*>(setGpuStats_native)},
         {"setInjectLayersPrSetDumpable", "()Z",
          reinterpret_cast<void*>(setInjectLayersPrSetDumpable_native)},
-        {"setAngleInfo",
-         "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)V",
+        {"nativeSetAngleInfo", "(Ljava/lang/String;ZLjava/lang/String;[Ljava/lang/String;)V",
          reinterpret_cast<void*>(setAngleInfo_native)},
-        {"getShouldUseAngle", "(Ljava/lang/String;)Z",
-         reinterpret_cast<void*>(shouldUseAngle_native)},
         {"setLayerPaths", "(Ljava/lang/ClassLoader;Ljava/lang/String;)V",
          reinterpret_cast<void*>(setLayerPaths_native)},
         {"setDebugLayers", "(Ljava/lang/String;)V", reinterpret_cast<void*>(setDebugLayers_native)},
diff --git a/core/proto/android/input/keyboard_configured.proto b/core/proto/android/input/keyboard_configured.proto
new file mode 100644
index 0000000..1699008
--- /dev/null
+++ b/core/proto/android/input/keyboard_configured.proto
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package com.android.internal.os;
+
+option java_outer_classname = "KeyboardConfiguredProto";
+
+/**
+ * RepeatedKeyboardLayout proto from input_extension_atoms.proto,
+ * duplicated here so that it's accessible in the build.
+ * Must be kept in sync with the version in input_extension_atoms.proto.
+ */
+
+// Message containing the repeated field for KeyboardLayoutConfig
+message RepeatedKeyboardLayoutConfig {
+  repeated KeyboardLayoutConfig keyboard_layout_config = 1;
+}
+
+// Keyboard layout configured when the device is connected
+// used in KeyboardConfigured atom
+message KeyboardLayoutConfig {
+  // Keyboard configuration details
+  // Layout type mappings found at:
+  // frameworks/base/core/res/res/values/attrs.xml
+  optional int32 keyboard_layout_type = 1;
+  // PK language language tag (e.g. en-US, ru-Cyrl, etc). This will follow
+  // BCP-47 language tag standards.
+  optional string keyboard_language_tag = 2;
+  // Selected keyboard layout name (e.g. English(US), English(Dvorak), etc.)
+  optional string keyboard_layout_name = 3;
+  // Criteria for layout selection (such as user, device, virtual keyboard based)
+  // IntDef annotation at:
+  // services/core/java/com/android/server/input/KeyboardMetricsCollector.java
+  optional int32 layout_selection_criteria = 4;
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index c14b730..af53f6d 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -6096,7 +6096,7 @@
     <!-- @hide @SystemApi Allows an application to observe usage time of apps. The app can register
          for callbacks when apps reach a certain usage time limit, etc. -->
     <permission android:name="android.permission.OBSERVE_APP_USAGE"
-        android:protectionLevel="signature|privileged" />
+        android:protectionLevel="signature|privileged|role" />
 
     <!-- @hide @TestApi @SystemApi Allows an application to change the app idle state of an app.
          <p>Not for use by third-party applications. -->
@@ -6726,7 +6726,7 @@
          it will be ignored.
         @hide -->
     <permission android:name="android.permission.MODIFY_DAY_NIGHT_MODE"
-      android:protectionLevel="signature|privileged" />
+      android:protectionLevel="signature|privileged|role" />
 
     <!-- @SystemApi Allows entering or exiting car mode using a specified priority.
         This permission is required to use UiModeManager while specifying a priority for the calling
diff --git a/core/res/res/color-night/notification_expand_button_state_tint.xml b/core/res/res/color-night/notification_expand_button_state_tint.xml
new file mode 100644
index 0000000..a794d53
--- /dev/null
+++ b/core/res/res/color-night/notification_expand_button_state_tint.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT 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_pressed="true" android:color="@android:color/system_on_surface_dark" android:alpha="0.06"/>
+    <item android:state_hovered="true" android:color="@android:color/system_on_surface_dark" android:alpha="0.03"/>
+    <item android:color="@android:color/system_on_surface_dark" android:alpha="0.00"/>
+</selector>
\ No newline at end of file
diff --git a/core/res/res/color/notification_expand_button_state_tint.xml b/core/res/res/color/notification_expand_button_state_tint.xml
new file mode 100644
index 0000000..67b2c25
--- /dev/null
+++ b/core/res/res/color/notification_expand_button_state_tint.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT 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_pressed="true" android:color="@android:color/system_on_surface_light" android:alpha="0.12"/>
+    <item android:state_hovered="true" android:color="@android:color/system_on_surface_light" android:alpha="0.08"/>
+    <item android:color="@android:color/system_on_surface_light" android:alpha="0.00"/>
+</selector>
\ No newline at end of file
diff --git a/core/res/res/drawable/expand_button_pill_bg.xml b/core/res/res/drawable/expand_button_pill_bg.xml
index f95044a..a14d33c 100644
--- a/core/res/res/drawable/expand_button_pill_bg.xml
+++ b/core/res/res/drawable/expand_button_pill_bg.xml
@@ -13,7 +13,17 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
-    <corners android:radius="@dimen/notification_expand_button_pill_height" />
-    <solid android:color="@android:color/white" />
-</shape>
\ No newline at end of file
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/expand_button_pill_colorized_layer">
+        <shape xmlns:android="http://schemas.android.com/apk/res/android">
+            <corners android:radius="@dimen/notification_expand_button_pill_height" />
+            <solid android:color="@android:color/white" />
+        </shape>
+    </item>
+    <item>
+        <shape xmlns:android="http://schemas.android.com/apk/res/android">
+            <corners android:radius="@dimen/notification_expand_button_pill_height" />
+            <solid android:color="@color/notification_expand_button_state_tint" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_bluetooth_share_icon.xml b/core/res/res/drawable/ic_bluetooth_share_icon.xml
deleted file mode 100644
index 6acfd57..0000000
--- a/core/res/res/drawable/ic_bluetooth_share_icon.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<!--
-     Copyright (C) 2019 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT 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 drawable should only be used by the Bluetooth application for its share target icon. -->
-<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:color/accent_device_default_light">
-
-    <path
-        android:fillColor="@android:color/white"
-        android:pathData="M17.71,7.71L12,2h-1v7.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L11,14.41V22h1l5.71-5.71L13.41,12L17.71,7.71z M13,5.83 l1.88,1.88L13,9.59V5.83z M14.88,16.29L13,18.17v-3.76L14.88,16.29z" />
-</vector>
diff --git a/core/res/res/layout/notification_expand_button.xml b/core/res/res/layout/notification_expand_button.xml
index 8eae064..63fe471 100644
--- a/core/res/res/layout/notification_expand_button.xml
+++ b/core/res/res/layout/notification_expand_button.xml
@@ -34,6 +34,7 @@
         android:background="@drawable/expand_button_pill_bg"
         android:gravity="center_vertical"
         android:layout_gravity="center_vertical"
+        android:duplicateParentState="true"
         >
 
         <TextView
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 52cc35d..f652d0b3 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -46,6 +46,7 @@
         <item><xliff:g id="id">@string/status_bar_secure</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_managed_profile</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_cast</xliff:g></item>
+        <item><xliff:g id="id">@string/status_bar_connected_display</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_screen_record</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_vpn</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_bluetooth</xliff:g></item>
@@ -72,6 +73,7 @@
     <string translatable="false" name="status_bar_sync_failing">sync_failing</string>
     <string translatable="false" name="status_bar_sync_active">sync_active</string>
     <string translatable="false" name="status_bar_cast">cast</string>
+    <string translatable="false" name="status_bar_connected_display">connected_display</string>
     <string translatable="false" name="status_bar_hotspot">hotspot</string>
     <string translatable="false" name="status_bar_location">location</string>
     <string translatable="false" name="status_bar_bluetooth">bluetooth</string>
@@ -1675,16 +1677,56 @@
          in the config_autoBrightnessLevels array.  This array should have size one greater
          than the size of the config_autoBrightnessLevels array.
          The brightness values must be between 0 and 255 and be non-decreasing.
+
          This must be overridden in platform specific overlays -->
     <integer-array name="config_autoBrightnessButtonBacklightValues">
     </integer-array>
 
-    <!-- Array of output values for keyboard backlight corresponding to the lux values
-         in the config_autoBrightnessLevels array.  This array should have size one greater
-         than the size of the config_autoBrightnessLevels array.
-         The brightness values must be between 0 and 255 and be non-decreasing.
+    <!-- Smoothing constant for Ambient keyboard backlight change. It should contain value
+         in the range (0.0, 1.0] that will be used to calculate smoothed lux values using
+         simple exponential smoothing. This value indicated how quickly we transition to
+         the lux values provided by the Ambient light sensor.
+         Simple formula for newLuxValue = (1-constant)*currLuxValue + constant*rawLuxValue, where
+         rawLuxValue is the value provided by the Ambient light sensor. (e.g. value of 1.0 means we
+         immediately start using the value provided by the Ambient light sensor)
          This must be overridden in platform specific overlays -->
-    <integer-array name="config_autoBrightnessKeyboardBacklightValues">
+    <item name="config_autoKeyboardBrightnessSmoothingConstant" format="float" type="dimen">
+        1.0
+    </item>
+
+    <!-- Array of output values for keyboard backlight corresponding to the lux values
+         in the config_autoKeyboardBacklight(Increase/Decrease)LuxThreshold arrays.
+         The brightness values must be between 0 and 255 and be non-decreasing.
+
+         This can be overridden in platform specific overlays -->
+    <integer-array name="config_autoKeyboardBacklightBrightnessValues">
+        <item>102</item>
+        <item>153</item>
+        <item>0</item>
+    </integer-array>
+
+    <!-- Array of threshold values for keyboard backlight corresponding to the values
+         in the config_autoKeyboardBacklightBrightnessValues array.
+         These lux values indicate when to move to a lower keyboard backlight value,
+         as defined in config_autoKeyboardBacklightBrightnessValues array.
+
+         This can be overridden in platform specific overlays -->
+    <integer-array name="config_autoKeyboardBacklightDecreaseLuxThreshold">
+        <item>-1</item>
+        <item>40</item>
+        <item>150</item>
+    </integer-array>
+
+    <!-- Array of threshold values for keyboard backlight corresponding to the values
+         in the config_autoKeyboardBacklightBrightnessValues array.
+         These lux values indicate when to move to a higher keyboard backlight value,
+         as defined in config_autoKeyboardBacklightBrightnessValues array.
+
+         This can be overridden in platform specific overlays -->
+    <integer-array name="config_autoKeyboardBacklightIncreaseLuxThreshold">
+        <item>55</item>
+        <item>200</item>
+        <item>-1</item>
     </integer-array>
 
     <!-- An array describing the screen's backlight values corresponding to the brightness
@@ -1817,6 +1859,17 @@
          specified -->
     <string name="default_wallpaper_component" translatable="false">@null</string>
 
+    <!-- Default wallpaper component per device color map, each item is a comma separated key-value
+         pair with key being a device color and value being the corresponding wallpaper component.
+         The component with its key matching the device color will be the default wallpaper, the
+         default wallpaper component will be the default if this config is not specified.
+
+         E.g. for SLV color, and com.android.example/com.android.example.SlVDefaultWallpaper
+         <item>SLV,com.android.example/com.android.example.SlVDefaultWallpaper</item> -->
+    <string-array name="default_wallpaper_component_per_device_color" translatable="false">
+        <!-- Add packages here -->
+    </string-array>
+
     <!-- By default a product has no distinct default lock wallpaper -->
     <item name="default_lock_wallpaper" type="drawable">@null</item>
 
@@ -4419,6 +4472,14 @@
     -->
     <string name="config_defaultContentCaptureService" translatable="false"></string>
 
+    <!-- The package name for the system's content protection service.
+         This service must be trusted, as it can be activated without explicit consent of the user.
+         If no service with the specified name exists on the device, content protection will be
+         disabled.
+         Example: "com.android.contentprotection/.ContentProtectionService"
+    -->
+    <string name="config_defaultContentProtectionService" translatable="false"></string>
+
     <!-- The package name for the system's augmented autofill service.
          This service must be trusted, as it can be activated without explicit consent of the user.
          If no service with the specified name exists on the device, augmented autofill wil be
@@ -5646,6 +5707,9 @@
          TODO(b/255532890) Enable when ignoreOrientationRequest is set -->
     <bool name="config_letterboxIsEnabledForTranslucentActivities">false</bool>
 
+    <!-- Whether per-app user aspect ratio override settings is enabled -->
+    <bool name="config_appCompatUserAppAspectRatioSettingsIsEnabled">false</bool>
+
     <!-- 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. -->
@@ -6188,7 +6252,7 @@
 
     <!-- Flag indicating whether the show Stylus pointer icon.
      If set, a pointer icon will be shown over the location of a stylus pointer.-->
-    <bool name="config_enableStylusPointerIcon">false</bool>
+    <bool name="config_enableStylusPointerIcon">true</bool>
 
     <!-- Determines whether SafetyCenter feature is enabled. -->
     <bool name="config_enableSafetyCenter">true</bool>
@@ -6449,4 +6513,9 @@
     <!-- Whether the AOSP support for app cloning building blocks is to be enabled for the
          device. -->
     <bool name="config_enableAppCloningBuildingBlocks">true</bool>
+
+    <!-- Enables or disables support for repair mode. The feature creates a secure
+         environment to protect the user's privacy when the device is being repaired.
+         Off by default, since OEMs may have had a similar feature on their devices. -->
+    <bool name="config_repairModeSupported">false</bool>
 </resources>
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 4ae54a0..08c40ba 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -67,6 +67,15 @@
     <bool name="auto_data_switch_ping_test_before_switch">true</bool>
     <java-symbol type="bool" name="auto_data_switch_ping_test_before_switch" />
 
+    <!-- Define the tolerated gap of score for auto data switch decision, larger than which the
+         device will switch to the SIM with higher score. The score is used in conjunction with the
+         score table defined in
+         CarrierConfigManager#KEY_AUTO_DATA_SWITCH_RAT_SIGNAL_SCORE_STRING_ARRAY.
+         If 0, the device always switch to the higher score SIM.
+         If < 0, the network type and signal strength based auto switch is disabled. -->
+    <integer name="auto_data_switch_score_tolerance">3000</integer>
+    <java-symbol type="integer" name="auto_data_switch_score_tolerance" />
+
     <!-- Boolean indicating whether the Iwlan data service supports persistence of iwlan ipsec
          tunnels across service restart. If iwlan tunnels are not persisted across restart,
          Framework will clean up dangling data connections when service restarts -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 2c72f45..93d1771 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3798,7 +3798,7 @@
          keyboard is connected -->
     <string name="show_ime">Keep it on screen while physical keyboard is active</string>
     <!-- Title of the physical keyboard category in the input method selector [CHAR LIMIT=30] -->
-    <string name="hardware">Show virtual keyboard</string>
+    <string name="hardware">Use on-screen keyboard</string>
 
     <!-- Title of the notification to prompt the user to configure physical keyboard settings. [CHAR LIMIT=NOTIF_TITLE] -->
     <string name="select_keyboard_layout_notification_title">Configure <xliff:g id="device_name" example="Foobar USB Keyboard">%s</xliff:g></string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index da0ba87..6aa8885 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -520,6 +520,7 @@
   <java-symbol type="dimen" name="config_viewConfigurationHandwritingSlop" />
   <java-symbol type="dimen" name="config_viewConfigurationHoverSlop" />
   <java-symbol type="dimen" name="config_ambiguousGestureMultiplier" />
+  <java-symbol type="dimen" name="config_autoKeyboardBrightnessSmoothingConstant" />
   <java-symbol type="dimen" name="config_viewMinFlingVelocity" />
   <java-symbol type="dimen" name="config_viewMaxFlingVelocity" />
   <java-symbol type="dimen" name="config_viewMinRotaryEncoderFlingVelocity" />
@@ -1888,11 +1889,13 @@
   <java-symbol type="anim" name="dream_activity_open_enter" />
   <java-symbol type="anim" name="dream_activity_close_exit" />
   <java-symbol type="array" name="config_autoBrightnessButtonBacklightValues" />
-  <java-symbol type="array" name="config_autoBrightnessKeyboardBacklightValues" />
   <java-symbol type="array" name="config_autoBrightnessLcdBacklightValues" />
   <java-symbol type="array" name="config_autoBrightnessLcdBacklightValues_doze" />
   <java-symbol type="array" name="config_autoBrightnessLevels" />
   <java-symbol type="array" name="config_autoBrightnessLevelsIdle" />
+  <java-symbol type="array" name="config_autoKeyboardBacklightBrightnessValues" />
+  <java-symbol type="array" name="config_autoKeyboardBacklightDecreaseLuxThreshold" />
+  <java-symbol type="array" name="config_autoKeyboardBacklightIncreaseLuxThreshold" />
   <java-symbol type="array" name="config_ambientThresholdLevels" />
   <java-symbol type="array" name="config_ambientBrighteningThresholds" />
   <java-symbol type="array" name="config_ambientDarkeningThresholds" />
@@ -2116,6 +2119,7 @@
   <java-symbol type="string" name="data_usage_rapid_body" />
   <java-symbol type="string" name="data_usage_rapid_app_body" />
   <java-symbol type="string" name="default_wallpaper_component" />
+  <java-symbol type="array" name="default_wallpaper_component_per_device_color" />
   <java-symbol type="string" name="device_storage_monitor_notification_channel" />
   <java-symbol type="string" name="dlg_ok" />
   <java-symbol type="string" name="dump_heap_notification" />
@@ -3047,6 +3051,7 @@
   <java-symbol type="id" name="header_text_secondary" />
   <java-symbol type="id" name="expand_button" />
   <java-symbol type="id" name="expand_button_pill" />
+  <java-symbol type="id" name="expand_button_pill_colorized_layer" />
   <java-symbol type="id" name="expand_button_number" />
   <java-symbol type="id" name="expand_button_icon" />
   <java-symbol type="id" name="alternate_expand_target" />
@@ -3096,6 +3101,7 @@
   <java-symbol type="string" name="status_bar_sync_failing" />
   <java-symbol type="string" name="status_bar_sync_active" />
   <java-symbol type="string" name="status_bar_cast" />
+  <java-symbol type="string" name="status_bar_connected_display" />
   <java-symbol type="string" name="status_bar_hotspot" />
   <java-symbol type="string" name="status_bar_location" />
   <java-symbol type="string" name="status_bar_bluetooth" />
@@ -3764,6 +3770,7 @@
   <java-symbol type="string" name="config_defaultTextClassifierPackage" />
   <java-symbol type="string" name="config_defaultWellbeingPackage" />
   <java-symbol type="string" name="config_defaultContentCaptureService" />
+  <java-symbol type="string" name="config_defaultContentProtectionService" />
   <java-symbol type="string" name="config_defaultAugmentedAutofillService" />
   <java-symbol type="string" name="config_defaultTranslationService" />
   <java-symbol type="string" name="config_defaultAppPredictionService" />
@@ -4426,9 +4433,6 @@
   <!-- Set to true to make assistant show in front of the dream/screensaver. -->
   <java-symbol type="bool" name="config_assistantOnTopOfDream"/>
 
-  <!-- Set to true to enable letterboxing on translucent activities. -->
-  <java-symbol type="bool" name="config_letterboxIsEnabledForTranslucentActivities" />
-
   <java-symbol type="string" name="config_overrideComponentUiPackage" />
 
   <java-symbol type="string" name="notification_channel_network_status" />
@@ -4524,6 +4528,12 @@
   <java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" />
   <java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" />
   <java-symbol type="bool" name="config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled" />
+  <!-- Set to true to enable letterboxing on translucent activities. -->
+  <java-symbol type="bool" name="config_letterboxIsEnabledForTranslucentActivities" />
+
+  <!-- Whether per-app user aspect ratio override settings is enabled -->
+  <java-symbol type="bool" name="config_appCompatUserAppAspectRatioSettingsIsEnabled" />
+
   <java-symbol type="bool" name="config_isCompatFakeFocusEnabled" />
   <java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
   <java-symbol type="bool" name="config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled" />
@@ -4914,6 +4924,8 @@
 
   <java-symbol type="bool" name="config_safetyProtectionEnabled" />
 
+  <java-symbol type="bool" name="config_repairModeSupported" />
+
   <java-symbol type="string" name="config_devicePolicyManagementUpdater" />
 
   <java-symbol type="string" name="config_deviceSpecificDeviceStatePolicyProvider" />
diff --git a/core/tests/BroadcastRadioTests/Android.bp b/core/tests/BroadcastRadioTests/Android.bp
index 436f058..85d54e0 100644
--- a/core/tests/BroadcastRadioTests/Android.bp
+++ b/core/tests/BroadcastRadioTests/Android.bp
@@ -45,6 +45,7 @@
     libs: ["android.test.base"],
     test_suites: [
         "general-tests",
+        "automotive-general-tests",
     ],
     // mockito-target-inline dependency
     jni_libs: [
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index c1deba3..129de64 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -1716,6 +1716,17 @@
             <meta-data android:name="android.view.im"
                        android:resource="@xml/ime_meta_handwriting"/>
         </service>
+
+        <activity android:name="android.widget.PointerIconTestActivity"
+                  android:label="PointerIconTestActivity"
+                  android:screenOrientation="portrait"
+                  android:exported="true"
+                  android:theme="@android:style/Theme.Material.Light">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+            </intent-filter>
+        </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/core/tests/coretests/res/layout/pointer_icon_test.xml b/core/tests/coretests/res/layout/pointer_icon_test.xml
new file mode 100644
index 0000000..a2a6447
--- /dev/null
+++ b/core/tests/coretests/res/layout/pointer_icon_test.xml
@@ -0,0 +1,59 @@
+<?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"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent">
+    <TextView
+        android:id="@+id/textview"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Test"/>
+
+    <EditText
+        android:id="@+id/edittext"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Test"/>
+
+    <Button
+        android:id="@+id/button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Test"/>
+
+    <ImageButton
+        android:id="@+id/imagebutton"
+        android:layout_width="50dp"
+        android:layout_height="50dp"/>
+
+    <Spinner
+        android:id="@+id/spinner"
+        android:layout_width="50dp"
+        android:layout_height="50dp"/>
+
+    <RadialTimePickerView
+        android:id="@+id/timepicker"
+        android:layout_width="200dp"
+        android:layout_height="200dp"/>
+
+    <CalendarView
+        android:id="@+id/calendar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/core/tests/coretests/res/values/overlayable_icons_test.xml b/core/tests/coretests/res/values/overlayable_icons_test.xml
index 7ea1848..6a50e9a 100644
--- a/core/tests/coretests/res/values/overlayable_icons_test.xml
+++ b/core/tests/coretests/res/values/overlayable_icons_test.xml
@@ -21,7 +21,6 @@
     <item>@*android:drawable/ic_audio_alarm</item>
     <item>@*android:drawable/ic_audio_alarm_mute</item>
     <item>@*android:drawable/ic_battery_80_24dp</item>
-    <item>@*android:drawable/ic_bluetooth_share_icon</item>
     <item>@*android:drawable/ic_bt_headphones_a2dp</item>
     <item>@*android:drawable/ic_bt_headset_hfp</item>
     <item>@*android:drawable/ic_bt_hearing_aid</item>
diff --git a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
index 81eb213..5ac99db 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
@@ -128,6 +128,7 @@
                 0x222222,                // colorBackground
                 0x333333,                // statusBarColor
                 0x444444,                // navigationBarColor
+                0,                       // statusBarAppearance
                 true,                    // ensureStatusBarContrastWhenTransparent
                 true,                    // ensureNavigationBarContrastWhenTransparent
                 RESIZE_MODE_RESIZEABLE,  // resizeMode
@@ -152,6 +153,7 @@
                 0x222222,                  // colorBackground
                 0x333333,                  // statusBarColor
                 0x444444,                  // navigationBarColor
+                0,                         // statusBarAppearance
                 false,                     // ensureStatusBarContrastWhenTransparent
                 false,                     // ensureNavigationBarContrastWhenTransparent
                 RESIZE_MODE_UNRESIZEABLE,  // resizeMode
@@ -167,6 +169,7 @@
                 0x2222222,               // colorBackground
                 0x3333332,               // statusBarColor
                 0x4444442,               // navigationBarColor
+                0,                       // statusBarAppearance
                 true,                    // ensureStatusBarContrastWhenTransparent
                 true,                    // ensureNavigationBarContrastWhenTransparent
                 RESIZE_MODE_RESIZEABLE,  // resizeMode
@@ -197,6 +200,7 @@
                 0x222222,                  // colorBackground
                 0x333333,                  // statusBarColor
                 0x444444,                  // navigationBarColor
+                0,                         // statusBarAppearance
                 false,                     // ensureStatusBarContrastWhenTransparent
                 false,                     // ensureNavigationBarContrastWhenTransparent
                 RESIZE_MODE_UNRESIZEABLE,  // resizeMode
@@ -219,6 +223,7 @@
                 0x222222,                  // colorBackground
                 0x333333,                  // statusBarColor
                 0x444444,                  // navigationBarColor
+                0,                         // statusBarAppearance
                 false,                     // ensureStatusBarContrastWhenTransparent
                 false,                     // ensureNavigationBarContrastWhenTransparent
                 RESIZE_MODE_UNRESIZEABLE,  // resizeMode
@@ -250,6 +255,7 @@
             assertEquals(td1.getBackgroundColor(), td2.getBackgroundColor());
             assertEquals(td1.getStatusBarColor(), td2.getStatusBarColor());
             assertEquals(td1.getNavigationBarColor(), td2.getNavigationBarColor());
+            assertEquals(td1.getStatusBarAppearance(), td2.getStatusBarAppearance());
             assertEquals(td1.getResizeMode(), td2.getResizeMode());
             assertEquals(td1.getMinWidth(), td2.getMinWidth());
             assertEquals(td1.getMinHeight(), td2.getMinHeight());
diff --git a/core/tests/coretests/src/android/content/ContentCaptureOptionsTest.java b/core/tests/coretests/src/android/content/ContentCaptureOptionsTest.java
index c6f4fa2..f8348d2 100644
--- a/core/tests/coretests/src/android/content/ContentCaptureOptionsTest.java
+++ b/core/tests/coretests/src/android/content/ContentCaptureOptionsTest.java
@@ -22,6 +22,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.os.Parcel;
 import android.util.ArraySet;
 import android.view.contentcapture.ContentCaptureManager.ContentCaptureClient;
 
@@ -40,16 +41,29 @@
 @RunWith(MockitoJUnitRunner.class)
 public class ContentCaptureOptionsTest {
 
-    private final ComponentName mContextComponent = new ComponentName("marco", "polo");
-    private final ComponentName mComp1 = new ComponentName("comp", "one");
-    private final ComponentName mComp2 = new ComponentName("two", "comp");
+    private static final ComponentName CONTEXT_COMPONENT = new ComponentName("marco", "polo");
+    private static final ComponentName COMPONENT1 = new ComponentName("comp", "one");
+    private static final ComponentName COMPONENT2 = new ComponentName("two", "comp");
+    private static final ContentCaptureOptions CONTENT_CAPTURE_OPTIONS =
+            new ContentCaptureOptions(
+                    /* loggingLevel= */ 1000,
+                    /* maxBufferSize= */ 1001,
+                    /* idleFlushingFrequencyMs= */ 1002,
+                    /* textChangeFlushingFrequencyMs= */ 1003,
+                    /* logHistorySize= */ 1004,
+                    /* disableFlushForViewTreeAppearing= */ true,
+                    /* enableReceiver= */ false,
+                    new ContentCaptureOptions.ContentProtectionOptions(
+                            /* enableReceiver= */ true,
+                            /* bufferSize= */ 2001),
+                    /* whitelistedComponents= */ toSet(COMPONENT1, COMPONENT2));
 
     @Mock private Context mContext;
     @Mock private ContentCaptureClient mClient;
 
     @Before
     public void setExpectation() {
-        when(mClient.contentCaptureClientGetComponentName()).thenReturn(mContextComponent);
+        when(mClient.contentCaptureClientGetComponentName()).thenReturn(CONTEXT_COMPONENT);
         when(mContext.getContentCaptureClient()).thenReturn(mClient);
     }
 
@@ -67,26 +81,27 @@
 
     @Test
     public void testIsWhitelisted_notWhitelisted() {
-        ContentCaptureOptions options = new ContentCaptureOptions(toSet(mComp1, mComp2));
+        ContentCaptureOptions options = new ContentCaptureOptions(toSet(COMPONENT1, COMPONENT2));
         assertThat(options.isWhitelisted(mContext)).isFalse();
     }
 
     @Test
     public void testIsWhitelisted_whitelisted() {
-        ContentCaptureOptions options = new ContentCaptureOptions(toSet(mComp1, mContextComponent));
+        ContentCaptureOptions options =
+                new ContentCaptureOptions(toSet(COMPONENT1, CONTEXT_COMPONENT));
         assertThat(options.isWhitelisted(mContext)).isTrue();
     }
 
     @Test
     public void testIsWhitelisted_invalidContext() {
-        ContentCaptureOptions options = new ContentCaptureOptions(toSet(mContextComponent));
+        ContentCaptureOptions options = new ContentCaptureOptions(toSet(CONTEXT_COMPONENT));
         Context invalidContext = mock(Context.class); // has no client
         assertThat(options.isWhitelisted(invalidContext)).isFalse();
     }
 
     @Test
     public void testIsWhitelisted_clientWithNullComponentName() {
-        ContentCaptureOptions options = new ContentCaptureOptions(toSet(mContextComponent));
+        ContentCaptureOptions options = new ContentCaptureOptions(toSet(CONTEXT_COMPONENT));
         ContentCaptureClient client = mock(ContentCaptureClient.class);
         Context context = mock(Context.class);
         when(context.getContentCaptureClient()).thenReturn(client);
@@ -94,8 +109,69 @@
         assertThat(options.isWhitelisted(context)).isFalse();
     }
 
+    @Test
+    public void testToString() {
+        String actual = CONTENT_CAPTURE_OPTIONS.toString();
+
+        String expected =
+                new StringBuilder("ContentCaptureOptions [")
+                        .append("loggingLevel=")
+                        .append(CONTENT_CAPTURE_OPTIONS.loggingLevel)
+                        .append(", maxBufferSize=")
+                        .append(CONTENT_CAPTURE_OPTIONS.maxBufferSize)
+                        .append(", idleFlushingFrequencyMs=")
+                        .append(CONTENT_CAPTURE_OPTIONS.idleFlushingFrequencyMs)
+                        .append(", textChangeFlushingFrequencyMs=")
+                        .append(CONTENT_CAPTURE_OPTIONS.textChangeFlushingFrequencyMs)
+                        .append(", logHistorySize=")
+                        .append(CONTENT_CAPTURE_OPTIONS.logHistorySize)
+                        .append(", disableFlushForViewTreeAppearing=")
+                        .append(CONTENT_CAPTURE_OPTIONS.disableFlushForViewTreeAppearing)
+                        .append(", enableReceiver=")
+                        .append(CONTENT_CAPTURE_OPTIONS.enableReceiver)
+                        .append(", contentProtectionOptions=ContentProtectionOptions [")
+                        .append("enableReceiver=")
+                        .append(CONTENT_CAPTURE_OPTIONS.contentProtectionOptions.enableReceiver)
+                        .append(", bufferSize=")
+                        .append(CONTENT_CAPTURE_OPTIONS.contentProtectionOptions.bufferSize)
+                        .append("], whitelisted=")
+                        .append(CONTENT_CAPTURE_OPTIONS.whitelistedComponents)
+                        .append(']')
+                        .toString();
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void testParcelSerializationDeserialization() {
+        Parcel parcel = Parcel.obtain();
+        CONTENT_CAPTURE_OPTIONS.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        ContentCaptureOptions actual = ContentCaptureOptions.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+
+        assertThat(actual).isNotNull();
+        assertThat(actual.loggingLevel).isEqualTo(CONTENT_CAPTURE_OPTIONS.loggingLevel);
+        assertThat(actual.maxBufferSize).isEqualTo(CONTENT_CAPTURE_OPTIONS.maxBufferSize);
+        assertThat(actual.idleFlushingFrequencyMs)
+                .isEqualTo(CONTENT_CAPTURE_OPTIONS.idleFlushingFrequencyMs);
+        assertThat(actual.textChangeFlushingFrequencyMs)
+                .isEqualTo(CONTENT_CAPTURE_OPTIONS.textChangeFlushingFrequencyMs);
+        assertThat(actual.logHistorySize).isEqualTo(CONTENT_CAPTURE_OPTIONS.logHistorySize);
+        assertThat(actual.disableFlushForViewTreeAppearing)
+                .isEqualTo(CONTENT_CAPTURE_OPTIONS.disableFlushForViewTreeAppearing);
+        assertThat(actual.enableReceiver).isEqualTo(CONTENT_CAPTURE_OPTIONS.enableReceiver);
+        assertThat(actual.contentProtectionOptions).isNotNull();
+        assertThat(actual.contentProtectionOptions.enableReceiver)
+                .isEqualTo(CONTENT_CAPTURE_OPTIONS.contentProtectionOptions.enableReceiver);
+        assertThat(actual.contentProtectionOptions.bufferSize)
+                .isEqualTo(CONTENT_CAPTURE_OPTIONS.contentProtectionOptions.bufferSize);
+        assertThat(actual.whitelistedComponents)
+                .containsExactlyElementsIn(CONTENT_CAPTURE_OPTIONS.whitelistedComponents);
+    }
+
     @NonNull
-    private ArraySet<ComponentName> toSet(@Nullable ComponentName... comps) {
+    private static ArraySet<ComponentName> toSet(@Nullable ComponentName... comps) {
         ArraySet<ComponentName> set = new ArraySet<>();
         if (comps != null) {
             for (int i = 0; i < comps.length; i++) {
diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterActivityTest.java b/core/tests/coretests/src/android/content/res/FontScaleConverterActivityTest.java
index df4fb44..0941a2b 100644
--- a/core/tests/coretests/src/android/content/res/FontScaleConverterActivityTest.java
+++ b/core/tests/coretests/src/android/content/res/FontScaleConverterActivityTest.java
@@ -137,7 +137,7 @@
             );
         });
 
-        PollingCheck.waitFor(/* timeout= */ 5000, () -> {
+        PollingCheck.waitFor(/* timeout= */ 7000, () -> {
             AtomicBoolean isActivityAtCorrectScale = new AtomicBoolean(false);
             rule.getScenario().onActivity(activity ->
                     isActivityAtCorrectScale.set(
@@ -146,12 +146,7 @@
                                 .fontScale == fontScale
                     )
             );
-            return isActivityAtCorrectScale.get() && InstrumentationRegistry
-                    .getInstrumentation()
-                    .getContext()
-                    .getResources()
-                    .getConfiguration()
-                    .fontScale == fontScale;
+            return isActivityAtCorrectScale.get();
         });
     }
 
diff --git a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java
new file mode 100644
index 0000000..a84ac55
--- /dev/null
+++ b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java
@@ -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 android.service.notification;
+
+import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@SmallTest
+@RunWith(Parameterized.class)
+public class NotificationRankingUpdateTest {
+
+    private static final String NOTIFICATION_CHANNEL_ID = "test_channel_id";
+    private static final String TEST_KEY = "key";
+
+    private NotificationChannel mNotificationChannel;
+
+    // TODO(b/284297289): remove this flag set once resolved.
+    @Parameterized.Parameters(name = "rankingUpdateAshmem={0}")
+    public static Boolean[] getRankingUpdateAshmem() {
+        return new Boolean[] { true, false };
+    }
+
+    @Parameterized.Parameter
+    public boolean mRankingUpdateAshmem;
+
+    @Before
+    public void setUp() {
+        mNotificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, "test channel",
+                NotificationManager.IMPORTANCE_DEFAULT);
+
+        SystemUiSystemPropertiesFlags.TEST_RESOLVER = flag -> {
+            if (flag.mSysPropKey.equals(RANKING_UPDATE_ASHMEM.mSysPropKey)) {
+                return mRankingUpdateAshmem;
+            }
+            return new SystemUiSystemPropertiesFlags.DebugResolver().isEnabled(flag);
+        };
+    }
+
+    @After
+    public void tearDown() {
+        SystemUiSystemPropertiesFlags.TEST_RESOLVER = null;
+    }
+
+    public NotificationListenerService.Ranking createTestRanking(String key, int rank) {
+        NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking();
+        ranking.populate(
+                /* key= */ key,
+                /* rank= */ rank,
+                /* matchesInterruptionFilter= */ false,
+                /* visibilityOverride= */ 0,
+                /* suppressedVisualEffects= */ 0,
+                mNotificationChannel.getImportance(),
+                /* explanation= */ null,
+                /* overrideGroupKey= */ null,
+                mNotificationChannel,
+                /* overridePeople= */ null,
+                /* snoozeCriteria= */ null,
+                /* showBadge= */ true,
+                /* userSentiment= */ 0,
+                /* hidden= */ false,
+                /* lastAudiblyAlertedMs= */ -1,
+                /* noisy= */ false,
+                /* smartActions= */ null,
+                /* smartReplies= */ null,
+                /* canBubble= */ false,
+                /* isTextChanged= */ false,
+                /* isConversation= */ false,
+                /* shortcutInfo= */ null,
+                /* rankingAdjustment= */ 0,
+                /* isBubble= */ false,
+                /* proposedImportance= */ 0,
+                /* sensitiveContent= */ false
+        );
+        return ranking;
+    }
+
+    @Test
+    public void testRankingUpdate_rankingConstructor() {
+        NotificationListenerService.Ranking ranking = createTestRanking(TEST_KEY, 123);
+        NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate(
+                new NotificationListenerService.Ranking[]{ranking});
+
+        NotificationListenerService.RankingMap retrievedRankings = rankingUpdate.getRankingMap();
+        NotificationListenerService.Ranking retrievedRanking =
+                new NotificationListenerService.Ranking();
+        assertTrue(retrievedRankings.getRanking(TEST_KEY, retrievedRanking));
+        assertEquals(123, retrievedRanking.getRank());
+    }
+
+    @Test
+    public void testRankingUpdate_parcelConstructor() {
+        NotificationListenerService.Ranking ranking = createTestRanking(TEST_KEY, 123);
+        NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate(
+                new NotificationListenerService.Ranking[]{ranking});
+
+        Parcel parceledRankingUpdate = Parcel.obtain();
+        rankingUpdate.writeToParcel(parceledRankingUpdate, 0);
+        parceledRankingUpdate.setDataPosition(0);
+
+        NotificationRankingUpdate retrievedRankingUpdate = new NotificationRankingUpdate(
+                parceledRankingUpdate);
+
+        NotificationListenerService.RankingMap retrievedRankings =
+                retrievedRankingUpdate.getRankingMap();
+        assertNotNull(retrievedRankings);
+        assertTrue(retrievedRankingUpdate.isFdNotNullAndClosed());
+        NotificationListenerService.Ranking retrievedRanking =
+                new NotificationListenerService.Ranking();
+        assertTrue(retrievedRankings.getRanking(TEST_KEY, retrievedRanking));
+        assertEquals(123, retrievedRanking.getRank());
+        assertTrue(retrievedRankingUpdate.equals(rankingUpdate));
+        parceledRankingUpdate.recycle();
+    }
+
+    @Test
+    public void testRankingUpdate_emptyParcelInCheck() {
+        NotificationListenerService.Ranking ranking = createTestRanking(TEST_KEY, 123);
+        NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate(
+                new NotificationListenerService.Ranking[]{ranking});
+
+        Parcel parceledRankingUpdate = Parcel.obtain();
+        rankingUpdate.writeToParcel(parceledRankingUpdate, 0);
+
+        // This will fail to read the parceledRankingUpdate, because the data position hasn't
+        // been reset, so it'll find no data to read.
+        NotificationRankingUpdate retrievedRankingUpdate = new NotificationRankingUpdate(
+                parceledRankingUpdate);
+        assertNull(retrievedRankingUpdate.getRankingMap());
+    }
+
+    @Test
+    public void testRankingUpdate_describeContents() {
+        NotificationListenerService.Ranking ranking = createTestRanking(TEST_KEY, 123);
+        NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate(
+                new NotificationListenerService.Ranking[]{ranking});
+        assertEquals(0, rankingUpdate.describeContents());
+    }
+
+    @Test
+    public void testRankingUpdate_equals() {
+        NotificationListenerService.Ranking ranking = createTestRanking(TEST_KEY, 123);
+        NotificationRankingUpdate rankingUpdate = new NotificationRankingUpdate(
+                new NotificationListenerService.Ranking[]{ranking});
+        // Reflexive equality.
+        assertTrue(rankingUpdate.equals(rankingUpdate));
+        // Null or wrong class inequality.
+        assertFalse(rankingUpdate.equals(null));
+        assertFalse(rankingUpdate.equals(ranking));
+
+        // Different ranking contents inequality.
+        NotificationListenerService.Ranking ranking2 = createTestRanking(TEST_KEY, 456);
+        NotificationRankingUpdate rankingUpdate2 = new NotificationRankingUpdate(
+                new NotificationListenerService.Ranking[]{ranking2});
+        assertFalse(rankingUpdate.equals(rankingUpdate2));
+
+        // Same ranking contents equality.
+        ranking2 = createTestRanking(TEST_KEY, 123);
+        rankingUpdate2 = new NotificationRankingUpdate(
+                new NotificationListenerService.Ranking[]{ranking2});
+        assertTrue(rankingUpdate.equals(rankingUpdate2));
+    }
+}
diff --git a/core/tests/coretests/src/android/text/StaticLayoutTest.java b/core/tests/coretests/src/android/text/StaticLayoutTest.java
index 925da49..0ebf03f 100644
--- a/core/tests/coretests/src/android/text/StaticLayoutTest.java
+++ b/core/tests/coretests/src/android/text/StaticLayoutTest.java
@@ -24,7 +24,6 @@
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.Paint.FontMetricsInt;
-import android.graphics.text.LineBreakConfig;
 import android.os.LocaleList;
 import android.platform.test.annotations.Presubmit;
 import android.text.Layout.Alignment;
@@ -926,24 +925,4 @@
         assertEquals(0, layout.getHeight(true));
         assertEquals(2, layout.getLineCount());
     }
-
-    @Test
-    public void testBuilder_autoPhraseBreaking() {
-        {
-            // setAutoPhraseBreaking true
-            LineBreakConfig lineBreakConfig = new LineBreakConfig.Builder()
-                    .setLineBreakWordStyle(LineBreakConfig.LINE_BREAK_STYLE_NONE)
-                    .setLineBreakWordStyle(LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE)
-                    .setAutoPhraseBreaking(true)
-                    .build();
-            final String text = "これが正解。";
-            // Obtain.
-            StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0,
-                    text.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
-            builder.setLineBreakConfig(lineBreakConfig);
-            builder.build();
-            assertEquals(LineBreakConfig.LINE_BREAK_WORD_STYLE_PHRASE,
-                    builder.getLineBreakWordStyle());
-        }
-    }
 }
diff --git a/core/tests/coretests/src/android/view/WindowInsetsTest.java b/core/tests/coretests/src/android/view/WindowInsetsTest.java
index b4ba23c..69abf5f 100644
--- a/core/tests/coretests/src/android/view/WindowInsetsTest.java
+++ b/core/tests/coretests/src/android/view/WindowInsetsTest.java
@@ -40,14 +40,14 @@
     @Test
     public void systemWindowInsets_afterConsuming_isConsumed() {
         assertTrue(new WindowInsets(WindowInsets.createCompatTypeMap(new Rect(1, 2, 3, 4)), null,
-                null, false, false, 0, null, null, null, null,
+                null, false, 0, 0, null, null, null, null,
                 WindowInsets.Type.systemBars(), false)
                 .consumeSystemWindowInsets().isConsumed());
     }
 
     @Test
     public void multiNullConstructor_isConsumed() {
-        assertTrue(new WindowInsets(null, null, null, false, false, 0, null, null, null, null,
+        assertTrue(new WindowInsets(null, null, null, false, 0, 0, null, null, null, null,
                 WindowInsets.Type.systemBars(), false).isConsumed());
     }
 
@@ -63,7 +63,7 @@
         boolean[] visible = new boolean[SIZE];
         WindowInsets.assignCompatInsets(maxInsets, new Rect(0, 10, 0, 0));
         WindowInsets.assignCompatInsets(insets, new Rect(0, 0, 0, 0));
-        WindowInsets windowInsets = new WindowInsets(insets, maxInsets, visible, false, false,
+        WindowInsets windowInsets = new WindowInsets(insets, maxInsets, visible, false, 0,
                 0, null, null, null, DisplayShape.NONE, systemBars(),
                 true /* compatIgnoreVisibility */);
         assertEquals(Insets.of(0, 10, 0, 0), windowInsets.getSystemWindowInsets());
diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java
index 17ed4c4..101f7c2 100644
--- a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java
@@ -15,14 +15,17 @@
  */
 package android.view.contentcapture;
 
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
+
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.Mockito.mock;
 import static org.testng.Assert.assertThrows;
 
 import android.content.ContentCaptureOptions;
 import android.content.Context;
 
+import com.android.internal.util.RingBuffer;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -37,9 +40,15 @@
 @RunWith(MockitoJUnitRunner.class)
 public class ContentCaptureManagerTest {
 
+    private static final int BUFFER_SIZE = 100;
+
+    private static final ContentCaptureOptions EMPTY_OPTIONS = new ContentCaptureOptions(null);
+
     @Mock
     private Context mMockContext;
 
+    @Mock private IContentCaptureManager mMockContentCaptureManager;
+
     @Test
     public void testConstructor_invalidParametersThrowsException() {
         assertThrows(NullPointerException.class,
@@ -48,11 +57,65 @@
     }
 
     @Test
+    public void testConstructor_contentProtection_default_bufferNotCreated() {
+        ContentCaptureManager manager =
+                new ContentCaptureManager(mMockContext, mMockContentCaptureManager, EMPTY_OPTIONS);
+
+        assertThat(manager.getContentProtectionEventBuffer()).isNull();
+    }
+
+    @Test
+    public void testConstructor_contentProtection_disabled_bufferNotCreated() {
+        ContentCaptureOptions options =
+                createOptions(
+                        new ContentCaptureOptions.ContentProtectionOptions(
+                                /* enableReceiver= */ false, BUFFER_SIZE));
+
+        ContentCaptureManager manager =
+                new ContentCaptureManager(mMockContext, mMockContentCaptureManager, options);
+
+        assertThat(manager.getContentProtectionEventBuffer()).isNull();
+    }
+
+    @Test
+    public void testConstructor_contentProtection_invalidBufferSize_bufferNotCreated() {
+        ContentCaptureOptions options =
+                createOptions(
+                        new ContentCaptureOptions.ContentProtectionOptions(
+                                /* enableReceiver= */ true, /* bufferSize= */ 0));
+
+        ContentCaptureManager manager =
+                new ContentCaptureManager(mMockContext, mMockContentCaptureManager, options);
+
+        assertThat(manager.getContentProtectionEventBuffer()).isNull();
+    }
+
+    @Test
+    public void testConstructor_contentProtection_enabled_bufferCreated() {
+        ContentCaptureOptions options =
+                createOptions(
+                        new ContentCaptureOptions.ContentProtectionOptions(
+                                /* enableReceiver= */ true, BUFFER_SIZE));
+
+        ContentCaptureManager manager =
+                new ContentCaptureManager(mMockContext, mMockContentCaptureManager, options);
+        RingBuffer<ContentCaptureEvent> buffer = manager.getContentProtectionEventBuffer();
+
+        assertThat(buffer).isNotNull();
+        ContentCaptureEvent[] expected = new ContentCaptureEvent[BUFFER_SIZE];
+        int offset = 3;
+        for (int i = 0; i < BUFFER_SIZE + offset; i++) {
+            ContentCaptureEvent event = new ContentCaptureEvent(i, TYPE_SESSION_STARTED);
+            buffer.append(event);
+            expected[(i + BUFFER_SIZE - offset) % BUFFER_SIZE] = event;
+        }
+        assertThat(buffer.toArray()).isEqualTo(expected);
+    }
+
+    @Test
     public void testRemoveData_invalidParametersThrowsException() {
-        final IContentCaptureManager mockService = mock(IContentCaptureManager.class);
-        final ContentCaptureOptions options = new ContentCaptureOptions(null);
         final ContentCaptureManager manager =
-                new ContentCaptureManager(mMockContext, mockService, options);
+                new ContentCaptureManager(mMockContext, mMockContentCaptureManager, EMPTY_OPTIONS);
 
         assertThrows(NullPointerException.class, () -> manager.removeData(null));
     }
@@ -60,10 +123,8 @@
     @Test
     @SuppressWarnings("GuardedBy")
     public void testFlushViewTreeAppearingEventDisabled_setAndGet() {
-        final IContentCaptureManager mockService = mock(IContentCaptureManager.class);
-        final ContentCaptureOptions options = new ContentCaptureOptions(null);
         final ContentCaptureManager manager =
-                new ContentCaptureManager(mMockContext, mockService, options);
+                new ContentCaptureManager(mMockContext, mMockContentCaptureManager, EMPTY_OPTIONS);
 
         assertThat(manager.getFlushViewTreeAppearingEventDisabled()).isFalse();
         manager.setFlushViewTreeAppearingEventDisabled(true);
@@ -71,4 +132,18 @@
         manager.setFlushViewTreeAppearingEventDisabled(false);
         assertThat(manager.getFlushViewTreeAppearingEventDisabled()).isFalse();
     }
+
+    private ContentCaptureOptions createOptions(
+            ContentCaptureOptions.ContentProtectionOptions contentProtectionOptions) {
+        return new ContentCaptureOptions(
+                /* loggingLevel= */ 0,
+                /* maxBufferSize= */ 0,
+                /* idleFlushingFrequencyMs= */ 0,
+                /* textChangeFlushingFrequencyMs= */ 0,
+                /* logHistorySize= */ 0,
+                /* disableFlushForViewTreeAppearing= */ false,
+                /* enableReceiver= */ true,
+                contentProtectionOptions,
+                /* whitelistedComponents= */ null);
+    }
 }
diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
index 27d58b8..23b9b9b 100644
--- a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
@@ -27,9 +27,12 @@
 import android.view.autofill.AutofillId;
 import android.view.contentcapture.ViewNode.ViewStructureImpl;
 
+import com.google.common.collect.ImmutableMap;
+
 import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
 
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestRule;
@@ -37,6 +40,8 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
+import java.util.Map;
+
 /**
  * Unit tests for {@link ContentCaptureSession}.
  *
@@ -126,6 +131,7 @@
                 () -> mSession1.notifyViewsDisappeared(new AutofillId(42, 108), new long[] {666}));
     }
 
+    @Ignore("b/286134492")
     @Test
     public void testNotifyViewsDisappeared_noSendTreeEventBeforeU() {
         MyContentCaptureSession session = new MyContentCaptureSession(121);
@@ -135,6 +141,7 @@
         assertThat(session.mInternalNotifyViewTreeEventFinishedCount).isEqualTo(0);
     }
 
+    @Ignore("b/286134492")
     @EnableCompatChanges({ContentCaptureSession.NOTIFY_NODES_DISAPPEAR_NOW_SENDS_TREE_EVENTS})
     @Test
     public void testNotifyViewsDisappeared_sendTreeEventSinceU() {
@@ -145,6 +152,34 @@
         assertThat(session.mInternalNotifyViewTreeEventFinishedCount).isEqualTo(1);
     }
 
+    @Test
+    public void testGetFlushReasonAsString() {
+        int invalidFlushReason = ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARED + 1;
+        Map<Integer, String> expectedMap =
+                new ImmutableMap.Builder<Integer, String>()
+                        .put(ContentCaptureSession.FLUSH_REASON_FULL, "FULL")
+                        .put(ContentCaptureSession.FLUSH_REASON_VIEW_ROOT_ENTERED, "VIEW_ROOT")
+                        .put(ContentCaptureSession.FLUSH_REASON_SESSION_STARTED, "STARTED")
+                        .put(ContentCaptureSession.FLUSH_REASON_SESSION_FINISHED, "FINISHED")
+                        .put(ContentCaptureSession.FLUSH_REASON_IDLE_TIMEOUT, "IDLE")
+                        .put(ContentCaptureSession.FLUSH_REASON_TEXT_CHANGE_TIMEOUT, "TEXT_CHANGE")
+                        .put(ContentCaptureSession.FLUSH_REASON_SESSION_CONNECTED, "CONNECTED")
+                        .put(ContentCaptureSession.FLUSH_REASON_FORCE_FLUSH, "FORCE_FLUSH")
+                        .put(
+                                ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARING,
+                                "VIEW_TREE_APPEARING")
+                        .put(
+                                ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARED,
+                                "VIEW_TREE_APPEARED")
+                        .put(invalidFlushReason, "UNKNOWN-" + invalidFlushReason)
+                        .build();
+
+        expectedMap.forEach(
+                (reason, expected) ->
+                        assertThat(ContentCaptureSession.getFlushReasonAsString(reason))
+                                .isEqualTo(expected));
+    }
+
     // Cannot use @Spy because we need to pass the session id on constructor
     private class MyContentCaptureSession extends ContentCaptureSession {
         int mInternalNotifyViewTreeEventStartedCount = 0;
diff --git a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
new file mode 100644
index 0000000..3373b8b
--- /dev/null
+++ b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.contentcapture;
+
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
+import android.content.Context;
+import android.content.pm.ParceledListSlice;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.contentprotection.ContentProtectionEventProcessor;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Test for {@link MainContentCaptureSession}.
+ *
+ * <p>Run with: {@code atest
+ * FrameworksCoreTests:android.view.contentcapture.MainContentCaptureSessionTest}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MainContentCaptureSessionTest {
+
+    private static final int BUFFER_SIZE = 100;
+
+    private static final int REASON = 123;
+
+    private static final ContentCaptureEvent EVENT =
+            new ContentCaptureEvent(/* sessionId= */ 0, TYPE_SESSION_STARTED);
+
+    private static final ComponentName COMPONENT_NAME =
+            new ComponentName("com.test.package", "TestClass");
+
+    private static final Context sContext = ApplicationProvider.getApplicationContext();
+
+    private static final ContentCaptureManager.StrippedContext sStrippedContext =
+            new ContentCaptureManager.StrippedContext(sContext);
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock private IContentCaptureManager mMockSystemServerInterface;
+
+    @Mock private ContentProtectionEventProcessor mMockContentProtectionEventProcessor;
+
+    @Mock private IContentCaptureDirectManager mMockContentCaptureDirectManager;
+
+    @Test
+    public void onSessionStarted_contentProtectionEnabled_processorCreated() {
+        MainContentCaptureSession session = createSession();
+        assertThat(session.mContentProtectionEventProcessor).isNull();
+
+        session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+
+        assertThat(session.mContentProtectionEventProcessor).isNotNull();
+    }
+
+    @Test
+    public void onSessionStarted_contentProtectionDisabled_processorNotCreated() {
+        MainContentCaptureSession session =
+                createSession(
+                        /* enableContentCaptureReceiver= */ true,
+                        /* enableContentProtectionReceiver= */ false);
+        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+
+        session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+
+        assertThat(session.mContentProtectionEventProcessor).isNull();
+        verifyZeroInteractions(mMockContentProtectionEventProcessor);
+    }
+
+    @Test
+    public void onSessionStarted_contentProtectionNoBuffer_processorNotCreated() {
+        ContentCaptureOptions options =
+                createOptions(
+                        /* enableContentCaptureReceiver= */ true,
+                        new ContentCaptureOptions.ContentProtectionOptions(
+                                /* enableReceiver= */ true, -BUFFER_SIZE));
+        MainContentCaptureSession session = createSession(options);
+        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+
+        session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+
+        assertThat(session.mContentProtectionEventProcessor).isNull();
+        verifyZeroInteractions(mMockContentProtectionEventProcessor);
+    }
+
+    @Test
+    public void onSessionStarted_noComponentName_processorNotCreated() {
+        MainContentCaptureSession session = createSession();
+        session.mComponentName = null;
+
+        session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+
+        assertThat(session.mContentProtectionEventProcessor).isNull();
+    }
+
+    @Test
+    public void sendEvent_contentCaptureDisabled_contentProtectionDisabled() {
+        MainContentCaptureSession session =
+                createSession(
+                        /* enableContentCaptureReceiver= */ false,
+                        /* enableContentProtectionReceiver= */ false);
+        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+
+        session.sendEvent(EVENT);
+
+        verifyZeroInteractions(mMockContentProtectionEventProcessor);
+        assertThat(session.mEvents).isNull();
+    }
+
+    @Test
+    public void sendEvent_contentCaptureDisabled_contentProtectionEnabled() {
+        MainContentCaptureSession session =
+                createSession(
+                        /* enableContentCaptureReceiver= */ false,
+                        /* enableContentProtectionReceiver= */ true);
+        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+
+        session.sendEvent(EVENT);
+
+        verify(mMockContentProtectionEventProcessor).processEvent(EVENT);
+        assertThat(session.mEvents).isNull();
+    }
+
+    @Test
+    public void sendEvent_contentCaptureEnabled_contentProtectionDisabled() {
+        MainContentCaptureSession session =
+                createSession(
+                        /* enableContentCaptureReceiver= */ true,
+                        /* enableContentProtectionReceiver= */ false);
+        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+
+        session.sendEvent(EVENT);
+
+        verifyZeroInteractions(mMockContentProtectionEventProcessor);
+        assertThat(session.mEvents).isNotNull();
+        assertThat(session.mEvents).containsExactly(EVENT);
+    }
+
+    @Test
+    public void sendEvent_contentCaptureEnabled_contentProtectionEnabled() {
+        MainContentCaptureSession session = createSession();
+        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+
+        session.sendEvent(EVENT);
+
+        verify(mMockContentProtectionEventProcessor).processEvent(EVENT);
+        assertThat(session.mEvents).isNotNull();
+        assertThat(session.mEvents).containsExactly(EVENT);
+    }
+
+    @Test
+    public void sendEvent_contentProtectionEnabled_processorNotCreated() {
+        MainContentCaptureSession session =
+                createSession(
+                        /* enableContentCaptureReceiver= */ false,
+                        /* enableContentProtectionReceiver= */ true);
+
+        session.sendEvent(EVENT);
+
+        verifyZeroInteractions(mMockContentProtectionEventProcessor);
+        assertThat(session.mEvents).isNull();
+    }
+
+    @Test
+    public void flush_contentCaptureDisabled_contentProtectionDisabled() throws Exception {
+        ContentCaptureOptions options =
+                createOptions(
+                        /* enableContentCaptureReceiver= */ false,
+                        /* enableContentProtectionReceiver= */ false);
+        MainContentCaptureSession session = createSession(options);
+        session.mEvents = new ArrayList<>(Arrays.asList(EVENT));
+        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+
+        session.flush(REASON);
+
+        verifyZeroInteractions(mMockContentProtectionEventProcessor);
+        verifyZeroInteractions(mMockContentCaptureDirectManager);
+        assertThat(session.mEvents).containsExactly(EVENT);
+    }
+
+    @Test
+    public void flush_contentCaptureDisabled_contentProtectionEnabled() {
+        MainContentCaptureSession session =
+                createSession(
+                        /* enableContentCaptureReceiver= */ false,
+                        /* enableContentProtectionReceiver= */ true);
+        session.mEvents = new ArrayList<>(Arrays.asList(EVENT));
+        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+
+        session.flush(REASON);
+
+        verifyZeroInteractions(mMockContentProtectionEventProcessor);
+        verifyZeroInteractions(mMockContentCaptureDirectManager);
+        assertThat(session.mEvents).containsExactly(EVENT);
+    }
+
+    @Test
+    public void flush_contentCaptureEnabled_contentProtectionDisabled() throws Exception {
+        ContentCaptureOptions options =
+                createOptions(
+                        /* enableContentCaptureReceiver= */ true,
+                        /* enableContentProtectionReceiver= */ false);
+        MainContentCaptureSession session = createSession(options);
+        session.mEvents = new ArrayList<>(Arrays.asList(EVENT));
+        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+
+        session.flush(REASON);
+
+        verifyZeroInteractions(mMockContentProtectionEventProcessor);
+        assertThat(session.mEvents).isEmpty();
+        assertEventFlushedContentCapture(options);
+    }
+
+    @Test
+    public void flush_contentCaptureEnabled_contentProtectionEnabled() throws Exception {
+        ContentCaptureOptions options =
+                createOptions(
+                        /* enableContentCaptureReceiver= */ true,
+                        /* enableContentProtectionReceiver= */ true);
+        MainContentCaptureSession session = createSession(options);
+        session.mEvents = new ArrayList<>(Arrays.asList(EVENT));
+        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+
+        session.flush(REASON);
+
+        verifyZeroInteractions(mMockContentProtectionEventProcessor);
+        assertThat(session.mEvents).isEmpty();
+        assertEventFlushedContentCapture(options);
+    }
+
+    @Test
+    public void destroySession() throws Exception {
+        MainContentCaptureSession session = createSession();
+        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+
+        session.destroySession();
+
+        verify(mMockSystemServerInterface).finishSession(anyInt());
+        verifyZeroInteractions(mMockContentProtectionEventProcessor);
+        assertThat(session.mDirectServiceInterface).isNull();
+        assertThat(session.mContentProtectionEventProcessor).isNull();
+    }
+
+    @Test
+    public void resetSession() {
+        MainContentCaptureSession session = createSession();
+        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+
+        session.resetSession(/* newState= */ 0);
+
+        verifyZeroInteractions(mMockSystemServerInterface);
+        verifyZeroInteractions(mMockContentProtectionEventProcessor);
+        assertThat(session.mDirectServiceInterface).isNull();
+        assertThat(session.mContentProtectionEventProcessor).isNull();
+    }
+
+    private static ContentCaptureOptions createOptions(
+            boolean enableContentCaptureReceiver,
+            ContentCaptureOptions.ContentProtectionOptions contentProtectionOptions) {
+        return new ContentCaptureOptions(
+                /* loggingLevel= */ 0,
+                BUFFER_SIZE,
+                /* idleFlushingFrequencyMs= */ 0,
+                /* textChangeFlushingFrequencyMs= */ 0,
+                /* logHistorySize= */ 0,
+                /* disableFlushForViewTreeAppearing= */ false,
+                enableContentCaptureReceiver,
+                contentProtectionOptions,
+                /* whitelistedComponents= */ null);
+    }
+
+    private static ContentCaptureOptions createOptions(
+            boolean enableContentCaptureReceiver, boolean enableContentProtectionReceiver) {
+        return createOptions(
+                enableContentCaptureReceiver,
+                new ContentCaptureOptions.ContentProtectionOptions(
+                        enableContentProtectionReceiver, BUFFER_SIZE));
+    }
+
+    private ContentCaptureManager createManager(ContentCaptureOptions options) {
+        return new ContentCaptureManager(sContext, mMockSystemServerInterface, options);
+    }
+
+    private MainContentCaptureSession createSession(ContentCaptureManager manager) {
+        MainContentCaptureSession session =
+                new MainContentCaptureSession(
+                        sStrippedContext,
+                        manager,
+                        new Handler(Looper.getMainLooper()),
+                        mMockSystemServerInterface);
+        session.mComponentName = COMPONENT_NAME;
+        return session;
+    }
+
+    private MainContentCaptureSession createSession(ContentCaptureOptions options) {
+        return createSession(createManager(options));
+    }
+
+    private MainContentCaptureSession createSession(
+            boolean enableContentCaptureReceiver, boolean enableContentProtectionReceiver) {
+        return createSession(
+                createOptions(enableContentCaptureReceiver, enableContentProtectionReceiver));
+    }
+
+    private MainContentCaptureSession createSession() {
+        return createSession(
+                /* enableContentCaptureReceiver= */ true,
+                /* enableContentProtectionReceiver= */ true);
+    }
+
+    private void assertEventFlushedContentCapture(ContentCaptureOptions options) throws Exception {
+        ArgumentCaptor<ParceledListSlice> captor = ArgumentCaptor.forClass(ParceledListSlice.class);
+        verify(mMockContentCaptureDirectManager)
+                .sendEvents(captor.capture(), eq(REASON), eq(options));
+
+        assertThat(captor.getValue()).isNotNull();
+        List<ContentCaptureEvent> actual = captor.getValue().getList();
+        assertThat(actual).isNotNull();
+        assertThat(actual).containsExactly(EVENT);
+    }
+}
diff --git a/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java b/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java
index 93315f1..a4e77f5 100644
--- a/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java
@@ -41,49 +41,60 @@
 
     private final Context mContext = InstrumentationRegistry.getTargetContext();
 
+    private final View mView = new View(mContext);
+
+    private final ViewStructureImpl mViewStructure = new ViewStructureImpl(mView);
+
+    private final ViewNode mViewNode = mViewStructure.getNode();
+
     @Mock
     private HtmlInfo mHtmlInfoMock;
 
     @Test
     public void testUnsupportedProperties() {
-        View view = new View(mContext);
+        mViewStructure.setChildCount(1);
+        assertThat(mViewNode.getChildCount()).isEqualTo(0);
 
-        ViewStructureImpl structure = new ViewStructureImpl(view);
-        ViewNode node = structure.getNode();
+        mViewStructure.addChildCount(1);
+        assertThat(mViewNode.getChildCount()).isEqualTo(0);
 
-        structure.setChildCount(1);
-        assertThat(node.getChildCount()).isEqualTo(0);
+        assertThat(mViewStructure.newChild(0)).isNull();
+        assertThat(mViewNode.getChildCount()).isEqualTo(0);
 
-        structure.addChildCount(1);
-        assertThat(node.getChildCount()).isEqualTo(0);
+        assertThat(mViewStructure.asyncNewChild(0)).isNull();
+        assertThat(mViewNode.getChildCount()).isEqualTo(0);
 
-        assertThat(structure.newChild(0)).isNull();
-        assertThat(node.getChildCount()).isEqualTo(0);
+        mViewStructure.asyncCommit();
+        assertThat(mViewNode.getChildCount()).isEqualTo(0);
 
-        assertThat(structure.asyncNewChild(0)).isNull();
-        assertThat(node.getChildCount()).isEqualTo(0);
+        mViewStructure.setWebDomain("Y U NO SET?");
+        assertThat(mViewNode.getWebDomain()).isNull();
 
-        structure.asyncCommit();
-        assertThat(node.getChildCount()).isEqualTo(0);
+        assertThat(mViewStructure.newHtmlInfoBuilder("WHATEVER")).isNull();
 
-        structure.setWebDomain("Y U NO SET?");
-        assertThat(node.getWebDomain()).isNull();
+        mViewStructure.setHtmlInfo(mHtmlInfoMock);
+        assertThat(mViewNode.getHtmlInfo()).isNull();
 
-        assertThat(structure.newHtmlInfoBuilder("WHATEVER")).isNull();
+        mViewStructure.setDataIsSensitive(true);
 
-        structure.setHtmlInfo(mHtmlInfoMock);
-        assertThat(node.getHtmlInfo()).isNull();
-
-        structure.setDataIsSensitive(true);
-
-        assertThat(structure.getTempRect()).isNull();
+        assertThat(mViewStructure.getTempRect()).isNull();
 
         // Graphic properties
-        structure.setElevation(6.66f);
-        assertThat(node.getElevation()).isWithin(1.0e-10f).of(0f);
-        structure.setAlpha(66.6f);
-        assertThat(node.getAlpha()).isWithin(1.0e-10f).of(1.0f);
-        structure.setTransformation(Matrix.IDENTITY_MATRIX);
-        assertThat(node.getTransformation()).isNull();
+        mViewStructure.setElevation(6.66f);
+        assertThat(mViewNode.getElevation()).isEqualTo(0f);
+        mViewStructure.setAlpha(66.6f);
+        assertThat(mViewNode.getAlpha()).isEqualTo(1.0f);
+        mViewStructure.setTransformation(Matrix.IDENTITY_MATRIX);
+        assertThat(mViewNode.getTransformation()).isNull();
+    }
+
+    @Test
+    public void testGetSet_textIdEntry() {
+        assertThat(mViewNode.getTextIdEntry()).isNull();
+
+        String expected = "TEXT_ID_ENTRY";
+        mViewNode.setTextIdEntry(expected);
+
+        assertThat(mViewNode.getTextIdEntry()).isEqualTo(expected);
     }
 }
diff --git a/core/tests/coretests/src/android/view/contentprotection/ContentProtectionEventProcessorTest.java b/core/tests/coretests/src/android/view/contentprotection/ContentProtectionEventProcessorTest.java
new file mode 100644
index 0000000..39a2e0e
--- /dev/null
+++ b/core/tests/coretests/src/android/view/contentprotection/ContentProtectionEventProcessorTest.java
@@ -0,0 +1,568 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.contentprotection;
+
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.ParceledListSlice;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.InputType;
+import android.view.View;
+import android.view.contentcapture.ContentCaptureEvent;
+import android.view.contentcapture.IContentCaptureManager;
+import android.view.contentcapture.ViewNode;
+import android.view.contentcapture.ViewNode.ViewStructureImpl;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.util.RingBuffer;
+
+import com.google.common.collect.ImmutableSet;
+
+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.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Test for {@link ContentProtectionEventProcessor}.
+ *
+ * <p>Run with: {@code atest
+ * FrameworksCoreTests:android.view.contentprotection.ContentProtectionEventProcessorTest}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ContentProtectionEventProcessorTest {
+
+    private static final String PACKAGE_NAME = "com.test.package.name";
+
+    private static final String ANDROID_CLASS_NAME = "android.test.some.class.name";
+
+    private static final String PASSWORD_TEXT = "ENTER PASSWORD HERE";
+
+    private static final String SUSPICIOUS_TEXT = "PLEASE SIGN IN";
+
+    private static final String SAFE_TEXT = "SAFE TEXT";
+
+    private static final ContentCaptureEvent PROCESS_EVENT = createProcessEvent();
+
+    private static final ContentCaptureEvent[] BUFFERED_EVENTS =
+            new ContentCaptureEvent[] {PROCESS_EVENT};
+
+    private static final Set<Integer> EVENT_TYPES_TO_STORE =
+            ImmutableSet.of(TYPE_VIEW_APPEARED, TYPE_VIEW_DISAPPEARED, TYPE_VIEW_TEXT_CHANGED);
+
+    private static final int RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS = 150;
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock private RingBuffer<ContentCaptureEvent> mMockEventBuffer;
+
+    @Mock private IContentCaptureManager mMockContentCaptureManager;
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+
+    private ContentProtectionEventProcessor mContentProtectionEventProcessor;
+
+    @Before
+    public void setup() {
+        mContentProtectionEventProcessor =
+                new ContentProtectionEventProcessor(
+                        mMockEventBuffer,
+                        new Handler(Looper.getMainLooper()),
+                        mMockContentCaptureManager,
+                        PACKAGE_NAME);
+    }
+
+    @Test
+    public void processEvent_buffer_storesOnlySubsetOfEventTypes() {
+        List<ContentCaptureEvent> expectedEvents = new ArrayList<>();
+        for (int type = -100; type <= 100; type++) {
+            ContentCaptureEvent event = createEvent(type);
+            if (EVENT_TYPES_TO_STORE.contains(type)) {
+                expectedEvents.add(event);
+            }
+
+            mContentProtectionEventProcessor.processEvent(event);
+        }
+
+        assertThat(expectedEvents).hasSize(EVENT_TYPES_TO_STORE.size());
+        expectedEvents.forEach((expectedEvent) -> verify(mMockEventBuffer).append(expectedEvent));
+        verifyNoMoreInteractions(mMockEventBuffer);
+    }
+
+    @Test
+    public void processEvent_buffer_setsTextIdEntry_withoutExistingViewNode() {
+        ContentCaptureEvent event = createStoreEvent();
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(event.getViewNode()).isNotNull();
+        assertThat(event.getViewNode().getTextIdEntry()).isEqualTo(PACKAGE_NAME);
+        verify(mMockEventBuffer).append(event);
+    }
+
+    @Test
+    public void processEvent_buffer_setsTextIdEntry_withExistingViewNode() {
+        ViewNode viewNode = new ViewNode();
+        viewNode.setTextIdEntry(PACKAGE_NAME + "TO BE OVERWRITTEN");
+        ContentCaptureEvent event = createStoreEvent();
+        event.setViewNode(viewNode);
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(event.getViewNode()).isSameInstanceAs(viewNode);
+        assertThat(viewNode.getTextIdEntry()).isEqualTo(PACKAGE_NAME);
+        verify(mMockEventBuffer).append(event);
+    }
+
+    @Test
+    public void processEvent_loginDetected_inspectsOnlyTypeViewAppeared() {
+        mContentProtectionEventProcessor.mPasswordFieldDetected = true;
+        mContentProtectionEventProcessor.mSuspiciousTextDetected = true;
+
+        for (int type = -100; type <= 100; type++) {
+            if (type == TYPE_VIEW_APPEARED) {
+                continue;
+            }
+
+            mContentProtectionEventProcessor.processEvent(createEvent(type));
+
+            assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isTrue();
+            assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue();
+        }
+
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void processEvent_loginDetected() throws Exception {
+        when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS);
+        mContentProtectionEventProcessor.mPasswordFieldDetected = true;
+        mContentProtectionEventProcessor.mSuspiciousTextDetected = true;
+
+        mContentProtectionEventProcessor.processEvent(PROCESS_EVENT);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse();
+        verify(mMockEventBuffer).clear();
+        verify(mMockEventBuffer).toArray();
+        assertOnLoginDetected();
+    }
+
+    @Test
+    public void processEvent_loginDetected_passwordFieldNotDetected() {
+        mContentProtectionEventProcessor.mSuspiciousTextDetected = true;
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+
+        mContentProtectionEventProcessor.processEvent(PROCESS_EVENT);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void processEvent_loginDetected_suspiciousTextNotDetected() {
+        mContentProtectionEventProcessor.mPasswordFieldDetected = true;
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse();
+
+        mContentProtectionEventProcessor.processEvent(PROCESS_EVENT);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isTrue();
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void processEvent_loginDetected_withoutViewNode() {
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse();
+
+        mContentProtectionEventProcessor.processEvent(PROCESS_EVENT);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void processEvent_loginDetected_belowResetLimit() throws Exception {
+        when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS);
+        mContentProtectionEventProcessor.mSuspiciousTextDetected = true;
+        ContentCaptureEvent event =
+                createAndroidPasswordFieldEvent(
+                        ANDROID_CLASS_NAME, InputType.TYPE_TEXT_VARIATION_PASSWORD);
+
+        for (int i = 0; i < RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS; i++) {
+            mContentProtectionEventProcessor.processEvent(PROCESS_EVENT);
+        }
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse();
+        verify(mMockEventBuffer).clear();
+        verify(mMockEventBuffer).toArray();
+        assertOnLoginDetected();
+    }
+
+    @Test
+    public void processEvent_loginDetected_aboveResetLimit() throws Exception {
+        mContentProtectionEventProcessor.mSuspiciousTextDetected = true;
+        ContentCaptureEvent event =
+                createAndroidPasswordFieldEvent(
+                        ANDROID_CLASS_NAME, InputType.TYPE_TEXT_VARIATION_PASSWORD);
+
+        for (int i = 0; i < RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS + 1; i++) {
+            mContentProtectionEventProcessor.processEvent(PROCESS_EVENT);
+        }
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isTrue();
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+    }
+
+    @Test
+    public void processEvent_multipleLoginsDetected_belowFlushThreshold() throws Exception {
+        when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS);
+
+        mContentProtectionEventProcessor.mPasswordFieldDetected = true;
+        mContentProtectionEventProcessor.mSuspiciousTextDetected = true;
+        mContentProtectionEventProcessor.processEvent(PROCESS_EVENT);
+
+        mContentProtectionEventProcessor.mPasswordFieldDetected = true;
+        mContentProtectionEventProcessor.mSuspiciousTextDetected = true;
+        mContentProtectionEventProcessor.processEvent(PROCESS_EVENT);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse();
+        verify(mMockEventBuffer).clear();
+        verify(mMockEventBuffer).toArray();
+        assertOnLoginDetected();
+    }
+
+    @Test
+    public void processEvent_multipleLoginsDetected_aboveFlushThreshold() throws Exception {
+        when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS);
+
+        mContentProtectionEventProcessor.mPasswordFieldDetected = true;
+        mContentProtectionEventProcessor.mSuspiciousTextDetected = true;
+        mContentProtectionEventProcessor.processEvent(PROCESS_EVENT);
+
+        mContentProtectionEventProcessor.mLastFlushTime = Instant.now().minusSeconds(5);
+
+        mContentProtectionEventProcessor.mPasswordFieldDetected = true;
+        mContentProtectionEventProcessor.mSuspiciousTextDetected = true;
+        mContentProtectionEventProcessor.processEvent(PROCESS_EVENT);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse();
+        verify(mMockEventBuffer, times(2)).clear();
+        verify(mMockEventBuffer, times(2)).toArray();
+        assertOnLoginDetected(PROCESS_EVENT, /* times= */ 2);
+    }
+
+    @Test
+    public void isPasswordField_android() {
+        ContentCaptureEvent event =
+                createAndroidPasswordFieldEvent(
+                        ANDROID_CLASS_NAME, InputType.TYPE_TEXT_VARIATION_PASSWORD);
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isTrue();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void isPasswordField_android_withoutClassName() {
+        ContentCaptureEvent event =
+                createAndroidPasswordFieldEvent(
+                        /* className= */ null, InputType.TYPE_TEXT_VARIATION_PASSWORD);
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void isPasswordField_android_wrongClassName() {
+        ContentCaptureEvent event =
+                createAndroidPasswordFieldEvent(
+                        "wrong.prefix" + ANDROID_CLASS_NAME,
+                        InputType.TYPE_TEXT_VARIATION_PASSWORD);
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void isPasswordField_android_wrongInputType() {
+        ContentCaptureEvent event =
+                createAndroidPasswordFieldEvent(
+                        ANDROID_CLASS_NAME, InputType.TYPE_TEXT_VARIATION_NORMAL);
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void isPasswordField_webView() throws Exception {
+        ContentCaptureEvent event =
+                createWebViewPasswordFieldEvent(
+                        /* className= */ null, /* eventText= */ null, PASSWORD_TEXT);
+        when(mMockEventBuffer.toArray()).thenReturn(new ContentCaptureEvent[] {event});
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        verify(mMockEventBuffer).clear();
+        verify(mMockEventBuffer).toArray();
+        assertOnLoginDetected(event, /* times= */ 1);
+    }
+
+    @Test
+    public void isPasswordField_webView_withClassName() {
+        ContentCaptureEvent event =
+                createWebViewPasswordFieldEvent(
+                        /* className= */ "any.class.name", /* eventText= */ null, PASSWORD_TEXT);
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void isPasswordField_webView_withSafeViewNodeText() {
+        ContentCaptureEvent event =
+                createWebViewPasswordFieldEvent(
+                        /* className= */ null, /* eventText= */ null, SAFE_TEXT);
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void isPasswordField_webView_withEventText() {
+        ContentCaptureEvent event =
+                createWebViewPasswordFieldEvent(/* className= */ null, PASSWORD_TEXT, SAFE_TEXT);
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void isSuspiciousText_withSafeText() {
+        ContentCaptureEvent event = createSuspiciousTextEvent(SAFE_TEXT, SAFE_TEXT);
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void isSuspiciousText_eventText_suspiciousText() {
+        ContentCaptureEvent event = createSuspiciousTextEvent(SUSPICIOUS_TEXT, SAFE_TEXT);
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void isSuspiciousText_viewNodeText_suspiciousText() {
+        ContentCaptureEvent event = createSuspiciousTextEvent(SAFE_TEXT, SUSPICIOUS_TEXT);
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void isSuspiciousText_eventText_passwordText() {
+        ContentCaptureEvent event = createSuspiciousTextEvent(PASSWORD_TEXT, SAFE_TEXT);
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    @Test
+    public void isSuspiciousText_viewNodeText_passwordText() {
+        // Specify the class to differ from {@link isPasswordField_webView} test in this version
+        ContentCaptureEvent event =
+                createProcessEvent(
+                        "test.class.not.a.web.view", /* inputType= */ 0, SAFE_TEXT, PASSWORD_TEXT);
+
+        mContentProtectionEventProcessor.processEvent(event);
+
+        assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue();
+        verify(mMockEventBuffer, never()).clear();
+        verify(mMockEventBuffer, never()).toArray();
+        verifyZeroInteractions(mMockContentCaptureManager);
+    }
+
+    private static ContentCaptureEvent createEvent(int type) {
+        return new ContentCaptureEvent(/* sessionId= */ 123, type);
+    }
+
+    private static ContentCaptureEvent createStoreEvent() {
+        return createEvent(TYPE_VIEW_TEXT_CHANGED);
+    }
+
+    private static ContentCaptureEvent createProcessEvent() {
+        return createEvent(TYPE_VIEW_APPEARED);
+    }
+
+    private ContentCaptureEvent createProcessEvent(
+            @Nullable String className,
+            int inputType,
+            @Nullable String eventText,
+            @Nullable String viewNodeText) {
+        View view = new View(mContext);
+        ViewStructureImpl viewStructure = new ViewStructureImpl(view);
+        if (className != null) {
+            viewStructure.setClassName(className);
+        }
+        if (viewNodeText != null) {
+            viewStructure.setText(viewNodeText);
+        }
+        viewStructure.setInputType(inputType);
+
+        ContentCaptureEvent event = createProcessEvent();
+        event.setViewNode(viewStructure.getNode());
+        if (eventText != null) {
+            event.setText(eventText);
+        }
+
+        return event;
+    }
+
+    private ContentCaptureEvent createAndroidPasswordFieldEvent(
+            @Nullable String className, int inputType) {
+        return createProcessEvent(
+                className, inputType, /* eventText= */ null, /* viewNodeText= */ null);
+    }
+
+    private ContentCaptureEvent createWebViewPasswordFieldEvent(
+            @Nullable String className, @Nullable String eventText, @Nullable String viewNodeText) {
+        return createProcessEvent(className, /* inputType= */ 0, eventText, viewNodeText);
+    }
+
+    private ContentCaptureEvent createSuspiciousTextEvent(
+            @Nullable String eventText, @Nullable String viewNodeText) {
+        return createProcessEvent(
+                /* className= */ null, /* inputType= */ 0, eventText, viewNodeText);
+    }
+
+    private void assertOnLoginDetected() throws Exception {
+        assertOnLoginDetected(PROCESS_EVENT, /* times= */ 1);
+    }
+
+    private void assertOnLoginDetected(ContentCaptureEvent event, int times) throws Exception {
+        ArgumentCaptor<ParceledListSlice> captor = ArgumentCaptor.forClass(ParceledListSlice.class);
+        verify(mMockContentCaptureManager, times(times)).onLoginDetected(captor.capture());
+
+        assertThat(captor.getValue()).isNotNull();
+        List<ContentCaptureEvent> actual = captor.getValue().getList();
+        assertThat(actual).isNotNull();
+        assertThat(actual).containsExactly(event);
+    }
+}
diff --git a/core/tests/coretests/src/android/view/contentprotection/ContentProtectionUtilsTest.java b/core/tests/coretests/src/android/view/contentprotection/ContentProtectionUtilsTest.java
new file mode 100644
index 0000000..1459799
--- /dev/null
+++ b/core/tests/coretests/src/android/view/contentprotection/ContentProtectionUtilsTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package android.view.contentprotection;
+
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.view.View;
+import android.view.contentcapture.ContentCaptureEvent;
+import android.view.contentcapture.ViewNode;
+import android.view.contentcapture.ViewNode.ViewStructureImpl;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for {@link ContentProtectionUtils}.
+ *
+ * <p>Run with: {@code atest
+ * FrameworksCoreTests:android.view.contentprotection.ContentProtectionUtilsTest}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ContentProtectionUtilsTest {
+
+    private static final String TEXT = "TEST_TEXT";
+
+    private static final ContentCaptureEvent EVENT = createEvent();
+
+    private static final ViewNode VIEW_NODE = new ViewNode();
+
+    private static final ViewNode VIEW_NODE_WITH_TEXT = createViewNodeWithText();
+
+    @Test
+    public void event_getEventText_null() {
+        String actual = ContentProtectionUtils.getEventText(EVENT);
+
+        assertThat(actual).isNull();
+    }
+
+    @Test
+    public void event_getEventText_notNull() {
+        ContentCaptureEvent event = createEvent();
+        event.setText(TEXT);
+
+        String actual = ContentProtectionUtils.getEventText(event);
+
+        assertThat(actual).isEqualTo(TEXT);
+    }
+
+    @Test
+    public void event_getViewNodeText_null() {
+        String actual = ContentProtectionUtils.getViewNodeText(EVENT);
+
+        assertThat(actual).isNull();
+    }
+
+    @Test
+    public void event_getViewNodeText_notNull() {
+        ContentCaptureEvent event = createEvent();
+        event.setViewNode(VIEW_NODE_WITH_TEXT);
+
+        String actual = ContentProtectionUtils.getViewNodeText(event);
+
+        assertThat(actual).isEqualTo(TEXT);
+    }
+
+    @Test
+    public void viewNode_getViewNodeText_null() {
+        String actual = ContentProtectionUtils.getViewNodeText(VIEW_NODE);
+
+        assertThat(actual).isNull();
+    }
+
+    @Test
+    public void viewNode_getViewNodeText_notNull() {
+        String actual = ContentProtectionUtils.getViewNodeText(VIEW_NODE_WITH_TEXT);
+
+        assertThat(actual).isEqualTo(TEXT);
+    }
+
+    private static ContentCaptureEvent createEvent() {
+        return new ContentCaptureEvent(/* sessionId= */ 123, TYPE_SESSION_STARTED);
+    }
+
+    private static ViewNode createViewNodeWithText() {
+        View view = new View(ApplicationProvider.getApplicationContext());
+        ViewStructureImpl viewStructure = new ViewStructureImpl(view);
+        viewStructure.setText(TEXT);
+        return viewStructure.getNode();
+    }
+}
diff --git a/core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java b/core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java
index f93cd18..0750cf1 100644
--- a/core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java
+++ b/core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java
@@ -37,6 +37,7 @@
 import android.text.style.ScaleXSpan;
 import android.text.style.StyleSpan;
 import android.text.style.TypefaceSpan;
+import android.text.util.Linkify;
 import android.view.ViewGroup;
 import android.widget.EditText;
 
@@ -53,7 +54,7 @@
 @RunWith(AndroidJUnit4.class)
 public class TextAppearanceInfoTest {
     private static final float EPSILON = 0.0000001f;
-    private static final String TEST_TEXT = "Happy birthday!";
+    private static final String TEST_TEXT = "Hello: google.com";
     private static final float TEXT_SIZE = 16.5f;
     private static final LocaleList TEXT_LOCALES = LocaleList.forLanguageTags("en,ja");
     private static final String FONT_FAMILY_NAME = "sans-serif";
@@ -84,39 +85,7 @@
 
     @Before
     public void setUp() {
-        mEditText.setText(mSpannableText);
-        mEditText.getPaint().setTextSize(TEXT_SIZE);
-        mEditText.setTextLocales(TEXT_LOCALES);
-        Typeface family = Typeface.create(FONT_FAMILY_NAME, Typeface.NORMAL);
-        mEditText.setTypeface(
-                Typeface.create(family, TEXT_WEIGHT, (TEXT_STYLE & Typeface.ITALIC) != 0));
-        mEditText.setAllCaps(ALL_CAPS);
-        mEditText.setShadowLayer(SHADOW_RADIUS, SHADOW_DX, SHADOW_DY, SHADOW_COLOR);
-        mEditText.setElegantTextHeight(ELEGANT_TEXT_HEIGHT);
-        mEditText.setFallbackLineSpacing(FALLBACK_LINE_SPACING);
-        mEditText.setLetterSpacing(LETTER_SPACING);
-        mEditText.setFontFeatureSettings(FONT_FEATURE_SETTINGS);
-        mEditText.setFontVariationSettings(FONT_VARIATION_SETTINGS);
-        mEditText.setLineBreakStyle(LINE_BREAK_STYLE);
-        mEditText.setLineBreakWordStyle(LINE_BREAK_WORD_STYLE);
-        mEditText.setTextScaleX(TEXT_SCALEX);
-        mEditText.setHighlightColor(HIGHLIGHT_TEXT_COLOR);
-        mEditText.setTextColor(TEXT_COLOR);
-        mEditText.setHintTextColor(HINT_TEXT_COLOR);
-        mEditText.setLinkTextColor(LINK_TEXT_COLOR);
-        ViewGroup.LayoutParams params =
-                new ViewGroup.LayoutParams(
-                        ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
-        mEditText.setLayoutParams(params);
-        mEditText.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
-        Bitmap bitmap =
-                Bitmap.createBitmap(
-                        Math.max(1, mEditText.getMeasuredWidth()),
-                        Math.max(1, mEditText.getMeasuredHeight()),
-                        Bitmap.Config.ARGB_8888);
-        mEditText.layout(0, 0, mEditText.getMeasuredWidth(), mEditText.getMeasuredHeight());
-        mCanvas = new Canvas(bitmap);
-        mEditText.draw(mCanvas);
+        initEditText(mSpannableText);
     }
 
     @Test
@@ -233,6 +202,43 @@
         assertEquals(info1.getSystemFontFamilyName(), FONT_FAMILY_NAME);
     }
 
+    private void initEditText(CharSequence text) {
+        mEditText.setText(text);
+        mEditText.getPaint().setTextSize(TEXT_SIZE);
+        mEditText.setTextLocales(TEXT_LOCALES);
+        Typeface family = Typeface.create(FONT_FAMILY_NAME, Typeface.NORMAL);
+        mEditText.setTypeface(
+                Typeface.create(family, TEXT_WEIGHT, (TEXT_STYLE & Typeface.ITALIC) != 0));
+        mEditText.setAllCaps(ALL_CAPS);
+        mEditText.setShadowLayer(SHADOW_RADIUS, SHADOW_DX, SHADOW_DY, SHADOW_COLOR);
+        mEditText.setElegantTextHeight(ELEGANT_TEXT_HEIGHT);
+        mEditText.setFallbackLineSpacing(FALLBACK_LINE_SPACING);
+        mEditText.setLetterSpacing(LETTER_SPACING);
+        mEditText.setFontFeatureSettings(FONT_FEATURE_SETTINGS);
+        mEditText.setFontVariationSettings(FONT_VARIATION_SETTINGS);
+        mEditText.setLineBreakStyle(LINE_BREAK_STYLE);
+        mEditText.setLineBreakWordStyle(LINE_BREAK_WORD_STYLE);
+        mEditText.setTextScaleX(TEXT_SCALEX);
+        mEditText.setHighlightColor(HIGHLIGHT_TEXT_COLOR);
+        mEditText.setTextColor(TEXT_COLOR);
+        mEditText.setHintTextColor(HINT_TEXT_COLOR);
+        mEditText.setHint("Hint text");
+        mEditText.setLinkTextColor(LINK_TEXT_COLOR);
+        mEditText.setAutoLinkMask(Linkify.WEB_URLS);
+        ViewGroup.LayoutParams params =
+                new ViewGroup.LayoutParams(
+                        ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+        mEditText.setLayoutParams(params);
+        mEditText.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+        Bitmap bitmap =
+                Bitmap.createBitmap(
+                        Math.max(1, mEditText.getMeasuredWidth()),
+                        Math.max(1, mEditText.getMeasuredHeight()),
+                        Bitmap.Config.ARGB_8888);
+        mEditText.layout(0, 0, mEditText.getMeasuredWidth(), mEditText.getMeasuredHeight());
+        mCanvas = new Canvas(bitmap);
+        mEditText.draw(mCanvas);
+    }
     private void assertTextAppearanceInfoContentsEqual(TextAppearanceInfo textAppearanceInfo) {
         assertEquals(textAppearanceInfo.getTextSize(), TEXT_SIZE, EPSILON);
         assertEquals(textAppearanceInfo.getTextLocales(), TEXT_LOCALES);
@@ -258,6 +264,15 @@
         assertEquals(textAppearanceInfo.getLinkTextColor(), LINK_TEXT_COLOR);
     }
 
+    @Test
+    public void testCreateFromTextView_withHintText() {
+        // Make hint text display
+        initEditText("");
+
+        // The text color should not be hint color
+        assertTextAppearanceInfoContentsEqual(TextAppearanceInfo.createFromTextView(mEditText));
+    }
+
     static class CustomForegroundColorSpan extends ForegroundColorSpan {
         @Nullable public TextPaint lastTextPaint = null;
 
diff --git a/core/tests/coretests/src/android/widget/EditTextCursorAnchorInfoTest.java b/core/tests/coretests/src/android/widget/EditTextCursorAnchorInfoTest.java
index 1a01987..62adc20 100644
--- a/core/tests/coretests/src/android/widget/EditTextCursorAnchorInfoTest.java
+++ b/core/tests/coretests/src/android/widget/EditTextCursorAnchorInfoTest.java
@@ -30,6 +30,7 @@
 import android.view.Gravity;
 import android.view.View;
 import android.view.inputmethod.CursorAnchorInfo;
+import android.view.inputmethod.EditorBoundsInfo;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -54,8 +55,15 @@
     private static final int[] sLocationOnScreen = new int[2];
     private static Typeface sTypeface;
     private static final float TEXT_SIZE = 1f;
-    // The line height of the test font font is 1.2 * textSize.
+    // The line height of the test font is 1.2 * textSize.
     private static final int LINE_HEIGHT = 12;
+    private static final int HW_BOUNDS_OFFSET_LEFT = 10;
+    private static final int HW_BOUNDS_OFFSET_TOP = 20;
+    private static final int HW_BOUNDS_OFFSET_RIGHT = 30;
+    private static final int HW_BOUNDS_OFFSET_BOTTOM = 40;
+
+
+    // Default text has 5 lines of text. The needed width is 50px and the needed height is 60px.
     private static final CharSequence DEFAULT_TEXT = "X\nXX\nXXX\nXXXX\nXXXXX";
     private static final ImmutableList<RectF> DEFAULT_LINE_BOUNDS = ImmutableList.of(
             new RectF(0f, 0f, 10f, LINE_HEIGHT),
@@ -131,6 +139,55 @@
     }
 
     @Test
+    public void testEditorBoundsInfo_allVisible() {
+        // The needed width and height of the DEFAULT_TEXT are 50 px and 60 px respectfully.
+        int width = 100;
+        int height = 200;
+        setupEditText(DEFAULT_TEXT, width, height);
+        CursorAnchorInfo cursorAnchorInfo =
+                mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
+        EditorBoundsInfo editorBoundsInfo = cursorAnchorInfo.getEditorBoundsInfo();
+        assertThat(editorBoundsInfo).isNotNull();
+        assertThat(editorBoundsInfo.getEditorBounds()).isEqualTo(new RectF(0, 0, width, height));
+        assertThat(editorBoundsInfo.getHandwritingBounds())
+                .isEqualTo(new RectF(-HW_BOUNDS_OFFSET_LEFT, -HW_BOUNDS_OFFSET_TOP,
+                        width + HW_BOUNDS_OFFSET_RIGHT, height + HW_BOUNDS_OFFSET_BOTTOM));
+    }
+
+    @Test
+    public void testEditorBoundsInfo_scrolled() {
+        // The height of the editor will be 60 px.
+        int width = 100;
+        int visibleTop = 10;
+        int visibleBottom = 30;
+        setupVerticalClippedEditText(width, visibleTop, visibleBottom);
+        CursorAnchorInfo cursorAnchorInfo =
+                mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
+        EditorBoundsInfo editorBoundsInfo = cursorAnchorInfo.getEditorBoundsInfo();
+        assertThat(editorBoundsInfo).isNotNull();
+        assertThat(editorBoundsInfo.getEditorBounds())
+                .isEqualTo(new RectF(0, visibleTop, width, visibleBottom));
+        assertThat(editorBoundsInfo.getHandwritingBounds())
+                .isEqualTo(new RectF(-HW_BOUNDS_OFFSET_LEFT, visibleTop - HW_BOUNDS_OFFSET_TOP,
+                        width + HW_BOUNDS_OFFSET_RIGHT, visibleBottom + HW_BOUNDS_OFFSET_BOTTOM));
+    }
+
+    @Test
+    public void testEditorBoundsInfo_invisible() {
+        // The height of the editor will be 60px. Scroll it to 70px will make it invisible.
+        int width = 100;
+        int visibleTop = 70;
+        int visibleBottom = 70;
+        setupVerticalClippedEditText(width, visibleTop, visibleBottom);
+        CursorAnchorInfo cursorAnchorInfo =
+                mEditText.getCursorAnchorInfo(0, sCursorAnchorInfoBuilder, sMatrix);
+        EditorBoundsInfo editorBoundsInfo = cursorAnchorInfo.getEditorBoundsInfo();
+        assertThat(editorBoundsInfo).isNotNull();
+        assertThat(editorBoundsInfo.getEditorBounds()).isEqualTo(new RectF(0, 0, 0, 0));
+        assertThat(editorBoundsInfo.getHandwritingBounds()).isEqualTo(new RectF(0, 0, 0, 0));
+    }
+
+    @Test
     public void testVisibleLineBounds_allVisible() {
         setupEditText(DEFAULT_TEXT, /* height= */ 100);
         CursorAnchorInfo cursorAnchorInfo =
@@ -465,32 +522,26 @@
     }
 
     private void setupVerticalClippedEditText(int visibleTop, int visibleBottom) {
+        setupVerticalClippedEditText(1000, visibleTop, visibleBottom);
+    }
+
+    /**
+     * Helper method to create an EditText in a vertical ScrollView so that its visible bounds
+     * is Rect(0, visibleTop, width, visibleBottom) in the EditText's coordinates. Both ScrollView
+     * and EditText's width is set to the given width.
+     */
+    private void setupVerticalClippedEditText(int width, int visibleTop, int visibleBottom) {
         ScrollView scrollView = new ScrollView(mActivity);
-        mEditText = new EditText(mActivity);
-        mEditText.setTypeface(sTypeface);
-        mEditText.setText(DEFAULT_TEXT);
-        mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, TEXT_SIZE);
-
-        mEditText.setPadding(0, 0, 0, 0);
-        mEditText.setCompoundDrawables(null, null, null, null);
-        mEditText.setCompoundDrawablePadding(0);
-
-        mEditText.scrollTo(0, 0);
-        mEditText.setLineSpacing(0f, 1f);
-
-        // Place the text layout top to the view's top.
-        mEditText.setGravity(Gravity.TOP);
-        int width = 1000;
-        int height = visibleBottom - visibleTop;
+        createEditText();
+        int scrollViewHeight = visibleBottom - visibleTop;
 
         scrollView.addView(mEditText, new FrameLayout.LayoutParams(
                 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
                 View.MeasureSpec.makeMeasureSpec(5 * LINE_HEIGHT, View.MeasureSpec.EXACTLY)));
         scrollView.measure(
                 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
-        scrollView.layout(0, 0, width, height);
-
+                View.MeasureSpec.makeMeasureSpec(scrollViewHeight, View.MeasureSpec.EXACTLY));
+        scrollView.layout(0, 0, width, scrollViewHeight);
         scrollView.scrollTo(0, visibleTop);
     }
 
@@ -499,6 +550,11 @@
         measureEditText(height);
     }
 
+    private void setupEditText(CharSequence text, int width, int height) {
+        createEditText(text);
+        measureEditText(width, height);
+    }
+
     private void setupEditText(CharSequence text, int height, float lineSpacing,
             float lineMultiplier) {
         createEditText(text);
@@ -537,6 +593,8 @@
         mEditText.setTypeface(sTypeface);
         mEditText.setText(text);
         mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, TEXT_SIZE);
+        mEditText.setHandwritingBoundsOffsets(HW_BOUNDS_OFFSET_LEFT, HW_BOUNDS_OFFSET_TOP,
+                HW_BOUNDS_OFFSET_RIGHT, HW_BOUNDS_OFFSET_BOTTOM);
 
         mEditText.setPadding(0, 0, 0, 0);
         mEditText.setCompoundDrawables(null, null, null, null);
diff --git a/core/tests/coretests/src/android/widget/PointerIconTest.java b/core/tests/coretests/src/android/widget/PointerIconTest.java
new file mode 100644
index 0000000..8e9e1a5
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/PointerIconTest.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.annotation.UiThread;
+import android.app.Instrumentation;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.frameworks.coretests.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PointerIconTest {
+    private Instrumentation mInstrumentation;
+
+    @Before
+    public void setup() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+    }
+
+    @Rule
+    public ActivityScenarioRule<PointerIconTestActivity> mActivityScenarioRule =
+            new ActivityScenarioRule<>(PointerIconTestActivity.class);
+
+    @Test
+    @UiThread
+    public void button_mouse_onResolvePointerIcon_returnsTypeHand() {
+        assertOnResolvePointerIconForMouseEvent(R.id.button, PointerIcon.TYPE_HAND);
+    }
+
+    @Test
+    @UiThread
+    public void button_mouse_disabled_onResolvePointerIcon_returnsNull() {
+        assertOnResolvePointerIconReturnNull(R.id.button, /* enabled */ false, /* clickable */ true,
+                /* isMouse */ true);
+    }
+
+    @Test
+    @UiThread
+    public void button_mouse_unclickable_onResolvePointerIcon_returnsNull() {
+        assertOnResolvePointerIconReturnNull(R.id.button, /* enabled */ true, /* clickable */ false,
+                /* isMouse */ true);
+    }
+
+    @Test
+    @UiThread
+    public void button_stylus_onResolvePointerIcon_returnsNull() {
+        assertOnResolvePointerIconReturnNull(R.id.button, /* enabled */ true, /* clickable */ true,
+                /* isMouse */ false);
+    }
+
+    @Test
+    @UiThread
+    public void imageButton_mouse_onResolvePointerIconreturnsTypeHand() {
+        assertOnResolvePointerIconForMouseEvent(R.id.imagebutton, PointerIcon.TYPE_HAND);
+    }
+
+    @Test
+    @UiThread
+    public void imageButton_mouse_diabled_onResolvePointerIcon_returnsNull() {
+        assertOnResolvePointerIconReturnNull(R.id.imagebutton, /* enabled */ false,
+                /* clickable */ true, /* isMouse */ true);
+    }
+
+    @Test
+    @UiThread
+    public void imageButton_mouse_unclickable_onResolvePointerIcon_returnsNull() {
+        assertOnResolvePointerIconReturnNull(R.id.imagebutton, /* enabled */ true,
+                /* clickable */ false, /* isMouse */ true);
+    }
+
+    @Test
+    @UiThread
+    public void imageButton_stylus_onResolvePointerIcon_returnsNull() {
+        assertOnResolvePointerIconReturnNull(R.id.imagebutton, /* enabled */ true,
+                /* clickable */ true, /* isMouse */ false);
+    }
+
+    @Test
+    @UiThread
+    public void textView_mouse_onResolvePointerIcon_returnsNull() {
+        assertOnResolvePointerIconReturnNull(R.id.textview, /* enabled */ true,
+                /* clickable */ true, /* isMouse */ true);
+    }
+
+    @Test
+    @UiThread
+    public void textView_stylus_onResolvePointerIcon_returnsNull() {
+        assertOnResolvePointerIconReturnNull(R.id.textview, /* enabled */ true,
+                /* clickable */ true, /* isMouse */ false);
+    }
+
+    @Test
+    @UiThread
+    public void editText_mouse_onResolvePointerIcon_returnsTypeText() {
+        assertOnResolvePointerIconForMouseEvent(R.id.edittext, PointerIcon.TYPE_TEXT);
+    }
+
+    @Test
+    @UiThread
+    public void editText_stylus_onResolvePointerIcon_returnsNull() {
+        assertOnResolvePointerIconReturnNull(R.id.edittext, /* enabled */ true,
+                /* clickable */ true, /* isMouse */ false);
+    }
+
+    @Test
+    @UiThread
+    public void spinner_mouse_onResolvePointerIcon_returnsTypeHand() {
+        assertOnResolvePointerIconForMouseEvent(R.id.spinner, PointerIcon.TYPE_HAND);
+    }
+
+    @Test
+    @UiThread
+    public void spinner_mouse_disabled_onResolvePointerIcon_returnsNull() {
+        assertOnResolvePointerIconReturnNull(R.id.spinner, /* enabled */ false,
+                /* clickable */ true, /* isMouse */ true);
+    }
+
+    @Test
+    @UiThread
+    public void spinner_mouse_unclickable_onResolvePointerIcon_returnsNull() {
+        assertOnResolvePointerIconReturnNull(R.id.spinner, /* enabled */ true,
+                /* clickable */ false, /* isMouse */ true);
+    }
+
+    @Test
+    @UiThread
+    public void spinner_stylus_onResolvePointerIcon_returnsNull() {
+        assertOnResolvePointerIconReturnNull(R.id.spinner, /* enabled */ true, /* clickable */ true,
+                /* isMouse */ false);
+    }
+
+    @Test
+    @UiThread
+    public void radialTimePickerView_mouse_onResolvePointerIcon_returnsTypeHand() {
+        assertOnResolvePointerIconForMouseEvent(R.id.timepicker, PointerIcon.TYPE_HAND);
+
+    }
+
+    @Test
+    @UiThread
+    public void radialTimePickerView_mouse_disabled_onResolvePointerIcon_returnsNull() {
+        assertOnResolvePointerIconReturnNull(R.id.timepicker, /* enabled */ false,
+                /* clickable */ true, /* isMouse */ true);
+    }
+
+    @Test
+    @UiThread
+    public void radialTimePickerView_stylus_onResolvePointerIcon_returnsNull() {
+        assertOnResolvePointerIconReturnNull(R.id.timepicker, /* enabled */ true,
+                /* clickable */ true, /* isMouse */ false);
+    }
+
+    @Test
+    @UiThread
+    public void calendarView_mouse_onResolvePointerIcon_returnsTypeHand() {
+        assertPointerIconForCalendarView(/* pointerType */ PointerIcon.TYPE_HAND,
+                /* isMouse */ true);
+    }
+
+    @Test
+    @UiThread
+    public void calendarView_stylus_onResolvePointerIcon_returnsNull() {
+        assertPointerIconForCalendarView(/* pointerType */ Integer.MIN_VALUE, /* isMouse */ false);
+    }
+
+    /**
+     * Assert {@link View#onResolvePointerIcon} method for {@link CalendarView}.
+     *
+     * @param pointerType the expected type of the {@link PointerIcon}.
+     *                    When {@link Integer#MIN_VALUE} is passed, it will verify that the
+     *                    returned {@link PointerIcon} is null.
+     * @param isMouse if true, mouse events are used to test the given view. Otherwise, it uses
+     *               stylus events to test the view.
+     */
+    void assertPointerIconForCalendarView(int pointerType, boolean isMouse) {
+        Calendar calendar = new GregorianCalendar();
+        calendar.set(2023, 0, 1);
+        long time = calendar.getTimeInMillis();
+        mActivityScenarioRule.getScenario().onActivity(activity -> {
+            CalendarView calendarView = activity.findViewById(R.id.calendar);
+            calendarView.setDate(time, /* animate */ false, /* center */true);
+        });
+
+        // Wait for setDate to finish and then verify.
+        mInstrumentation.waitForIdleSync();
+        mActivityScenarioRule.getScenario().onActivity(activity -> {
+            CalendarView calendarView = activity.findViewById(R.id.calendar);
+            Rect bounds = new Rect();
+            calendarView.getBoundsForDate(time, bounds);
+            MotionEvent event = createHoverEvent(isMouse, bounds.centerX(), bounds.centerY());
+            PointerIcon icon = calendarView.onResolvePointerIcon(event, /* pointerIndex */ 0);
+            if (pointerType != Integer.MIN_VALUE) {
+                assertThat(icon.getType()).isEqualTo(pointerType);
+            } else {
+                assertThat(icon).isNull();
+            }
+        });
+    }
+
+    /**
+     * Assert that the given view's {@link View#onResolvePointerIcon(MotionEvent, int)} method
+     * returns a {@link PointerIcon} with the specified pointer type. The passed {@link MotionEvent}
+     * locates at the center of the view.
+     *
+     * @param resId the resource id of the view to be tested.
+     * @param pointerType the expected pointer type. When {@link Integer#MIN_VALUE} is passed, it
+     *                   will verify that the returned {@link PointerIcon} is null.
+     */
+    public void assertOnResolvePointerIconForMouseEvent(int resId, int pointerType) {
+        mActivityScenarioRule.getScenario().onActivity(activity -> {
+            View view = activity.findViewById(resId);
+            MotionEvent event = createHoverEvent(/* isMouse */ true, /* x */ 0, /* y */ 0);
+            PointerIcon icon = view.onResolvePointerIcon(event, /* pointerIndex */ 0);
+            if (pointerType != Integer.MIN_VALUE) {
+                assertThat(icon.getType()).isEqualTo(pointerType);
+            } else {
+                assertThat(icon).isNull();
+            }
+        });
+    }
+
+    /**
+     * Assert that the given view's {@link View#onResolvePointerIcon(MotionEvent, int)} method
+     * returns a {@link PointerIcon} with the specified pointer type. The passed {@link MotionEvent}
+     * locates at the center of the view.
+     *
+     * @param resId the resource id of the view to be tested.
+     * @param enabled whether the tested  view is enabled.
+     * @param clickable whether the tested view is clickable.
+     * @param isMouse if true, mouse events are used to test the given view. Otherwise, it uses
+     *               stylus events to test the view.
+     */
+    public void assertOnResolvePointerIconReturnNull(int resId, boolean enabled, boolean clickable,
+            boolean isMouse) {
+        mActivityScenarioRule.getScenario().onActivity(activity -> {
+            View view = activity.findViewById(resId);
+            view.setEnabled(enabled);
+            view.setClickable(clickable);
+            MotionEvent event = createHoverEvent(isMouse, /* x */ 0, /* y */ 0);
+            PointerIcon icon = view.onResolvePointerIcon(event, /* pointerIndex */ 0);
+            assertThat(icon).isNull();
+        });
+    }
+
+
+    /**
+     * Create a hover {@link MotionEvent} for testing.
+     *
+     * @param isMouse if true, a {@link MotionEvent} from mouse is returned. Otherwise,
+     *               a {@link MotionEvent} from stylus is returned.
+     * @param x the x coordinate of the returned {@link MotionEvent}
+     * @param y the y coordinate of the returned {@link MotionEvent}
+     */
+    private MotionEvent createHoverEvent(boolean isMouse, int x, int y) {
+        MotionEvent.PointerProperties[] properties = MotionEvent.PointerProperties.createArray(1);
+        properties[0].toolType =
+                isMouse ? MotionEvent.TOOL_TYPE_MOUSE : MotionEvent.TOOL_TYPE_STYLUS;
+
+        MotionEvent.PointerCoords[] coords = MotionEvent.PointerCoords.createArray(1);
+        coords[0].x = x;
+        coords[0].y = y;
+
+        int source = isMouse ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_STYLUS;
+        long eventTime = SystemClock.uptimeMillis();
+        return MotionEvent.obtain(/* downTime */ 0, eventTime, MotionEvent.ACTION_HOVER_MOVE,
+                /* pointerCount */ 1, properties, coords, /* metaState */ 0, /* buttonState */ 0,
+                /* xPrecision */ 1, /* yPrecision */ 1, /* deviceId */ 0, /* edgeFlags */ 0,
+                source, /* flags */ 0);
+    }
+
+}
diff --git a/core/tests/coretests/src/android/widget/PointerIconTestActivity.java b/core/tests/coretests/src/android/widget/PointerIconTestActivity.java
new file mode 100644
index 0000000..d491fb0
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/PointerIconTestActivity.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 android.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.android.frameworks.coretests.R;
+
+public class PointerIconTestActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.pointer_icon_test);
+
+        RadialTimePickerView timePicker = findViewById(R.id.timepicker);
+        // Set the time of TimePicker to 0:00 so that the test is stable.
+        timePicker.setCurrentHour(0);
+        timePicker.setCurrentMinute(0);
+    }
+}
diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
index 73aa936..c442755 100644
--- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java
+++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.spy;
@@ -39,11 +40,15 @@
 import android.os.Binder;
 import android.os.Looper;
 import android.os.Parcel;
+import android.util.AttributeSet;
 import android.util.SizeF;
 import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -827,4 +832,71 @@
         verify(visitor, times(1)).accept(eq(icon3S.getUri()));
         verify(visitor, times(1)).accept(eq(icon4S.getUri()));
     }
+
+    @Test
+    public void layoutInflaterFactory_nothingSet_returnsNull() {
+        final RemoteViews rv = new RemoteViews(mPackage, R.layout.remote_views_test);
+        assertNull(rv.getLayoutInflaterFactory());
+    }
+
+    @Test
+    public void layoutInflaterFactory_replacesImageView_viewReplaced() {
+        final RemoteViews rv = new RemoteViews(mPackage, R.layout.remote_views_test);
+        final View replacement = new FrameLayout(mContext);
+        replacement.setId(1337);
+
+        LayoutInflater.Factory2 factory = createLayoutInflaterFactory("ImageView", replacement);
+        rv.setLayoutInflaterFactory(factory);
+
+        // Now inflate the views.
+        View inflated = rv.apply(mContext, mContainer);
+
+        assertEquals(factory, rv.getLayoutInflaterFactory());
+        View replacedFrameLayout = inflated.findViewById(1337);
+        assertNotNull(replacedFrameLayout);
+        assertEquals(replacement, replacedFrameLayout);
+        // ImageView should be fully replaced.
+        assertNull(inflated.findViewById(R.id.image));
+    }
+
+    @Test
+    public void layoutInflaterFactory_replacesImageView_settersStillFunctional() {
+        final RemoteViews rv = new RemoteViews(mPackage, R.layout.remote_views_test);
+        final TextView replacement = new TextView(mContext);
+        replacement.setId(R.id.text);
+        final String testText = "testText";
+        rv.setLayoutInflaterFactory(createLayoutInflaterFactory("TextView", replacement));
+        rv.setTextViewText(R.id.text, testText);
+
+
+        // Now inflate the views.
+        View inflated = rv.apply(mContext, mContainer);
+
+        TextView replacedTextView = inflated.findViewById(R.id.text);
+        assertSame(replacement, replacedTextView);
+        assertEquals(testText, replacedTextView.getText());
+    }
+
+    private static LayoutInflater.Factory2 createLayoutInflaterFactory(String viewTypeToReplace,
+            View replacementView) {
+        return new LayoutInflater.Factory2() {
+            @Nullable
+            @Override
+            public View onCreateView(@Nullable View parent, @NonNull String name,
+                                     @NonNull Context context, @NonNull AttributeSet attrs) {
+                if (viewTypeToReplace.equals(name)) {
+                    return replacementView;
+                }
+
+                return null;
+            }
+
+            @Nullable
+            @Override
+            public View onCreateView(@NonNull String name, @NonNull Context context,
+                                     @NonNull AttributeSet attrs) {
+                return null;
+            }
+        };
+    }
 }
diff --git a/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java b/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java
index a1a4265..84dd274 100644
--- a/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java
@@ -169,7 +169,7 @@
 
     private WindowInsets insetsWith(Insets content, DisplayCutout cutout) {
         return new WindowInsets(WindowInsets.createCompatTypeMap(content.toRect()), null, null,
-                false, false, 0, cutout, null, null, null, WindowInsets.Type.systemBars(), false);
+                false, 0, 0, cutout, null, null, null, WindowInsets.Type.systemBars(), false);
     }
 
     private ViewGroup createViewGroupWithId(int id) {
diff --git a/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java b/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java
index 6167c4b..1a668f7 100644
--- a/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java
@@ -37,10 +37,23 @@
     }
 
     @Test
+    public void testUserRepairMode_isNotRegularUser() {
+        assertTrue(LockPatternUtils.USER_REPAIR_MODE < 0);
+    }
+
+    @Test
     public void testUserFrp_isNotAReservedSpecialUser() throws Exception {
         assertNotEquals(UserHandle.USER_NULL, LockPatternUtils.USER_FRP);
         assertNotEquals(UserHandle.USER_ALL, LockPatternUtils.USER_FRP);
         assertNotEquals(UserHandle.USER_CURRENT, LockPatternUtils.USER_FRP);
         assertNotEquals(UserHandle.USER_CURRENT_OR_SELF, LockPatternUtils.USER_FRP);
     }
+
+    @Test
+    public void testUserRepairMode_isNotAReservedSpecialUser() throws Exception {
+        assertNotEquals(UserHandle.USER_NULL, LockPatternUtils.USER_REPAIR_MODE);
+        assertNotEquals(UserHandle.USER_ALL, LockPatternUtils.USER_REPAIR_MODE);
+        assertNotEquals(UserHandle.USER_CURRENT, LockPatternUtils.USER_REPAIR_MODE);
+        assertNotEquals(UserHandle.USER_CURRENT_OR_SELF, LockPatternUtils.USER_REPAIR_MODE);
+    }
 }
diff --git a/core/tests/mockingcoretests/Android.bp b/core/tests/mockingcoretests/Android.bp
index 96811be..741e535 100644
--- a/core/tests/mockingcoretests/Android.bp
+++ b/core/tests/mockingcoretests/Android.bp
@@ -56,7 +56,10 @@
     ],
 
     platform_apis: true,
-    test_suites: ["device-tests"],
+    test_suites: [
+        "device-tests",
+        "automotive-tests",
+    ],
 
     certificate: "platform",
 }
diff --git a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
index be2c27d..3e0e36d 100644
--- a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
+++ b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
@@ -181,7 +181,7 @@
 
             // Verify for ON_START state. Activity should be relaunched.
             getInstrumentation().runOnMainSync(() -> clientSession.startActivity(r));
-            recreateAndVerifyRelaunched(activityThread, activity[0], r, ON_START);
+            recreateAndVerifyRelaunched(activityThread, activity[0], r, ON_PAUSE);
 
             // Verify for ON_RESUME state. Activity should be relaunched.
             getInstrumentation().runOnMainSync(() -> clientSession.resumeActivity(r));
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 93e44f1..94e23e7 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -3385,6 +3385,12 @@
       "group": "WM_DEBUG_REMOTE_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RemoteAnimationController.java"
     },
+    "975028389": {
+      "message": "unable to call receiver for empty keyboard shortcuts",
+      "level": "ERROR",
+      "group": "WM_ERROR",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "975275467": {
       "message": "Set animatingExit: reason=remove\/isAnimating win=%s",
       "level": "VERBOSE",
diff --git a/graphics/java/android/graphics/text/LineBreakConfig.java b/graphics/java/android/graphics/text/LineBreakConfig.java
index d0327159..0c493f5 100644
--- a/graphics/java/android/graphics/text/LineBreakConfig.java
+++ b/graphics/java/android/graphics/text/LineBreakConfig.java
@@ -94,11 +94,6 @@
         private @LineBreakWordStyle int mLineBreakWordStyle =
                 LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE;
 
-        // Whether or not enabling phrase breaking automatically.
-        // TODO(b/226012260): Remove this and add LINE_BREAK_WORD_STYLE_PHRASE_AUTO after
-        // the experiment.
-        private boolean mAutoPhraseBreaking = false;
-
         /**
          * Builder constructor.
          */
@@ -128,22 +123,12 @@
         }
 
         /**
-         * Enables or disables the automation of {@link LINE_BREAK_WORD_STYLE_PHRASE}.
-         *
-         * @hide
-         */
-        public @NonNull Builder setAutoPhraseBreaking(boolean autoPhraseBreaking) {
-            mAutoPhraseBreaking = autoPhraseBreaking;
-            return this;
-        }
-
-        /**
          * Builds a {@link LineBreakConfig} instance.
          *
          * @return The {@code LineBreakConfig} instance.
          */
         public @NonNull LineBreakConfig build() {
-            return new LineBreakConfig(mLineBreakStyle, mLineBreakWordStyle, mAutoPhraseBreaking);
+            return new LineBreakConfig(mLineBreakStyle, mLineBreakWordStyle);
         }
     }
 
@@ -164,23 +149,6 @@
                 .build();
     }
 
-    /**
-     * Create the LineBreakConfig instance.
-     *
-     * @param lineBreakStyle the line break style for text wrapping.
-     * @param lineBreakWordStyle the line break word style for text wrapping.
-     * @return the {@link LineBreakConfig} instance.     *
-     * @hide
-     */
-    public static @NonNull LineBreakConfig getLineBreakConfig(@LineBreakStyle int lineBreakStyle,
-            @LineBreakWordStyle int lineBreakWordStyle, boolean autoPhraseBreaking) {
-        LineBreakConfig.Builder builder = new LineBreakConfig.Builder();
-        return builder.setLineBreakStyle(lineBreakStyle)
-                .setLineBreakWordStyle(lineBreakWordStyle)
-                .setAutoPhraseBreaking(autoPhraseBreaking)
-                .build();
-    }
-
     /** @hide */
     public static final LineBreakConfig NONE =
             new Builder().setLineBreakStyle(LINE_BREAK_STYLE_NONE)
@@ -188,7 +156,6 @@
 
     private final @LineBreakStyle int mLineBreakStyle;
     private final @LineBreakWordStyle int mLineBreakWordStyle;
-    private final boolean mAutoPhraseBreaking;
 
     /**
      * Constructor with line-break parameters.
@@ -197,10 +164,9 @@
      * {@code LineBreakConfig} instance.
      */
     private LineBreakConfig(@LineBreakStyle int lineBreakStyle,
-            @LineBreakWordStyle int lineBreakWordStyle, boolean autoPhraseBreaking) {
+            @LineBreakWordStyle int lineBreakWordStyle) {
         mLineBreakStyle = lineBreakStyle;
         mLineBreakWordStyle = lineBreakWordStyle;
-        mAutoPhraseBreaking = autoPhraseBreaking;
     }
 
     /**
@@ -221,17 +187,6 @@
         return mLineBreakWordStyle;
     }
 
-    /**
-     * Used to identify if the automation of {@link LINE_BREAK_WORD_STYLE_PHRASE} is enabled.
-     *
-     * @return The result that records whether or not the automation of
-     * {@link LINE_BREAK_WORD_STYLE_PHRASE} is enabled.
-     * @hide
-     */
-    public boolean getAutoPhraseBreaking() {
-        return mAutoPhraseBreaking;
-    }
-
     @Override
     public boolean equals(Object o) {
         if (o == null) return false;
@@ -239,8 +194,7 @@
         if (!(o instanceof LineBreakConfig)) return false;
         LineBreakConfig that = (LineBreakConfig) o;
         return (mLineBreakStyle == that.mLineBreakStyle)
-                && (mLineBreakWordStyle == that.mLineBreakWordStyle)
-                && (mAutoPhraseBreaking == that.mAutoPhraseBreaking);
+                && (mLineBreakWordStyle == that.mLineBreakWordStyle);
     }
 
     @Override
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 89f4890..4cedd41 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -308,7 +308,8 @@
 
             forAllTaskContainers(taskContainer -> {
                 synchronized (mLock) {
-                    final List<TaskFragmentContainer> containers = taskContainer.mContainers;
+                    final List<TaskFragmentContainer> containers =
+                            taskContainer.getTaskFragmentContainers();
                     // Clean up the TaskFragmentContainers by the z-order from the lowest.
                     for (int i = 0; i < containers.size(); i++) {
                         final TaskFragmentContainer container = containers.get(i);
@@ -611,8 +612,7 @@
             @NonNull TaskContainer taskContainer) {
         // Update all TaskFragments in the Task. Make a copy of the list since some may be
         // removed on updating.
-        final List<TaskFragmentContainer> containers =
-                new ArrayList<>(taskContainer.mContainers);
+        final List<TaskFragmentContainer> containers = taskContainer.getTaskFragmentContainers();
         for (int i = containers.size() - 1; i >= 0; i--) {
             final TaskFragmentContainer container = containers.get(i);
             // Wait until onTaskFragmentAppeared to update new container.
@@ -1331,7 +1331,8 @@
         // Check pending appeared activity first because there can be a delay for the server
         // update.
         for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
-            final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers;
+            final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i)
+                    .getTaskFragmentContainers();
             for (int j = containers.size() - 1; j >= 0; j--) {
                 final TaskFragmentContainer container = containers.get(j);
                 if (container.hasPendingAppearedActivity(activityToken)) {
@@ -1342,7 +1343,8 @@
 
         // Check appeared activity if there is no such pending appeared activity.
         for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
-            final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers;
+            final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i)
+                    .getTaskFragmentContainers();
             for (int j = containers.size() - 1; j >= 0; j--) {
                 final TaskFragmentContainer container = containers.get(j);
                 if (container.hasAppearedActivity(activityToken)) {
@@ -1418,7 +1420,7 @@
         if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) {
             removeExistingSecondaryContainers(wct, primaryContainer);
         }
-        primaryContainer.getTaskContainer().mSplitContainers.add(splitContainer);
+        primaryContainer.getTaskContainer().addSplitContainer(splitContainer);
     }
 
     /** Cleanups all the dependencies when the TaskFragment is entering PIP. */
@@ -1430,8 +1432,9 @@
             return;
         }
         final List<SplitContainer> splitsToRemove = new ArrayList<>();
+        final List<SplitContainer> splitContainers = taskContainer.getSplitContainers();
         final Set<TaskFragmentContainer> containersToUpdate = new ArraySet<>();
-        for (SplitContainer splitContainer : taskContainer.mSplitContainers) {
+        for (SplitContainer splitContainer : splitContainers) {
             if (splitContainer.getPrimaryContainer() != container
                     && splitContainer.getSecondaryContainer() != container) {
                 continue;
@@ -1449,7 +1452,7 @@
             }
         }
         container.resetDependencies();
-        taskContainer.mSplitContainers.removeAll(splitsToRemove);
+        taskContainer.removeSplitContainers(splitsToRemove);
         // If there is any TaskFragment split with the PIP TaskFragment, update their presentations
         // since the split is dismissed.
         // We don't want to close any of them even if they are dependencies of the PIP TaskFragment.
@@ -1471,7 +1474,7 @@
     void removeContainers(@NonNull TaskContainer taskContainer,
             @NonNull List<TaskFragmentContainer> containers) {
         // Remove all split containers that included this one
-        taskContainer.mContainers.removeAll(containers);
+        taskContainer.removeTaskFragmentContainers(containers);
         // Marked as a pending removal which will be removed after it is actually removed on the
         // server side (#onTaskFragmentVanished).
         // In this way, we can keep track of the Task bounds until we no longer have any
@@ -1481,7 +1484,8 @@
 
         // Cleanup any split references.
         final List<SplitContainer> containersToRemove = new ArrayList<>();
-        for (SplitContainer splitContainer : taskContainer.mSplitContainers) {
+        final List<SplitContainer> splitContainers = taskContainer.getSplitContainers();
+        for (SplitContainer splitContainer : splitContainers) {
             if (containersToRemove.contains(splitContainer)) {
                 // Don't need to check because it has been in the remove list.
                 continue;
@@ -1492,10 +1496,12 @@
                 containersToRemove.add(splitContainer);
             }
         }
-        taskContainer.mSplitContainers.removeAll(containersToRemove);
+        taskContainer.removeSplitContainers(containersToRemove);
 
         // Cleanup any dependent references.
-        for (TaskFragmentContainer containerToUpdate : taskContainer.mContainers) {
+        final List<TaskFragmentContainer> taskFragmentContainers =
+                taskContainer.getTaskFragmentContainers();
+        for (TaskFragmentContainer containerToUpdate : taskFragmentContainers) {
             containerToUpdate.removeContainersToFinishOnExit(containers);
         }
     }
@@ -1534,8 +1540,9 @@
         if (taskContainer == null) {
             return null;
         }
-        for (int i = taskContainer.mContainers.size() - 1; i >= 0; i--) {
-            final TaskFragmentContainer container = taskContainer.mContainers.get(i);
+        final List<TaskFragmentContainer> containers = taskContainer.getTaskFragmentContainers();
+        for (int i = containers.size() - 1; i >= 0; i--) {
+            final TaskFragmentContainer container = containers.get(i);
             if (!container.isFinished() && (container.getRunningActivityCount() > 0
                     // We may be waiting for the top TaskFragment to become non-empty after
                     // creation. In that case, we don't want to treat the TaskFragment below it as
@@ -1629,7 +1636,7 @@
     /** Whether the given split is the topmost split in the Task. */
     private boolean isTopMostSplit(@NonNull SplitContainer splitContainer) {
         final List<SplitContainer> splitContainers = splitContainer.getPrimaryContainer()
-                .getTaskContainer().mSplitContainers;
+                .getTaskContainer().getSplitContainers();
         return splitContainer == splitContainers.get(splitContainers.size() - 1);
     }
 
@@ -1641,7 +1648,8 @@
         if (container == null) {
             return null;
         }
-        final List<SplitContainer> splitContainers = container.getTaskContainer().mSplitContainers;
+        final List<SplitContainer> splitContainers =
+                container.getTaskContainer().getSplitContainers();
         if (splitContainers.isEmpty()) {
             return null;
         }
@@ -1665,7 +1673,7 @@
             @NonNull TaskFragmentContainer firstContainer,
             @NonNull TaskFragmentContainer secondContainer) {
         final List<SplitContainer> splitContainers = firstContainer.getTaskContainer()
-                .mSplitContainers;
+                .getSplitContainers();
         for (int i = splitContainers.size() - 1; i >= 0; i--) {
             final SplitContainer splitContainer = splitContainers.get(i);
             final TaskFragmentContainer primary = splitContainer.getPrimaryContainer();
@@ -1930,7 +1938,8 @@
     @GuardedBy("mLock")
     TaskFragmentContainer getContainer(@NonNull IBinder fragmentToken) {
         for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
-            final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers;
+            final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i)
+                    .getTaskFragmentContainers();
             for (TaskFragmentContainer container : containers) {
                 if (container.getTaskFragmentToken().equals(fragmentToken)) {
                     return container;
@@ -1945,7 +1954,7 @@
     @GuardedBy("mLock")
     SplitContainer getSplitContainer(@NonNull IBinder token) {
         for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
-            final List<SplitContainer> containers = mTaskContainers.valueAt(i).mSplitContainers;
+            final List<SplitContainer> containers = mTaskContainers.valueAt(i).getSplitContainers();
             for (SplitContainer container : containers) {
                 if (container.getToken().equals(token)) {
                     return container;
@@ -2091,7 +2100,7 @@
                 }
                 for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
                     final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i)
-                            .mContainers;
+                            .getTaskFragmentContainers();
                     for (int j = containers.size() - 1; j >= 0; j--) {
                         final TaskFragmentContainer container = containers.get(j);
                         if (!container.hasActivity(activityToken)
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 4b15bb1..4580c98 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -51,11 +51,11 @@
 
     /** Active TaskFragments in this Task. */
     @NonNull
-    final List<TaskFragmentContainer> mContainers = new ArrayList<>();
+    private final List<TaskFragmentContainer> mContainers = new ArrayList<>();
 
     /** Active split pairs in this Task. */
     @NonNull
-    final List<SplitContainer> mSplitContainers = new ArrayList<>();
+    private final List<SplitContainer> mSplitContainers = new ArrayList<>();
 
     @NonNull
     private final Configuration mConfiguration;
@@ -207,6 +207,53 @@
         return false;
     }
 
+    /**
+     * Returns a list of {@link SplitContainer}. Do not modify the containers directly on the
+     * returned list. Use {@link #addSplitContainer} or {@link #removeSplitContainers} instead.
+     */
+    @NonNull
+    List<SplitContainer> getSplitContainers() {
+        return mSplitContainers;
+    }
+
+    void addSplitContainer(@NonNull SplitContainer splitContainer) {
+        mSplitContainers.add(splitContainer);
+    }
+
+    void removeSplitContainers(@NonNull List<SplitContainer> containers) {
+        mSplitContainers.removeAll(containers);
+    }
+
+    void addTaskFragmentContainer(@NonNull TaskFragmentContainer taskFragmentContainer) {
+        mContainers.add(taskFragmentContainer);
+    }
+
+    void addTaskFragmentContainer(int index, @NonNull TaskFragmentContainer taskFragmentContainer) {
+        mContainers.add(index, taskFragmentContainer);
+    }
+
+    void removeTaskFragmentContainer(@NonNull TaskFragmentContainer taskFragmentContainer) {
+        mContainers.remove(taskFragmentContainer);
+    }
+
+    void removeTaskFragmentContainers(@NonNull List<TaskFragmentContainer> taskFragmentContainer) {
+        mContainers.removeAll(taskFragmentContainer);
+    }
+
+    void clearTaskFragmentContainer() {
+        mContainers.clear();
+    }
+
+    /**
+     * Returns a list of {@link TaskFragmentContainer}. Do not modify the containers directly on
+     * the returned list. Use {@link #addTaskFragmentContainer},
+     * {@link #removeTaskFragmentContainer} or other related methods instead.
+     */
+    @NonNull
+    List<TaskFragmentContainer> getTaskFragmentContainers() {
+        return mContainers;
+    }
+
     /** Adds the descriptors of split states in this Task to {@code outSplitStates}. */
     void getSplitStates(@NonNull List<SplitInfo> outSplitStates) {
         for (SplitContainer container : mSplitContainers) {
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 60be9d1..61df335 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -180,23 +180,25 @@
                 throw new IllegalArgumentException(
                         "pairedPrimaryContainer must be in the same Task");
             }
-            final int primaryIndex = taskContainer.mContainers.indexOf(pairedPrimaryContainer);
-            taskContainer.mContainers.add(primaryIndex + 1, this);
+            final int primaryIndex = taskContainer.indexOf(pairedPrimaryContainer);
+            taskContainer.addTaskFragmentContainer(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;
+            final List<TaskFragmentContainer> containers =
+                    taskContainer.getTaskFragmentContainers();
+            int i = containers.size() - 1;
             for (; i >= 0; i--) {
-                final TaskFragmentContainer container = taskContainer.mContainers.get(i);
+                final TaskFragmentContainer container = containers.get(i);
                 if (!container.isEmpty() || container.getPendingAppearedIntent() == null) {
                     break;
                 }
             }
-            taskContainer.mContainers.add(i + 1, this);
+            taskContainer.addTaskFragmentContainer(i + 1, this);
         } else {
-            taskContainer.mContainers.add(this);
+            taskContainer.addTaskFragmentContainer(this);
         }
         if (pendingAppearedActivity != null) {
             addPendingAppearedActivity(pendingAppearedActivity);
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 ff08782..9e26472 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
@@ -183,23 +183,23 @@
         // tf2 has running activity so is active.
         final TaskFragmentContainer tf2 = mock(TaskFragmentContainer.class);
         doReturn(1).when(tf2).getRunningActivityCount();
-        taskContainer.mContainers.add(tf2);
+        taskContainer.addTaskFragmentContainer(tf2);
         // tf3 is finished so is not active.
         final TaskFragmentContainer tf3 = mock(TaskFragmentContainer.class);
         doReturn(true).when(tf3).isFinished();
         doReturn(false).when(tf3).isWaitingActivityAppear();
-        taskContainer.mContainers.add(tf3);
+        taskContainer.addTaskFragmentContainer(tf3);
         mSplitController.mTaskContainers.put(TASK_ID, taskContainer);
 
         assertWithMessage("Must return tf2 because tf3 is not active.")
                 .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2);
 
-        taskContainer.mContainers.remove(tf3);
+        taskContainer.removeTaskFragmentContainer(tf3);
 
         assertWithMessage("Must return tf2 because tf2 has running activity.")
                 .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2);
 
-        taskContainer.mContainers.remove(tf2);
+        taskContainer.removeTaskFragmentContainer(tf2);
 
         assertWithMessage("Must return tf because we are waiting for tf1 to appear.")
                 .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1);
@@ -320,11 +320,11 @@
         doReturn(tf).when(splitContainer).getSecondaryContainer();
         doReturn(createTestTaskContainer()).when(splitContainer).getTaskContainer();
         doReturn(createSplitRule(mActivity, mActivity)).when(splitContainer).getSplitRule();
-        final List<SplitContainer> splitContainers =
-                mSplitController.getTaskContainer(TASK_ID).mSplitContainers;
-        splitContainers.add(splitContainer);
+        final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID);
+        taskContainer.addSplitContainer(splitContainer);
         // Add a mock SplitContainer on top of splitContainer
-        splitContainers.add(1, mock(SplitContainer.class));
+        final SplitContainer splitContainer2 = mock(SplitContainer.class);
+        taskContainer.addSplitContainer(splitContainer2);
 
         mSplitController.updateContainer(mTransaction, tf);
 
@@ -332,7 +332,9 @@
 
         // Verify if one or both containers in the top SplitContainer are finished,
         // dismissPlaceholder() won't be called.
-        splitContainers.remove(1);
+        final ArrayList<SplitContainer> splitContainersToRemove = new ArrayList<>();
+        splitContainersToRemove.add(splitContainer2);
+        taskContainer.removeSplitContainers(splitContainersToRemove);
         doReturn(true).when(tf).isFinished();
 
         mSplitController.updateContainer(mTransaction, tf);
@@ -363,7 +365,8 @@
         final Activity r1 = createMockActivity();
         addSplitTaskFragments(r0, r1);
         final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID);
-        final TaskFragmentContainer taskFragmentContainer = taskContainer.mContainers.get(0);
+        final TaskFragmentContainer taskFragmentContainer =
+                taskContainer.getTaskFragmentContainers().get(0);
         spyOn(taskContainer);
 
         // No update when the Task is invisible.
@@ -377,7 +380,7 @@
         doReturn(true).when(taskContainer).isVisible();
         mSplitController.updateContainer(mTransaction, taskFragmentContainer);
 
-        verify(mSplitPresenter).updateSplitContainer(taskContainer.mSplitContainers.get(0),
+        verify(mSplitPresenter).updateSplitContainer(taskContainer.getSplitContainers().get(0),
                 mTransaction);
     }
 
@@ -1090,8 +1093,8 @@
         verify(mTransaction).finishActivity(mActivity.getActivityToken());
         verify(mTransaction).finishActivity(secondaryActivity0.getActivityToken());
         verify(mTransaction).finishActivity(secondaryActivity1.getActivityToken());
-        assertTrue(taskContainer.mContainers.isEmpty());
-        assertTrue(taskContainer.mSplitContainers.isEmpty());
+        assertTrue(taskContainer.getTaskFragmentContainers().isEmpty());
+        assertTrue(taskContainer.getSplitContainers().isEmpty());
     }
 
     @Test
@@ -1363,15 +1366,13 @@
         TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
         tf.setInfo(mTransaction, createMockTaskFragmentInfo(tf, mActivity));
 
-        List<TaskFragmentContainer> containers = mSplitController.mTaskContainers.get(TASK_ID)
-                .mContainers;
-
-        assertEquals(containers.get(0), tf);
+        final TaskContainer taskContainer = mSplitController.mTaskContainers.get(TASK_ID);
+        assertEquals(taskContainer.getTaskFragmentContainers().get(0), tf);
 
         mSplitController.finishActivityStacks(Collections.singleton(tf.getTaskFragmentToken()));
 
         verify(mSplitPresenter).deleteTaskFragment(any(), eq(tf.getTaskFragmentToken()));
-        assertTrue(containers.isEmpty());
+        assertTrue(taskContainer.getTaskFragmentContainers().isEmpty());
     }
 
     @Test
@@ -1381,10 +1382,8 @@
         bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity));
         topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity()));
 
-        List<TaskFragmentContainer> containers = mSplitController.mTaskContainers.get(TASK_ID)
-                .mContainers;
-
-        assertEquals(containers.size(), 2);
+        final TaskContainer taskContainer = mSplitController.mTaskContainers.get(TASK_ID);
+        assertEquals(taskContainer.getTaskFragmentContainers().size(), 2);
 
         Set<IBinder> activityStackTokens = new ArraySet<>(new IBinder[]{
                 topTf.getTaskFragmentToken(), bottomTf.getTaskFragmentToken()});
@@ -1403,7 +1402,7 @@
                         + "regardless of the order in ActivityStack set",
                 topTf.getTaskFragmentToken(), fragmentTokens.get(1));
 
-        assertTrue(containers.isEmpty());
+        assertTrue(taskContainer.getTaskFragmentContainers().isEmpty());
     }
 
     @Test
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
index 13e7092..11af1d1 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
@@ -127,7 +127,7 @@
         assertFalse(taskContainer.isEmpty());
 
         taskContainer.mFinishedContainer.add(tf.getTaskFragmentToken());
-        taskContainer.mContainers.clear();
+        taskContainer.clearTaskFragmentContainer();
 
         assertFalse(taskContainer.isEmpty());
     }
@@ -152,13 +152,13 @@
         assertNull(taskContainer.getTopNonFinishingActivity());
 
         final TaskFragmentContainer tf0 = mock(TaskFragmentContainer.class);
-        taskContainer.mContainers.add(tf0);
+        taskContainer.addTaskFragmentContainer(tf0);
         final Activity activity0 = mock(Activity.class);
         doReturn(activity0).when(tf0).getTopNonFinishingActivity();
         assertEquals(activity0, taskContainer.getTopNonFinishingActivity());
 
         final TaskFragmentContainer tf1 = mock(TaskFragmentContainer.class);
-        taskContainer.mContainers.add(tf1);
+        taskContainer.addTaskFragmentContainer(tf1);
         assertEquals(activity0, taskContainer.getTopNonFinishingActivity());
 
         final Activity activity1 = mock(Activity.class);
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 38d27bc..35e88ed 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -42,16 +42,19 @@
 filegroup {
     name: "wm_shell_util-sources",
     srcs: [
-        "src/com/android/wm/shell/util/**/*.java",
-        "src/com/android/wm/shell/common/split/SplitScreenConstants.java",
-        "src/com/android/wm/shell/sysui/ShellSharedConstants.java",
-        "src/com/android/wm/shell/common/TransactionPool.java",
-        "src/com/android/wm/shell/common/bubbles/*.java",
-        "src/com/android/wm/shell/common/TriangleShape.java",
         "src/com/android/wm/shell/animation/Interpolators.java",
+        "src/com/android/wm/shell/animation/PhysicsAnimator.kt",
+        "src/com/android/wm/shell/common/bubbles/*.kt",
+        "src/com/android/wm/shell/common/bubbles/*.java",
+        "src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt",
+        "src/com/android/wm/shell/common/split/SplitScreenConstants.java",
+        "src/com/android/wm/shell/common/TransactionPool.java",
+        "src/com/android/wm/shell/common/TriangleShape.java",
+        "src/com/android/wm/shell/draganddrop/DragAndDropConstants.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",
+        "src/com/android/wm/shell/sysui/ShellSharedConstants.java",
+        "src/com/android/wm/shell/util/**/*.java",
     ],
     path: "src",
 }
diff --git a/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_section.xml b/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_section.xml
new file mode 100644
index 0000000..d99d64d
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_section.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true">
+        <ripple android:color="#99999999">
+            <item android:drawable="@drawable/bubble_manage_menu_bg" />
+        </ripple>
+    </item>
+    <item android:drawable="@drawable/bubble_manage_menu_bg" />
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
index 5d77713..ce24275 100644
--- a/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
+++ b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml
@@ -13,13 +13,20 @@
   ~ 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">
-    <group android:translateY="8.0">
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="128dp"
+    android:height="4dp"
+    android:viewportWidth="128"
+    android:viewportHeight="4"
+    >
+    <group>
+        <clip-path
+            android:pathData="M2 0H126C127.105 0 128 0.895431 128 2C128 3.10457 127.105 4 126 4H2C0.895431 4 0 3.10457 0 2C0 0.895431 0.895431 0 2 0Z"
+            />
         <path
-            android:fillColor="@android:color/black" android:pathData="M3,5V3H21V5Z"/>
+            android:pathData="M0 0V4H128V0"
+            android:fillColor="@android:color/black"
+            />
     </group>
 </vector>
diff --git a/libs/WindowManager/Shell/res/drawable/ic_expand_less.xml b/libs/WindowManager/Shell/res/drawable/ic_expand_less.xml
new file mode 100644
index 0000000..f450846
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/ic_expand_less.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
+  -->
+<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="M18.59,16.41L20,15L12,7L4,15L5.41,16.41L12,9.83"
+      android:fillColor="#5F6368"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml
new file mode 100644
index 0000000..ddcd5c6
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml
@@ -0,0 +1,41 @@
+<?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.bubbles.bar.BubbleBarMenuItemView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/bubble_bar_manage_menu_item_height"
+    android:gravity="center_vertical"
+    android:paddingStart="@dimen/bubble_menu_padding"
+    android:paddingEnd="@dimen/bubble_menu_padding"
+    android:background="@drawable/bubble_manage_menu_row">
+
+    <ImageView
+        android:id="@+id/bubble_bar_menu_item_icon"
+        android:layout_width="@dimen/bubble_bar_manage_menu_item_icon_size"
+        android:layout_height="@dimen/bubble_bar_manage_menu_item_icon_size"
+        android:contentDescription="@null"/>
+
+    <TextView
+        android:id="@+id/bubble_bar_menu_item_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:textColor="?android:attr/textColorPrimary"
+        android:textAppearance="@*android:style/TextAppearance.DeviceDefault" />
+
+</com.android.wm.shell.bubbles.bar.BubbleBarMenuItemView>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml
new file mode 100644
index 0000000..82e5aee
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml
@@ -0,0 +1,77 @@
+<?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.bubbles.bar.BubbleBarMenuView
+    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:layout_gravity="center_horizontal"
+    android:minWidth="@dimen/bubble_bar_manage_menu_min_width"
+    android:orientation="vertical"
+    android:elevation="@dimen/bubble_manage_menu_elevation"
+    android:paddingTop="@dimen/bubble_bar_manage_menu_padding_top"
+    android:paddingHorizontal="@dimen/bubble_bar_manage_menu_padding"
+    android:paddingBottom="@dimen/bubble_bar_manage_menu_padding"
+    android:clipToPadding="false">
+
+    <LinearLayout
+        android:id="@+id/bubble_bar_manage_menu_bubble_section"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/bubble_bar_manage_menu_item_height"
+        android:orientation="horizontal"
+        android:gravity="center_vertical"
+        android:paddingStart="14dp"
+        android:paddingEnd="12dp"
+        android:background="@drawable/bubble_manage_menu_section"
+        android:elevation="@dimen/bubble_manage_menu_elevation">
+
+        <ImageView
+            android:id="@+id/bubble_bar_manage_menu_bubble_icon"
+            android:layout_width="@dimen/bubble_menu_icon_size"
+            android:layout_height="@dimen/bubble_menu_icon_size"
+            android:contentDescription="@null" />
+
+        <TextView
+            android:id="@+id/bubble_bar_manage_menu_bubble_title"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:layout_weight="1"
+            android:textColor="?android:attr/textColorPrimary"
+            android:textAppearance="@*android:style/TextAppearance.DeviceDefault" />
+
+        <ImageView
+            android:id="@+id/bubble_bar_manage_menu_dismiss_icon"
+            android:layout_width="@dimen/bubble_bar_manage_menu_dismiss_icon_size"
+            android:layout_height="@dimen/bubble_bar_manage_menu_dismiss_icon_size"
+            android:layout_marginStart="8dp"
+            android:contentDescription="@null"
+            android:src="@drawable/ic_expand_less"
+            app:tint="?android:attr/textColorPrimary" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/bubble_bar_manage_menu_actions_section"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:layout_marginTop="@dimen/bubble_bar_manage_menu_section_spacing"
+        android:background="@drawable/bubble_manage_menu_bg"
+        android:elevation="@dimen/bubble_manage_menu_elevation" />
+
+</com.android.wm.shell.bubbles.bar.BubbleBarMenuView>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml
index 1d6864c..0ca912e 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml
@@ -28,6 +28,7 @@
         android:layout_width="176dp"
         android:layout_height="42dp"
         android:paddingHorizontal="24dp"
+        android:paddingVertical="19dp"
         android:contentDescription="@string/handle_text"
         android:src="@drawable/decor_handle_dark"
         tools:tint="@color/desktop_mode_caption_handle_bar_dark"
diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml
index 171a6b2..f2a0785 100644
--- a/libs/WindowManager/Shell/res/values/colors.xml
+++ b/libs/WindowManager/Shell/res/values/colors.xml
@@ -30,6 +30,9 @@
     <color name="bubbles_light">#FFFFFF</color>
     <color name="bubbles_dark">@color/GM2_grey_800</color>
     <color name="bubbles_icon_tint">@color/GM2_grey_700</color>
+    <color name="bubble_bar_expanded_view_handle_light">#EBffffff</color>
+    <color name="bubble_bar_expanded_view_handle_dark">#99000000</color>
+    <color name="bubble_bar_expanded_view_menu_close">#DC362E</color>
 
     <!-- PiP -->
     <color name="pip_custom_close_bg">#D93025</color>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 2be34c9..3d4b55a 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -228,8 +228,26 @@
     <dimen name="bubble_user_education_stack_padding">16dp</dimen>
     <!-- Size of the bubble bar (height), should match transient_taskbar_size in Launcher. -->
     <dimen name="bubblebar_size">72dp</dimen>
-    <!-- The size of the drag handle / menu shown along with a bubble bar expanded view. -->
-    <dimen name="bubblebar_expanded_view_menu_size">16dp</dimen>
+    <!-- The size of the caption bar inset at the top of bubble bar expanded view. -->
+    <dimen name="bubble_bar_expanded_view_caption_height">32dp</dimen>
+    <!-- The height of the dots shown for the caption menu in the bubble bar expanded view.. -->
+    <dimen name="bubble_bar_expanded_view_caption_dot_size">4dp</dimen>
+    <!-- The spacing between the dots for the caption menu in the bubble bar expanded view.. -->
+    <dimen name="bubble_bar_expanded_view_caption_dot_spacing">4dp</dimen>
+    <!-- Minimum width of the bubble bar manage menu. -->
+    <dimen name="bubble_bar_manage_menu_min_width">200dp</dimen>
+    <!-- Size of the dismiss icon in the bubble bar manage menu. -->
+    <dimen name="bubble_bar_manage_menu_dismiss_icon_size">16dp</dimen>
+    <!-- Padding of the bubble bar manage menu, provides space for menu shadows -->
+    <dimen name="bubble_bar_manage_menu_padding">8dp</dimen>
+    <!-- Top padding of the bubble bar manage menu -->
+    <dimen name="bubble_bar_manage_menu_padding_top">2dp</dimen>
+    <!-- Spacing between sections of the bubble bar manage menu -->
+    <dimen name="bubble_bar_manage_menu_section_spacing">2dp</dimen>
+    <!-- Height of an item in the bubble bar manage menu. -->
+    <dimen name="bubble_bar_manage_menu_item_height">52dp</dimen>
+    <!-- Size of the icons in the bubble bar manage menu. -->
+    <dimen name="bubble_bar_manage_menu_item_icon_size">20dp</dimen>
 
     <!-- Bottom and end margin for compat buttons. -->
     <dimen name="compat_button_margin">24dp</dimen>
@@ -398,7 +416,11 @@
     <!-- The radius of the caption menu shadow. -->
     <dimen name="desktop_mode_handle_menu_shadow_radius">2dp</dimen>
 
-    <dimen name="freeform_resize_handle">30dp</dimen>
+    <dimen name="freeform_resize_handle">15dp</dimen>
 
     <dimen name="freeform_resize_corner">44dp</dimen>
+
+    <!-- The height of the area at the top of the screen where a freeform task will transition to
+    fullscreen if dragged until the top bound of the task is within the area. -->
+    <dimen name="desktop_mode_transition_area_height">16dp</dimen>
 </resources>
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 102f2cb..7e09c98 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
@@ -422,6 +422,7 @@
         }
         if (mBubbleBarExpandedView != null) {
             mBubbleBarExpandedView.cleanUpExpandedState();
+            mBubbleBarExpandedView = null;
         }
         if (mIntent != null) {
             mIntent.unregisterCancelListener(mIntentCancelListener);
@@ -549,10 +550,10 @@
     /**
      * Set visibility of bubble in the expanded state.
      *
-     * @param visibility {@code true} if the expanded bubble should be visible on the screen.
-     *
-     * Note that this contents visibility doesn't affect visibility at {@link android.view.View},
+     * <p>Note that this contents visibility doesn't affect visibility at {@link android.view.View},
      * and setting {@code false} actually means rendering the expanded view in transparent.
+     *
+     * @param visibility {@code true} if the expanded bubble should be visible on the screen.
      */
     @Override
     public void setTaskViewVisibility(boolean visibility) {
@@ -855,7 +856,8 @@
         return mIsAppBubble;
     }
 
-    Intent getSettingsIntent(final Context context) {
+    /** Creates open app settings intent */
+    public Intent getSettingsIntent(final Context context) {
         final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS);
         intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
         final int uid = getUid(context);
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 0fdfbb8..6880237 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
@@ -64,7 +64,6 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.service.notification.NotificationListenerService;
@@ -92,6 +91,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.WindowManagerShellWrapper;
 import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
+import com.android.wm.shell.bubbles.properties.BubbleProperties;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.FloatingContentCoordinator;
@@ -143,16 +143,6 @@
     private static final String SYSTEM_DIALOG_REASON_KEY = "reason";
     private static final String SYSTEM_DIALOG_REASON_GESTURE_NAV = "gestureNav";
 
-    // TODO(b/256873975) Should use proper flag when available to shell/launcher
-    /**
-     * Whether bubbles are showing in the bubble bar from launcher. This is only available
-     * on large screens and {@link BubbleController#isShowingAsBubbleBar()} should be used
-     * to check all conditions that indicate if the bubble bar is in use.
-     */
-    private static final boolean BUBBLE_BAR_ENABLED =
-            SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
-
-
     /**
      * Common interface to send updates to bubble views.
      */
@@ -195,6 +185,7 @@
     private final ShellController mShellController;
     private final ShellCommandHandler mShellCommandHandler;
     private final IWindowManager mWmService;
+    private final BubbleProperties mBubbleProperties;
 
     // Used to post to main UI thread
     private final ShellExecutor mMainExecutor;
@@ -291,7 +282,8 @@
             @ShellBackgroundThread ShellExecutor bgExecutor,
             TaskViewTransitions taskViewTransitions,
             SyncTransactionQueue syncQueue,
-            IWindowManager wmService) {
+            IWindowManager wmService,
+            BubbleProperties bubbleProperties) {
         mContext = context;
         mShellCommandHandler = shellCommandHandler;
         mShellController = shellController;
@@ -328,6 +320,7 @@
         mDragAndDropController = dragAndDropController;
         mSyncQueue = syncQueue;
         mWmService = wmService;
+        mBubbleProperties = bubbleProperties;
         shellInit.addInitCallback(this::onInit, this);
     }
 
@@ -518,11 +511,14 @@
     /**
      * Sets a listener to be notified of bubble updates. This is used by launcher so that
      * it may render bubbles in itself. Only one listener is supported.
+     *
+     * <p>If bubble bar is supported, bubble views will be updated to switch to bar mode.
      */
     public void registerBubbleStateListener(Bubbles.BubbleStateListener listener) {
-        if (isShowingAsBubbleBar()) {
-            // Only set the listener if bubble bar is showing.
+        if (canShowAsBubbleBar() && listener != null) {
+            // Only set the listener if we can show the bubble bar.
             mBubbleStateListener = listener;
+            setUpBubbleViewsForMode();
             sendInitialListenerUpdate();
         } else {
             mBubbleStateListener = null;
@@ -531,9 +527,15 @@
 
     /**
      * Unregisters the {@link Bubbles.BubbleStateListener}.
+     *
+     * <p>If there's an existing listener, then we're switching back to stack mode and bubble views
+     * will be updated accordingly.
      */
     public void unregisterBubbleStateListener() {
-        mBubbleStateListener = null;
+        if (mBubbleStateListener != null) {
+            mBubbleStateListener = null;
+            setUpBubbleViewsForMode();
+        }
     }
 
     /**
@@ -645,8 +647,12 @@
 
     /** Whether bubbles are showing in the bubble bar. */
     public boolean isShowingAsBubbleBar() {
-        // TODO(b/269670598): should also check that we're in gesture nav
-        return BUBBLE_BAR_ENABLED && mBubblePositioner.isLargeScreen();
+        return canShowAsBubbleBar() && mBubbleStateListener != null;
+    }
+
+    /** Whether the current configuration supports showing as bubble bar. */
+    private boolean canShowAsBubbleBar() {
+        return mBubbleProperties.isBubbleBarEnabled() && mBubblePositioner.isLargeScreen();
     }
 
     /** Whether this userId belongs to the current user. */
@@ -719,6 +725,7 @@
             // TODO(b/273312602): consider foldables where we do need a stack view when folded
             if (mLayerView == null) {
                 mLayerView = new BubbleBarLayerView(mContext, this);
+                mLayerView.setUnBubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
             }
         } else {
             if (mStackView == null) {
@@ -773,12 +780,12 @@
         try {
             mAddedToWindowManager = true;
             registerBroadcastReceiver();
-            mBubbleData.getOverflow().initialize(this);
+            mBubbleData.getOverflow().initialize(this, isShowingAsBubbleBar());
             // (TODO: b/273314541) some duplication in the inset listener
             if (isShowingAsBubbleBar()) {
                 mWindowManager.addView(mLayerView, mWmLayoutParams);
                 mLayerView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
-                    if (!windowInsets.equals(mWindowInsets)) {
+                    if (!windowInsets.equals(mWindowInsets) && mLayerView != null) {
                         mWindowInsets = windowInsets;
                         mBubblePositioner.update();
                         mLayerView.onDisplaySizeChanged();
@@ -788,7 +795,7 @@
             } else {
                 mWindowManager.addView(mStackView, mWmLayoutParams);
                 mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
-                    if (!windowInsets.equals(mWindowInsets)) {
+                    if (!windowInsets.equals(mWindowInsets) && mStackView != null) {
                         mWindowInsets = windowInsets;
                         mBubblePositioner.update();
                         mStackView.onDisplaySizeChanged();
@@ -1065,9 +1072,18 @@
      * Expands and selects the provided bubble as long as it already exists in the stack or the
      * overflow.
      *
-     * This is used by external callers (launcher).
+     * <p>This is used by external callers (launcher).
      */
-    public void expandStackAndSelectBubbleFromLauncher(String key) {
+    @VisibleForTesting
+    public void expandStackAndSelectBubbleFromLauncher(String key, boolean onLauncherHome) {
+        mBubblePositioner.setShowingInBubbleBar(onLauncherHome);
+
+        if (BubbleOverflow.KEY.equals(key)) {
+            mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow());
+            mLayerView.showExpandedView(mBubbleData.getOverflow());
+            return;
+        }
+
         Bubble b = mBubbleData.getAnyBubbleWithkey(key);
         if (b == null) {
             return;
@@ -1220,6 +1236,13 @@
     }
 
     /**
+     * Dismiss bubble if it exists and remove it from the stack
+     */
+    public void dismissBubble(Bubble bubble, @Bubbles.DismissReason int reason) {
+        mBubbleData.dismissBubbleWithKey(bubble.getKey(), reason);
+    }
+
+    /**
      * Performs a screenshot that may exclude the bubble layer, if one is present. The screenshot
      * can be access via the supplied {@link SynchronousScreenCaptureListener#getBuffer()}
      * asynchronously.
@@ -1259,7 +1282,9 @@
             return;
         }
         mOverflowDataLoadNeeded = false;
-        mDataRepository.loadBubbles(mCurrentUserId, (bubbles) -> {
+        List<UserInfo> users = mUserManager.getAliveUsers();
+        List<Integer> userIds = users.stream().map(userInfo -> userInfo.id).toList();
+        mDataRepository.loadBubbles(mCurrentUserId, userIds, (bubbles) -> {
             bubbles.forEach(bubble -> {
                 if (mBubbleData.hasAnyBubbleWithKey(bubble.getKey())) {
                     // if the bubble is already active, there's no need to push it to overflow
@@ -1278,6 +1303,50 @@
         });
     }
 
+    void setUpBubbleViewsForMode() {
+        mBubbleViewCallback = isShowingAsBubbleBar()
+                ? mBubbleBarViewCallback
+                : mBubbleStackViewCallback;
+
+        // reset the overflow so that it can be re-added later if needed.
+        if (mStackView != null) {
+            mStackView.resetOverflowView();
+            mStackView.removeAllViews();
+        }
+        // cleanup existing bubble views so they can be recreated later if needed.
+        mBubbleData.getBubbles().forEach(Bubble::cleanupViews);
+
+        // remove the current bubble container from window manager, null it out, and create a new
+        // container based on the current mode.
+        removeFromWindowManagerMaybe();
+        mLayerView = null;
+        mStackView = null;
+        ensureBubbleViewsAndWindowCreated();
+
+        // inflate bubble views
+        BubbleViewInfoTask.Callback callback = null;
+        if (!isShowingAsBubbleBar()) {
+            callback = b -> {
+                if (mStackView != null) {
+                    mStackView.addBubble(b);
+                    mStackView.setSelectedBubble(b);
+                } else {
+                    Log.w(TAG, "Tried to add a bubble to the stack but the stack is null");
+                }
+            };
+        }
+        for (int i = mBubbleData.getBubbles().size() - 1; i >= 0; i--) {
+            Bubble bubble = mBubbleData.getBubbles().get(i);
+            bubble.inflate(callback,
+                    mContext,
+                    this,
+                    mStackView,
+                    mLayerView,
+                    mBubbleIconFactory,
+                    false /* skipInflation */);
+        }
+    }
+
     /**
      * Adds or updates a bubble associated with the provided notification entry.
      *
@@ -1738,7 +1807,7 @@
             // Update the cached state for queries from SysUI
             mImpl.mCachedState.update(update);
 
-            if (isShowingAsBubbleBar() && mBubbleStateListener != null) {
+            if (isShowingAsBubbleBar()) {
                 BubbleBarUpdate bubbleBarUpdate = update.toBubbleBarUpdate();
                 // Some updates aren't relevant to the bubble bar so check first.
                 if (bubbleBarUpdate.anythingChanged()) {
@@ -1846,7 +1915,7 @@
             if (mStackView != null) {
                 mStackView.setVisibility(VISIBLE);
             }
-            if (mLayerView != null && isStackExpanded()) {
+            if (mLayerView != null) {
                 mLayerView.setVisibility(VISIBLE);
             }
         }
@@ -1860,10 +1929,17 @@
     }
 
     @VisibleForTesting
+    @Nullable
     public BubbleStackView getStackView() {
         return mStackView;
     }
 
+    @VisibleForTesting
+    @Nullable
+    public BubbleBarLayerView getLayerView() {
+        return mLayerView;
+    }
+
     /**
      * Check if notification panel is in an expanded state.
      * Makes a call to System UI process and delivers the result via {@code callback} on the
@@ -2002,22 +2078,18 @@
 
         @Override
         public void registerBubbleListener(IBubblesListener listener) {
-            mMainExecutor.execute(() -> {
-                mListener.register(listener);
-            });
+            mMainExecutor.execute(() -> mListener.register(listener));
         }
 
         @Override
         public void unregisterBubbleListener(IBubblesListener listener) {
-            mMainExecutor.execute(() -> mListener.unregister());
+            mMainExecutor.execute(mListener::unregister);
         }
 
         @Override
         public void showBubble(String key, boolean onLauncherHome) {
-            mMainExecutor.execute(() -> {
-                mBubblePositioner.setShowingInBubbleBar(onLauncherHome);
-                mController.expandStackAndSelectBubbleFromLauncher(key);
-            });
+            mMainExecutor.execute(
+                    () -> mController.expandStackAndSelectBubbleFromLauncher(key, onLauncherHome));
         }
 
         @Override
@@ -2029,11 +2101,6 @@
         public void collapseBubbles() {
             mMainExecutor.execute(() -> mController.collapseStack());
         }
-
-        @Override
-        public void onTaskbarStateChanged(int newState) {
-            // TODO (b/269670598)
-        }
     }
 
     private class BubblesImpl implements Bubbles {
@@ -2144,7 +2211,7 @@
                     pw.println("   suppressing: " + key);
                 }
 
-                pw.print("mAppBubbleTaskIds: " + mAppBubbleTaskIds.values());
+                pw.println("mAppBubbleTaskIds: " + mAppBubbleTaskIds.values());
             }
         }
 
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 e37c785..896a334 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
@@ -17,7 +17,6 @@
 
 import android.annotation.SuppressLint
 import android.annotation.UserIdInt
-import android.content.Context
 import android.content.pm.LauncherApps
 import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED
 import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC
@@ -25,6 +24,8 @@
 import android.content.pm.UserInfo
 import android.os.UserHandle
 import android.util.Log
+import android.util.SparseArray
+import com.android.internal.annotations.VisibleForTesting
 import com.android.wm.shell.bubbles.Bubbles.BubbleMetadataFlagListener
 import com.android.wm.shell.bubbles.storage.BubbleEntity
 import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
@@ -33,19 +34,19 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancelAndJoin
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.yield
 
-internal class BubbleDataRepository(
-    context: Context,
+class BubbleDataRepository(
     private val launcherApps: LauncherApps,
-    private val mainExecutor: ShellExecutor
+    private val mainExecutor: ShellExecutor,
+    private val persistentRepository: BubblePersistentRepository,
 ) {
     private val volatileRepository = BubbleVolatileRepository(launcherApps)
-    private val persistentRepository = BubblePersistentRepository(context)
 
-    private val ioScope = CoroutineScope(Dispatchers.IO)
+    private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
     private var job: Job? = null
 
     // For use in Bubble construction.
@@ -98,6 +99,43 @@
         if (volatileRepository.sanitizeBubbles(userIds)) persistToDisk()
     }
 
+    /**
+     * Removes all entities that don't have a user in the activeUsers list, if any entities were
+     * removed it persists the new list to disk.
+     */
+    private fun filterForActiveUsersAndPersist(
+            activeUsers: List<Int>,
+            entitiesByUser: SparseArray<List<BubbleEntity>>
+    ): SparseArray<List<BubbleEntity>> {
+        val validEntitiesByUser = SparseArray<List<BubbleEntity>>()
+        var entitiesChanged = false
+        for (i in 0 until entitiesByUser.size()) {
+            val parentUserId = entitiesByUser.keyAt(i)
+            if (activeUsers.contains(parentUserId)) {
+                val validEntities = mutableListOf<BubbleEntity>()
+                // Check if each of the bubbles in the top-level user still has a valid user
+                // as it could belong to a profile and have a different id from the parent.
+                for (entity in entitiesByUser.get(parentUserId)) {
+                    if (activeUsers.contains(entity.userId)) {
+                        validEntities.add(entity)
+                    } else {
+                        entitiesChanged = true
+                    }
+                }
+                if (validEntities.isNotEmpty()) {
+                    validEntitiesByUser.put(parentUserId, validEntities)
+                }
+            } else {
+                entitiesChanged = true
+            }
+        }
+        if (entitiesChanged) {
+            persistToDisk(validEntitiesByUser)
+            return validEntitiesByUser
+        }
+        return entitiesByUser
+    }
+
     private fun transform(bubbles: List<Bubble>): List<BubbleEntity> {
         return bubbles.mapNotNull { b ->
             BubbleEntity(
@@ -129,15 +167,17 @@
      * Job C resumes and reaches yield() and is then cancelled
      * Job D resumes and performs another blocking I/O
      */
-    private fun persistToDisk() {
+    private fun persistToDisk(
+            entitiesByUser: SparseArray<List<BubbleEntity>> = volatileRepository.bubbles
+    ) {
         val prev = job
-        job = ioScope.launch {
+        job = coroutineScope.launch {
             // if there was an ongoing disk I/O operation, they can be cancelled
             prev?.cancelAndJoin()
             // check for cancellation before disk I/O
             yield()
             // save to disk
-            persistentRepository.persistsToDisk(volatileRepository.bubbles)
+            persistentRepository.persistsToDisk(entitiesByUser)
         }
     }
 
@@ -148,7 +188,12 @@
      *           bubbles.
      */
     @SuppressLint("WrongConstant")
-    fun loadBubbles(userId: Int, cb: (List<Bubble>) -> Unit) = ioScope.launch {
+    @VisibleForTesting
+    fun loadBubbles(
+            userId: Int,
+            currentUsers: List<Int>,
+            cb: (List<Bubble>) -> Unit
+    ) = coroutineScope.launch {
         /**
          * Load BubbleEntity from disk.
          * e.g.
@@ -159,7 +204,12 @@
          * ]
          */
         val entitiesByUser = persistentRepository.readFromDisk()
-        val entities = entitiesByUser.get(userId) ?: return@launch
+
+        // Before doing anything, validate that the entities we loaded are valid & have an existing
+        // user.
+        val validEntitiesByUser = filterForActiveUsersAndPersist(currentUsers, entitiesByUser)
+
+        val entities = validEntitiesByUser.get(userId) ?: return@launch
         volatileRepository.addBubbles(userId, entities)
         /**
          * Extract userId/packageName from these entities.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index e1a3f3a..6718565 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -948,9 +948,9 @@
             mTaskView.onLocationChanged();
         }
         if (mIsOverflow) {
-            post(() -> {
-                mOverflowView.show();
-            });
+            // post this to the looper so that the view has a chance to be laid out before it can
+            // calculate row and column sizes correctly.
+            post(() -> mOverflowView.show());
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
index df7f5b4..df19757 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
@@ -44,6 +44,7 @@
 
     private val inflater: LayoutInflater = LayoutInflater.from(context)
     private var expandedView: BubbleExpandedView?
+    private var bubbleBarExpandedView: BubbleBarExpandedView? = null
     private var overflowBtn: BadgedImageView?
 
     init {
@@ -53,19 +54,26 @@
     }
 
     /** Call before use and again if cleanUpExpandedState was called. */
-    fun initialize(controller: BubbleController) {
-        createExpandedView()
-        getExpandedView()?.initialize(controller, controller.stackView, true /* isOverflow */)
+    fun initialize(controller: BubbleController, forBubbleBar: Boolean) {
+        if (forBubbleBar) {
+            createBubbleBarExpandedView().initialize(controller, true /* isOverflow */)
+        } else {
+            createExpandedView()
+                .initialize(controller, controller.stackView, true /* isOverflow */)
+        }
     }
 
     fun cleanUpExpandedState() {
         expandedView?.cleanUpExpandedState()
         expandedView = null
+        bubbleBarExpandedView?.cleanUpExpandedState()
+        bubbleBarExpandedView = null
     }
 
     fun update() {
         updateResources()
         getExpandedView()?.applyThemeAttrs()
+        getBubbleBarExpandedView()?.applyThemeAttrs()
         // Apply inset and new style to fresh icon drawable.
         getIconView()?.setIconImageResource(R.drawable.bubble_ic_overflow_button)
         updateBtnTheme()
@@ -151,26 +159,39 @@
         overflowBtn?.updateDotVisibility(true /* animate */)
     }
 
-    fun createExpandedView(): BubbleExpandedView? {
-        expandedView =
+    /** Creates the expanded view for bubbles showing in the stack view. */
+    private fun createExpandedView(): BubbleExpandedView {
+        val view =
             inflater.inflate(
                 R.layout.bubble_expanded_view,
                 null /* root */,
                 false /* attachToRoot */
             ) as BubbleExpandedView
-        expandedView?.applyThemeAttrs()
+        view.applyThemeAttrs()
+        expandedView = view
         updateResources()
-        return expandedView
+        return view
     }
 
     override fun getExpandedView(): BubbleExpandedView? {
         return expandedView
     }
 
-    override fun getBubbleBarExpandedView(): BubbleBarExpandedView? {
-        return null
+    /** Creates the expanded view for bubbles showing in the bubble bar. */
+    private fun createBubbleBarExpandedView(): BubbleBarExpandedView {
+        val view =
+            inflater.inflate(
+                R.layout.bubble_bar_expanded_view,
+                null, /* root */
+                false /* attachToRoot*/
+            ) as BubbleBarExpandedView
+        view.applyThemeAttrs()
+        bubbleBarExpandedView = view
+        return view
     }
 
+    override fun getBubbleBarExpandedView(): BubbleBarExpandedView? = bubbleBarExpandedView
+
     override fun getDotColor(): Int {
         return dotColor
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index d101b0c..cb08f93 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -732,17 +732,25 @@
     /**
      * How wide the expanded view should be when showing from the bubble bar.
      */
-    public int getExpandedViewWidthForBubbleBar() {
-        return mExpandedViewLargeScreenWidth;
+    public int getExpandedViewWidthForBubbleBar(boolean isOverflow) {
+        return isOverflow ? mOverflowWidth : mExpandedViewLargeScreenWidth;
     }
 
     /**
      * How tall the expanded view should be when showing from the bubble bar.
      */
-    public int getExpandedViewHeightForBubbleBar() {
+    public int getExpandedViewHeightForBubbleBar(boolean isOverflow) {
+        return isOverflow
+                ? mOverflowHeight
+                : getExpandedViewBottomForBubbleBar() - mInsets.top - mExpandedViewPadding;
+    }
+
+    /** The bottom position of the expanded view when showing above the bubble bar. */
+    public int getExpandedViewBottomForBubbleBar() {
         return getAvailableRect().height()
+                + mInsets.top
                 - mBubbleBarSize
-                - mExpandedViewPadding * 2
+                - mExpandedViewPadding
                 - getBubbleBarHomeAdjustment();
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 68fea41..282db9e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -88,6 +88,8 @@
 import com.android.wm.shell.bubbles.animation.StackAnimationController;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.bubbles.DismissView;
+import com.android.wm.shell.common.bubbles.RelativeTouchListener;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 
 import java.io.PrintWriter;
@@ -307,6 +309,7 @@
 
         String bubblesOnScreen = BubbleDebugConfig.formatBubblesString(
                 getBubblesOnScreen(), getExpandedBubble());
+        pw.print("  stack visibility :       "); pw.println(getVisibility());
         pw.print("  bubbles on screen:       "); pw.println(bubblesOnScreen);
         pw.print("  gestureInProgress:       "); pw.println(mIsGestureInProgress);
         pw.print("  showingDismiss:          "); pw.println(mDismissView.isShowing());
@@ -968,6 +971,8 @@
         mBubbleContainer.bringToFront();
 
         mBubbleOverflow = mBubbleData.getOverflow();
+
+        resetOverflowView();
         mBubbleContainer.addView(mBubbleOverflow.getIconView(),
                 mBubbleContainer.getChildCount() /* index */,
                 new FrameLayout.LayoutParams(mPositioner.getBubbleSize(),
@@ -1179,6 +1184,7 @@
             removeView(mDismissView);
         }
         mDismissView = new DismissView(getContext());
+        DismissViewUtils.setup(mDismissView);
         int elevation = getResources().getDimensionPixelSize(R.dimen.bubble_elevation);
 
         addView(mDismissView);
@@ -1494,9 +1500,6 @@
         getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
         getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater);
         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
-        if (mBubbleOverflow != null) {
-            mBubbleOverflow.cleanUpExpandedState();
-        }
     }
 
     @Override
@@ -3411,6 +3414,19 @@
     }
 
     /**
+     * Removes the overflow view from the stack. This allows for re-adding it later to a new stack.
+     */
+    void resetOverflowView() {
+        BadgedImageView overflowIcon = mBubbleOverflow.getIconView();
+        if (overflowIcon != null) {
+            PhysicsAnimationLayout parent = (PhysicsAnimationLayout) overflowIcon.getParent();
+            if (parent != null) {
+                parent.removeViewNoAnimation(overflowIcon);
+            }
+        }
+    }
+
+    /**
      * Holds some commonly queried information about the stack.
      */
     public static class StackViewState {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index 7a58159..da4a989 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -15,6 +15,7 @@
  */
 package com.android.wm.shell.bubbles;
 
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
@@ -110,6 +111,9 @@
                 try {
                     options.setTaskAlwaysOnTop(true);
                     options.setLaunchedFromBubble(true);
+                    options.setPendingIntentBackgroundActivityStartMode(
+                            MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+                    options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
 
                     Intent fillInIntent = new Intent();
                     // Apply flags to make behaviour match documentLaunchMode=always.
@@ -117,11 +121,19 @@
                     fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
 
                     if (mBubble.isAppBubble()) {
-                        PendingIntent pi = PendingIntent.getActivity(mContext, 0,
-                                mBubble.getAppBubbleIntent(),
-                                PendingIntent.FLAG_MUTABLE,
-                                null);
-                        mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
+                        Context context =
+                                mContext.createContextAsUser(
+                                        mBubble.getUser(), Context.CONTEXT_RESTRICTED);
+                        PendingIntent pi = PendingIntent.getActivity(
+                                context,
+                                /* requestCode= */ 0,
+                                mBubble.getAppBubbleIntent()
+                                        .addFlags(FLAG_ACTIVITY_NEW_DOCUMENT)
+                                        .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
+                                PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
+                                /* options= */ null);
+                        mTaskView.startActivity(pi, /* fillInIntent= */ null, options,
+                                launchBounds);
                     } else if (mBubble.hasMetadataShortcutId()) {
                         options.setApplyActivityFlagsForBubbles(true);
                         mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
index 8ab9841..66e6930 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
@@ -104,7 +104,11 @@
 
     @Override
     protected BubbleViewInfo doInBackground(Void... voids) {
-        if (mController.get().isShowingAsBubbleBar()) {
+        if (!verifyState()) {
+            // If we're in an inconsistent state, then switched modes and should just bail now.
+            return null;
+        }
+        if (mLayerView.get() != null) {
             return BubbleViewInfo.populateForBubbleBar(mContext.get(), mController.get(),
                     mLayerView.get(), mIconFactory, mBubble, mSkipInflation);
         } else {
@@ -118,7 +122,11 @@
         if (isCancelled() || viewInfo == null) {
             return;
         }
+
         mMainExecutor.execute(() -> {
+            if (!verifyState()) {
+                return;
+            }
             mBubble.setViewInfo(viewInfo);
             if (mCallback != null) {
                 mCallback.onBubbleViewsReady(mBubble);
@@ -126,6 +134,14 @@
         });
     }
 
+    private boolean verifyState() {
+        if (mController.get().isShowingAsBubbleBar()) {
+            return mLayerView.get() != null;
+        } else {
+            return mStackView.get() != null;
+        }
+    }
+
     /**
      * Info necessary to render a bubble.
      */
@@ -160,39 +176,14 @@
                 LayoutInflater inflater = LayoutInflater.from(c);
                 info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate(
                         R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */);
-                info.bubbleBarExpandedView.initialize(controller);
+                info.bubbleBarExpandedView.initialize(controller, false /* isOverflow */);
             }
 
-            if (b.getShortcutInfo() != null) {
-                info.shortcutInfo = b.getShortcutInfo();
-            }
-
-            // App name & app icon
-            PackageManager pm = BubbleController.getPackageManagerForUser(c,
-                    b.getUser().getIdentifier());
-            ApplicationInfo appInfo;
-            Drawable badgedIcon;
-            Drawable appIcon;
-            try {
-                appInfo = pm.getApplicationInfo(
-                        b.getPackageName(),
-                        PackageManager.MATCH_UNINSTALLED_PACKAGES
-                                | PackageManager.MATCH_DISABLED_COMPONENTS
-                                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
-                                | PackageManager.MATCH_DIRECT_BOOT_AWARE);
-                if (appInfo != null) {
-                    info.appName = String.valueOf(pm.getApplicationLabel(appInfo));
-                }
-                appIcon = pm.getApplicationIcon(b.getPackageName());
-                badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser());
-            } catch (PackageManager.NameNotFoundException exception) {
-                // If we can't find package... don't think we should show the bubble.
-                Log.w(TAG, "Unable to find package: " + b.getPackageName());
+            if (!populateCommonInfo(info, c, b, iconFactory)) {
+                // if we failed to update common fields return null
                 return null;
             }
 
-            info.rawBadgeBitmap = iconFactory.getBadgeBitmap(badgedIcon, false).icon;
-
             return info;
         }
 
@@ -215,66 +206,11 @@
                 info.expandedView.initialize(controller, stackView, false /* isOverflow */);
             }
 
-            if (b.getShortcutInfo() != null) {
-                info.shortcutInfo = b.getShortcutInfo();
-            }
-
-            // App name & app icon
-            PackageManager pm = BubbleController.getPackageManagerForUser(c,
-                    b.getUser().getIdentifier());
-            ApplicationInfo appInfo;
-            Drawable badgedIcon;
-            Drawable appIcon;
-            try {
-                appInfo = pm.getApplicationInfo(
-                        b.getPackageName(),
-                        PackageManager.MATCH_UNINSTALLED_PACKAGES
-                                | PackageManager.MATCH_DISABLED_COMPONENTS
-                                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
-                                | PackageManager.MATCH_DIRECT_BOOT_AWARE);
-                if (appInfo != null) {
-                    info.appName = String.valueOf(pm.getApplicationLabel(appInfo));
-                }
-                appIcon = pm.getApplicationIcon(b.getPackageName());
-                badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser());
-            } catch (PackageManager.NameNotFoundException exception) {
-                // If we can't find package... don't think we should show the bubble.
-                Log.w(TAG, "Unable to find package: " + b.getPackageName());
+            if (!populateCommonInfo(info, c, b, iconFactory)) {
+                // if we failed to update common fields return null
                 return null;
             }
 
-            // Badged bubble image
-            Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo,
-                    b.getIcon());
-            if (bubbleDrawable == null) {
-                // Default to app icon
-                bubbleDrawable = appIcon;
-            }
-
-            BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon,
-                    b.isImportantConversation());
-            info.badgeBitmap = badgeBitmapInfo.icon;
-            // Raw badge bitmap never includes the important conversation ring
-            info.rawBadgeBitmap = b.isImportantConversation()
-                    ? iconFactory.getBadgeBitmap(badgedIcon, false).icon
-                    : badgeBitmapInfo.icon;
-
-            float[] bubbleBitmapScale = new float[1];
-            info.bubbleBitmap = iconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale);
-
-            // Dot color & placement
-            Path iconPath = PathParser.createPathFromPathData(
-                    c.getResources().getString(com.android.internal.R.string.config_icon_mask));
-            Matrix matrix = new Matrix();
-            float scale = bubbleBitmapScale[0];
-            float radius = DEFAULT_PATH_SIZE / 2f;
-            matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
-                    radius /* pivot y */);
-            iconPath.transform(matrix);
-            info.dotPath = iconPath;
-            info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
-                    Color.WHITE, WHITE_SCRIM_ALPHA);
-
             // Flyout
             info.flyoutMessage = b.getFlyoutMessage();
             if (info.flyoutMessage != null) {
@@ -285,6 +221,75 @@
         }
     }
 
+    /**
+     * Modifies the given {@code info} object and populates common fields in it.
+     *
+     * <p>This method returns {@code true} if the update was successful and {@code false} otherwise.
+     * Callers should assume that the info object is unusable if the update was unsuccessful.
+     */
+    private static boolean populateCommonInfo(
+            BubbleViewInfo info, Context c, Bubble b, BubbleIconFactory iconFactory) {
+        if (b.getShortcutInfo() != null) {
+            info.shortcutInfo = b.getShortcutInfo();
+        }
+
+        // App name & app icon
+        PackageManager pm = BubbleController.getPackageManagerForUser(c,
+                b.getUser().getIdentifier());
+        ApplicationInfo appInfo;
+        Drawable badgedIcon;
+        Drawable appIcon;
+        try {
+            appInfo = pm.getApplicationInfo(
+                    b.getPackageName(),
+                    PackageManager.MATCH_UNINSTALLED_PACKAGES
+                            | PackageManager.MATCH_DISABLED_COMPONENTS
+                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+                            | PackageManager.MATCH_DIRECT_BOOT_AWARE);
+            if (appInfo != null) {
+                info.appName = String.valueOf(pm.getApplicationLabel(appInfo));
+            }
+            appIcon = pm.getApplicationIcon(b.getPackageName());
+            badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser());
+        } catch (PackageManager.NameNotFoundException exception) {
+            // If we can't find package... don't think we should show the bubble.
+            Log.w(TAG, "Unable to find package: " + b.getPackageName());
+            return false;
+        }
+
+        // Badged bubble image
+        Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo, b.getIcon());
+        if (bubbleDrawable == null) {
+            // Default to app icon
+            bubbleDrawable = appIcon;
+        }
+
+        BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon,
+                b.isImportantConversation());
+        info.badgeBitmap = badgeBitmapInfo.icon;
+        // Raw badge bitmap never includes the important conversation ring
+        info.rawBadgeBitmap = b.isImportantConversation() // is this needed for bar?
+                ? iconFactory.getBadgeBitmap(badgedIcon, false).icon
+                : badgeBitmapInfo.icon;
+
+        float[] bubbleBitmapScale = new float[1];
+        info.bubbleBitmap = iconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale);
+
+        // Dot color & placement
+        Path iconPath = PathParser.createPathFromPathData(
+                c.getResources().getString(com.android.internal.R.string.config_icon_mask));
+        Matrix matrix = new Matrix();
+        float scale = bubbleBitmapScale[0];
+        float radius = DEFAULT_PATH_SIZE / 2f;
+        matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
+                radius /* pivot y */);
+        iconPath.transform(matrix);
+        info.dotPath = iconPath;
+        info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
+                Color.WHITE, WHITE_SCRIM_ALPHA);
+        return true;
+    }
+
     @Nullable
     static Drawable loadSenderAvatar(@NonNull final Context context, @Nullable final Icon icon) {
         Objects.requireNonNull(context);
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 4d329dd..759246e 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
@@ -60,9 +60,11 @@
             DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE,
             DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT,
             DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED,
-            DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK, DISMISS_USER_REMOVED})
+            DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK, DISMISS_USER_REMOVED,
+            DISMISS_SWITCH_TO_STACK})
     @Target({FIELD, LOCAL_VARIABLE, PARAMETER})
-    @interface DismissReason {}
+    @interface DismissReason {
+    }
 
     int DISMISS_USER_GESTURE = 1;
     int DISMISS_AGED = 2;
@@ -80,6 +82,7 @@
     int DISMISS_NO_BUBBLE_UP = 14;
     int DISMISS_RELOAD_FROM_DISK = 15;
     int DISMISS_USER_REMOVED = 16;
+    int DISMISS_SWITCH_TO_STACK = 17;
 
     /** Returns a binder that can be passed to an external process to manipulate Bubbles. */
     default IBubbles createExternalInterface() {
@@ -120,8 +123,8 @@
 
     /**
      * This method has different behavior depending on:
-     *    - if an app bubble exists
-     *    - if an app bubble is expanded
+     * - 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)
@@ -135,13 +138,13 @@
      * the bubble or bubble stack.
      *
      * Some notes:
-     *    - Only one app bubble is supported at a time, regardless of users. Multi-users support is
-     *      tracked in b/273533235.
-     *    - Calling this method with a different intent than the existing app bubble will do nothing
+     * - Only one app bubble is supported at a time, regardless of users. Multi-users support is
+     * tracked in b/273533235.
+     * - 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.
-     * @param user the {@link UserHandle} of the user to start this activity for.
-     * @param icon the {@link Icon} to use for the bubble view.
+     * @param user   the {@link UserHandle} of the user to start this activity for.
+     * @param icon   the {@link Icon} to use for the bubble view.
      */
     void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon);
 
@@ -172,13 +175,12 @@
      * {@link Bubble#setSuppressNotification}.  For the case of suppressed summaries, we also add
      * {@link BubbleData#addSummaryToSuppress}.
      *
-     * @param entry the notification of the BubbleEntry should be removed.
-     * @param children the list of child notification of the BubbleEntry from 1st param entry,
-     *                 this will be null if entry does have no children.
+     * @param entry          the notification of the BubbleEntry should be removed.
+     * @param children       the list of child notification of the BubbleEntry from 1st param entry,
+     *                       this will be null if entry does have no children.
      * @param removeCallback the remove callback for SystemUI side to remove notification, the int
      *                       number should be list position of children list and use -1 for
      *                       removing the parent notification.
-     *
      * @return true if we want to intercept the dismissal of the entry, else false.
      */
     boolean handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children,
@@ -200,9 +202,9 @@
     /**
      * Called when new notification entry updated.
      *
-     * @param entry the {@link BubbleEntry} by the notification.
+     * @param entry          the {@link BubbleEntry} by the notification.
      * @param shouldBubbleUp {@code true} if this notification should bubble up.
-     * @param fromSystem {@code true} if this update is from NotificationManagerService.
+     * @param fromSystem     {@code true} if this update is from NotificationManagerService.
      */
     void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem);
 
@@ -218,7 +220,7 @@
      * filtering and sorting. This is used to dismiss or create bubbles based on changes in
      * permissions on the notification channel or the global setting.
      *
-     * @param rankingMap the updated ranking map from NotificationListenerService
+     * @param rankingMap     the updated ranking map from NotificationListenerService
      * @param entryDataByKey a map of ranking key to bubble entry and whether the entry should
      *                       bubble up
      */
@@ -230,9 +232,9 @@
      * Called when a notification channel is modified, in response to
      * {@link NotificationListenerService#onNotificationChannelModified}.
      *
-     * @param pkg the package the notification channel belongs to.
-     * @param user the user the notification channel belongs to.
-     * @param channel the channel being modified.
+     * @param pkg              the package the notification channel belongs to.
+     * @param user             the user the notification channel belongs to.
+     * @param channel          the channel being modified.
      * @param modificationType the type of modification that occurred to the channel.
      */
     void onNotificationChannelModified(
@@ -300,7 +302,7 @@
          * Called when the expansion state of the bubble stack changes.
          *
          * @param isExpanding whether it's expanding or collapsing
-         * @param key the notification key associated with bubble being expanded
+         * @param key         the notification key associated with bubble being expanded
          */
         void onBubbleExpandChanged(boolean isExpanding, String key);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt
deleted file mode 100644
index 67ecb91..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt
+++ /dev/null
@@ -1,159 +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.wm.shell.bubbles
-
-import android.animation.ObjectAnimator
-import android.content.Context
-import android.graphics.Color
-import android.graphics.drawable.GradientDrawable
-import android.util.IntProperty
-import android.view.Gravity
-import android.view.View
-import android.view.ViewGroup
-import android.view.WindowInsets
-import android.view.WindowManager
-import android.widget.FrameLayout
-import androidx.dynamicanimation.animation.DynamicAnimation
-import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY
-import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW
-import com.android.wm.shell.R
-import com.android.wm.shell.animation.PhysicsAnimator
-import com.android.wm.shell.common.DismissCircleView
-
-/*
- * View that handles interactions between DismissCircleView and BubbleStackView.
- */
-class DismissView(context: Context) : FrameLayout(context) {
-
-    var circle = DismissCircleView(context)
-    var isShowing = false
-    var targetSizeResId: Int
-
-    private val animator = PhysicsAnimator.getInstance(circle)
-    private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY)
-    private val DISMISS_SCRIM_FADE_MS = 200L
-    private var wm: WindowManager =
-            context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
-    private var gradientDrawable = createGradient()
-
-    private val GRADIENT_ALPHA: IntProperty<GradientDrawable> =
-            object : IntProperty<GradientDrawable>("alpha") {
-        override fun setValue(d: GradientDrawable, percent: Int) {
-            d.alpha = percent
-        }
-        override fun get(d: GradientDrawable): Int {
-            return d.alpha
-        }
-    }
-
-    init {
-        setLayoutParams(LayoutParams(
-            ViewGroup.LayoutParams.MATCH_PARENT,
-            resources.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height),
-            Gravity.BOTTOM))
-        updatePadding()
-        setClipToPadding(false)
-        setClipChildren(false)
-        setVisibility(View.INVISIBLE)
-        setBackgroundDrawable(gradientDrawable)
-
-        targetSizeResId = R.dimen.dismiss_circle_size
-        val targetSize: Int = resources.getDimensionPixelSize(targetSizeResId)
-        addView(circle, LayoutParams(targetSize, targetSize,
-                Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL))
-        // start with circle offscreen so it's animated up
-        circle.setTranslationY(resources.getDimensionPixelSize(
-                R.dimen.floating_dismiss_gradient_height).toFloat())
-    }
-
-    /**
-     * Animates this view in.
-     */
-    fun show() {
-        if (isShowing) return
-        isShowing = true
-        setVisibility(View.VISIBLE)
-        val alphaAnim = ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA,
-                gradientDrawable.alpha, 255)
-        alphaAnim.setDuration(DISMISS_SCRIM_FADE_MS)
-        alphaAnim.start()
-
-        animator.cancel()
-        animator
-            .spring(DynamicAnimation.TRANSLATION_Y, 0f, spring)
-            .start()
-    }
-
-    /**
-     * Animates this view out, as well as the circle that encircles the bubbles, if they
-     * were dragged into the target and encircled.
-     */
-    fun hide() {
-        if (!isShowing) return
-        isShowing = false
-        val alphaAnim = ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA,
-                gradientDrawable.alpha, 0)
-        alphaAnim.setDuration(DISMISS_SCRIM_FADE_MS)
-        alphaAnim.start()
-        animator
-            .spring(DynamicAnimation.TRANSLATION_Y, height.toFloat(),
-                spring)
-            .withEndActions({ setVisibility(View.INVISIBLE) })
-            .start()
-    }
-
-    /**
-     * Cancels the animator for the dismiss target.
-     */
-    fun cancelAnimators() {
-        animator.cancel()
-    }
-
-    fun updateResources() {
-        updatePadding()
-        layoutParams.height = resources.getDimensionPixelSize(
-                R.dimen.floating_dismiss_gradient_height)
-
-        val targetSize = resources.getDimensionPixelSize(targetSizeResId)
-        circle.layoutParams.width = targetSize
-        circle.layoutParams.height = targetSize
-        circle.requestLayout()
-    }
-
-    private fun createGradient(): GradientDrawable {
-        val gradientColor = context.resources.getColor(android.R.color.system_neutral1_900)
-        val alpha = 0.7f * 255
-        val gradientColorWithAlpha = Color.argb(alpha.toInt(),
-                Color.red(gradientColor),
-                Color.green(gradientColor),
-                Color.blue(gradientColor))
-        val gd = GradientDrawable(
-                GradientDrawable.Orientation.BOTTOM_TOP,
-                intArrayOf(gradientColorWithAlpha, Color.TRANSPARENT))
-        gd.setDither(true)
-        gd.setAlpha(0)
-        return gd
-    }
-
-    private fun updatePadding() {
-        val insets: WindowInsets = wm.getCurrentWindowMetrics().getWindowInsets()
-        val navInset = insets.getInsetsIgnoringVisibility(
-                WindowInsets.Type.navigationBars())
-        setPadding(0, 0, 0, navInset.bottom +
-                resources.getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin))
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissViewExt.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissViewExt.kt
new file mode 100644
index 0000000..ed36240
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissViewExt.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.
+ */
+@file:JvmName("DismissViewUtils")
+
+package com.android.wm.shell.bubbles
+
+import com.android.wm.shell.R
+import com.android.wm.shell.common.bubbles.DismissView
+
+fun DismissView.setup() {
+    setup(DismissView.Config(
+            targetSizeResId = R.dimen.dismiss_circle_size,
+            iconSizeResId = R.dimen.dismiss_target_x_size,
+            bottomMarginResId = R.dimen.floating_dismiss_bottom_margin,
+            floatingGradientHeightResId = R.dimen.floating_dismiss_gradient_height,
+            floatingGradientColorResId = android.R.color.system_neutral1_900,
+            backgroundResId = R.drawable.dismiss_circle_background,
+            iconResId = R.drawable.pip_ic_close_white
+    ))
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
index 862e818..20ae846 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
@@ -35,6 +35,4 @@
 
     oneway void collapseBubbles() = 5;
 
-    oneway void onTaskbarStateChanged(in int newState) = 6;
-
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt
deleted file mode 100644
index ea9d065..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RelativeTouchListener.kt
+++ /dev/null
@@ -1,168 +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.wm.shell.bubbles
-
-import android.graphics.PointF
-import android.view.MotionEvent
-import android.view.VelocityTracker
-import android.view.View
-import android.view.ViewConfiguration
-import kotlin.math.hypot
-
-/**
- * Listener which receives [onDown], [onMove], and [onUp] events, with relevant information about
- * the coordinates of the touch and the view relative to the initial ACTION_DOWN event and the
- * view's initial position.
- */
-abstract class RelativeTouchListener : View.OnTouchListener {
-
-    /**
-     * Called when an ACTION_DOWN event is received for the given view.
-     *
-     * @return False if the object is not interested in MotionEvents at this time, or true if we
-     * should consume this event and subsequent events, and begin calling [onMove].
-     */
-    abstract fun onDown(v: View, ev: MotionEvent): Boolean
-
-    /**
-     * Called when an ACTION_MOVE event is received for the given view. This signals that the view
-     * is being dragged.
-     *
-     * @param viewInitialX The view's translationX value when this touch gesture started.
-     * @param viewInitialY The view's translationY value when this touch gesture started.
-     * @param dx Horizontal distance covered since the initial ACTION_DOWN event, in pixels.
-     * @param dy Vertical distance covered since the initial ACTION_DOWN event, in pixels.
-     */
-    abstract fun onMove(
-        v: View,
-        ev: MotionEvent,
-        viewInitialX: Float,
-        viewInitialY: Float,
-        dx: Float,
-        dy: Float
-    )
-
-    /**
-     * Called when an ACTION_UP event is received for the given view. This signals that a drag or
-     * fling gesture has completed.
-     *
-     * @param viewInitialX The view's translationX value when this touch gesture started.
-     * @param viewInitialY The view's translationY value when this touch gesture started.
-     * @param dx Horizontal distance covered, in pixels.
-     * @param dy Vertical distance covered, in pixels.
-     * @param velX The final horizontal velocity of the gesture, in pixels/second.
-     * @param velY The final vertical velocity of the gesture, in pixels/second.
-     */
-    abstract fun onUp(
-        v: View,
-        ev: MotionEvent,
-        viewInitialX: Float,
-        viewInitialY: Float,
-        dx: Float,
-        dy: Float,
-        velX: Float,
-        velY: Float
-    )
-
-    /** The raw coordinates of the last ACTION_DOWN event. */
-    private val touchDown = PointF()
-
-    /** The coordinates of the view, at the time of the last ACTION_DOWN event. */
-    private val viewPositionOnTouchDown = PointF()
-
-    private val velocityTracker = VelocityTracker.obtain()
-
-    private var touchSlop: Int = -1
-    private var movedEnough = false
-
-    private var performedLongClick = false
-
-    @Suppress("UNCHECKED_CAST")
-    override fun onTouch(v: View, ev: MotionEvent): Boolean {
-        addMovement(ev)
-
-        val dx = ev.rawX - touchDown.x
-        val dy = ev.rawY - touchDown.y
-
-        when (ev.action) {
-            MotionEvent.ACTION_DOWN -> {
-                if (!onDown(v, ev)) {
-                    return false
-                }
-
-                // Grab the touch slop, it might have changed if the config changed since the
-                // last gesture.
-                touchSlop = ViewConfiguration.get(v.context).scaledTouchSlop
-
-                touchDown.set(ev.rawX, ev.rawY)
-                viewPositionOnTouchDown.set(v.translationX, v.translationY)
-
-                performedLongClick = false
-                v.handler.postDelayed({
-                    if (v.isLongClickable) {
-                        performedLongClick = v.performLongClick()
-                    }
-                }, ViewConfiguration.getLongPressTimeout().toLong())
-            }
-
-            MotionEvent.ACTION_MOVE -> {
-                if (!movedEnough && hypot(dx, dy) > touchSlop && !performedLongClick) {
-                    movedEnough = true
-                    v.handler.removeCallbacksAndMessages(null)
-                }
-
-                if (movedEnough) {
-                    onMove(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy)
-                }
-            }
-
-            MotionEvent.ACTION_UP -> {
-                if (movedEnough) {
-                    velocityTracker.computeCurrentVelocity(1000 /* units */)
-                    onUp(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy,
-                            velocityTracker.xVelocity, velocityTracker.yVelocity)
-                } else if (!performedLongClick) {
-                    v.performClick()
-                } else {
-                    v.handler.removeCallbacksAndMessages(null)
-                }
-
-                velocityTracker.clear()
-                movedEnough = false
-            }
-
-            MotionEvent.ACTION_CANCEL -> {
-                v.handler.removeCallbacksAndMessages(null)
-                velocityTracker.clear()
-                movedEnough = false
-            }
-        }
-
-        return true
-    }
-
-    /**
-     * Adds a movement to the velocity tracker using raw screen coordinates.
-     */
-    private fun addMovement(event: MotionEvent) {
-        val deltaX = event.rawX - event.x
-        val deltaY = event.rawY - event.y
-        event.offsetLocation(deltaX, deltaY)
-        velocityTracker.addMovement(event)
-        event.offsetLocation(-deltaX, -deltaY)
-    }
-}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java
index beb1c5f..f3cc514 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java
@@ -327,6 +327,12 @@
         addViewInternal(child, index, params, false /* isReorder */);
     }
 
+    /** Removes the child view immediately. */
+    public void removeViewNoAnimation(View view) {
+        super.removeView(view);
+        view.setTag(R.id.physics_animator_tag, null);
+    }
+
     @Override
     public void removeView(View view) {
         if (mController != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
index 23f65f9..e97390d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
@@ -25,8 +25,11 @@
 import android.util.Log;
 import android.widget.FrameLayout;
 
+import androidx.annotation.Nullable;
+
 import com.android.wm.shell.animation.Interpolators;
 import com.android.wm.shell.animation.PhysicsAnimator;
+import com.android.wm.shell.bubbles.BubbleOverflow;
 import com.android.wm.shell.bubbles.BubblePositioner;
 import com.android.wm.shell.bubbles.BubbleViewProvider;
 import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix;
@@ -110,7 +113,8 @@
     /**
      * Animates the provided bubble's expanded view to the expanded state.
      */
-    public void animateExpansion(BubbleViewProvider expandedBubble) {
+    public void animateExpansion(BubbleViewProvider expandedBubble,
+            @Nullable Runnable afterAnimation) {
         mExpandedBubble = expandedBubble;
         if (mExpandedBubble == null) {
             return;
@@ -159,6 +163,9 @@
                     bev.setAnimationMatrix(null);
                     updateExpandedView();
                     bev.setSurfaceZOrderedOnTop(false);
+                    if (afterAnimation != null) {
+                        afterAnimation.run();
+                    }
                 })
                 .start();
     }
@@ -215,9 +222,10 @@
         }
         BubbleBarExpandedView bbev = mExpandedBubble.getBubbleBarExpandedView();
 
+        boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY);
         final int padding = mPositioner.getBubbleBarExpandedViewPadding();
-        final int width = mPositioner.getExpandedViewWidthForBubbleBar();
-        final int height = mPositioner.getExpandedViewHeightForBubbleBar();
+        final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded);
+        final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded);
         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) bbev.getLayoutParams();
         lp.width = width;
         lp.height = height;
@@ -227,7 +235,8 @@
         } else {
             bbev.setX(mPositioner.getAvailableRect().width() - width - padding);
         }
-        bbev.setY(mPositioner.getInsets().top + padding);
+        bbev.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height);
         bbev.updateLocation();
+        bbev.maybeShowOverflow();
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index b8f049b..6b6d6ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -16,12 +16,16 @@
 
 package com.android.wm.shell.bubbles.bar;
 
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Color;
+import android.graphics.Insets;
 import android.graphics.Outline;
+import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewOutlineProvider;
 import android.widget.FrameLayout;
@@ -30,9 +34,14 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.bubbles.Bubble;
 import com.android.wm.shell.bubbles.BubbleController;
+import com.android.wm.shell.bubbles.BubbleOverflowContainerView;
 import com.android.wm.shell.bubbles.BubbleTaskViewHelper;
+import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.taskview.TaskView;
 
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
 /**
  * Expanded view of a bubble when it's part of the bubble bar.
  *
@@ -44,12 +53,18 @@
     private static final int INVALID_TASK_ID = -1;
 
     private BubbleController mController;
+    private boolean mIsOverflow;
     private BubbleTaskViewHelper mBubbleTaskViewHelper;
+    private BubbleBarMenuViewController mMenuViewController;
+    private @Nullable Supplier<Rect> mLayerBoundsSupplier;
+    private @Nullable Consumer<String> mUnBubbleConversationCallback;
 
-    private HandleView mMenuView;
-    private TaskView mTaskView;
+    private BubbleBarHandleView mHandleView = new BubbleBarHandleView(getContext());
+    private @Nullable TaskView mTaskView;
+    private @Nullable BubbleOverflowContainerView mOverflowView;
 
-    private int mMenuHeight;
+    private int mCaptionHeight;
+
     private int mBackgroundColor;
     private float mCornerRadius = 0f;
 
@@ -83,11 +98,9 @@
         super.onFinishInflate();
         Context context = getContext();
         setElevation(getResources().getDimensionPixelSize(R.dimen.bubble_elevation));
-        mMenuHeight = context.getResources().getDimensionPixelSize(
-                R.dimen.bubblebar_expanded_view_menu_size);
-        mMenuView = new HandleView(context);
-        addView(mMenuView);
-
+        mCaptionHeight = context.getResources().getDimensionPixelSize(
+                R.dimen.bubble_bar_expanded_view_caption_height);
+        addView(mHandleView);
         applyThemeAttrs();
         setClipToOutline(true);
         setOutlineProvider(new ViewOutlineProvider() {
@@ -98,23 +111,78 @@
         });
     }
 
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        // Hide manage menu when view disappears
+        mMenuViewController.hideMenu(false /* animated */);
+    }
+
     /** Set the BubbleController on the view, must be called before doing anything else. */
-    public void initialize(BubbleController controller) {
+    public void initialize(BubbleController controller, boolean isOverflow) {
         mController = controller;
-        mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, mController,
-                /* listener= */ this,
-                /* viewParent= */ this);
-        mTaskView = mBubbleTaskViewHelper.getTaskView();
-        addView(mTaskView);
-        mTaskView.setEnableSurfaceClipping(true);
-        mTaskView.setCornerRadius(mCornerRadius);
+        mIsOverflow = isOverflow;
+
+        if (mIsOverflow) {
+            mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate(
+                    R.layout.bubble_overflow_container, null /* root */);
+            mOverflowView.setBubbleController(mController);
+            addView(mOverflowView);
+        } else {
+
+            mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, mController,
+                    /* listener= */ this,
+                    /* viewParent= */ this);
+            mTaskView = mBubbleTaskViewHelper.getTaskView();
+            addView(mTaskView);
+            mTaskView.setEnableSurfaceClipping(true);
+            mTaskView.setCornerRadius(mCornerRadius);
+
+            // Handle view needs to draw on top of task view.
+            bringChildToFront(mHandleView);
+        }
+        mMenuViewController = new BubbleBarMenuViewController(mContext, this);
+        mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() {
+            @Override
+            public void onMenuVisibilityChanged(boolean visible) {
+                if (mTaskView == null || mLayerBoundsSupplier == null) return;
+                // Updates the obscured touchable region for the task surface.
+                mTaskView.setObscuredTouchRect(visible ? mLayerBoundsSupplier.get() : null);
+            }
+
+            @Override
+            public void onUnBubbleConversation(Bubble bubble) {
+                if (mUnBubbleConversationCallback != null) {
+                    mUnBubbleConversationCallback.accept(bubble.getKey());
+                }
+            }
+
+            @Override
+            public void onOpenAppSettings(Bubble bubble) {
+                mController.collapseStack();
+                mContext.startActivityAsUser(bubble.getSettingsIntent(mContext), bubble.getUser());
+            }
+
+            @Override
+            public void onDismissBubble(Bubble bubble) {
+                mController.dismissBubble(bubble, Bubbles.DISMISS_USER_REMOVED);
+            }
+        });
+        mHandleView.setOnClickListener(view -> {
+            mMenuViewController.showMenu(true /* animated */);
+        });
+    }
+
+    public BubbleBarHandleView getHandleView() {
+        return mHandleView;
     }
 
     // TODO (b/275087636): call this when theme/config changes
-    void applyThemeAttrs() {
+    /** Updates the view based on the current theme. */
+    public void applyThemeAttrs() {
         boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
                 mContext.getResources());
-        final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
+        final TypedArray ta = mContext.obtainStyledAttributes(new int[]{
                 android.R.attr.dialogCornerRadius,
                 android.R.attr.colorBackgroundFloating});
         mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0;
@@ -123,14 +191,12 @@
 
         ta.recycle();
 
-        mMenuView.setCornerRadius(mCornerRadius);
-        mMenuHeight = getResources().getDimensionPixelSize(
-                R.dimen.bubblebar_expanded_view_menu_size);
+        mCaptionHeight = getResources().getDimensionPixelSize(
+                R.dimen.bubble_bar_expanded_view_caption_height);
 
         if (mTaskView != null) {
             mTaskView.setCornerRadius(mCornerRadius);
-            mTaskView.setElevation(150);
-            updateMenuColor();
+            updateHandleColor(true /* animated */);
         }
     }
 
@@ -138,35 +204,33 @@
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
         int height = MeasureSpec.getSize(heightMeasureSpec);
-
-        // Add corner radius here so that the menu extends behind the rounded corners of TaskView.
-        int menuViewHeight = Math.min((int) (mMenuHeight + mCornerRadius), height);
-        measureChild(mMenuView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight,
+        int menuViewHeight = Math.min(mCaptionHeight, height);
+        measureChild(mHandleView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight,
                 MeasureSpec.getMode(heightMeasureSpec)));
 
         if (mTaskView != null) {
-            int taskViewHeight = height - menuViewHeight;
-            measureChild(mTaskView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(taskViewHeight,
+            measureChild(mTaskView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(height,
                     MeasureSpec.getMode(heightMeasureSpec)));
         }
     }
 
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        // Drag handle above
-        final int dragHandleBottom = t + mMenuView.getMeasuredHeight();
-        mMenuView.layout(l, t, r, dragHandleBottom);
+        super.onLayout(changed, l, t, r, b);
+        final int captionBottom = t + mCaptionHeight;
         if (mTaskView != null) {
-            // Subtract radius so that the menu extends behind the rounded corners of TaskView.
-            mTaskView.layout(l, (int) (dragHandleBottom - mCornerRadius), r,
-                    dragHandleBottom + mTaskView.getMeasuredHeight());
+            mTaskView.layout(l, t, r,
+                    t + mTaskView.getMeasuredHeight());
+            mTaskView.setCaptionInsets(Insets.of(0, mCaptionHeight, 0, 0));
         }
+        // Handle draws on top of task view in the caption area.
+        mHandleView.layout(l, t, r, captionBottom);
     }
 
     @Override
     public void onTaskCreated() {
         setContentVisibility(true);
-        updateMenuColor();
+        updateHandleColor(false /* animated */);
     }
 
     @Override
@@ -187,11 +251,13 @@
             }
             mBubbleTaskViewHelper.cleanUpTaskView();
         }
+        mMenuViewController.hideMenu(false /* animated */);
     }
 
-    /** Updates the bubble shown in this task view. */
+    /** Updates the bubble shown in the expanded view. */
     public void update(Bubble bubble) {
         mBubbleTaskViewHelper.update(bubble);
+        mMenuViewController.updateMenu(bubble);
     }
 
     /** The task id of the activity shown in the task view, if it exists. */
@@ -199,12 +265,33 @@
         return mBubbleTaskViewHelper != null ? mBubbleTaskViewHelper.getTaskId() : INVALID_TASK_ID;
     }
 
+    /** Sets layer bounds supplier used for obscured touchable region of task view */
+    void setLayerBoundsSupplier(@Nullable Supplier<Rect> supplier) {
+        mLayerBoundsSupplier = supplier;
+    }
+
+    /** Sets the function to call to un-bubble the given conversation. */
+    public void setUnBubbleConversationCallback(
+            @Nullable Consumer<String> unBubbleConversationCallback) {
+        mUnBubbleConversationCallback = unBubbleConversationCallback;
+    }
+
     /**
      * Call when the location or size of the view has changed to update TaskView.
      */
     public void updateLocation() {
-        if (mTaskView == null) return;
-        mTaskView.onLocationChanged();
+        if (mTaskView != null) {
+            mTaskView.onLocationChanged();
+        }
+    }
+
+    /** Shows the expanded view for the overflow if it exists. */
+    void maybeShowOverflow() {
+        if (mOverflowView != null) {
+            // post this to the looper so that the view has a chance to be laid out before it can
+            // calculate row and column sizes correctly.
+            post(() -> mOverflowView.show());
+        }
     }
 
     /** Sets the alpha of the task view. */
@@ -218,17 +305,21 @@
         }
     }
 
-    /** Updates the menu bar to be the status bar color specified by the app. */
-    private void updateMenuColor() {
-        if (mTaskView == null) return;
-        ActivityManager.RunningTaskInfo info = mTaskView.getTaskInfo();
-        final int taskBgColor = info.taskDescription.getStatusBarColor();
-        final int color = Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb();
-        if (color != -1) {
-            mMenuView.setBackgroundColor(color);
-        } else {
-            mMenuView.setBackgroundColor(mBackgroundColor);
+    /**
+     * Updates the handle color based on the task view status bar or background color; if those
+     * are transparent it defaults to the background color pulled from system theme attributes.
+     */
+    private void updateHandleColor(boolean animated) {
+        if (mTaskView == null || mTaskView.getTaskInfo() == null) return;
+        int color = mBackgroundColor;
+        ActivityManager.TaskDescription taskDescription = mTaskView.getTaskInfo().taskDescription;
+        if (taskDescription.getStatusBarColor() != Color.TRANSPARENT) {
+            color = taskDescription.getStatusBarColor();
+        } else if (taskDescription.getBackgroundColor() != Color.TRANSPARENT) {
+            color = taskDescription.getBackgroundColor();
         }
+        final boolean isRegionDark = Color.luminance(color) <= 0.5;
+        mHandleView.updateHandleColor(isRegionDark, animated);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java
new file mode 100644
index 0000000..2b7a070
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java
@@ -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.wm.shell.bubbles.bar;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Outline;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+
+import androidx.annotation.ColorInt;
+import androidx.core.content.ContextCompat;
+
+import com.android.wm.shell.R;
+
+/**
+ * Handle view to show at the top of a bubble bar expanded view.
+ */
+public class BubbleBarHandleView extends View {
+    private static final long COLOR_CHANGE_DURATION = 120;
+
+    // The handle view is currently rendered as 3 evenly spaced dots.
+    private int mDotSize;
+    private int mDotSpacing;
+    // Path used to draw the dots
+    private final Path mPath = new Path();
+
+    private @ColorInt int mHandleLightColor;
+    private @ColorInt int mHandleDarkColor;
+    private @Nullable ObjectAnimator mColorChangeAnim;
+
+    public BubbleBarHandleView(Context context) {
+        this(context, null /* attrs */);
+    }
+
+    public BubbleBarHandleView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0 /* defStyleAttr */);
+    }
+
+    public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
+    }
+
+    public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mDotSize = getResources().getDimensionPixelSize(
+                R.dimen.bubble_bar_expanded_view_caption_dot_size);
+        mDotSpacing = getResources().getDimensionPixelSize(
+                R.dimen.bubble_bar_expanded_view_caption_dot_spacing);
+        mHandleLightColor = ContextCompat.getColor(getContext(),
+                R.color.bubble_bar_expanded_view_handle_light);
+        mHandleDarkColor = ContextCompat.getColor(getContext(),
+                R.color.bubble_bar_expanded_view_handle_dark);
+
+        setClipToOutline(true);
+        setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                final int handleCenterX = view.getWidth() / 2;
+                final int handleCenterY = view.getHeight() / 2;
+                final int handleTotalWidth = mDotSize * 3 + mDotSpacing * 2;
+                final int handleLeft = handleCenterX - handleTotalWidth / 2;
+                final int handleTop = handleCenterY - mDotSize / 2;
+                final int handleBottom = handleTop + mDotSize;
+                RectF dot1 = new RectF(
+                        handleLeft, handleTop,
+                        handleLeft + mDotSize, handleBottom);
+                RectF dot2 = new RectF(
+                        dot1.right + mDotSpacing, handleTop,
+                        dot1.right + mDotSpacing + mDotSize, handleBottom
+                );
+                RectF dot3 = new RectF(
+                        dot2.right + mDotSpacing, handleTop,
+                        dot2.right + mDotSpacing + mDotSize, handleBottom
+                );
+                mPath.reset();
+                mPath.addOval(dot1, Path.Direction.CW);
+                mPath.addOval(dot2, Path.Direction.CW);
+                mPath.addOval(dot3, Path.Direction.CW);
+                outline.setPath(mPath);
+            }
+        });
+    }
+
+    /**
+     * Updates the handle color.
+     *
+     * @param isRegionDark Whether the background behind the handle is dark, and thus the handle
+     *                     should be light (and vice versa).
+     * @param animated      Whether to animate the change, or apply it immediately.
+     */
+    public void updateHandleColor(boolean isRegionDark, boolean animated) {
+        int newColor = isRegionDark ? mHandleLightColor : mHandleDarkColor;
+        if (mColorChangeAnim != null) {
+            mColorChangeAnim.cancel();
+        }
+        if (animated) {
+            mColorChangeAnim = ObjectAnimator.ofArgb(this, "backgroundColor", newColor);
+            mColorChangeAnim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mColorChangeAnim = null;
+                }
+            });
+            mColorChangeAnim.setDuration(COLOR_CHANGE_DURATION);
+            mColorChangeAnim.start();
+        } else {
+            setBackgroundColor(newColor);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index b1a725b..8ead18b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -24,14 +24,18 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.graphics.drawable.ColorDrawable;
+import android.view.TouchDelegate;
 import android.view.View;
 import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
 
 import com.android.wm.shell.bubbles.BubbleController;
+import com.android.wm.shell.bubbles.BubbleOverflow;
 import com.android.wm.shell.bubbles.BubblePositioner;
 import com.android.wm.shell.bubbles.BubbleViewProvider;
 
+import java.util.function.Consumer;
+
 /**
  * Similar to {@link com.android.wm.shell.bubbles.BubbleStackView}, this view is added to window
  * manager to display bubbles. However, it is only used when bubbles are being displayed in
@@ -53,6 +57,7 @@
     @Nullable
     private BubbleViewProvider mExpandedBubble;
     private BubbleBarExpandedView mExpandedView;
+    private @Nullable Consumer<String> mUnBubbleConversationCallback;
 
     // TODO(b/273310265) - currently the view is always on the right, need to update for RTL.
     /** Whether the expanded view is displaying on the left of the screen or not. */
@@ -64,6 +69,10 @@
     private final Region mTouchableRegion = new Region();
     private final Rect mTempRect = new Rect();
 
+    // Used to ensure touch target size for the menu shown on a bubble expanded view
+    private TouchDelegate mHandleTouchDelegate;
+    private final Rect mHandleTouchBounds = new Rect();
+
     public BubbleBarLayerView(Context context, BubbleController controller) {
         super(context);
         mBubbleController = controller;
@@ -143,15 +152,34 @@
         if (mExpandedView == null) {
             mExpandedBubble = b;
             mExpandedView = expandedView;
-            final int width = mPositioner.getExpandedViewWidthForBubbleBar();
-            final int height = mPositioner.getExpandedViewHeightForBubbleBar();
+            boolean isOverflowExpanded = b.getKey().equals(BubbleOverflow.KEY);
+            final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded);
+            final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded);
             mExpandedView.setVisibility(GONE);
+            mExpandedView.setUnBubbleConversationCallback(mUnBubbleConversationCallback);
+            mExpandedView.setLayerBoundsSupplier(() -> new Rect(0, 0, getWidth(), getHeight()));
+            mExpandedView.setUnBubbleConversationCallback(bubbleKey -> {
+                if (mUnBubbleConversationCallback != null) {
+                    mUnBubbleConversationCallback.accept(bubbleKey);
+                }
+            });
+            mExpandedView.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height);
             addView(mExpandedView, new FrameLayout.LayoutParams(width, height));
         }
 
         mIsExpanded = true;
         mBubbleController.getSysuiProxy().onStackExpandChanged(true);
-        mAnimationHelper.animateExpansion(mExpandedBubble);
+        mAnimationHelper.animateExpansion(mExpandedBubble, () -> {
+            if (mExpandedView == null) return;
+            // Touch delegate for the menu
+            BubbleBarHandleView view = mExpandedView.getHandleView();
+            view.getBoundsOnScreen(mHandleTouchBounds);
+            mHandleTouchBounds.top -= mPositioner.getBubblePaddingTop();
+            mHandleTouchDelegate = new TouchDelegate(mHandleTouchBounds,
+                    mExpandedView.getHandleView());
+            setTouchDelegate(mHandleTouchDelegate);
+        });
+
         showScrim(true);
     }
 
@@ -162,15 +190,23 @@
         mAnimationHelper.animateCollapse(() -> removeView(viewToRemove));
         mBubbleController.getSysuiProxy().onStackExpandChanged(false);
         mExpandedView = null;
+        setTouchDelegate(null);
         showScrim(false);
     }
 
+    /** Sets the function to call to un-bubble the given conversation. */
+    public void setUnBubbleConversationCallback(
+            @Nullable Consumer<String> unBubbleConversationCallback) {
+        mUnBubbleConversationCallback = unBubbleConversationCallback;
+    }
+
     /** Updates the expanded view size and position. */
     private void updateExpandedView() {
         if (mExpandedView == null) return;
+        boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY);
         final int padding = mPositioner.getBubbleBarExpandedViewPadding();
-        final int width = mPositioner.getExpandedViewWidthForBubbleBar();
-        final int height = mPositioner.getExpandedViewHeightForBubbleBar();
+        final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded);
+        final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded);
         FrameLayout.LayoutParams lp = (LayoutParams) mExpandedView.getLayoutParams();
         lp.width = width;
         lp.height = height;
@@ -180,7 +216,7 @@
         } else {
             mExpandedView.setX(mPositioner.getAvailableRect().width() - width - padding);
         }
-        mExpandedView.setY(mPositioner.getInsets().top + padding);
+        mExpandedView.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height);
         mExpandedView.updateLocation();
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java
new file mode 100644
index 0000000..00b9777
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java
@@ -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 this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.bubbles.bar;
+
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.Icon;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.wm.shell.R;
+
+/**
+ * Bubble bar expanded view menu item view to display menu action details
+ */
+public class BubbleBarMenuItemView extends LinearLayout {
+    private ImageView mImageView;
+    private TextView mTextView;
+
+    public BubbleBarMenuItemView(Context context) {
+        this(context, null /* attrs */);
+    }
+
+    public BubbleBarMenuItemView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0 /* defStyleAttr */);
+    }
+
+    public BubbleBarMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
+    }
+
+    public BubbleBarMenuItemView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mImageView = findViewById(R.id.bubble_bar_menu_item_icon);
+        mTextView = findViewById(R.id.bubble_bar_menu_item_title);
+    }
+
+    /**
+     * Update menu item with the details and tint color
+     */
+    void update(Icon icon, String title, @ColorInt int tint) {
+        if (tint == Color.TRANSPARENT) {
+            final TypedArray typedArray = getContext().obtainStyledAttributes(
+                    new int[]{android.R.attr.textColorPrimary});
+            mTextView.setTextColor(typedArray.getColor(0, Color.BLACK));
+        } else {
+            icon.setTint(tint);
+            mTextView.setTextColor(tint);
+        }
+
+        mImageView.setImageIcon(icon);
+        mTextView.setText(title);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
new file mode 100644
index 0000000..211fe0d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.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.wm.shell.bubbles.bar;
+
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.Icon;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.bubbles.Bubble;
+
+import java.util.ArrayList;
+
+/**
+ * Bubble bar expanded view menu
+ */
+public class BubbleBarMenuView extends LinearLayout {
+    private ViewGroup mBubbleSectionView;
+    private ViewGroup mActionsSectionView;
+    private ImageView mBubbleIconView;
+    private TextView mBubbleTitleView;
+
+    public BubbleBarMenuView(Context context) {
+        this(context, null /* attrs */);
+    }
+
+    public BubbleBarMenuView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0 /* defStyleAttr */);
+    }
+
+    public BubbleBarMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
+    }
+
+    public BubbleBarMenuView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mBubbleSectionView = findViewById(R.id.bubble_bar_manage_menu_bubble_section);
+        mActionsSectionView = findViewById(R.id.bubble_bar_manage_menu_actions_section);
+        mBubbleIconView = findViewById(R.id.bubble_bar_manage_menu_bubble_icon);
+        mBubbleTitleView = findViewById(R.id.bubble_bar_manage_menu_bubble_title);
+    }
+
+    /** Update menu details with bubble info */
+    void updateInfo(Bubble bubble) {
+        if (bubble.getIcon() != null) {
+            mBubbleIconView.setImageIcon(bubble.getIcon());
+        } else {
+            mBubbleIconView.setImageBitmap(bubble.getBubbleIcon());
+        }
+        mBubbleTitleView.setText(bubble.getTitle());
+    }
+
+    /**
+     * Update menu action items views
+     * @param actions used to populate menu item views
+     */
+    void updateActions(ArrayList<MenuAction> actions) {
+        mActionsSectionView.removeAllViews();
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+
+        for (MenuAction action : actions) {
+            BubbleBarMenuItemView itemView = (BubbleBarMenuItemView) inflater.inflate(
+                    R.layout.bubble_bar_menu_item, mActionsSectionView, false);
+            itemView.update(action.mIcon, action.mTitle, action.mTint);
+            itemView.setOnClickListener(action.mOnClick);
+            mActionsSectionView.addView(itemView);
+        }
+    }
+
+    /** Sets on close menu listener */
+    void setOnCloseListener(Runnable onClose) {
+        mBubbleSectionView.setOnClickListener(view -> {
+            onClose.run();
+        });
+    }
+
+    /**
+     * Overridden to proxy to section views alpha.
+     * @implNote
+     * If animate alpha on the parent (menu container) view, section view shadows get distorted.
+     * To prevent distortion and artifacts alpha changes applied directly on the section views.
+     */
+    @Override
+    public void setAlpha(float alpha) {
+        mBubbleSectionView.setAlpha(alpha);
+        mActionsSectionView.setAlpha(alpha);
+    }
+
+    /**
+     * Overridden to proxy section view alpha value.
+     * @implNote
+     * The assumption is that both section views have the same alpha value
+     */
+    @Override
+    public float getAlpha() {
+        return mBubbleSectionView.getAlpha();
+    }
+
+    /**
+     * Menu action details used to create menu items
+     */
+    static class MenuAction {
+        private Icon mIcon;
+        private @ColorInt int mTint;
+        private String mTitle;
+        private OnClickListener mOnClick;
+
+        MenuAction(Icon icon, String title, OnClickListener onClick) {
+            this(icon, title, Color.TRANSPARENT, onClick);
+        }
+
+        MenuAction(Icon icon, String title, @ColorInt int tint, OnClickListener onClick) {
+            this.mIcon = icon;
+            this.mTitle = title;
+            this.mTint = tint;
+            this.mOnClick = onClick;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
new file mode 100644
index 0000000..8be140c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.bubbles.bar;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Icon;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.core.content.ContextCompat;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.animation.PhysicsAnimator;
+import com.android.wm.shell.bubbles.Bubble;
+
+import java.util.ArrayList;
+
+/**
+ * Manages bubble bar expanded view menu presentation and animations
+ */
+class BubbleBarMenuViewController {
+    private static final float MENU_INITIAL_SCALE = 0.5f;
+    private final Context mContext;
+    private final ViewGroup mRootView;
+    private @Nullable Listener mListener;
+    private @Nullable Bubble mBubble;
+    private @Nullable BubbleBarMenuView mMenuView;
+    /** A transparent view used to intercept touches to collapse menu when presented */
+    private @Nullable View mScrimView;
+    private @Nullable PhysicsAnimator<BubbleBarMenuView> mMenuAnimator;
+    private PhysicsAnimator.SpringConfig mMenuSpringConfig;
+
+    BubbleBarMenuViewController(Context context, ViewGroup rootView) {
+        mContext = context;
+        mRootView = rootView;
+        mMenuSpringConfig = new PhysicsAnimator.SpringConfig(
+                SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+    }
+
+    /** Sets menu actions listener */
+    void setListener(@Nullable Listener listener) {
+        mListener = listener;
+    }
+
+    /** Update menu with bubble */
+    void updateMenu(@NonNull Bubble bubble) {
+        mBubble = bubble;
+    }
+
+    /**
+     * Show bubble bar expanded view menu
+     * @param animated if should animate transition
+     */
+    void showMenu(boolean animated) {
+        if (mMenuView == null || mScrimView == null) {
+            setupMenu();
+        }
+        cancelAnimations();
+        mMenuView.setVisibility(View.VISIBLE);
+        mScrimView.setVisibility(View.VISIBLE);
+        Runnable endActions = () -> {
+            mMenuView.getChildAt(0).requestAccessibilityFocus();
+            if (mListener != null) {
+                mListener.onMenuVisibilityChanged(true /* isShown */);
+            }
+        };
+        if (animated) {
+            animateTransition(true /* show */, endActions);
+        } else {
+            endActions.run();
+        }
+    }
+
+    /**
+     * Hide bubble bar expanded view menu
+     * @param animated if should animate transition
+     */
+    void hideMenu(boolean animated) {
+        if (mMenuView == null || mScrimView == null) return;
+        cancelAnimations();
+        Runnable endActions = () -> {
+            mMenuView.setVisibility(View.GONE);
+            mScrimView.setVisibility(View.GONE);
+            if (mListener != null) {
+                mListener.onMenuVisibilityChanged(false /* isShown */);
+            }
+        };
+        if (animated) {
+            animateTransition(false /* show */, endActions);
+        } else {
+            endActions.run();
+        }
+    }
+
+    /**
+     * Animate show/hide menu transition
+     * @param show if should show or hide the menu
+     * @param endActions will be called when animation ends
+     */
+    private void animateTransition(boolean show, Runnable endActions) {
+        if (mMenuView == null) return;
+        mMenuAnimator = PhysicsAnimator.getInstance(mMenuView);
+        mMenuAnimator.setDefaultSpringConfig(mMenuSpringConfig);
+        mMenuAnimator
+                .spring(DynamicAnimation.ALPHA, show ? 1f : 0f)
+                .spring(DynamicAnimation.SCALE_Y, show ? 1f : MENU_INITIAL_SCALE)
+                .withEndActions(() -> {
+                    mMenuAnimator = null;
+                    endActions.run();
+                })
+                .start();
+    }
+
+    /** Cancel running animations */
+    private void cancelAnimations() {
+        if (mMenuAnimator != null) {
+            mMenuAnimator.cancel();
+            mMenuAnimator = null;
+        }
+    }
+
+    /** Sets up and inflate menu views */
+    private void setupMenu() {
+        // Menu view setup
+        mMenuView = (BubbleBarMenuView) LayoutInflater.from(mContext).inflate(
+                R.layout.bubble_bar_menu_view, mRootView, false);
+        mMenuView.setAlpha(0f);
+        mMenuView.setPivotY(0f);
+        mMenuView.setScaleY(MENU_INITIAL_SCALE);
+        mMenuView.setOnCloseListener(() -> hideMenu(true  /* animated */));
+        if (mBubble != null) {
+            mMenuView.updateInfo(mBubble);
+            mMenuView.updateActions(createMenuActions(mBubble));
+        }
+        // Scrim view setup
+        mScrimView = new View(mContext);
+        mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+        mScrimView.setOnClickListener(view -> hideMenu(true  /* animated */));
+        // Attach to root view
+        mRootView.addView(mScrimView);
+        mRootView.addView(mMenuView);
+    }
+
+    /**
+     * Creates menu actions to populate menu view
+     * @param bubble used to create actions depending on bubble type
+     */
+    private ArrayList<BubbleBarMenuView.MenuAction> createMenuActions(Bubble bubble) {
+        ArrayList<BubbleBarMenuView.MenuAction> menuActions = new ArrayList<>();
+        Resources resources = mContext.getResources();
+
+        if (bubble.isConversation()) {
+            // Don't bubble conversation action
+            menuActions.add(new BubbleBarMenuView.MenuAction(
+                    Icon.createWithResource(mContext, R.drawable.bubble_ic_stop_bubble),
+                    resources.getString(R.string.bubbles_dont_bubble_conversation),
+                    view -> {
+                        hideMenu(true /* animated */);
+                        if (mListener != null) {
+                            mListener.onUnBubbleConversation(bubble);
+                        }
+                    }
+            ));
+            // Open settings action
+            Icon appIcon = bubble.getRawAppBadge() != null ? Icon.createWithBitmap(
+                    bubble.getRawAppBadge()) : null;
+            menuActions.add(new BubbleBarMenuView.MenuAction(
+                    appIcon,
+                    resources.getString(R.string.bubbles_app_settings, bubble.getAppName()),
+                    view -> {
+                        hideMenu(true /* animated */);
+                        if (mListener != null) {
+                            mListener.onOpenAppSettings(bubble);
+                        }
+                    }
+            ));
+        }
+
+        // Dismiss bubble action
+        menuActions.add(new BubbleBarMenuView.MenuAction(
+                Icon.createWithResource(resources, R.drawable.ic_remove_no_shadow),
+                resources.getString(R.string.bubble_dismiss_text),
+                ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_menu_close),
+                view -> {
+                    hideMenu(true /* animated */);
+                    if (mListener != null) {
+                        mListener.onDismissBubble(bubble);
+                    }
+                }
+        ));
+
+        return menuActions;
+    }
+
+    /**
+     * Bubble bar expanded view menu actions listener
+     */
+    interface Listener {
+        /**
+         * Called when manage menu is shown/hidden
+         * If animated will be called when animation ends
+         */
+        void onMenuVisibilityChanged(boolean visible);
+
+        /**
+         * Un-bubbles conversation and removes the bubble from the stack
+         * This conversation will not be bubbled with new messages
+         * @see com.android.wm.shell.bubbles.BubbleController
+         */
+        void onUnBubbleConversation(Bubble bubble);
+
+        /**
+         * Launches app notification bubble settings for the bubble with intent created in:
+         * {@code Bubble.getSettingsIntent}
+         */
+        void onOpenAppSettings(Bubble bubble);
+
+        /**
+         * Dismiss bubble and remove it from the bubble stack
+         */
+        void onDismissBubble(Bubble bubble);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java
deleted file mode 100644
index 9ee8a9d..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.bubbles.bar;
-
-import android.content.Context;
-import android.view.Gravity;
-import android.widget.LinearLayout;
-
-/**
- * Handle / menu view to show at the top of a bubble bar expanded view.
- */
-public class HandleView extends LinearLayout {
-
-    // TODO(b/273307221): implement the manage menu in this view.
-    public HandleView(Context context) {
-        super(context);
-        setOrientation(LinearLayout.HORIZONTAL);
-        setGravity(Gravity.CENTER);
-    }
-
-    /**
-     * The menu extends past the top of the TaskView because of the rounded corners. This means
-     * to center content in the menu we must subtract the radius (i.e. the amount of space covered
-     * by TaskView).
-     */
-    public void setCornerRadius(float radius) {
-        setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (int) radius);
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/BubbleProperties.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/BubbleProperties.kt
new file mode 100644
index 0000000..85aaa8e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/BubbleProperties.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.wm.shell.bubbles.properties
+
+/**
+ * An interface for exposing bubble properties via flags which can be controlled easily in tests.
+ */
+interface BubbleProperties {
+    /**
+     * Whether bubble bar is enabled.
+     *
+     * When this is `true`, depending on additional factors, such as screen size and taskbar state,
+     * bubbles will be displayed in the bubble bar instead of floating.
+     *
+     * When this is `false`, bubbles will be floating.
+     */
+    val isBubbleBarEnabled: Boolean
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt
new file mode 100644
index 0000000..9d8b9a6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.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.wm.shell.bubbles.properties
+
+import android.os.SystemProperties
+
+/** Provides bubble properties in production. */
+object ProdBubbleProperties : BubbleProperties {
+
+    // TODO(b/256873975) Should use proper flag when available to shell/launcher
+    override val isBubbleBarEnabled =
+        SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java
deleted file mode 100644
index e0c782d..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DismissCircleView.java
+++ /dev/null
@@ -1,64 +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.wm.shell.common;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.view.Gravity;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-
-import com.android.wm.shell.R;
-
-/**
- * Circular view with a semitransparent, circular background with an 'X' inside it.
- *
- * This is used by both Bubbles and PIP as the dismiss target.
- */
-public class DismissCircleView extends FrameLayout {
-
-    private final ImageView mIconView = new ImageView(getContext());
-
-    public DismissCircleView(Context context) {
-        super(context);
-        final Resources res = getResources();
-
-        setBackground(res.getDrawable(R.drawable.dismiss_circle_background));
-
-        mIconView.setImageDrawable(res.getDrawable(R.drawable.pip_ic_close_white));
-        addView(mIconView);
-
-        setViewSizes();
-    }
-
-    @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-        final Resources res = getResources();
-        setBackground(res.getDrawable(R.drawable.dismiss_circle_background));
-        setViewSizes();
-    }
-
-    /** Retrieves the current dimensions for the icon and circle and applies them. */
-    private void setViewSizes() {
-        final Resources res = getResources();
-        final int iconSize = res.getDimensionPixelSize(R.dimen.dismiss_target_x_size);
-        mIconView.setLayoutParams(
-                new FrameLayout.LayoutParams(iconSize, iconSize, Gravity.CENTER));
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/LaunchAdjacentController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/LaunchAdjacentController.kt
new file mode 100644
index 0000000..81592c3
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/LaunchAdjacentController.kt
@@ -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.wm.shell.common
+
+import android.window.WindowContainerToken
+import android.window.WindowContainerTransaction
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG
+import com.android.wm.shell.util.KtProtoLog
+
+/**
+ * Controller to manage behavior of activities launched with
+ * [android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT].
+ */
+class LaunchAdjacentController(private val syncQueue: SyncTransactionQueue) {
+
+    /** Allows to temporarily disable launch adjacent handling */
+    var launchAdjacentEnabled: Boolean = true
+        set(value) {
+            if (field != value) {
+                KtProtoLog.d(WM_SHELL_TASK_ORG, "set launch adjacent flag root enabled=%b", value)
+                field = value
+                container?.let { c ->
+                    if (value) {
+                        enableContainer(c)
+                    } else {
+                        disableContainer((c))
+                    }
+                }
+            }
+        }
+    private var container: WindowContainerToken? = null
+
+    /**
+     * Set [container] as the new launch adjacent flag root container.
+     *
+     * If launch adjacent handling is disabled through [setLaunchAdjacentEnabled], won't set the
+     * container until after it is enabled again.
+     *
+     * @see WindowContainerTransaction.setLaunchAdjacentFlagRoot
+     */
+    fun setLaunchAdjacentRoot(container: WindowContainerToken) {
+        KtProtoLog.d(WM_SHELL_TASK_ORG, "set new launch adjacent flag root container")
+        this.container = container
+        if (launchAdjacentEnabled) {
+            enableContainer(container)
+        }
+    }
+
+    /**
+     * Clear a container previously set through [setLaunchAdjacentRoot].
+     *
+     * Always clears the container, regardless of [launchAdjacentEnabled] value.
+     *
+     * @see WindowContainerTransaction.clearLaunchAdjacentFlagRoot
+     */
+    fun clearLaunchAdjacentRoot() {
+        KtProtoLog.d(WM_SHELL_TASK_ORG, "clear launch adjacent flag root container")
+        container?.let {
+            disableContainer(it)
+            container = null
+        }
+    }
+
+    private fun enableContainer(container: WindowContainerToken) {
+        KtProtoLog.v(WM_SHELL_TASK_ORG, "enable launch adjacent flag root container")
+        val wct = WindowContainerTransaction()
+        wct.setLaunchAdjacentFlagRoot(container)
+        syncQueue.queue(wct)
+    }
+
+    private fun disableContainer(container: WindowContainerToken) {
+        KtProtoLog.v(WM_SHELL_TASK_ORG, "disable launch adjacent flag root container")
+        val wct = WindowContainerTransaction()
+        wct.clearLaunchAdjacentFlagRoot(container)
+        syncQueue.queue(wct)
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/common/OWNERS
new file mode 100644
index 0000000..7af0389
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/OWNERS
@@ -0,0 +1 @@
+madym@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissCircleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissCircleView.java
new file mode 100644
index 0000000..7c5bb21
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissCircleView.java
@@ -0,0 +1,76 @@
+/*
+ * 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.wm.shell.common.bubbles;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.view.Gravity;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import androidx.annotation.DimenRes;
+import androidx.annotation.DrawableRes;
+import androidx.core.content.ContextCompat;
+
+/**
+ * Circular view with a semitransparent, circular background with an 'X' inside it.
+ *
+ * This is used by both Bubbles and PIP as the dismiss target.
+ */
+public class DismissCircleView extends FrameLayout {
+    @DrawableRes int mBackgroundResId;
+    @DimenRes int mIconSizeResId;
+
+    private final ImageView mIconView = new ImageView(getContext());
+
+    public DismissCircleView(Context context) {
+        super(context);
+        addView(mIconView);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        setBackground(ContextCompat.getDrawable(getContext(), mBackgroundResId));
+        setViewSizes();
+    }
+
+    /**
+     * Sets up view with the provided resource ids.
+     * Decouples resource dependency in order to be used externally (e.g. Launcher)
+     *
+     * @param backgroundResId drawable resource id of the circle background
+     * @param iconResId drawable resource id of the icon for the dismiss view
+     * @param iconSizeResId dimen resource id of the icon size
+     */
+    public void setup(@DrawableRes int backgroundResId, @DrawableRes int iconResId,
+            @DimenRes int iconSizeResId) {
+        mBackgroundResId = backgroundResId;
+        mIconSizeResId = iconSizeResId;
+
+        setBackground(ContextCompat.getDrawable(getContext(), backgroundResId));
+        mIconView.setImageDrawable(ContextCompat.getDrawable(getContext(), iconResId));
+        setViewSizes();
+    }
+
+    /** Retrieves the current dimensions for the icon and circle and applies them. */
+    private void setViewSizes() {
+        final int iconSize = getResources().getDimensionPixelSize(mIconSizeResId);
+        mIconView.setLayoutParams(
+                new FrameLayout.LayoutParams(iconSize, iconSize, Gravity.CENTER));
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt
new file mode 100644
index 0000000..d275a0b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt
@@ -0,0 +1,222 @@
+/*
+ * 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.wm.shell.common.bubbles
+
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.GradientDrawable
+import android.util.IntProperty
+import android.util.Log
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowInsets
+import android.view.WindowManager
+import android.widget.FrameLayout
+import androidx.annotation.ColorRes
+import androidx.annotation.DimenRes
+import androidx.annotation.DrawableRes
+import androidx.core.content.ContextCompat
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY
+import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW
+import com.android.wm.shell.animation.PhysicsAnimator
+
+/**
+ * View that handles interactions between DismissCircleView and BubbleStackView.
+ *
+ * @note [setup] method should be called after initialisation
+ */
+class DismissView(context: Context) : FrameLayout(context) {
+    /**
+     * The configuration is used to provide module specific resource ids
+     *
+     * @see [setup] method
+     */
+    data class Config(
+            /** dimen resource id of the dismiss target circle view size */
+            @DimenRes val targetSizeResId: Int,
+            /** dimen resource id of the icon size in the dismiss target */
+            @DimenRes val iconSizeResId: Int,
+            /** dimen resource id of the bottom margin for the dismiss target */
+            @DimenRes var bottomMarginResId: Int,
+            /** dimen resource id of the height for dismiss area gradient */
+            @DimenRes val floatingGradientHeightResId: Int,
+            /** color resource id of the dismiss area gradient color */
+            @ColorRes val floatingGradientColorResId: Int,
+            /** drawable resource id of the dismiss target background */
+            @DrawableRes val backgroundResId: Int,
+            /** drawable resource id of the icon for the dismiss target */
+            @DrawableRes val iconResId: Int
+    )
+
+    companion object {
+        private const val SHOULD_SETUP =
+                "The view isn't ready. Should be called after `setup`"
+        private val TAG = DismissView::class.simpleName
+    }
+
+    var circle = DismissCircleView(context)
+    var isShowing = false
+    var config: Config? = null
+
+    private val animator = PhysicsAnimator.getInstance(circle)
+    private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY)
+    private val DISMISS_SCRIM_FADE_MS = 200L
+    private var wm: WindowManager =
+            context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+    private var gradientDrawable: GradientDrawable? = null
+
+    private val GRADIENT_ALPHA: IntProperty<GradientDrawable> =
+            object : IntProperty<GradientDrawable>("alpha") {
+        override fun setValue(d: GradientDrawable, percent: Int) {
+            d.alpha = percent
+        }
+        override fun get(d: GradientDrawable): Int {
+            return d.alpha
+        }
+    }
+
+    init {
+        setClipToPadding(false)
+        setClipChildren(false)
+        setVisibility(View.INVISIBLE)
+        addView(circle)
+    }
+
+    /**
+     * Sets up view with the provided resource ids.
+     *
+     * Decouples resource dependency in order to be used externally (e.g. Launcher). Usually called
+     * with default params in module specific extension:
+     * @see [DismissView.setup] in DismissViewExt.kt
+     */
+    fun setup(config: Config) {
+        this.config = config
+
+        // Setup layout
+        layoutParams = LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                resources.getDimensionPixelSize(config.floatingGradientHeightResId),
+                Gravity.BOTTOM)
+        updatePadding()
+
+        // Setup gradient
+        gradientDrawable = createGradient(color = config.floatingGradientColorResId)
+        setBackgroundDrawable(gradientDrawable)
+
+        // Setup DismissCircleView
+        circle.setup(config.backgroundResId, config.iconResId, config.iconSizeResId)
+        val targetSize: Int = resources.getDimensionPixelSize(config.targetSizeResId)
+        circle.layoutParams = LayoutParams(targetSize, targetSize,
+                Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)
+        // Initial position with circle offscreen so it's animated up
+        circle.translationY = resources.getDimensionPixelSize(config.floatingGradientHeightResId)
+                .toFloat()
+    }
+
+    /**
+     * Animates this view in.
+     */
+    fun show() {
+        if (isShowing) return
+        val gradientDrawable = checkExists(gradientDrawable) ?: return
+        isShowing = true
+        setVisibility(View.VISIBLE)
+        val alphaAnim = ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA,
+                gradientDrawable.alpha, 255)
+        alphaAnim.setDuration(DISMISS_SCRIM_FADE_MS)
+        alphaAnim.start()
+
+        animator.cancel()
+        animator
+            .spring(DynamicAnimation.TRANSLATION_Y, 0f, spring)
+            .start()
+    }
+
+    /**
+     * Animates this view out, as well as the circle that encircles the bubbles, if they
+     * were dragged into the target and encircled.
+     */
+    fun hide() {
+        if (!isShowing) return
+        val gradientDrawable = checkExists(gradientDrawable) ?: return
+        isShowing = false
+        val alphaAnim = ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA,
+                gradientDrawable.alpha, 0)
+        alphaAnim.setDuration(DISMISS_SCRIM_FADE_MS)
+        alphaAnim.start()
+        animator
+            .spring(DynamicAnimation.TRANSLATION_Y, height.toFloat(),
+                spring)
+            .withEndActions({ setVisibility(View.INVISIBLE) })
+            .start()
+    }
+
+    /**
+     * Cancels the animator for the dismiss target.
+     */
+    fun cancelAnimators() {
+        animator.cancel()
+    }
+
+    fun updateResources() {
+        val config = checkExists(config) ?: return
+        updatePadding()
+        layoutParams.height = resources.getDimensionPixelSize(config.floatingGradientHeightResId)
+        val targetSize = resources.getDimensionPixelSize(config.targetSizeResId)
+        circle.layoutParams.width = targetSize
+        circle.layoutParams.height = targetSize
+        circle.requestLayout()
+    }
+
+    private fun createGradient(@ColorRes color: Int): GradientDrawable {
+        val gradientColor = ContextCompat.getColor(context, color)
+        val alpha = 0.7f * 255
+        val gradientColorWithAlpha = Color.argb(alpha.toInt(),
+                Color.red(gradientColor),
+                Color.green(gradientColor),
+                Color.blue(gradientColor))
+        val gd = GradientDrawable(
+                GradientDrawable.Orientation.BOTTOM_TOP,
+                intArrayOf(gradientColorWithAlpha, Color.TRANSPARENT))
+        gd.setDither(true)
+        gd.setAlpha(0)
+        return gd
+    }
+
+    private fun updatePadding() {
+        val config = checkExists(config) ?: return
+        val insets: WindowInsets = wm.getCurrentWindowMetrics().getWindowInsets()
+        val navInset = insets.getInsetsIgnoringVisibility(
+                WindowInsets.Type.navigationBars())
+        setPadding(0, 0, 0, navInset.bottom +
+                resources.getDimensionPixelSize(config.bottomMarginResId))
+    }
+
+    /**
+     * Checks if the value is set up and exists, if not logs an exception.
+     * Used for convenient logging in case `setup` wasn't called before
+     *
+     * @return value provided as argument
+     */
+    private fun <T>checkExists(value: T?): T? {
+        if (value == null) Log.e(TAG, SHOULD_SETUP)
+        return value
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt
new file mode 100644
index 0000000..cc37bd3a4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt
@@ -0,0 +1,168 @@
+/*
+ * 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.wm.shell.common.bubbles
+
+import android.graphics.PointF
+import android.view.MotionEvent
+import android.view.VelocityTracker
+import android.view.View
+import android.view.ViewConfiguration
+import kotlin.math.hypot
+
+/**
+ * Listener which receives [onDown], [onMove], and [onUp] events, with relevant information about
+ * the coordinates of the touch and the view relative to the initial ACTION_DOWN event and the
+ * view's initial position.
+ */
+abstract class RelativeTouchListener : View.OnTouchListener {
+
+    /**
+     * Called when an ACTION_DOWN event is received for the given view.
+     *
+     * @return False if the object is not interested in MotionEvents at this time, or true if we
+     * should consume this event and subsequent events, and begin calling [onMove].
+     */
+    abstract fun onDown(v: View, ev: MotionEvent): Boolean
+
+    /**
+     * Called when an ACTION_MOVE event is received for the given view. This signals that the view
+     * is being dragged.
+     *
+     * @param viewInitialX The view's translationX value when this touch gesture started.
+     * @param viewInitialY The view's translationY value when this touch gesture started.
+     * @param dx Horizontal distance covered since the initial ACTION_DOWN event, in pixels.
+     * @param dy Vertical distance covered since the initial ACTION_DOWN event, in pixels.
+     */
+    abstract fun onMove(
+        v: View,
+        ev: MotionEvent,
+        viewInitialX: Float,
+        viewInitialY: Float,
+        dx: Float,
+        dy: Float
+    )
+
+    /**
+     * Called when an ACTION_UP event is received for the given view. This signals that a drag or
+     * fling gesture has completed.
+     *
+     * @param viewInitialX The view's translationX value when this touch gesture started.
+     * @param viewInitialY The view's translationY value when this touch gesture started.
+     * @param dx Horizontal distance covered, in pixels.
+     * @param dy Vertical distance covered, in pixels.
+     * @param velX The final horizontal velocity of the gesture, in pixels/second.
+     * @param velY The final vertical velocity of the gesture, in pixels/second.
+     */
+    abstract fun onUp(
+        v: View,
+        ev: MotionEvent,
+        viewInitialX: Float,
+        viewInitialY: Float,
+        dx: Float,
+        dy: Float,
+        velX: Float,
+        velY: Float
+    )
+
+    /** The raw coordinates of the last ACTION_DOWN event. */
+    private val touchDown = PointF()
+
+    /** The coordinates of the view, at the time of the last ACTION_DOWN event. */
+    private val viewPositionOnTouchDown = PointF()
+
+    private val velocityTracker = VelocityTracker.obtain()
+
+    private var touchSlop: Int = -1
+    private var movedEnough = false
+
+    private var performedLongClick = false
+
+    @Suppress("UNCHECKED_CAST")
+    override fun onTouch(v: View, ev: MotionEvent): Boolean {
+        addMovement(ev)
+
+        val dx = ev.rawX - touchDown.x
+        val dy = ev.rawY - touchDown.y
+
+        when (ev.action) {
+            MotionEvent.ACTION_DOWN -> {
+                if (!onDown(v, ev)) {
+                    return false
+                }
+
+                // Grab the touch slop, it might have changed if the config changed since the
+                // last gesture.
+                touchSlop = ViewConfiguration.get(v.context).scaledTouchSlop
+
+                touchDown.set(ev.rawX, ev.rawY)
+                viewPositionOnTouchDown.set(v.translationX, v.translationY)
+
+                performedLongClick = false
+                v.handler.postDelayed({
+                    if (v.isLongClickable) {
+                        performedLongClick = v.performLongClick()
+                    }
+                }, ViewConfiguration.getLongPressTimeout().toLong())
+            }
+
+            MotionEvent.ACTION_MOVE -> {
+                if (!movedEnough && hypot(dx, dy) > touchSlop && !performedLongClick) {
+                    movedEnough = true
+                    v.handler.removeCallbacksAndMessages(null)
+                }
+
+                if (movedEnough) {
+                    onMove(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy)
+                }
+            }
+
+            MotionEvent.ACTION_UP -> {
+                if (movedEnough) {
+                    velocityTracker.computeCurrentVelocity(1000 /* units */)
+                    onUp(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy,
+                            velocityTracker.xVelocity, velocityTracker.yVelocity)
+                } else if (!performedLongClick) {
+                    v.performClick()
+                } else {
+                    v.handler.removeCallbacksAndMessages(null)
+                }
+
+                velocityTracker.clear()
+                movedEnough = false
+            }
+
+            MotionEvent.ACTION_CANCEL -> {
+                v.handler.removeCallbacksAndMessages(null)
+                velocityTracker.clear()
+                movedEnough = false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Adds a movement to the velocity tracker using raw screen coordinates.
+     */
+    private fun addMovement(event: MotionEvent) {
+        val deltaX = event.rawX - event.x
+        val deltaY = event.rawY - event.y
+        event.offsetLocation(deltaX, deltaY)
+        velocityTracker.addMovement(event)
+        event.offsetLocation(-deltaX, -deltaY)
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
index c76937d..ec26800 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
@@ -76,6 +76,9 @@
     private int mCurrentHeight;
     private AnimatorSet mAnimator;
     private boolean mTouching;
+    private boolean mHovering;
+    private final int mHoveringWidth;
+    private final int mHoveringHeight;
 
     public DividerHandleView(Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
@@ -87,6 +90,8 @@
         mCurrentHeight = mHeight;
         mTouchingWidth = mWidth > mHeight ? mWidth / 2 : mWidth;
         mTouchingHeight = mHeight > mWidth ? mHeight / 2 : mHeight;
+        mHoveringWidth = mWidth > mHeight ? ((int) (mWidth * 1.5f)) : mWidth;
+        mHoveringHeight = mHeight > mWidth ? ((int) (mHeight * 1.5f)) : mHeight;
     }
 
     /** Sets touching state for this handle view. */
@@ -94,24 +99,32 @@
         if (touching == mTouching) {
             return;
         }
+        setInputState(touching, animate, mTouchingWidth, mTouchingHeight);
+        mTouching = touching;
+    }
+
+    /** Sets hovering state for this handle view. */
+    public void setHovering(boolean hovering, boolean animate) {
+        if (hovering == mHovering) {
+            return;
+        }
+        setInputState(hovering, animate, mHoveringWidth, mHoveringHeight);
+        mHovering = hovering;
+    }
+
+    private void setInputState(boolean stateOn, boolean animate, int stateWidth, int stateHeight) {
         if (mAnimator != null) {
             mAnimator.cancel();
             mAnimator = null;
         }
         if (!animate) {
-            if (touching) {
-                mCurrentWidth = mTouchingWidth;
-                mCurrentHeight = mTouchingHeight;
-            } else {
-                mCurrentWidth = mWidth;
-                mCurrentHeight = mHeight;
-            }
+            mCurrentWidth = stateOn ? stateWidth : mWidth;
+            mCurrentHeight = stateOn ? stateHeight : mHeight;
             invalidate();
         } else {
-            animateToTarget(touching ? mTouchingWidth : mWidth,
-                    touching ? mTouchingHeight : mHeight, touching);
+            animateToTarget(stateOn ? stateWidth : mWidth,
+                    stateOn ? stateHeight : mHeight, stateOn);
         }
-        mTouching = touching;
     }
 
     private void animateToTarget(int targetWidth, int targetHeight, boolean touching) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
index 69f0bad..2dbc444 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
@@ -17,14 +17,19 @@
 package com.android.wm.shell.common.split;
 
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
+import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
 import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
 
+import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CURSOR_HOVER_STATES_ENABLED;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.provider.DeviceConfig;
 import android.util.AttributeSet;
 import android.util.Property;
 import android.view.GestureDetector;
@@ -32,6 +37,7 @@
 import android.view.InsetsSource;
 import android.view.InsetsState;
 import android.view.MotionEvent;
+import android.view.PointerIcon;
 import android.view.SurfaceControlViewHost;
 import android.view.VelocityTracker;
 import android.view.View;
@@ -46,6 +52,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.DividerSnapAlgorithm;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
@@ -222,7 +229,7 @@
             for (int i = insetsState.sourceSize() - 1; i >= 0; i--) {
                 final InsetsSource source = insetsState.sourceAt(i);
                 if (source.getType() == WindowInsets.Type.navigationBars()
-                        && source.insetsRoundedCornerFrame()) {
+                        && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)) {
                     mTempRect.inset(source.calculateVisibleInsets(mTempRect));
                 }
             }
@@ -270,6 +277,12 @@
     }
 
     @Override
+    public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+        return PointerIcon.getSystemIcon(getContext(),
+                isLandscape() ? TYPE_HORIZONTAL_DOUBLE_ARROW : TYPE_VERTICAL_DOUBLE_ARROW);
+    }
+
+    @Override
     public boolean onTouch(View v, MotionEvent event) {
         if (mSplitLayout == null || !mInteractive) {
             return false;
@@ -371,6 +384,43 @@
         mViewHost.relayout(lp);
     }
 
+    @Override
+    public boolean onHoverEvent(MotionEvent event) {
+        if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, CURSOR_HOVER_STATES_ENABLED,
+                /* defaultValue = */ false)) {
+            return false;
+        }
+
+        if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
+            setHovering();
+            return true;
+        } else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
+            releaseHovering();
+            return true;
+        }
+        return false;
+    }
+
+    @VisibleForTesting
+    void setHovering() {
+        mHandle.setHovering(true, true);
+        mHandle.animate()
+                .setInterpolator(Interpolators.TOUCH_RESPONSE)
+                .setDuration(TOUCH_ANIMATION_DURATION)
+                .translationZ(mTouchElevation)
+                .start();
+    }
+
+    @VisibleForTesting
+    void releaseHovering() {
+        mHandle.setHovering(false, true);
+        mHandle.animate()
+                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
+                .translationZ(0)
+                .start();
+    }
+
     /**
      * Set divider should interactive to user or not.
      *
@@ -384,7 +434,7 @@
                 "Set divider bar %s from %s", interactive ? "interactive" : "non-interactive",
                 from);
         mInteractive = interactive;
-        if (!mInteractive && mMoving) {
+        if (!mInteractive && hideHandle && mMoving) {
             final int position = mSplitLayout.getDividePosition();
             mSplitLayout.flingDividePosition(
                     mLastDraggingPosition,
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 a9ccdf6..2b10377 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
@@ -323,7 +323,11 @@
             }
         }
         if (mShown) {
-            fadeOutDecor(()-> animFinishedCallback.accept(true));
+            fadeOutDecor(()-> {
+                if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
+                    animFinishedCallback.accept(true);
+                }
+            });
         } else {
             // Decor surface is hidden so release it directly.
             releaseDecor(t);
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 f70d3ae..e8fa638 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
@@ -593,9 +593,6 @@
     void flingDividePosition(int from, int to, int duration,
             @Nullable Runnable flingFinishedCallback) {
         if (from == to) {
-            // No animation run, still callback to stop resizing.
-            mSplitLayoutHandler.onLayoutSizeChanged(this);
-
             if (flingFinishedCallback != null) {
                 flingFinishedCallback.run();
             }
@@ -773,15 +770,13 @@
             ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) {
         boolean boundsChanged = false;
         if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) {
-            wct.setBounds(task1.token, mBounds1);
-            wct.setSmallestScreenWidthDp(task1.token, getSmallestWidthDp(mBounds1));
+            setTaskBounds(wct, task1, mBounds1);
             mWinBounds1.set(mBounds1);
             mWinToken1 = task1.token;
             boundsChanged = true;
         }
         if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) {
-            wct.setBounds(task2.token, mBounds2);
-            wct.setSmallestScreenWidthDp(task2.token, getSmallestWidthDp(mBounds2));
+            setTaskBounds(wct, task2, mBounds2);
             mWinBounds2.set(mBounds2);
             mWinToken2 = task2.token;
             boundsChanged = true;
@@ -789,6 +784,13 @@
         return boundsChanged;
     }
 
+    /** Set bounds to the {@link WindowContainerTransaction} for single task. */
+    public void setTaskBounds(WindowContainerTransaction wct,
+            ActivityManager.RunningTaskInfo task, Rect bounds) {
+        wct.setBounds(task.token, bounds);
+        wct.setSmallestScreenWidthDp(task.token, getSmallestWidthDp(bounds));
+    }
+
     private int getSmallestWidthDp(Rect bounds) {
         mTempRect.set(bounds);
         mTempRect.inset(getDisplayStableInsets(mContext));
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
index 0289da9..d7ea1c0 100644
--- 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
@@ -89,4 +89,9 @@
             int userId1, int userId2) {
         return (packageName1 != null && packageName1.equals(packageName2)) && (userId1 == userId2);
     }
+
+    /** Generates a common log message for split screen failures */
+    public static String splitFailureMessage(String caller, String reason) {
+        return "(" + caller + ") Splitscreen aborted: " + reason;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
index 12d51f5..47d58af 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
@@ -25,6 +25,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.LaunchAdjacentController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.SystemWindows;
@@ -86,13 +87,14 @@
             TransactionPool transactionPool,
             IconProvider iconProvider,
             Optional<RecentTasksController> recentTasks,
+            LaunchAdjacentController launchAdjacentController,
             @ShellMainThread ShellExecutor mainExecutor,
             Handler mainHandler,
             SystemWindows systemWindows) {
         return new TvSplitScreenController(context, shellInit, shellCommandHandler, shellController,
                 shellTaskOrganizer, syncQueue, rootTDAOrganizer, displayController,
                 displayImeController, displayInsetsController, dragAndDropController, transitions,
-                transactionPool, iconProvider, recentTasks, mainExecutor, mainHandler,
-                systemWindows);
+                transactionPool, iconProvider, recentTasks, launchAdjacentController, mainExecutor,
+                mainHandler, systemWindows);
     }
 }
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 c491fed..34a6e0a 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
@@ -46,6 +46,7 @@
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.DockStateReader;
 import com.android.wm.shell.common.FloatingContentCoordinator;
+import com.android.wm.shell.common.LaunchAdjacentController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.SystemWindows;
@@ -275,6 +276,13 @@
         return new WindowManagerShellWrapper(mainExecutor);
     }
 
+    @WMSingleton
+    @Provides
+    static LaunchAdjacentController provideLaunchAdjacentController(
+            SyncTransactionQueue syncQueue) {
+        return new LaunchAdjacentController(syncQueue);
+    }
+
     //
     // Back animation
     //
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 cff3172..20c3bd2 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
@@ -36,17 +36,21 @@
 import com.android.wm.shell.bubbles.BubbleDataRepository;
 import com.android.wm.shell.bubbles.BubbleLogger;
 import com.android.wm.shell.bubbles.BubblePositioner;
+import com.android.wm.shell.bubbles.properties.ProdBubbleProperties;
+import com.android.wm.shell.bubbles.storage.BubblePersistentRepository;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.FloatingContentCoordinator;
+import com.android.wm.shell.common.LaunchAdjacentController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.SystemWindows;
 import com.android.wm.shell.common.TabletopModeController;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.annotations.ShellAnimationThread;
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.desktopmode.DesktopModeController;
@@ -55,6 +59,7 @@
 import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;
 import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
+import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.freeform.FreeformComponents;
 import com.android.wm.shell.freeform.FreeformTaskListener;
@@ -180,11 +185,12 @@
             IWindowManager wmService) {
         return new BubbleController(context, shellInit, shellCommandHandler, shellController, data,
                 null /* synchronizer */, floatingContentCoordinator,
-                new BubbleDataRepository(context, launcherApps, mainExecutor),
+                new BubbleDataRepository(launcherApps, mainExecutor,
+                        new BubblePersistentRepository(context)),
                 statusBarService, windowManager, windowManagerShellWrapper, userManager,
                 launcherApps, logger, taskStackListener, organizer, positioner, displayController,
                 oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor,
-                taskViewTransitions, syncQueue, wmService);
+                taskViewTransitions, syncQueue, wmService, ProdBubbleProperties.INSTANCE);
     }
 
     //
@@ -202,8 +208,7 @@
             SyncTransactionQueue syncQueue,
             Transitions transitions,
             Optional<DesktopModeController> desktopModeController,
-            Optional<DesktopTasksController> desktopTasksController,
-            Optional<SplitScreenController> splitScreenController) {
+            Optional<DesktopTasksController> desktopTasksController) {
         if (DesktopModeStatus.isAnyEnabled()) {
             return new DesktopModeWindowDecorViewModel(
                     context,
@@ -214,16 +219,15 @@
                     syncQueue,
                     transitions,
                     desktopModeController,
-                    desktopTasksController,
-                    splitScreenController);
+                    desktopTasksController);
         }
         return new CaptionWindowDecorViewModel(
-                    context,
-                    mainHandler,
-                    mainChoreographer,
-                    taskOrganizer,
-                    displayController,
-                    syncQueue);
+                context,
+                mainHandler,
+                mainChoreographer,
+                taskOrganizer,
+                displayController,
+                syncQueue);
     }
 
     //
@@ -263,8 +267,13 @@
     static FreeformTaskTransitionHandler provideFreeformTaskTransitionHandler(
             ShellInit shellInit,
             Transitions transitions,
-            WindowDecorViewModel windowDecorViewModel) {
-        return new FreeformTaskTransitionHandler(shellInit, transitions, windowDecorViewModel);
+            Context context,
+            WindowDecorViewModel windowDecorViewModel,
+            DisplayController displayController,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellAnimationThread ShellExecutor animExecutor) {
+        return new FreeformTaskTransitionHandler(shellInit, transitions, context,
+                windowDecorViewModel, displayController, mainExecutor, animExecutor);
     }
 
     @WMSingleton
@@ -327,11 +336,14 @@
             TransactionPool transactionPool,
             IconProvider iconProvider,
             Optional<RecentTasksController> recentTasks,
+            LaunchAdjacentController launchAdjacentController,
+            Optional<WindowDecorViewModel> windowDecorViewModel,
             @ShellMainThread ShellExecutor mainExecutor) {
         return new SplitScreenController(context, shellInit, shellCommandHandler, shellController,
                 shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, displayController,
                 displayImeController, displayInsetsController, dragAndDropController, transitions,
-                transactionPool, iconProvider, recentTasks, mainExecutor);
+                transactionPool, iconProvider, recentTasks, launchAdjacentController,
+                windowDecorViewModel, mainExecutor);
     }
 
     //
@@ -535,11 +547,13 @@
             Optional<PipTouchHandler> pipTouchHandlerOptional,
             Optional<RecentsTransitionHandler> recentsTransitionHandler,
             KeyguardTransitionHandler keyguardTransitionHandler,
+            Optional<DesktopModeController> desktopModeController,
+            Optional<DesktopTasksController> desktopTasksController,
             Optional<UnfoldTransitionHandler> unfoldHandler,
             Transitions transitions) {
         return new DefaultMixedHandler(shellInit, transitions, splitScreenOptional,
                 pipTouchHandlerOptional, recentsTransitionHandler, keyguardTransitionHandler,
-                unfoldHandler);
+                desktopModeController, desktopTasksController, unfoldHandler);
     }
 
     @WMSingleton
@@ -573,13 +587,13 @@
         animators.add(fullscreenAnimator);
 
         return new UnfoldAnimationController(
-                        shellInit,
-                        transactionPool,
-                        progressProvider.get(),
-                        animators,
-                        unfoldTransitionHandler,
-                        mainExecutor
-                );
+                shellInit,
+                transactionPool,
+                progressProvider.get(),
+                animators,
+                unfoldTransitionHandler,
+                mainExecutor
+        );
     }
 
     @Provides
@@ -671,6 +685,7 @@
     static DesktopTasksController provideDesktopTasksController(
             Context context,
             ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             ShellController shellController,
             DisplayController displayController,
             ShellTaskOrganizer shellTaskOrganizer,
@@ -679,13 +694,16 @@
             Transitions transitions,
             EnterDesktopTaskTransitionHandler enterDesktopTransitionHandler,
             ExitDesktopTaskTransitionHandler exitDesktopTransitionHandler,
+            ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
             @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
+            LaunchAdjacentController launchAdjacentController,
             @ShellMainThread ShellExecutor mainExecutor
     ) {
-        return new DesktopTasksController(context, shellInit, shellController, displayController,
-                shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, transitions,
-                enterDesktopTransitionHandler, exitDesktopTransitionHandler,
-                desktopModeTaskRepository, mainExecutor);
+        return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController,
+                displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer,
+                transitions, enterDesktopTransitionHandler, exitDesktopTransitionHandler,
+                toggleResizeDesktopTaskTransitionHandler, desktopModeTaskRepository,
+                launchAdjacentController, mainExecutor);
     }
 
     @WMSingleton
@@ -697,6 +715,13 @@
 
     @WMSingleton
     @Provides
+    static ToggleResizeDesktopTaskTransitionHandler provideToggleResizeDesktopTaskTransitionHandler(
+            Transitions transitions) {
+        return new ToggleResizeDesktopTaskTransitionHandler(transitions);
+    }
+
+    @WMSingleton
+    @Provides
     static ExitDesktopTaskTransitionHandler provideExitDesktopTaskTransitionHandler(
             Transitions transitions,
             Context context
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 b9d2be2..db6c258 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
@@ -33,6 +33,7 @@
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.WindowConfiguration;
 import android.content.Context;
+import android.content.res.TypedArray;
 import android.database.ContentObserver;
 import android.graphics.Region;
 import android.net.Uri;
@@ -414,6 +415,25 @@
     }
 
     /**
+     * Applies the proper surface states (rounded corners) to tasks when desktop mode is active.
+     * This is intended to be used when desktop mode is part of another animation but isn't, itself,
+     * animating.
+     */
+    public void syncSurfaceState(@NonNull TransitionInfo info,
+            SurfaceControl.Transaction finishTransaction) {
+        // Add rounded corners to freeform windows
+        final TypedArray ta = mContext.obtainStyledAttributes(
+                new int[]{android.R.attr.dialogCornerRadius});
+        final int cornerRadius = ta.getDimensionPixelSize(0, 0);
+        ta.recycle();
+        for (TransitionInfo.Change change: info.getChanges()) {
+            if (change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+                finishTransaction.setCornerRadius(change.getLeash(), cornerRadius);
+            }
+        }
+    }
+
+    /**
      * A {@link ContentObserver} for listening to changes to {@link Settings.System#DESKTOP_MODE}
      */
     private final class SettingsObserver extends ContentObserver {
@@ -500,6 +520,11 @@
         }
 
         @Override
+        public void showDesktopApp(int taskId) throws RemoteException {
+            // TODO
+        }
+
+        @Override
         public int getVisibleTaskCount(int displayId) throws RemoteException {
             int[] result = new int[1];
             executeRemoteCallWithTaskPermission(mController, "getVisibleTaskCount",
@@ -508,5 +533,20 @@
             );
             return result[0];
         }
+
+        @Override
+        public void stashDesktopApps(int displayId) throws RemoteException {
+            // Stashing of desktop apps not needed. Apps always launch on desktop
+        }
+
+        @Override
+        public void hideStashedDesktopApps(int displayId) throws RemoteException {
+            // Stashing of desktop apps not needed. Apps always launch on desktop
+        }
+
+        @Override
+        public void setTaskListener(IDesktopTaskListener listener) throws RemoteException {
+            // TODO(b/261234402): move visibility from sysui state to listener
+        }
     }
 }
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 3ab175d..711df0d 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
@@ -25,6 +25,7 @@
 import androidx.core.util.valueIterator
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 import com.android.wm.shell.util.KtProtoLog
+import java.io.PrintWriter
 import java.util.concurrent.Executor
 import java.util.function.Consumer
 
@@ -43,6 +44,7 @@
          */
         val activeTasks: ArraySet<Int> = ArraySet(),
         val visibleTasks: ArraySet<Int> = ArraySet(),
+        var stashed: Boolean = false
     )
 
     // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0).
@@ -85,8 +87,10 @@
         visibleTasksListeners[visibleTasksListener] = executor
         displayData.keyIterator().forEach { displayId ->
             val visibleTasks = getVisibleTaskCount(displayId)
+            val stashed = isStashed(displayId)
             executor.execute {
                 visibleTasksListener.onVisibilityChanged(displayId, visibleTasks > 0)
+                visibleTasksListener.onStashedChanged(displayId, stashed)
             }
         }
     }
@@ -312,6 +316,52 @@
     }
 
     /**
+     * Update stashed status on display with id [displayId]
+     */
+    fun setStashed(displayId: Int, stashed: Boolean) {
+        val data = displayData.getOrCreate(displayId)
+        val oldValue = data.stashed
+        data.stashed = stashed
+        if (oldValue != stashed) {
+            KtProtoLog.d(
+                    WM_SHELL_DESKTOP_MODE,
+                    "DesktopTaskRepo: mark stashed=%b displayId=%d",
+                    stashed,
+                    displayId
+            )
+            visibleTasksListeners.forEach { (listener, executor) ->
+                executor.execute { listener.onStashedChanged(displayId, stashed) }
+            }
+        }
+    }
+
+    /**
+     * Check if display with id [displayId] has desktop tasks stashed
+     */
+    fun isStashed(displayId: Int): Boolean {
+        return displayData[displayId]?.stashed ?: false
+    }
+
+    internal fun dump(pw: PrintWriter, prefix: String) {
+        val innerPrefix = "$prefix  "
+        pw.println("${prefix}DesktopModeTaskRepository")
+        dumpDisplayData(pw, innerPrefix)
+        pw.println("${innerPrefix}freeformTasksInZOrder=${freeformTasksInZOrder.toDumpString()}")
+        pw.println("${innerPrefix}activeTasksListeners=${activeTasksListeners.size}")
+        pw.println("${innerPrefix}visibleTasksListeners=${visibleTasksListeners.size}")
+    }
+
+    private fun dumpDisplayData(pw: PrintWriter, prefix: String) {
+        val innerPrefix = "$prefix  "
+        displayData.forEach { displayId, data ->
+            pw.println("${prefix}Display $displayId:")
+            pw.println("${innerPrefix}activeTasks=${data.activeTasks.toDumpString()}")
+            pw.println("${innerPrefix}visibleTasks=${data.visibleTasks.toDumpString()}")
+            pw.println("${innerPrefix}stashed=${data.stashed}")
+        }
+    }
+
+    /**
      * Defines interface for classes that can listen to changes for active tasks in desktop mode.
      */
     interface ActiveTasksListener {
@@ -331,5 +381,15 @@
          */
         @JvmDefault
         fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {}
+
+        /**
+         * Called when the desktop stashed status changes.
+         */
+        @JvmDefault
+        fun onStashedChanged(displayId: Int, stashed: Boolean) {}
     }
 }
+
+private fun <T> Iterable<T>.toDumpString(): String {
+    return joinToString(separator = ", ", prefix = "[", postfix = "]")
+}
\ No newline at end of file
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 91bb155..40ea276 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
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.desktopmode
 
+import android.R
 import android.app.ActivityManager.RunningTaskInfo
 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
@@ -24,19 +25,21 @@
 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
 import android.app.WindowConfiguration.WindowingMode
 import android.content.Context
+import android.content.res.TypedArray
 import android.graphics.Point
 import android.graphics.Rect
 import android.graphics.Region
 import android.os.IBinder
 import android.os.SystemProperties
+import android.util.DisplayMetrics.DENSITY_DEFAULT
 import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
 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
 import android.window.TransitionRequestInfo
-import android.window.WindowContainerToken
 import android.window.WindowContainerTransaction
 import androidx.annotation.BinderThread
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
@@ -44,18 +47,23 @@
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.ExecutorUtils
 import com.android.wm.shell.common.ExternalInterfaceBinder
+import com.android.wm.shell.common.LaunchAdjacentController
 import com.android.wm.shell.common.RemoteCallable
 import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.common.SingleInstanceRemoteListener
 import com.android.wm.shell.common.SyncTransactionQueue
 import com.android.wm.shell.common.annotations.ExternalThread
 import com.android.wm.shell.common.annotations.ShellMainThread
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.sysui.ShellSharedConstants
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.util.KtProtoLog
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import java.io.PrintWriter
 import java.util.concurrent.Executor
 import java.util.function.Consumer
 
@@ -63,6 +71,7 @@
 class DesktopTasksController(
         private val context: Context,
         shellInit: ShellInit,
+        private val shellCommandHandler: ShellCommandHandler,
         private val shellController: ShellController,
         private val displayController: DisplayController,
         private val shellTaskOrganizer: ShellTaskOrganizer,
@@ -71,7 +80,10 @@
         private val transitions: Transitions,
         private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler,
         private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler,
+        private val toggleResizeDesktopTaskTransitionHandler:
+        ToggleResizeDesktopTaskTransitionHandler,
         private val desktopModeTaskRepository: DesktopModeTaskRepository,
+        private val launchAdjacentController: LaunchAdjacentController,
         @ShellMainThread private val mainExecutor: ShellExecutor
 ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler {
 
@@ -82,6 +94,15 @@
         visualIndicator?.releaseVisualIndicator(t)
         visualIndicator = null
     }
+    private val taskVisibilityListener = object : VisibleTasksListener {
+        override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {
+            launchAdjacentController.launchAdjacentEnabled = !hasVisibleFreeformTasks
+        }
+    }
+
+    private val transitionAreaHeight
+        get() = context.resources.getDimensionPixelSize(
+                com.android.wm.shell.R.dimen.desktop_mode_transition_area_height)
 
     init {
         desktopMode = DesktopModeImpl()
@@ -92,12 +113,14 @@
 
     private fun onInit() {
         KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController")
+        shellCommandHandler.addDumpCallback(this::dump, this)
         shellController.addExternalInterface(
             ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE,
             { createExternalInterface() },
             this
         )
         transitions.addHandler(this)
+        desktopModeTaskRepository.addVisibleTasksListener(taskVisibilityListener, mainExecutor)
     }
 
     /** Show all tasks, that are part of the desktop, on top of launcher */
@@ -118,27 +141,58 @@
         }
     }
 
+    /**
+     * Stash desktop tasks on display with id [displayId].
+     *
+     * When desktop tasks are stashed, launcher home screen icons are fully visible. New apps
+     * launched in this state will be added to the desktop. Existing desktop tasks will be brought
+     * back to front during the launch.
+     */
+    fun stashDesktopApps(displayId: Int) {
+        KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: stashDesktopApps")
+        desktopModeTaskRepository.setStashed(displayId, true)
+    }
+
+    /**
+     * Clear the stashed state for the given display
+     */
+    fun hideStashedDesktopApps(displayId: Int) {
+        KtProtoLog.v(
+                WM_SHELL_DESKTOP_MODE,
+                "DesktopTasksController: hideStashedApps displayId=%d",
+                displayId
+        )
+        desktopModeTaskRepository.setStashed(displayId, false)
+    }
+
     /** Get number of tasks that are marked as visible */
     fun getVisibleTaskCount(displayId: Int): Int {
         return desktopModeTaskRepository.getVisibleTaskCount(displayId)
     }
 
     /** Move a task with given `taskId` to desktop */
-    fun moveToDesktop(taskId: Int) {
-        shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToDesktop(task) }
+    fun moveToDesktop(taskId: Int, wct: WindowContainerTransaction = WindowContainerTransaction()) {
+        shellTaskOrganizer.getRunningTaskInfo(taskId)?.let {
+            task -> moveToDesktop(task, wct)
+        }
     }
 
-    /** Move a task to desktop */
-    fun moveToDesktop(task: RunningTaskInfo) {
+    /**
+     * Move a task to desktop
+     */
+    fun moveToDesktop(
+            task: RunningTaskInfo,
+            wct: WindowContainerTransaction = WindowContainerTransaction()
+    ) {
         KtProtoLog.v(
             WM_SHELL_DESKTOP_MODE,
             "DesktopTasksController: moveToDesktop taskId=%d",
             task.taskId
         )
-        val wct = WindowContainerTransaction()
         // Bring other apps to front first
         bringDesktopAppsToFront(task.displayId, wct)
-        addMoveToDesktopChanges(wct, task.token)
+        addMoveToDesktopChanges(wct, task)
+
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
         } else {
@@ -158,7 +212,7 @@
         )
         val wct = WindowContainerTransaction()
         moveHomeTaskToFront(wct)
-        addMoveToDesktopChanges(wct, taskInfo.getToken())
+        addMoveToDesktopChanges(wct, taskInfo)
         wct.setBounds(taskInfo.token, startBounds)
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
@@ -178,7 +232,7 @@
         )
         val wct = WindowContainerTransaction()
         bringDesktopAppsToFront(taskInfo.displayId, wct)
-        addMoveToDesktopChanges(wct, taskInfo.getToken())
+        addMoveToDesktopChanges(wct, taskInfo)
         wct.setBounds(taskInfo.token, freeformBounds)
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
@@ -204,7 +258,7 @@
         )
 
         val wct = WindowContainerTransaction()
-        addMoveToFullscreenChanges(wct, task.token)
+        addMoveToFullscreenChanges(wct, task)
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
         } else {
@@ -218,16 +272,24 @@
      */
     fun cancelMoveToFreeform(task: RunningTaskInfo, position: Point) {
         KtProtoLog.v(
-                WM_SHELL_DESKTOP_MODE,
-                "DesktopTasksController: cancelMoveToFreeform taskId=%d",
-                task.taskId
+            WM_SHELL_DESKTOP_MODE,
+            "DesktopTasksController: cancelMoveToFreeform taskId=%d",
+            task.taskId
         )
         val wct = WindowContainerTransaction()
-        addMoveToFullscreenChanges(wct, task.token)
+        wct.setBounds(task.token, null)
+
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            enterDesktopTaskTransitionHandler.startCancelMoveToDesktopMode(wct, position,
-                    mOnAnimationFinishedCallback)
+            enterDesktopTaskTransitionHandler.startCancelMoveToDesktopMode(
+                wct, position) { t ->
+                val callbackWCT = WindowContainerTransaction()
+                visualIndicator?.releaseVisualIndicator(t)
+                visualIndicator = null
+                addMoveToFullscreenChanges(callbackWCT, task)
+                transitions.startTransition(TRANSIT_CHANGE, callbackWCT, null /* handler */)
+            }
         } else {
+            addMoveToFullscreenChanges(wct, task)
             shellTaskOrganizer.applyTransaction(wct)
             releaseVisualIndicator()
         }
@@ -240,7 +302,7 @@
                 task.taskId
         )
         val wct = WindowContainerTransaction()
-        addMoveToFullscreenChanges(wct, task.token)
+        addMoveToFullscreenChanges(wct, task)
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             exitDesktopTaskTransitionHandler.startTransition(
@@ -252,6 +314,11 @@
     }
 
     /** Move a task to the front */
+    fun moveTaskToFront(taskId: Int) {
+        shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveTaskToFront(task) }
+    }
+
+    /** Move a task to the front */
     fun moveTaskToFront(taskInfo: RunningTaskInfo) {
         KtProtoLog.v(
             WM_SHELL_DESKTOP_MODE,
@@ -331,6 +398,49 @@
         }
     }
 
+    /** Quick-resizes a desktop task, toggling between the stable bounds and the default bounds. */
+    fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo, windowDecor: DesktopModeWindowDecoration) {
+        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
+
+        val stableBounds = Rect()
+        displayLayout.getStableBounds(stableBounds)
+        val destinationBounds = Rect()
+        if (taskInfo.configuration.windowConfiguration.bounds == stableBounds) {
+            // The desktop task is currently occupying the whole stable bounds, toggle to the
+            // default bounds.
+            getDefaultDesktopTaskBounds(
+                density = taskInfo.configuration.densityDpi.toFloat() / DENSITY_DEFAULT,
+                stableBounds = stableBounds,
+                outBounds = destinationBounds
+            )
+        } else {
+            // Toggle to the stable bounds.
+            destinationBounds.set(stableBounds)
+        }
+
+        val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            toggleResizeDesktopTaskTransitionHandler.startTransition(
+                wct,
+                taskInfo.taskId,
+                windowDecor
+            )
+        } else {
+            shellTaskOrganizer.applyTransaction(wct)
+        }
+    }
+
+    private fun getDefaultDesktopTaskBounds(density: Float, stableBounds: Rect, outBounds: Rect) {
+        val width = (DESKTOP_MODE_DEFAULT_WIDTH_DP * density + 0.5f).toInt()
+        val height = (DESKTOP_MODE_DEFAULT_HEIGHT_DP * density + 0.5f).toInt()
+        outBounds.set(0, 0, width, height)
+        // Center the task in stable bounds
+        outBounds.offset(
+            stableBounds.centerX() - outBounds.centerX(),
+            stableBounds.centerY() - outBounds.centerY()
+        )
+    }
+
     /**
      * Get windowing move for a given `taskId`
      *
@@ -397,83 +507,169 @@
         transition: IBinder,
         request: TransitionRequestInfo
     ): WindowContainerTransaction? {
+        KtProtoLog.v(
+            WM_SHELL_DESKTOP_MODE,
+            "DesktopTasksController: handleRequest request=%s",
+            request
+        )
         // Check if we should skip handling this transition
+        var reason = ""
         val shouldHandleRequest =
             when {
                 // Only handle open or to front transitions
-                request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> false
+                request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> {
+                    reason = "transition type not handled (${request.type})"
+                    false
+                }
                 // Only handle when it is a task transition
-                request.triggerTask == null -> false
+                request.triggerTask == null -> {
+                    reason = "triggerTask is null"
+                    false
+                }
                 // Only handle standard type tasks
-                request.triggerTask.activityType != ACTIVITY_TYPE_STANDARD -> false
+                request.triggerTask.activityType != ACTIVITY_TYPE_STANDARD -> {
+                    reason = "activityType not handled (${request.triggerTask.activityType})"
+                    false
+                }
                 // Only handle fullscreen or freeform tasks
                 request.triggerTask.windowingMode != WINDOWING_MODE_FULLSCREEN &&
-                        request.triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> false
+                        request.triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> {
+                    reason = "windowingMode not handled (${request.triggerTask.windowingMode})"
+                    false
+                }
                 // Otherwise process it
                 else -> true
             }
 
         if (!shouldHandleRequest) {
+            KtProtoLog.v(
+                    WM_SHELL_DESKTOP_MODE,
+                    "DesktopTasksController: skipping handleRequest reason=%s",
+                    reason
+            )
             return null
         }
 
         val task: RunningTaskInfo = request.triggerTask
-        val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId)
 
-        // Check if we should switch a fullscreen task to freeform
-        if (task.windowingMode == WINDOWING_MODE_FULLSCREEN) {
-            // If there are any visible desktop tasks, switch the task to freeform
-            if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) {
-                KtProtoLog.d(
-                    WM_SHELL_DESKTOP_MODE,
-                    "DesktopTasksController: switch fullscreen task to freeform on transition" +
-                        " taskId=%d",
-                    task.taskId
-                )
-                return WindowContainerTransaction().also { wct ->
-                    addMoveToDesktopChanges(wct, task.token)
-                }
+        val result = when {
+            // If display has tasks stashed, handle as stashed launch
+            desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task)
+            // Check if fullscreen task should be updated
+            task.windowingMode == WINDOWING_MODE_FULLSCREEN -> handleFullscreenTaskLaunch(task)
+            // Check if freeform task should be updated
+            task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task)
+            else -> {
+                null
             }
         }
+        KtProtoLog.v(
+                WM_SHELL_DESKTOP_MODE,
+                "DesktopTasksController: handleRequest result=%s",
+                result ?: "null"
+        )
+        return result
+    }
 
-        // CHeck if we should switch a freeform task to fullscreen
-        if (task.windowingMode == WINDOWING_MODE_FREEFORM) {
-            // If no visible desktop tasks, switch this task to freeform as the transition came
-            // outside of this controller
-            if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) {
-                KtProtoLog.d(
+    /**
+     * Applies the proper surface states (rounded corners) to tasks when desktop mode is active.
+     * This is intended to be used when desktop mode is part of another animation but isn't, itself,
+     * animating.
+     */
+    fun syncSurfaceState(
+            info: TransitionInfo,
+            finishTransaction: SurfaceControl.Transaction
+    ) {
+        // Add rounded corners to freeform windows
+        val ta: TypedArray = context.obtainStyledAttributes(
+                intArrayOf(R.attr.dialogCornerRadius))
+        val cornerRadius = ta.getDimensionPixelSize(0, 0).toFloat()
+        ta.recycle()
+        info.changes
+                .filter { it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM }
+                .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) }
+    }
+
+    private fun handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
+        KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch")
+        val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId)
+        if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) {
+            KtProtoLog.d(
                     WM_SHELL_DESKTOP_MODE,
                     "DesktopTasksController: switch freeform task to fullscreen oon transition" +
-                        " taskId=%d",
+                            " taskId=%d",
                     task.taskId
-                )
-                return WindowContainerTransaction().also { wct ->
-                    addMoveToFullscreenChanges(wct, task.token)
-                }
+            )
+            return WindowContainerTransaction().also { wct ->
+                addMoveToFullscreenChanges(wct, task)
             }
         }
         return null
     }
 
+    private fun handleFullscreenTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
+        KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFullscreenTaskLaunch")
+        val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId)
+        if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) {
+            KtProtoLog.d(
+                    WM_SHELL_DESKTOP_MODE,
+                    "DesktopTasksController: switch fullscreen task to freeform on transition" +
+                            " taskId=%d",
+                    task.taskId
+            )
+            return WindowContainerTransaction().also { wct ->
+                addMoveToDesktopChanges(wct, task)
+            }
+        }
+        return null
+    }
+
+    private fun handleStashedTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction {
+        KtProtoLog.d(
+                WM_SHELL_DESKTOP_MODE,
+                "DesktopTasksController: launch apps with stashed on transition taskId=%d",
+                task.taskId
+        )
+        val wct = WindowContainerTransaction()
+        bringDesktopAppsToFront(task.displayId, wct)
+        addMoveToDesktopChanges(wct, task)
+        desktopModeTaskRepository.setStashed(task.displayId, false)
+        return wct
+    }
+
     private fun addMoveToDesktopChanges(
         wct: WindowContainerTransaction,
-        token: WindowContainerToken
+        taskInfo: RunningTaskInfo
     ) {
-        wct.setWindowingMode(token, WINDOWING_MODE_FREEFORM)
-        wct.reorder(token, true /* onTop */)
+        val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode
+        val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FREEFORM) {
+            // Display windowing is freeform, set to undefined and inherit it
+            WINDOWING_MODE_UNDEFINED
+        } else {
+            WINDOWING_MODE_FREEFORM
+        }
+        wct.setWindowingMode(taskInfo.token, targetWindowingMode)
+        wct.reorder(taskInfo.token, true /* onTop */)
         if (isDesktopDensityOverrideSet()) {
-            wct.setDensityDpi(token, getDesktopDensityDpi())
+            wct.setDensityDpi(taskInfo.token, getDesktopDensityDpi())
         }
     }
 
     private fun addMoveToFullscreenChanges(
         wct: WindowContainerTransaction,
-        token: WindowContainerToken
+        taskInfo: RunningTaskInfo
     ) {
-        wct.setWindowingMode(token, WINDOWING_MODE_FULLSCREEN)
-        wct.setBounds(token, null)
+        val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode
+        val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+            // Display windowing is fullscreen, set to undefined and inherit it
+            WINDOWING_MODE_UNDEFINED
+        } else {
+            WINDOWING_MODE_FULLSCREEN
+        }
+        wct.setWindowingMode(taskInfo.token, targetWindowingMode)
+        wct.setBounds(taskInfo.token, null)
         if (isDesktopDensityOverrideSet()) {
-            wct.setDensityDpi(token, getFullscreenDensityDpi())
+            wct.setDensityDpi(taskInfo.token, getFullscreenDensityDpi())
         }
     }
 
@@ -508,13 +704,12 @@
             y: Float
     ) {
         if (taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
-            val statusBarHeight = getStatusBarHeight(taskInfo)
-            if (y <= statusBarHeight && visualIndicator == null) {
+            if (y <= transitionAreaHeight && visualIndicator == null) {
                 visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo,
                         displayController, context, taskSurface, shellTaskOrganizer,
                         rootTaskDisplayAreaOrganizer)
                 visualIndicator?.createFullscreenIndicatorWithAnimatedBounds()
-            } else if (y > statusBarHeight && visualIndicator != null) {
+            } else if (y > transitionAreaHeight && visualIndicator != null) {
                 releaseVisualIndicator()
             }
         }
@@ -524,14 +719,18 @@
      * Perform checks required on drag end. Move to fullscreen if drag ends in status bar area.
      *
      * @param taskInfo the task being dragged.
-     * @param position position of surface when drag ends
+     * @param position position of surface when drag ends.
+     * @param y the Y position of the motion event.
+     * @param windowDecor the window decoration for the task being dragged
      */
     fun onDragPositioningEnd(
             taskInfo: RunningTaskInfo,
-            position: Point
+            position: Point,
+            y: Float,
+            windowDecor: DesktopModeWindowDecoration
     ) {
-        val statusBarHeight = getStatusBarHeight(taskInfo)
-        if (position.y <= statusBarHeight && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
+        if (y <= transitionAreaHeight && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
+            windowDecor.incrementRelayoutBlock()
             moveToFullscreenWithAnimation(taskInfo, position)
         }
     }
@@ -549,9 +748,9 @@
             taskSurface: SurfaceControl,
             y: Float
     ) {
-        // If the motion event is above the status bar, return since we do not need to show the
-        // visual indicator at this point.
-        if (y < getStatusBarHeight(taskInfo)) {
+        // If the motion event is above the status bar and the visual indicator is not yet visible,
+        // return since we do not need to show the visual indicator at this point.
+        if (y < getStatusBarHeight(taskInfo) && visualIndicator == null) {
             return
         }
         if (visualIndicator == null) {
@@ -632,6 +831,12 @@
         desktopModeTaskRepository.setTaskCornerListener(listener, callbackExecutor)
     }
 
+    private fun dump(pw: PrintWriter, prefix: String) {
+        val innerPrefix = "$prefix  "
+        pw.println("${prefix}DesktopTasksController")
+        desktopModeTaskRepository.dump(pw, innerPrefix)
+    }
+
     /** The interface for calls from outside the shell, within the host process. */
     @ExternalThread
     private inner class DesktopModeImpl : DesktopMode {
@@ -658,8 +863,51 @@
     @BinderThread
     private class IDesktopModeImpl(private var controller: DesktopTasksController?) :
         IDesktopMode.Stub(), ExternalInterfaceBinder {
+
+        private lateinit var remoteListener:
+                SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>
+
+        private val listener: VisibleTasksListener = object : VisibleTasksListener {
+            override fun onVisibilityChanged(displayId: Int, visible: Boolean) {
+                KtProtoLog.v(
+                        WM_SHELL_DESKTOP_MODE,
+                        "IDesktopModeImpl: onVisibilityChanged display=%d visible=%b",
+                        displayId,
+                        visible
+                )
+                remoteListener.call { l -> l.onVisibilityChanged(displayId, visible) }
+            }
+
+            override fun onStashedChanged(displayId: Int, stashed: Boolean) {
+                KtProtoLog.v(
+                        WM_SHELL_DESKTOP_MODE,
+                        "IDesktopModeImpl: onStashedChanged display=%d stashed=%b",
+                        displayId,
+                        stashed
+                )
+                remoteListener.call { l -> l.onStashedChanged(displayId, stashed) }
+            }
+        }
+
+        init {
+            remoteListener =
+                    SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>(
+                            controller,
+                            { c ->
+                                c.desktopModeTaskRepository.addVisibleTasksListener(
+                                        listener,
+                                        c.mainExecutor
+                                )
+                            },
+                            { c ->
+                                c.desktopModeTaskRepository.removeVisibleTasksListener(listener)
+                            }
+                    )
+        }
+
         /** Invalidates this instance, preventing future calls from updating the controller. */
         override fun invalidate() {
+            remoteListener.unregister()
             controller = null
         }
 
@@ -670,6 +918,27 @@
             ) { c -> c.showDesktopApps(displayId) }
         }
 
+        override fun stashDesktopApps(displayId: Int) {
+            ExecutorUtils.executeRemoteCallWithTaskPermission(
+                    controller,
+                    "stashDesktopApps"
+            ) { c -> c.stashDesktopApps(displayId) }
+        }
+
+        override fun hideStashedDesktopApps(displayId: Int) {
+            ExecutorUtils.executeRemoteCallWithTaskPermission(
+                    controller,
+                    "hideStashedDesktopApps"
+            ) { c -> c.hideStashedDesktopApps(displayId) }
+        }
+
+        override fun showDesktopApp(taskId: Int) {
+            ExecutorUtils.executeRemoteCallWithTaskPermission(
+                    controller,
+                    "showDesktopApp"
+            ) { c -> c.moveTaskToFront(taskId) }
+        }
+
         override fun getVisibleTaskCount(displayId: Int): Int {
             val result = IntArray(1)
             ExecutorUtils.executeRemoteCallWithTaskPermission(
@@ -680,13 +949,33 @@
             )
             return result[0]
         }
+
+        override fun setTaskListener(listener: IDesktopTaskListener?) {
+            KtProtoLog.v(
+                    WM_SHELL_DESKTOP_MODE,
+                    "IDesktopModeImpl: set task listener=%s",
+                    listener ?: "null"
+            )
+            ExecutorUtils.executeRemoteCallWithTaskPermission(
+                    controller,
+                    "setTaskListener"
+            ) { _ -> listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() }
+        }
     }
 
     companion object {
         private val DESKTOP_DENSITY_OVERRIDE =
-            SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 0)
+            SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 284)
         private val DESKTOP_DENSITY_ALLOWED_RANGE = (100..1000)
 
+        // Override default freeform task width when desktop mode is enabled. In dips.
+        private val DESKTOP_MODE_DEFAULT_WIDTH_DP =
+            SystemProperties.getInt("persist.wm.debug.desktop_mode.default_width", 840)
+
+        // Override default freeform task height when desktop mode is enabled. In dips.
+        private val DESKTOP_MODE_DEFAULT_HEIGHT_DP =
+            SystemProperties.getInt("persist.wm.debug.desktop_mode.default_height", 630)
+
         /**
          * Check if desktop density override is enabled
          */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
index 3733b91..3e175f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
@@ -17,7 +17,6 @@
 package com.android.wm.shell.desktopmode;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -200,7 +199,7 @@
         }
 
         if (type == Transitions.TRANSIT_CANCEL_ENTERING_DESKTOP_MODE
-                && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+                && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
                 && mPosition != null) {
             // This Transition animates a task to fullscreen after being dragged from the status
             // bar and then released back into the status bar area
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 899d672..ee3a080 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
@@ -16,6 +16,8 @@
 
 package com.android.wm.shell.desktopmode;
 
+import com.android.wm.shell.desktopmode.IDesktopTaskListener;
+
 /**
  * Interface that is exposed to remote callers to manipulate desktop mode features.
  */
@@ -24,6 +26,18 @@
     /** Show apps on the desktop on the given display */
     void showDesktopApps(int displayId);
 
+    /** Stash apps on the desktop to allow launching another app from home screen */
+    void stashDesktopApps(int displayId);
+
+    /** Hide apps that may be stashed */
+    void hideStashedDesktopApps(int displayId);
+
+    /** Bring task with the given id to front */
+    oneway void showDesktopApp(int taskId);
+
     /** Get count of visible desktop tasks on the given display */
     int getVisibleTaskCount(int displayId);
+
+    /** Set listener that will receive callbacks about updates to desktop tasks */
+    oneway void setTaskListener(IDesktopTaskListener listener);
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
new file mode 100644
index 0000000..39128a8
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.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 com.android.wm.shell.desktopmode;
+
+/**
+ * Allows external processes to register a listener in WMShell to get updates about desktop task
+ * state.
+ */
+interface IDesktopTaskListener {
+
+    /** Desktop task visibility has change. Visible if at least 1 task is visible. */
+    oneway void onVisibilityChanged(int displayId, boolean visible);
+
+    /** Desktop task stashed status has changed. */
+    oneway void onStashedChanged(int displayId, boolean stashed);
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
new file mode 100644
index 0000000..94788e4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.desktopmode
+
+import android.animation.Animator
+import android.animation.RectEvaluator
+import android.animation.ValueAnimator
+import android.graphics.Rect
+import android.os.IBinder
+import android.util.SparseArray
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import androidx.core.animation.addListener
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import java.util.function.Supplier
+
+/** Handles the animation of quick resizing of desktop tasks. */
+class ToggleResizeDesktopTaskTransitionHandler(
+    private val transitions: Transitions,
+    private val transactionSupplier: Supplier<SurfaceControl.Transaction>
+) : Transitions.TransitionHandler {
+
+    private val rectEvaluator = RectEvaluator(Rect())
+    private val taskToDecorationMap = SparseArray<DesktopModeWindowDecoration>()
+
+    private var boundsAnimator: Animator? = null
+
+    constructor(
+        transitions: Transitions
+    ) : this(transitions, Supplier { SurfaceControl.Transaction() })
+
+    /** Starts a quick resize transition. */
+    fun startTransition(
+        wct: WindowContainerTransaction,
+        taskId: Int,
+        windowDecoration: DesktopModeWindowDecoration
+    ) {
+        // Pause relayout until the transition animation finishes.
+        windowDecoration.incrementRelayoutBlock()
+        transitions.startTransition(TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE, wct, this)
+        taskToDecorationMap.put(taskId, windowDecoration)
+    }
+
+    override fun startAnimation(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+        finishCallback: Transitions.TransitionFinishCallback
+    ): Boolean {
+        val change = findRelevantChange(info)
+        val leash = change.leash
+        val taskId = change.taskInfo.taskId
+        val startBounds = change.startAbsBounds
+        val endBounds = change.endAbsBounds
+        val windowDecor =
+            taskToDecorationMap.removeReturnOld(taskId)
+                ?: throw IllegalStateException("Window decoration not found for task $taskId")
+
+        val tx = transactionSupplier.get()
+        boundsAnimator?.cancel()
+        boundsAnimator =
+            ValueAnimator.ofObject(rectEvaluator, startBounds, endBounds)
+                .setDuration(RESIZE_DURATION_MS)
+                .apply {
+                    addListener(
+                        onStart = {
+                            startTransaction
+                                .setPosition(
+                                    leash,
+                                    startBounds.left.toFloat(),
+                                    startBounds.top.toFloat()
+                                )
+                                .setWindowCrop(leash, startBounds.width(), startBounds.height())
+                                .show(leash)
+                            windowDecor.showResizeVeil(startTransaction, startBounds)
+                        },
+                        onEnd = {
+                            finishTransaction
+                                .setPosition(
+                                    leash,
+                                    endBounds.left.toFloat(),
+                                    endBounds.top.toFloat()
+                                )
+                                .setWindowCrop(leash, endBounds.width(), endBounds.height())
+                                .show(leash)
+                            windowDecor.hideResizeVeil()
+                            finishCallback.onTransitionFinished(null, null)
+                            boundsAnimator = null
+                        }
+                    )
+                    addUpdateListener { anim ->
+                        val rect = anim.animatedValue as Rect
+                        tx.setPosition(leash, rect.left.toFloat(), rect.top.toFloat())
+                            .setWindowCrop(leash, rect.width(), rect.height())
+                            .show(leash)
+                        windowDecor.updateResizeVeil(tx, rect)
+                    }
+                    start()
+                }
+        return true
+    }
+
+    override fun handleRequest(
+        transition: IBinder,
+        request: TransitionRequestInfo
+    ): WindowContainerTransaction? {
+        return null
+    }
+
+    private fun findRelevantChange(info: TransitionInfo): TransitionInfo.Change {
+        val matchingChanges =
+            info.changes.filter { c ->
+                !isWallpaper(c) && isValidTaskChange(c) && c.mode == TRANSIT_CHANGE
+            }
+        if (matchingChanges.size != 1) {
+            throw IllegalStateException(
+                "Expected 1 relevant change but found: ${matchingChanges.size}"
+            )
+        }
+        return matchingChanges.first()
+    }
+
+    private fun isWallpaper(change: TransitionInfo.Change): Boolean {
+        return (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0
+    }
+
+    private fun isValidTaskChange(change: TransitionInfo.Change): Boolean {
+        return change.taskInfo != null && change.taskInfo?.taskId != -1
+    }
+
+    companion object {
+        private const val RESIZE_DURATION_MS = 300L
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
index 99922fb..f9ea1d4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
@@ -21,11 +21,17 @@
 building to check the log state (is enabled) before printing the print format style log.
 
 **Notes**
-- ProtoLogs currently only work from soong builds (ie. via make/mp). We need to reimplement the
-  tool for use with SysUI-studio
+- ProtoLogs are only fully supported from soong builds (ie. via make/mp). In SysUI-studio it falls
+  back to log via Logcat
 - Non-text ProtoLogs are not currently supported with the Shell library (you can't view them with
   traces in Winscope)
 
+### Kotlin
+
+Protolog tool does not yet have support for Kotlin code (see [b/168581922](https://b.corp.google.com/issues/168581922)).
+For logging in Kotlin, use the [KtProtoLog](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt)
+class which has a similar API to the Java ProtoLog class.
+
 ### Enabling ProtoLog command line logging
 Run these commands to enable protologs for both WM Core and WM Shell to print to logcat.
 ```shell
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
index 04fc79a..55e34fe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
@@ -19,9 +19,15 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.app.ActivityManager;
 import android.app.WindowConfiguration;
+import android.content.Context;
+import android.graphics.Rect;
 import android.os.IBinder;
+import android.util.ArrayMap;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.TransitionInfo;
@@ -31,6 +37,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
@@ -39,23 +47,37 @@
 import java.util.List;
 
 /**
- * The {@link Transitions.TransitionHandler} that handles freeform task maximizing and restoring
- * transitions.
+ * The {@link Transitions.TransitionHandler} that handles freeform task maximizing, closing, and
+ * restoring transitions.
  */
 public class FreeformTaskTransitionHandler
         implements Transitions.TransitionHandler, FreeformTaskTransitionStarter {
-
+    private static final int CLOSE_ANIM_DURATION = 400;
+    private final Context mContext;
     private final Transitions mTransitions;
     private final WindowDecorViewModel mWindowDecorViewModel;
+    private final DisplayController mDisplayController;
+    private final ShellExecutor mMainExecutor;
+    private final ShellExecutor mAnimExecutor;
 
     private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
 
+    private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>();
+
     public FreeformTaskTransitionHandler(
             ShellInit shellInit,
             Transitions transitions,
-            WindowDecorViewModel windowDecorViewModel) {
+            Context context,
+            WindowDecorViewModel windowDecorViewModel,
+            DisplayController displayController,
+            ShellExecutor mainExecutor,
+            ShellExecutor animExecutor) {
         mTransitions = transitions;
+        mContext = context;
         mWindowDecorViewModel = windowDecorViewModel;
+        mDisplayController = displayController;
+        mMainExecutor = mainExecutor;
+        mAnimExecutor = animExecutor;
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             shellInit.addInitCallback(this::onInit, this);
         }
@@ -103,6 +125,14 @@
             @NonNull SurfaceControl.Transaction finishT,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
         boolean transitionHandled = false;
+        final ArrayList<Animator> animations = new ArrayList<>();
+        final Runnable onAnimFinish = () -> {
+            if (!animations.isEmpty()) return;
+            mMainExecutor.execute(() -> {
+                mAnimations.remove(transition);
+                finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+            });
+        };
         for (TransitionInfo.Change change : info.getChanges()) {
             if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) {
                 continue;
@@ -121,21 +151,45 @@
                 case WindowManager.TRANSIT_TO_BACK:
                     transitionHandled |= startMinimizeTransition(transition);
                     break;
+                case WindowManager.TRANSIT_CLOSE:
+                    if (change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+                        transitionHandled |= startCloseTransition(transition, change,
+                                finishT, animations, onAnimFinish);
+                    }
+                    break;
             }
         }
-
-        mPendingTransitionTokens.remove(transition);
-
         if (!transitionHandled) {
             return false;
         }
-
+        mAnimations.put(transition, animations);
+        // startT must be applied before animations start.
         startT.apply();
-        mTransitions.getMainExecutor().execute(
-                () -> finishCallback.onTransitionFinished(null, null));
+        mAnimExecutor.execute(() -> {
+            for (Animator anim : animations) {
+                anim.start();
+            }
+        });
+        // Run this here in case no animators are created.
+        onAnimFinish.run();
+        mPendingTransitionTokens.remove(transition);
         return true;
     }
 
+    @Override
+    public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ArrayList<Animator> animations = mAnimations.get(mergeTarget);
+        if (animations == null) return;
+        mAnimExecutor.execute(() -> {
+            for (Animator anim : animations) {
+                anim.end();
+            }
+        });
+
+    }
+
     private boolean startChangeTransition(
             IBinder transition,
             int type,
@@ -165,6 +219,36 @@
         return mPendingTransitionTokens.contains(transition);
     }
 
+    private boolean startCloseTransition(IBinder transition, TransitionInfo.Change change,
+            SurfaceControl.Transaction finishT, ArrayList<Animator> animations,
+            Runnable onAnimFinish) {
+        if (!mPendingTransitionTokens.contains(transition)) return false;
+        int screenHeight = mDisplayController
+                .getDisplayLayout(change.getTaskInfo().displayId).height();
+        ValueAnimator animator = new ValueAnimator();
+        animator.setDuration(CLOSE_ANIM_DURATION)
+                .setFloatValues(0f, 1f);
+        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        SurfaceControl sc = change.getLeash();
+        finishT.hide(sc);
+        Rect startBounds = new Rect(change.getTaskInfo().configuration.windowConfiguration
+                .getBounds());
+        animator.addUpdateListener(animation -> {
+            t.setPosition(sc, startBounds.left,
+                    startBounds.top + (animation.getAnimatedFraction() * screenHeight));
+            t.apply();
+        });
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                animations.remove(animator);
+                onAnimFinish.run();
+            }
+        });
+        animations.add(animator);
+        return true;
+    }
+
     @Nullable
     @Override
     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
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 9729a40..da455f8 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
@@ -33,9 +33,10 @@
 import androidx.annotation.NonNull;
 
 import com.android.wm.shell.R;
-import com.android.wm.shell.bubbles.DismissView;
-import com.android.wm.shell.common.DismissCircleView;
+import com.android.wm.shell.bubbles.DismissViewUtils;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.bubbles.DismissCircleView;
+import com.android.wm.shell.common.bubbles.DismissView;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 import com.android.wm.shell.pip.PipUiEventLogger;
 
@@ -106,6 +107,7 @@
         }
 
         mTargetViewContainer = new DismissView(mContext);
+        DismissViewUtils.setup(mTargetViewContainer);
         mTargetView = mTargetViewContainer.getCircle();
         mTargetViewContainer.setOnApplyWindowInsetsListener((view, windowInsets) -> {
             if (!windowInsets.equals(mWindowInsets)) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index 2a61445..b0fa993 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -49,7 +49,7 @@
             Consts.TAG_WM_SPLIT_SCREEN),
     WM_SHELL_SYSUI_EVENTS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
             Consts.TAG_WM_SHELL),
-    WM_SHELL_DESKTOP_MODE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+    WM_SHELL_DESKTOP_MODE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
             Consts.TAG_WM_SHELL),
     WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
             Consts.TAG_WM_SHELL),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 843e5af..837f118 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -567,7 +567,8 @@
                         && taskInfo.configuration.windowConfiguration.isAlwaysOnTop()) {
                     // Tasks that are always on top (e.g. bubbles), will handle their own transition
                     // as they are on top of everything else. So cancel the merge here.
-                    cancel("task #" + taskInfo.taskId + " is always_on_top");
+                    cancel(false /* toHome */, false /* withScreenshots */,
+                            "task #" + taskInfo.taskId + " is always_on_top");
                     return;
                 }
                 final boolean isRootTask = taskInfo != null
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
index 89538cb..e52235f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
@@ -24,6 +24,9 @@
 import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
+
+import java.util.Optional;
 
 /**
  * Main stage for split-screen mode. When split-screen is active all standard activity types launch
@@ -35,9 +38,10 @@
 
     MainStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
             StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
-            SurfaceSession surfaceSession, IconProvider iconProvider) {
+            SurfaceSession surfaceSession, IconProvider iconProvider,
+            Optional<WindowDecorViewModel> windowDecorViewModel) {
         super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
-                iconProvider);
+                iconProvider, windowDecorViewModel);
     }
 
     boolean isActive() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
index 8639b36..9903113 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
@@ -25,6 +25,9 @@
 import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
+
+import java.util.Optional;
 
 /**
  * Side stage for split-screen mode. Only tasks that are explicitly pinned to this stage show up
@@ -37,9 +40,10 @@
 
     SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
             StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
-            SurfaceSession surfaceSession, IconProvider iconProvider) {
+            SurfaceSession surfaceSession, IconProvider iconProvider,
+            Optional<WindowDecorViewModel> windowDecorViewModel) {
         super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
-                iconProvider);
+                iconProvider, windowDecorViewModel);
     }
 
     boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) {
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 e7a367f..3669bce 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
@@ -31,6 +31,7 @@
 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.common.split.SplitScreenUtils.splitFailureMessage;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN;
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
@@ -50,6 +51,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.ArrayMap;
+import android.util.Log;
 import android.util.Slog;
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.IRemoteAnimationRunner;
@@ -76,6 +78,7 @@
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.ExternalInterfaceBinder;
+import com.android.wm.shell.common.LaunchAdjacentController;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SingleInstanceRemoteListener;
@@ -94,6 +97,7 @@
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
@@ -171,6 +175,8 @@
     private final TransactionPool mTransactionPool;
     private final IconProvider mIconProvider;
     private final Optional<RecentTasksController> mRecentTasksOptional;
+    private final LaunchAdjacentController mLaunchAdjacentController;
+    private final Optional<WindowDecorViewModel> mWindowDecorViewModel;
     private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler;
     private final String[] mAppsSupportMultiInstances;
 
@@ -197,6 +203,8 @@
             TransactionPool transactionPool,
             IconProvider iconProvider,
             Optional<RecentTasksController> recentTasks,
+            LaunchAdjacentController launchAdjacentController,
+            Optional<WindowDecorViewModel> windowDecorViewModel,
             ShellExecutor mainExecutor) {
         mShellCommandHandler = shellCommandHandler;
         mShellController = shellController;
@@ -213,6 +221,8 @@
         mTransactionPool = transactionPool;
         mIconProvider = iconProvider;
         mRecentTasksOptional = recentTasks;
+        mLaunchAdjacentController = launchAdjacentController;
+        mWindowDecorViewModel = windowDecorViewModel;
         mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this);
         // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic
         //                    override for this controller from the base module
@@ -242,6 +252,8 @@
             TransactionPool transactionPool,
             IconProvider iconProvider,
             RecentTasksController recentTasks,
+            LaunchAdjacentController launchAdjacentController,
+            WindowDecorViewModel windowDecorViewModel,
             ShellExecutor mainExecutor,
             StageCoordinator stageCoordinator) {
         mShellCommandHandler = shellCommandHandler;
@@ -259,6 +271,8 @@
         mTransactionPool = transactionPool;
         mIconProvider = iconProvider;
         mRecentTasksOptional = Optional.of(recentTasks);
+        mLaunchAdjacentController = launchAdjacentController;
+        mWindowDecorViewModel = Optional.of(windowDecorViewModel);
         mStageCoordinator = stageCoordinator;
         mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this);
         shellInit.addInitCallback(this::onInit, this);
@@ -291,13 +305,15 @@
             mStageCoordinator = createStageCoordinator();
         }
         mDragAndDropController.ifPresent(controller -> controller.setSplitScreenController(this));
+        mWindowDecorViewModel.ifPresent(viewModel -> viewModel.setSplitScreenController(this));
     }
 
     protected StageCoordinator createStageCoordinator() {
         return new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
                 mTaskOrganizer, mDisplayController, mDisplayImeController,
-                mDisplayInsetsController, mTransitions, mTransactionPool,
-                mIconProvider, mMainExecutor, mRecentTasksOptional);
+                mDisplayInsetsController, mTransitions, mTransactionPool, mIconProvider,
+                mMainExecutor, mRecentTasksOptional, mLaunchAdjacentController,
+                mWindowDecorViewModel);
     }
 
     @Override
@@ -537,6 +553,8 @@
             } else {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                         "Cancel entering split as not supporting multi-instances");
+                Log.w(TAG, splitFailureMessage("startShortcut",
+                        "app package " + packageName + " does not support multi-instance"));
                 Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
                         Toast.LENGTH_SHORT).show();
                 return;
@@ -566,6 +584,8 @@
                 taskId = INVALID_TASK_ID;
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                         "Cancel entering split as not supporting multi-instances");
+                Log.w(TAG, splitFailureMessage("startShortcutAndTaskWithLegacyTransition",
+                        "app package " + packageName1 + " does not support multi-instance"));
                 Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
                         Toast.LENGTH_SHORT).show();
             }
@@ -598,6 +618,8 @@
                 taskId = INVALID_TASK_ID;
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                         "Cancel entering split as not supporting multi-instances");
+                Log.w(TAG, splitFailureMessage("startShortcutAndTask",
+                        "app package " + packageName1 + " does not support multi-instance"));
                 Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
                         Toast.LENGTH_SHORT).show();
             }
@@ -633,6 +655,8 @@
                 taskId = INVALID_TASK_ID;
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                         "Cancel entering split as not supporting multi-instances");
+                Log.w(TAG, splitFailureMessage("startIntentAndTaskWithLegacyTransition",
+                        "app package " + packageName1 + " does not support multi-instance"));
                 Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
                         Toast.LENGTH_SHORT).show();
             }
@@ -663,6 +687,8 @@
                 taskId = INVALID_TASK_ID;
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                         "Cancel entering split as not supporting multi-instances");
+                Log.w(TAG, splitFailureMessage("startIntentAndTask",
+                        "app package " + packageName1 + " does not support multi-instance"));
                 Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
                         Toast.LENGTH_SHORT).show();
             }
@@ -691,6 +717,8 @@
                 pendingIntent2 = null;
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                         "Cancel entering split as not supporting multi-instances");
+                Log.w(TAG, splitFailureMessage("startIntentsWithLegacyTransition",
+                        "app package " + packageName1 + " does not support multi-instance"));
                 Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
                         Toast.LENGTH_SHORT).show();
             }
@@ -720,6 +748,8 @@
                 pendingIntent2 = null;
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                         "Cancel entering split as not supporting multi-instances");
+                Log.w(TAG, splitFailureMessage("startIntents",
+                        "app package " + packageName1 + " does not support multi-instance"));
                 Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
                         Toast.LENGTH_SHORT).show();
             }
@@ -766,6 +796,8 @@
             } else {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                         "Cancel entering split as not supporting multi-instances");
+                Log.w(TAG, splitFailureMessage("startIntent",
+                        "app package " + packageName1 + " does not support multi-instance"));
                 Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
                         Toast.LENGTH_SHORT).show();
                 return;
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 64f7598..6961dab 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
@@ -28,6 +28,7 @@
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.view.WindowManager.transitTypeToString;
@@ -41,6 +42,7 @@
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
 import static com.android.wm.shell.common.split.SplitScreenConstants.splitPositionToString;
 import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
+import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage;
 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;
@@ -121,6 +123,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.LaunchAdjacentController;
 import com.android.wm.shell.common.ScreenshotUtils;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
@@ -138,6 +141,7 @@
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.util.SplitBounds;
 import com.android.wm.shell.util.TransitionUtil;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -193,6 +197,8 @@
     // if user is opening another task(s).
     private final ArrayList<Integer> mPausingTasks = new ArrayList<>();
     private final Optional<RecentTasksController> mRecentTasks;
+    private final LaunchAdjacentController mLaunchAdjacentController;
+    private final Optional<WindowDecorViewModel> mWindowDecorViewModel;
 
     private final Rect mTempRect1 = new Rect();
     private final Rect mTempRect2 = new Rect();
@@ -270,7 +276,9 @@
             DisplayInsetsController displayInsetsController, Transitions transitions,
             TransactionPool transactionPool,
             IconProvider iconProvider, ShellExecutor mainExecutor,
-            Optional<RecentTasksController> recentTasks) {
+            Optional<RecentTasksController> recentTasks,
+            LaunchAdjacentController launchAdjacentController,
+            Optional<WindowDecorViewModel> windowDecorViewModel) {
         mContext = context;
         mDisplayId = displayId;
         mSyncQueue = syncQueue;
@@ -278,6 +286,8 @@
         mLogger = new SplitscreenEventLogger();
         mMainExecutor = mainExecutor;
         mRecentTasks = recentTasks;
+        mLaunchAdjacentController = launchAdjacentController;
+        mWindowDecorViewModel = windowDecorViewModel;
 
         taskOrganizer.createRootTask(displayId, WINDOWING_MODE_FULLSCREEN, this /* listener */);
 
@@ -288,7 +298,8 @@
                 mMainStageListener,
                 mSyncQueue,
                 mSurfaceSession,
-                iconProvider);
+                iconProvider,
+                mWindowDecorViewModel);
         mSideStage = new SideStage(
                 mContext,
                 mTaskOrganizer,
@@ -296,7 +307,8 @@
                 mSideStageListener,
                 mSyncQueue,
                 mSurfaceSession,
-                iconProvider);
+                iconProvider,
+                mWindowDecorViewModel);
         mDisplayController = displayController;
         mDisplayImeController = displayImeController;
         mDisplayInsetsController = displayInsetsController;
@@ -323,7 +335,9 @@
             DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
             Transitions transitions, TransactionPool transactionPool,
             ShellExecutor mainExecutor,
-            Optional<RecentTasksController> recentTasks) {
+            Optional<RecentTasksController> recentTasks,
+            LaunchAdjacentController launchAdjacentController,
+            Optional<WindowDecorViewModel> windowDecorViewModel) {
         mContext = context;
         mDisplayId = displayId;
         mSyncQueue = syncQueue;
@@ -340,6 +354,8 @@
         mLogger = new SplitscreenEventLogger();
         mMainExecutor = mainExecutor;
         mRecentTasks = recentTasks;
+        mLaunchAdjacentController = launchAdjacentController;
+        mWindowDecorViewModel = windowDecorViewModel;
         mDisplayController.addDisplayWindowListener(this);
         transitions.addHandler(this);
         mSplitUnsupportedToast = Toast.makeText(mContext,
@@ -459,6 +475,8 @@
                 if (isEnteringSplit && mSideStage.getChildCount() == 0) {
                     mMainExecutor.execute(() -> exitSplitScreen(
                             null /* childrenToTop */, EXIT_REASON_UNKNOWN));
+                    Log.w(TAG, splitFailureMessage("startShortcut",
+                            "side stage was not populated"));
                     mSplitUnsupportedToast.show();
                 }
 
@@ -546,6 +564,8 @@
                 if (isEnteringSplit && mSideStage.getChildCount() == 0) {
                     mMainExecutor.execute(() -> exitSplitScreen(
                             null /* childrenToTop */, EXIT_REASON_UNKNOWN));
+                    Log.w(TAG, splitFailureMessage("startIntentLegacy",
+                            "side stage was not populated"));
                     mSplitUnsupportedToast.show();
                 }
 
@@ -1076,6 +1096,8 @@
             mMainExecutor.execute(() ->
                     exitSplitScreen(mMainStage.getChildCount() == 0
                             ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
+            Log.w(TAG, splitFailureMessage("onRemoteAnimationFinishedOrCancelled",
+                    "main or side stage was not populated."));
             mSplitUnsupportedToast.show();
         } else {
             mSyncQueue.queue(evictWct);
@@ -1095,6 +1117,8 @@
         if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
             mMainExecutor.execute(() -> exitSplitScreen(mMainStage.getChildCount() == 0
                     ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
+            Log.w(TAG, splitFailureMessage("onRemoteAnimationFinished",
+                    "main or side stage was not populated"));
             mSplitUnsupportedToast.show();
             return;
         }
@@ -1279,20 +1303,12 @@
         final boolean mainStageVisible = mMainStage.mRootTaskInfo.isVisible;
         final boolean oneStageVisible =
                 mMainStage.mRootTaskInfo.isVisible != mSideStage.mRootTaskInfo.isVisible;
-        if (oneStageVisible) {
+        if (oneStageVisible && !ENABLE_SHELL_TRANSITIONS) {
             // Dismiss split because there's show-when-locked activity showing on top of keyguard.
             // Also make sure the task contains show-when-locked activity remains on top after split
             // dismissed.
-            if (!ENABLE_SHELL_TRANSITIONS) {
-                final StageTaskListener toTop = mainStageVisible ? mMainStage : mSideStage;
-                exitSplitScreen(toTop, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
-            } else {
-                final int dismissTop = mainStageVisible ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
-                final WindowContainerTransaction wct = new WindowContainerTransaction();
-                prepareExitSplitScreen(dismissTop, wct);
-                mSplitTransitions.startDismissTransition(wct, this, dismissTop,
-                        EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
-            }
+            final StageTaskListener toTop = mainStageVisible ? mMainStage : mSideStage;
+            exitSplitScreen(toTop, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
         }
     }
 
@@ -1547,6 +1563,8 @@
             // split bounds.
             wct.setSmallestScreenWidthDp(mMainStage.mRootTaskInfo.token,
                     SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
+            mSplitLayout.getInvisibleBounds(mTempRect1);
+            mSplitLayout.setTaskBounds(wct, mSideStage.mRootTaskInfo, mTempRect1);
         }
         wct.reorder(mRootTaskInfo.token, true);
         setRootForceTranslucent(false, wct);
@@ -1733,7 +1751,7 @@
         if (mRootTaskInfo == null || mRootTaskInfo.taskId != taskInfo.taskId) {
             throw new IllegalArgumentException(this + "\n Unknown task info changed: " + taskInfo);
         }
-
+        mWindowDecorViewModel.ifPresent(viewModel -> viewModel.onTaskInfoChanged(taskInfo));
         mRootTaskInfo = taskInfo;
         if (mSplitLayout != null
                 && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)
@@ -1781,7 +1799,6 @@
         wct.reparent(mSideStage.mRootTaskInfo.token, mRootTaskInfo.token, true);
         // Make the stages adjacent to each other so they occlude what's behind them.
         wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
-        wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token);
         setRootForceTranslucent(true, wct);
         mSplitLayout.getInvisibleBounds(mTempRect1);
         wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
@@ -1789,6 +1806,7 @@
         mSyncQueue.runInSync(t -> {
             t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.top);
         });
+        mLaunchAdjacentController.setLaunchAdjacentRoot(mSideStage.mRootTaskInfo.token);
     }
 
     /** Callback when split roots have child task appeared under it, this is a little different from
@@ -1818,9 +1836,7 @@
 
     private void onRootTaskVanished() {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        if (mRootTaskInfo != null) {
-            wct.clearLaunchAdjacentFlagRoot(mRootTaskInfo.token);
-        }
+        mLaunchAdjacentController.clearLaunchAdjacentRoot();
         applyExitSplitScreen(null /* childrenToTop */, wct, EXIT_REASON_ROOT_TASK_VANISHED);
         mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, mSplitLayout);
     }
@@ -2364,6 +2380,15 @@
                     // so appends operations to exit split.
                     prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, out);
                 }
+            } else if (type == TRANSIT_KEYGUARD_OCCLUDE && triggerTask.topActivity != null
+                    && isSplitScreenVisible()) {
+                // Split include show when lock activity case, check the top activity under which
+                // stage and move it to the top.
+                int top = triggerTask.topActivity.equals(mMainStage.mRootTaskInfo.topActivity)
+                        ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
+                prepareExitSplitScreen(top, out);
+                mSplitTransitions.setDismissTransition(transition, top,
+                        EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
             }
 
             // When split in the background, it should be only opening/dismissing transition and
@@ -2692,12 +2717,13 @@
             }
         } else {
             if (mainChild == null || sideChild == null) {
-                Log.w(TAG, "Launched 2 tasks in split, but didn't receive"
-                        + " 2 tasks in transition. Possibly one of them failed to launch");
                 final int dismissTop = mainChild != null ? STAGE_TYPE_MAIN :
                         (sideChild != null ? STAGE_TYPE_SIDE : STAGE_TYPE_UNDEFINED);
                 mSplitTransitions.mPendingEnter.cancel(
                         (cancelWct, cancelT) -> prepareExitSplitScreen(dismissTop, cancelWct));
+                Log.w(TAG, splitFailureMessage("startPendingEnterAnimation",
+                        "launched 2 tasks in split, but didn't receive "
+                        + "2 tasks in transition. Possibly one of them failed to launch"));
                 mSplitUnsupportedToast.show();
                 return true;
             }
@@ -3157,7 +3183,7 @@
         }
 
         @Override
-        public void onNoLongerSupportMultiWindow() {
+        public void onNoLongerSupportMultiWindow(ActivityManager.RunningTaskInfo taskInfo) {
             if (mMainStage.isActive()) {
                 final boolean isMainStage = mMainStageListener == this;
                 if (!ENABLE_SHELL_TRANSITIONS) {
@@ -3172,6 +3198,9 @@
                 prepareExitSplitScreen(stageType, wct);
                 mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType,
                         EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
+                Log.w(TAG, splitFailureMessage("onNoLongerSupportMultiWindow",
+                        "app package " + taskInfo.baseActivity.getPackageName()
+                        + " does not support splitscreen, or is a controlled activity type"));
                 mSplitUnsupportedToast.show();
             }
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index a01eddb..e2c55e4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -51,8 +51,10 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.split.SplitDecorManager;
 import com.android.wm.shell.splitscreen.SplitScreen.StageType;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
 import java.io.PrintWriter;
+import java.util.Optional;
 import java.util.function.Predicate;
 
 /**
@@ -79,7 +81,7 @@
 
         void onRootTaskVanished();
 
-        void onNoLongerSupportMultiWindow();
+        void onNoLongerSupportMultiWindow(ActivityManager.RunningTaskInfo taskInfo);
     }
 
     private final Context mContext;
@@ -87,6 +89,7 @@
     private final SurfaceSession mSurfaceSession;
     private final SyncTransactionQueue mSyncQueue;
     private final IconProvider mIconProvider;
+    private final Optional<WindowDecorViewModel> mWindowDecorViewModel;
 
     protected ActivityManager.RunningTaskInfo mRootTaskInfo;
     protected SurfaceControl mRootLeash;
@@ -98,12 +101,14 @@
 
     StageTaskListener(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
             StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
-            SurfaceSession surfaceSession, IconProvider iconProvider) {
+            SurfaceSession surfaceSession, IconProvider iconProvider,
+            Optional<WindowDecorViewModel> windowDecorViewModel) {
         mContext = context;
         mCallbacks = callbacks;
         mSyncQueue = syncQueue;
         mSurfaceSession = surfaceSession;
         mIconProvider = iconProvider;
+        mWindowDecorViewModel = windowDecorViewModel;
         taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
     }
 
@@ -202,6 +207,7 @@
     @Override
     @CallSuper
     public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+        mWindowDecorViewModel.ifPresent(viewModel -> viewModel.onTaskInfoChanged(taskInfo));
         if (mRootTaskInfo.taskId == taskInfo.taskId) {
             // Inflates split decor view only when the root task is visible.
             if (!ENABLE_SHELL_TRANSITIONS && mRootTaskInfo.isVisible != taskInfo.isVisible) {
@@ -220,7 +226,7 @@
                     taskInfo.getWindowingMode())) {
                 // Leave split screen if the task no longer supports multi window or have
                 // uncontrolled task.
-                mCallbacks.onNoLongerSupportMultiWindow();
+                mCallbacks.onNoLongerSupportMultiWindow(taskInfo);
                 return;
             }
             mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java
index 27d520d..a2301b1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java
@@ -27,6 +27,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.LaunchAdjacentController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.SystemWindows;
@@ -58,6 +59,7 @@
     private final TransactionPool mTransactionPool;
     private final IconProvider mIconProvider;
     private final Optional<RecentTasksController> mRecentTasksOptional;
+    private final LaunchAdjacentController mLaunchAdjacentController;
 
     private final Handler mMainHandler;
     private final SystemWindows mSystemWindows;
@@ -77,13 +79,15 @@
             TransactionPool transactionPool,
             IconProvider iconProvider,
             Optional<RecentTasksController> recentTasks,
+            LaunchAdjacentController launchAdjacentController,
             ShellExecutor mainExecutor,
             Handler mainHandler,
             SystemWindows systemWindows) {
         super(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer,
                 syncQueue, rootTDAOrganizer, displayController, displayImeController,
                 displayInsetsController, dragAndDropController, transitions, transactionPool,
-                iconProvider, recentTasks, mainExecutor);
+                iconProvider, recentTasks, launchAdjacentController, Optional.empty(),
+                mainExecutor);
 
         mTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
@@ -96,6 +100,7 @@
         mTransactionPool = transactionPool;
         mIconProvider = iconProvider;
         mRecentTasksOptional = recentTasks;
+        mLaunchAdjacentController = launchAdjacentController;
 
         mMainHandler = mainHandler;
         mSystemWindows = systemWindows;
@@ -111,7 +116,7 @@
                 mTaskOrganizer, mDisplayController, mDisplayImeController,
                 mDisplayInsetsController, mTransitions, mTransactionPool,
                 mIconProvider, mMainExecutor, mMainHandler,
-                mRecentTasksOptional, mSystemWindows);
+                mRecentTasksOptional, mLaunchAdjacentController, mSystemWindows);
     }
 
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java
index 4d563fb..7947691 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java
@@ -24,6 +24,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.LaunchAdjacentController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.SystemWindows;
@@ -51,10 +52,11 @@
             IconProvider iconProvider, ShellExecutor mainExecutor,
             Handler mainHandler,
             Optional<RecentTasksController> recentTasks,
+            LaunchAdjacentController launchAdjacentController,
             SystemWindows systemWindows) {
         super(context, displayId, syncQueue, taskOrganizer, displayController, displayImeController,
                 displayInsetsController, transitions, transactionPool, iconProvider,
-                mainExecutor, recentTasks);
+                mainExecutor, recentTasks, launchAdjacentController, Optional.empty());
 
         mTvSplitMenuController = new TvSplitMenuController(context, this,
                 systemWindows, mainHandler);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
index 4faa929..0d77a2e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
@@ -26,6 +26,7 @@
 import android.content.Intent;
 import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
+import android.graphics.Insets;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.view.SurfaceControl;
@@ -69,8 +70,10 @@
     private final Rect mTmpRect = new Rect();
     private final Rect mTmpRootRect = new Rect();
     private final int[] mTmpLocation = new int[2];
+    private final Rect mBoundsOnScreen = new Rect();
     private final TaskViewTaskController mTaskViewTaskController;
     private Region mObscuredTouchRegion;
+    private Insets mCaptionInsets;
 
     public TaskView(Context context, TaskViewTaskController taskViewTaskController) {
         super(context, null, 0, 0, true /* disableBackgroundLayer */);
@@ -169,6 +172,25 @@
     }
 
     /**
+     * Sets a region of the task to inset to allow for a caption bar. Currently only top insets
+     * are supported.
+     * <p>
+     * This region will be factored in as an area of taskview that is not touchable activity
+     * content (i.e. you don't need to additionally set {@link #setObscuredTouchRect(Rect)} for
+     * the caption area).
+     *
+     * @param captionInsets the insets to apply to task view.
+     */
+    public void setCaptionInsets(Insets captionInsets) {
+        mCaptionInsets = captionInsets;
+        if (captionInsets == null) {
+            // If captions are null we can set them now; otherwise they'll get set in
+            // onComputeInternalInsets.
+            mTaskViewTaskController.setCaptionInsets(null);
+        }
+    }
+
+    /**
      * Call when view position or size has changed. Do not call when animating.
      */
     public void onLocationChanged() {
@@ -230,6 +252,15 @@
         getLocationInWindow(mTmpLocation);
         mTmpRect.set(mTmpLocation[0], mTmpLocation[1],
                 mTmpLocation[0] + getWidth(), mTmpLocation[1] + getHeight());
+        if (mCaptionInsets != null) {
+            mTmpRect.inset(mCaptionInsets);
+            getBoundsOnScreen(mBoundsOnScreen);
+            mTaskViewTaskController.setCaptionInsets(new Rect(
+                    mBoundsOnScreen.left,
+                    mBoundsOnScreen.top,
+                    mBoundsOnScreen.right + getWidth(),
+                    mBoundsOnScreen.top + mCaptionInsets.top));
+        }
         inoutInfo.touchableRegion.op(mTmpRect, Region.Op.DIFFERENCE);
 
         if (mObscuredTouchRegion != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
index 163cf50..064af04 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
@@ -33,6 +33,7 @@
 import android.util.CloseGuard;
 import android.util.Slog;
 import android.view.SurfaceControl;
+import android.view.WindowInsets;
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
@@ -82,6 +83,10 @@
     private TaskView.Listener mListener;
     private Executor mListenerExecutor;
 
+    /** Used to inset the activity content to allow space for a caption bar. */
+    private final Binder mCaptionInsetsOwner = new Binder();
+    private Rect mCaptionInsets;
+
     public TaskViewTaskController(Context context, ShellTaskOrganizer organizer,
             TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) {
         mContext = context;
@@ -436,6 +441,32 @@
         mTaskViewTransitions.closeTaskView(wct, this);
     }
 
+    /**
+     * Sets a region of the task to inset to allow for a caption bar.
+     *
+     * @param captionInsets the rect for the insets in screen coordinates.
+     */
+    void setCaptionInsets(Rect captionInsets) {
+        if (mCaptionInsets != null && mCaptionInsets.equals(captionInsets)) {
+            return;
+        }
+        mCaptionInsets = captionInsets;
+        applyCaptionInsetsIfNeeded();
+    }
+
+    void applyCaptionInsetsIfNeeded() {
+        if (mTaskToken == null) return;
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        if (mCaptionInsets != null) {
+            wct.addInsetsSource(mTaskToken, mCaptionInsetsOwner, 0,
+                    WindowInsets.Type.captionBar(), mCaptionInsets);
+        } else {
+            wct.removeInsetsSource(mTaskToken, mCaptionInsetsOwner, 0,
+                    WindowInsets.Type.captionBar());
+        }
+        mTaskOrganizer.applyTransaction(wct);
+    }
+
     /** Should be called when the client surface is destroyed. */
     public void surfaceDestroyed() {
         mSurfaceCreated = false;
@@ -564,6 +595,7 @@
             mTaskViewTransitions.updateBoundsState(this, boundsOnScreen);
             mTaskViewTransitions.updateVisibilityState(this, true /* visible */);
             wct.setBounds(mTaskToken, boundsOnScreen);
+            applyCaptionInsetsIfNeeded();
         } else {
             // The surface has already been destroyed before the task has appeared,
             // so go ahead and hide the task entirely
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
index 5baf2e3..16f0e39 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
@@ -202,15 +202,10 @@
         if (taskView == null) return null;
         // Opening types should all be initiated by shell
         if (!TransitionUtil.isClosingType(request.getType())) return null;
-        PendingTransition pending = findPendingCloseTransition(taskView);
-        if (pending == null) {
-            pending = new PendingTransition(request.getType(), null, taskView, null /* cookie */);
-        }
-        if (pending.mClaimed != null) {
-            throw new IllegalStateException("Task is closing in 2 collecting transitions?"
-                    + " This state doesn't make sense");
-        }
+        PendingTransition pending = new PendingTransition(request.getType(), null,
+                taskView, null /* cookie */);
         pending.mClaimed = transition;
+        mPending.add(pending);
         return new WindowContainerTransaction();
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index d0a361a..d9edde1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -44,6 +44,9 @@
 
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.common.split.SplitScreenUtils;
+import com.android.wm.shell.desktopmode.DesktopModeController;
+import com.android.wm.shell.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.phone.PipTouchHandler;
@@ -70,6 +73,8 @@
     private RecentsTransitionHandler mRecentsHandler;
     private StageCoordinator mSplitHandler;
     private final KeyguardTransitionHandler mKeyguardHandler;
+    private DesktopModeController mDesktopModeController;
+    private DesktopTasksController mDesktopTasksController;
     private UnfoldTransitionHandler mUnfoldHandler;
 
     private static class MixedTransition {
@@ -87,8 +92,11 @@
         /** Keyguard exit/occlude/unocclude transition. */
         static final int TYPE_KEYGUARD = 5;
 
+        /** Recents Transition while in desktop mode. */
+        static final int TYPE_RECENTS_DURING_DESKTOP = 6;
+
         /** Fuld/Unfold transition. */
-        static final int TYPE_UNFOLD = 6;
+        static final int TYPE_UNFOLD = 7;
 
         /** The default animation for this mixed transition. */
         static final int ANIM_TYPE_DEFAULT = 0;
@@ -142,6 +150,8 @@
             Optional<PipTouchHandler> pipTouchHandlerOptional,
             Optional<RecentsTransitionHandler> recentsHandlerOptional,
             KeyguardTransitionHandler keyguardHandler,
+            Optional<DesktopModeController> desktopModeControllerOptional,
+            Optional<DesktopTasksController> desktopTasksControllerOptional,
             Optional<UnfoldTransitionHandler> unfoldHandler) {
         mPlayer = player;
         mKeyguardHandler = keyguardHandler;
@@ -159,6 +169,8 @@
                 if (mRecentsHandler != null) {
                     mRecentsHandler.addMixer(this);
                 }
+                mDesktopModeController = desktopModeControllerOptional.orElse(null);
+                mDesktopTasksController = desktopTasksControllerOptional.orElse(null);
                 mUnfoldHandler = unfoldHandler.orElse(null);
             }, this);
         }
@@ -239,7 +251,8 @@
 
     @Override
     public Transitions.TransitionHandler handleRecentsRequest(WindowContainerTransaction outWCT) {
-        if (mRecentsHandler != null && mSplitHandler.isSplitScreenVisible()) {
+        if (mRecentsHandler != null && (mSplitHandler.isSplitScreenVisible()
+                || DesktopModeStatus.isActive(mPlayer.getContext()))) {
             return this;
         }
         return null;
@@ -254,6 +267,13 @@
                     MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition);
             mixed.mLeftoversHandler = mRecentsHandler;
             mActiveTransitions.add(mixed);
+        } else if (DesktopModeStatus.isActive(mPlayer.getContext())) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
+                    + "desktop mode is active, so treat it as Mixed.");
+            final MixedTransition mixed = new MixedTransition(
+                    MixedTransition.TYPE_RECENTS_DURING_DESKTOP, transition);
+            mixed.mLeftoversHandler = mRecentsHandler;
+            mActiveTransitions.add(mixed);
         } else {
             throw new IllegalStateException("Accepted a recents transition but don't know how to"
                     + " handle it");
@@ -340,6 +360,9 @@
         } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
             return animateKeyguard(mixed, info, startTransaction, finishTransaction,
                     finishCallback);
+        } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) {
+            return animateRecentsDuringDesktop(mixed, info, startTransaction, finishTransaction,
+                    finishCallback);
         } else if (mixed.mType == MixedTransition.TYPE_UNFOLD) {
             return animateUnfold(mixed, info, startTransaction, finishTransaction, finishCallback);
         } else {
@@ -641,6 +664,30 @@
         return true;
     }
 
+    private boolean animateRecentsDuringDesktop(@NonNull final MixedTransition mixed,
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        boolean consumed = mRecentsHandler.startAnimation(
+                mixed.mTransition, info, startTransaction, finishTransaction, finishCallback);
+        if (!consumed) {
+            return false;
+        }
+        //Sync desktop mode state (proto 1)
+        if (mDesktopModeController != null) {
+            mDesktopModeController.syncSurfaceState(info, finishTransaction);
+            return true;
+        }
+        //Sync desktop mode state (proto 2)
+        if (mDesktopTasksController != null) {
+            mDesktopTasksController.syncSurfaceState(info, finishTransaction);
+            return true;
+        }
+
+        return false;
+    }
+
     private boolean animateUnfold(@NonNull final MixedTransition mixed,
             @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
@@ -727,6 +774,9 @@
                         finishCallback);
             } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
                 mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+            } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) {
+                mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
+                        finishCallback);
             } else if (mixed.mType == MixedTransition.TYPE_UNFOLD) {
                 mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
             } else {
@@ -754,6 +804,8 @@
             mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
         } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
             mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT);
+        } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) {
+            mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
         } else if (mixed.mType == MixedTransition.TYPE_UNFOLD) {
             mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 2327d86..7565996 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -27,6 +27,7 @@
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.view.WindowManager.fixScale;
+import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
 import static android.window.TransitionInfo.FLAG_IS_OCCLUDED;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
@@ -161,6 +162,10 @@
     public static final int TRANSIT_CANCEL_ENTERING_DESKTOP_MODE =
             WindowManager.TRANSIT_FIRST_CUSTOM + 13;
 
+    /** Transition type to animate the toggle resize between the max and default desktop sizes. */
+    public static final int TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE =
+            WindowManager.TRANSIT_FIRST_CUSTOM + 14;
+
     private final WindowOrganizer mOrganizer;
     private final Context mContext;
     private final ShellExecutor mMainExecutor;
@@ -724,11 +729,15 @@
         final int changeSize = info.getChanges().size();
         boolean taskChange = false;
         boolean transferStartingWindow = false;
+        int noAnimationBehindStartingWindow = 0;
         boolean allOccluded = changeSize > 0;
         for (int i = changeSize - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
             taskChange |= change.getTaskInfo() != null;
             transferStartingWindow |= change.hasFlags(FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT);
+            if (change.hasAllFlags(FLAG_IS_BEHIND_STARTING_WINDOW | FLAG_NO_ANIMATION)) {
+                noAnimationBehindStartingWindow++;
+            }
             if (!change.hasFlags(FLAG_IS_OCCLUDED)) {
                 allOccluded = false;
             }
@@ -736,9 +745,11 @@
         // There does not need animation when:
         // A. Transfer starting window. Apply transfer starting window directly if there is no other
         // task change. Since this is an activity->activity situation, we can detect it by selecting
-        // transitions with only 2 changes where neither are tasks and one is a starting-window
-        // recipient.
-        if (!taskChange && transferStartingWindow && changeSize == 2
+        // transitions with only 2 changes where
+        // 1. neither are tasks, and
+        // 2. one is a starting-window recipient, or all change is behind starting window.
+        if (!taskChange && (transferStartingWindow || noAnimationBehindStartingWindow == changeSize)
+                && changeSize == 2
                 // B. It's visibility change if the TRANSIT_TO_BACK/TO_FRONT happened when all
                 // changes are underneath another change.
                 || ((info.getType() == TRANSIT_TO_BACK || info.getType() == TRANSIT_TO_FRONT)
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 f81fc6f..6bba0d1 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
@@ -110,7 +110,7 @@
         for (int i = state.sourceSize() - 1; i >= 0; i--) {
             final InsetsSource source = state.sourceAt(i);
             if (source.getType() == WindowInsets.Type.navigationBars()
-                    && source.insetsRoundedCornerFrame()) {
+                    && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)) {
                 return source;
             }
         }
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 a4cf149..21994a9 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
@@ -50,11 +50,11 @@
 import com.android.wm.shell.unfold.UnfoldAnimationController;
 import com.android.wm.shell.unfold.UnfoldBackgroundController;
 
+import dagger.Lazy;
+
 import java.util.Optional;
 import java.util.concurrent.Executor;
 
-import dagger.Lazy;
-
 /**
  * This helper class contains logic that calculates scaling and cropping parameters
  * for the folding/unfolding animation. As an input it receives TaskInfo objects and
@@ -149,7 +149,7 @@
         for (int i = state.sourceSize() - 1; i >= 0; i--) {
             final InsetsSource source = state.sourceAt(i);
             if (source.getType() == WindowInsets.Type.navigationBars()
-                    && source.insetsRoundedCornerFrame()) {
+                    && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)) {
                 return source;
             }
         }
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
index 39fb793..cf16920 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -33,11 +33,14 @@
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
+import androidx.annotation.Nullable;
+
 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.splitscreen.SplitScreenController;
 import com.android.wm.shell.transition.Transitions;
 
 /**
@@ -89,6 +92,9 @@
     }
 
     @Override
+    public void setSplitScreenController(SplitScreenController splitScreenController) {}
+
+    @Override
     public boolean onTaskOpening(
             RunningTaskInfo taskInfo,
             SurfaceControl taskSurface,
@@ -187,7 +193,7 @@
 
         final DragPositioningCallback dragPositioningCallback =
                 new FluidResizeTaskPositioner(mTaskOrganizer, windowDecoration, mDisplayController,
-                        null /* disallowedAreaForEndBounds */);
+                        0 /* disallowedAreaForEndBoundsHeight */);
         final CaptionTouchEventListener touchEventListener =
                 new CaptionTouchEventListener(taskInfo, dragPositioningCallback);
         windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
@@ -254,7 +260,7 @@
          * @return {@code true} if a drag is happening; or {@code false} if it is not
          */
         @Override
-        public boolean handleMotionEvent(MotionEvent e) {
+        public boolean handleMotionEvent(@Nullable View v, MotionEvent e) {
             final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
             if (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
                 return false;
@@ -268,7 +274,10 @@
                     return false;
                 }
                 case MotionEvent.ACTION_MOVE: {
-                    int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+                    if (e.findPointerIndex(mDragPointerId) == -1) {
+                        mDragPointerId = e.getPointerId(0);
+                    }
+                    final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
                     mDragPositioningCallback.onDragPositioningMove(
                             e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
                     mIsDragging = true;
@@ -276,7 +285,10 @@
                 }
                 case MotionEvent.ACTION_UP:
                 case MotionEvent.ACTION_CANCEL: {
-                    int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+                    if (e.findPointerIndex(mDragPointerId) == -1) {
+                        mDragPointerId = e.getPointerId(0);
+                    }
+                    final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
                     mDragPositioningCallback.onDragPositioningEnd(
                             e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
                     final boolean wasDragging = mIsDragging;
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
index 116af70..ce81910 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -113,7 +113,7 @@
         mRelayoutParams.reset();
         mRelayoutParams.mRunningTaskInfo = taskInfo;
         mRelayoutParams.mLayoutResId = R.layout.caption_window_decor;
-        mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
+        mRelayoutParams.mCaptionHeightId = getCaptionHeightId();
         mRelayoutParams.mShadowRadiusId = shadowRadiusID;
         mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
 
@@ -143,8 +143,11 @@
                     mHandler,
                     mChoreographer,
                     mDisplay.getDisplayId(),
+                    0 /* taskCornerRadius */,
                     mDecorationContainerSurface,
-                    mDragPositioningCallback);
+                    mDragPositioningCallback,
+                    mSurfaceControlBuilderSupplier,
+                    mSurfaceControlTransactionSupplier);
         }
 
         final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext())
@@ -221,4 +224,9 @@
         closeDragResizeListener();
         super.close();
     }
+
+    @Override
+    int getCaptionHeightId() {
+        return R.dimen.freeform_decor_caption_height;
+    }
 }
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 9fd57d7..7245bc9 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
@@ -19,6 +19,8 @@
 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 static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 
 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;
@@ -42,6 +44,7 @@
 import android.os.Looper;
 import android.util.SparseArray;
 import android.view.Choreographer;
+import android.view.GestureDetector;
 import android.view.InputChannel;
 import android.view.InputEvent;
 import android.view.InputEventReceiver;
@@ -53,6 +56,7 @@
 import android.view.WindowManager;
 import android.window.TransitionInfo;
 import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -106,7 +110,7 @@
     private final Supplier<SurfaceControl.Transaction> mTransactionFactory;
     private final Transitions mTransitions;
 
-    private Optional<SplitScreenController> mSplitScreenController;
+    private SplitScreenController mSplitScreenController;
 
     private ValueAnimator mDragToDesktopValueAnimator;
     private final Rect mDragToDesktopAnimationStartBounds = new Rect();
@@ -121,8 +125,7 @@
             SyncTransactionQueue syncQueue,
             Transitions transitions,
             Optional<DesktopModeController> desktopModeController,
-            Optional<DesktopTasksController> desktopTasksController,
-            Optional<SplitScreenController> splitScreenController) {
+            Optional<DesktopTasksController> desktopTasksController) {
         this(
                 context,
                 mainHandler,
@@ -133,7 +136,6 @@
                 transitions,
                 desktopModeController,
                 desktopTasksController,
-                splitScreenController,
                 new DesktopModeWindowDecoration.Factory(),
                 new InputMonitorFactory(),
                 SurfaceControl.Transaction::new);
@@ -150,7 +152,6 @@
             Transitions transitions,
             Optional<DesktopModeController> desktopModeController,
             Optional<DesktopTasksController> desktopTasksController,
-            Optional<SplitScreenController> splitScreenController,
             DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
             InputMonitorFactory inputMonitorFactory,
             Supplier<SurfaceControl.Transaction> transactionFactory) {
@@ -160,7 +161,6 @@
         mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
         mTaskOrganizer = taskOrganizer;
         mDisplayController = displayController;
-        mSplitScreenController = splitScreenController;
         mSyncQueue = syncQueue;
         mTransitions = transitions;
         mDesktopModeController = desktopModeController;
@@ -177,6 +177,11 @@
     }
 
     @Override
+    public void setSplitScreenController(SplitScreenController splitScreenController) {
+        mSplitScreenController = splitScreenController;
+    }
+
+    @Override
     public boolean onTaskOpening(
             ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl taskSurface,
@@ -193,7 +198,10 @@
             @NonNull TransitionInfo info,
             @NonNull TransitionInfo.Change change) {
         if (change.getMode() == WindowManager.TRANSIT_CHANGE
-                && (info.getType() == Transitions.TRANSIT_ENTER_DESKTOP_MODE)) {
+                && (info.getType() == Transitions.TRANSIT_ENTER_DESKTOP_MODE
+                || info.getType() == Transitions.TRANSIT_CANCEL_ENTERING_DESKTOP_MODE
+                || info.getType() == Transitions.TRANSIT_EXIT_DESKTOP_MODE
+                || info.getType() == Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE)) {
             mWindowDecorByTaskId.get(change.getTaskInfo().taskId)
                     .addTransitionPausingRelayout(transition);
         }
@@ -236,7 +244,6 @@
             SurfaceControl.Transaction startT,
             SurfaceControl.Transaction finishT) {
         final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
-
         if (!shouldShowWindowDecor(taskInfo)) {
             if (decoration != null) {
                 destroyWindowDecoration(taskInfo);
@@ -275,15 +282,17 @@
         }
     }
 
-    private class DesktopModeTouchEventListener implements
-            View.OnClickListener, View.OnTouchListener, DragDetector.MotionEventHandler {
+    private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
+            implements View.OnClickListener, View.OnTouchListener, DragDetector.MotionEventHandler {
 
         private final int mTaskId;
         private final WindowContainerToken mTaskToken;
         private final DragPositioningCallback mDragPositioningCallback;
         private final DragDetector mDragDetector;
+        private final GestureDetector mGestureDetector;
 
         private boolean mIsDragging;
+        private boolean mShouldClick;
         private int mDragPointerId = -1;
 
         private DesktopModeTouchEventListener(
@@ -293,6 +302,7 @@
             mTaskToken = taskInfo.token;
             mDragPositioningCallback = dragPositioningCallback;
             mDragDetector = new DragDetector(this);
+            mGestureDetector = new GestureDetector(mContext, this);
         }
 
         @Override
@@ -301,14 +311,14 @@
             final int id = v.getId();
             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()
+                if (mSplitScreenController != null
+                        && mSplitScreenController.isSplitScreenVisible()) {
+                    int remainingTaskPosition = mTaskId == mSplitScreenController
                             .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT).taskId
                             ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT;
-                    ActivityManager.RunningTaskInfo remainingTask = mSplitScreenController.get()
+                    ActivityManager.RunningTaskInfo remainingTask = mSplitScreenController
                             .getTaskInfo(remainingTaskPosition);
-                    mSplitScreenController.get().moveTaskToFullscreen(remainingTask.taskId);
+                    mSplitScreenController.moveTaskToFullscreen(remainingTask.taskId);
                 }
             } else if (id == R.id.back_button) {
                 mTaskOperations.injectBackKey();
@@ -321,7 +331,13 @@
                 }
             } else if (id == R.id.desktop_button) {
                 mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true));
-                mDesktopTasksController.ifPresent(c -> c.moveToDesktop(mTaskId));
+                if (mDesktopTasksController.isPresent()) {
+                    final WindowContainerTransaction wct = new WindowContainerTransaction();
+                    // App sometimes draws before the insets from WindowDecoration#relayout have
+                    // been added, so they must be added here
+                    mWindowDecorByTaskId.get(mTaskId).addCaptionInset(wct);
+                    mDesktopTasksController.get().moveToDesktop(mTaskId, wct);
+                }
                 decoration.closeHandleMenu();
             } else if (id == R.id.fullscreen_button) {
                 mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(false));
@@ -347,7 +363,7 @@
                 return false;
             }
             moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId));
-            return mDragDetector.onMotionEvent(e);
+            return mDragDetector.onMotionEvent(v, e);
         }
 
         private void moveTaskToFront(RunningTaskInfo taskInfo) {
@@ -362,7 +378,7 @@
          * @return {@code true} if the motion event is handled.
          */
         @Override
-        public boolean handleMotionEvent(MotionEvent e) {
+        public boolean handleMotionEvent(@Nullable View v, MotionEvent e) {
             final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
             if (DesktopModeStatus.isProto2Enabled()
                     && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
@@ -373,6 +389,9 @@
                     == WINDOWING_MODE_FULLSCREEN) {
                 return false;
             }
+            if (mGestureDetector.onTouchEvent(e)) {
+                return true;
+            }
             switch (e.getActionMasked()) {
                 case MotionEvent.ACTION_DOWN: {
                     mDragPointerId = e.getPointerId(0);
@@ -380,21 +399,38 @@
                             0 /* ctrlType */, e.getRawX(0),
                             e.getRawY(0));
                     mIsDragging = false;
-                    return false;
+                    mShouldClick = true;
+                    return true;
                 }
                 case MotionEvent.ACTION_MOVE: {
                     final DesktopModeWindowDecoration decoration =
                             mWindowDecorByTaskId.get(mTaskId);
+                    if (e.findPointerIndex(mDragPointerId) == -1) {
+                        mDragPointerId = e.getPointerId(0);
+                    }
                     final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
                     mDesktopTasksController.ifPresent(c -> c.onDragPositioningMove(taskInfo,
                             decoration.mTaskSurface, e.getRawY(dragPointerIdx)));
                     mDragPositioningCallback.onDragPositioningMove(
                             e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
                     mIsDragging = true;
+                    mShouldClick = false;
                     return true;
                 }
                 case MotionEvent.ACTION_UP:
                 case MotionEvent.ACTION_CANCEL: {
+                    final boolean wasDragging = mIsDragging;
+                    if (!wasDragging) {
+                        if (mShouldClick && v != null) {
+                            v.performClick();
+                            mShouldClick = false;
+                            return true;
+                        }
+                        return false;
+                    }
+                    if (e.findPointerIndex(mDragPointerId) == -1) {
+                        mDragPointerId = e.getPointerId(0);
+                    }
                     final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
                     // Position of the task is calculated by subtracting the raw location of the
                     // motion event (the location of the motion relative to the display) by the
@@ -405,14 +441,22 @@
                     mDragPositioningCallback.onDragPositioningEnd(
                             e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
                     mDesktopTasksController.ifPresent(c -> c.onDragPositioningEnd(taskInfo,
-                            position));
-                    final boolean wasDragging = mIsDragging;
+                            position, e.getRawY(), mWindowDecorByTaskId.get(mTaskId)));
                     mIsDragging = false;
-                    return wasDragging;
+                    return true;
                 }
             }
             return true;
         }
+
+        @Override
+        public boolean onDoubleTap(@NonNull MotionEvent e) {
+            final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+            mDesktopTasksController.ifPresent(c -> {
+                c.toggleDesktopTaskSize(taskInfo, mWindowDecorByTaskId.get(taskInfo.taskId));
+            });
+            return true;
+        }
     }
 
     // InputEventReceiver to listen for touch input outside of caption bounds
@@ -540,9 +584,11 @@
                             relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds());
                     boolean dragFromStatusBarAllowed = false;
                     if (DesktopModeStatus.isProto2Enabled()) {
-                        // In proto2 any full screen task can be dragged to freeform
-                        dragFromStatusBarAllowed = relevantDecor.mTaskInfo.getWindowingMode()
-                                == WINDOWING_MODE_FULLSCREEN;
+                        // In proto2 any full screen or multi-window task can be dragged to
+                        // freeform.
+                        final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode();
+                        dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN
+                                || windowingMode == WINDOWING_MODE_MULTI_WINDOW;
                     }
 
                     if (dragFromStatusBarAllowed && relevantDecor.checkTouchEventInHandle(ev)) {
@@ -571,9 +617,9 @@
                         return;
                     } else if (mDragToDesktopAnimationStarted) {
                         Point position = new Point((int) ev.getX(), (int) ev.getY());
+                        relevantDecor.incrementRelayoutBlock();
                         mDesktopTasksController.ifPresent(
-                                c -> c.cancelMoveToFreeform(relevantDecor.mTaskInfo,
-                                        position));
+                                c -> c.cancelMoveToFreeform(relevantDecor.mTaskInfo, position));
                         mDragToDesktopAnimationStarted = false;
                         return;
                     }
@@ -699,8 +745,7 @@
 
     @Nullable
     private DesktopModeWindowDecoration getRelevantWindowDecor(MotionEvent ev) {
-        if (mSplitScreenController.isPresent()
-                && mSplitScreenController.get().isSplitScreenVisible()) {
+        if (mSplitScreenController != null && mSplitScreenController.isSplitScreenVisible()) {
             // We can't look at focused task here as only one task will have focus.
             return getSplitScreenDecor(ev);
         } else {
@@ -711,9 +756,9 @@
     @Nullable
     private DesktopModeWindowDecoration getSplitScreenDecor(MotionEvent ev) {
         ActivityManager.RunningTaskInfo topOrLeftTask =
-                mSplitScreenController.get().getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
+                mSplitScreenController.getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
         ActivityManager.RunningTaskInfo bottomOrRightTask =
-                mSplitScreenController.get().getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
+                mSplitScreenController.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);
@@ -765,11 +810,12 @@
 
     private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
         if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true;
-        if (mSplitScreenController.isPresent()
-                && mSplitScreenController.get().isTaskRootOrStageRoot(taskInfo.taskId)) {
+        if (mSplitScreenController != null
+                && mSplitScreenController.isTaskRootOrStageRoot(taskInfo.taskId)) {
             return false;
         }
         return DesktopModeStatus.isProto2Enabled()
+                && taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED
                 && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
                 && mDisplayController.getDisplayContext(taskInfo.displayId)
                 .getResources().getConfiguration().smallestScreenWidthDp >= 600;
@@ -796,9 +842,10 @@
                         mMainChoreographer,
                         mSyncQueue);
         mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
+        windowDecoration.createResizeVeil();
 
         final DragPositioningCallback dragPositioningCallback = createDragPositioningCallback(
-                windowDecoration, taskInfo);
+                windowDecoration);
         final DesktopModeTouchEventListener touchEventListener =
                 new DesktopModeTouchEventListener(taskInfo, dragPositioningCallback);
 
@@ -811,20 +858,17 @@
         incrementEventReceiverTasks(taskInfo.displayId);
     }
     private DragPositioningCallback createDragPositioningCallback(
-            @NonNull DesktopModeWindowDecoration windowDecoration,
-            @NonNull RunningTaskInfo taskInfo) {
-        final int screenWidth = mDisplayController.getDisplayLayout(taskInfo.displayId).width();
-        final Rect disallowedAreaForEndBounds = new Rect(0, 0, screenWidth,
-                getStatusBarHeight(taskInfo.displayId));
+            @NonNull DesktopModeWindowDecoration windowDecoration) {
+        final int transitionAreaHeight = mContext.getResources().getDimensionPixelSize(
+                R.dimen.desktop_mode_transition_area_height);
         if (!DesktopModeStatus.isVeiledResizeEnabled()) {
             return new FluidResizeTaskPositioner(mTaskOrganizer, windowDecoration,
-                    mDisplayController, disallowedAreaForEndBounds, mDragStartListener,
-                    mTransactionFactory);
+                    mDisplayController, mDragStartListener, mTransactionFactory,
+                    transitionAreaHeight);
         } else {
-            windowDecoration.createResizeVeil();
             return new VeiledResizeTaskPositioner(mTaskOrganizer, windowDecoration,
-                    mDisplayController, disallowedAreaForEndBounds, mDragStartListener,
-                    mTransitions);
+                    mDisplayController, mDragStartListener, mTransitions,
+                    transitionAreaHeight);
         }
     }
 
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 ce11b26..a359395 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
@@ -23,7 +23,6 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
-import android.content.res.TypedArray;
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -39,6 +38,7 @@
 import android.view.ViewConfiguration;
 import android.window.WindowContainerTransaction;
 
+import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
@@ -178,15 +178,12 @@
         mRelayoutParams.reset();
         mRelayoutParams.mRunningTaskInfo = taskInfo;
         mRelayoutParams.mLayoutResId = windowDecorLayoutId;
-        mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
+        mRelayoutParams.mCaptionHeightId = getCaptionHeightId();
         mRelayoutParams.mShadowRadiusId = shadowRadiusID;
         mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
 
-        final TypedArray ta = mContext.obtainStyledAttributes(
-                new int[]{android.R.attr.dialogCornerRadius});
-        mRelayoutParams.mCornerRadius = ta.getDimensionPixelSize(0, 0);
-        ta.recycle();
-
+        mRelayoutParams.mCornerRadius =
+                (int) ScreenDecorationsUtils.getWindowCornerRadius(mContext);
         relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
         // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
 
@@ -235,8 +232,11 @@
                     mHandler,
                     mChoreographer,
                     mDisplay.getDisplayId(),
+                    mRelayoutParams.mCornerRadius,
                     mDecorationContainerSurface,
-                    mDragPositioningCallback);
+                    mDragPositioningCallback,
+                    mSurfaceControlBuilderSupplier,
+                    mSurfaceControlTransactionSupplier);
         }
 
         final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext())
@@ -294,23 +294,37 @@
     }
 
     /**
-     * Fade in the resize veil
+     * Show the resize veil.
      */
-    void showResizeVeil(Rect taskBounds) {
+    public void showResizeVeil(Rect taskBounds) {
         mResizeVeil.showVeil(mTaskSurface, taskBounds);
     }
 
     /**
+     * Show the resize veil.
+     */
+    public void showResizeVeil(SurfaceControl.Transaction tx, Rect taskBounds) {
+        mResizeVeil.showVeil(tx, mTaskSurface, taskBounds, false /* fadeIn */);
+    }
+
+    /**
      * Set new bounds for the resize veil
      */
-    void updateResizeVeil(Rect newBounds) {
+    public void updateResizeVeil(Rect newBounds) {
         mResizeVeil.updateResizeVeil(newBounds);
     }
 
     /**
+     * Set new bounds for the resize veil
+     */
+    public void updateResizeVeil(SurfaceControl.Transaction tx, Rect newBounds) {
+        mResizeVeil.updateResizeVeil(tx, newBounds);
+    }
+
+    /**
      * Fade the resize veil out.
      */
-    void hideResizeVeil() {
+    public void hideResizeVeil() {
         mResizeVeil.hideVeil();
     }
 
@@ -479,6 +493,11 @@
         }
     }
 
+    @Override
+    int getCaptionHeightId() {
+        return R.dimen.freeform_decor_caption_height;
+    }
+
     /**
      * Add transition to mTransitionsPausingRelayout
      */
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 65b5a7a..da26898 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
@@ -24,6 +24,9 @@
 
 import android.graphics.PointF;
 import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.Nullable;
 
 /**
  * A detector for touch inputs that differentiates between drag and click inputs. It receives a flow
@@ -54,14 +57,24 @@
      *
      * @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) {
+        return onMotionEvent(null /* view */, ev);
+    }
+
+    /**
+     * 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(View v, MotionEvent ev) {
         final boolean isTouchScreen =
                 (ev.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
         if (!isTouchScreen) {
             // Only touches generate noisy moves, so mouse/trackpad events don't need to filtered
             // to take the slop threshold into consideration.
-            return mEventHandler.handleMotionEvent(ev);
+            return mEventHandler.handleMotionEvent(v, ev);
         }
         switch (ev.getActionMasked()) {
             case ACTION_DOWN: {
@@ -69,12 +82,15 @@
                 float rawX = ev.getRawX(0);
                 float rawY = ev.getRawY(0);
                 mInputDownPoint.set(rawX, rawY);
-                mResultOfDownAction = mEventHandler.handleMotionEvent(ev);
+                mResultOfDownAction = mEventHandler.handleMotionEvent(v, ev);
                 return mResultOfDownAction;
             }
             case ACTION_MOVE: {
+                if (ev.findPointerIndex(mDragPointerId) == -1) {
+                    mDragPointerId = ev.getPointerId(0);
+                }
+                final int dragPointerIndex = ev.findPointerIndex(mDragPointerId);
                 if (!mIsDragEvent) {
-                    int dragPointerIndex = ev.findPointerIndex(mDragPointerId);
                     float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x;
                     float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y;
                     // Touches generate noisy moves, so only once the move is past the touch
@@ -84,7 +100,7 @@
                 // The event handler should only be notified about 'move' events if a drag has been
                 // detected.
                 if (mIsDragEvent) {
-                    return mEventHandler.handleMotionEvent(ev);
+                    return mEventHandler.handleMotionEvent(v, ev);
                 } else {
                     return mResultOfDownAction;
                 }
@@ -92,10 +108,10 @@
             case ACTION_UP:
             case ACTION_CANCEL: {
                 resetState();
-                return mEventHandler.handleMotionEvent(ev);
+                return mEventHandler.handleMotionEvent(v, ev);
             }
             default:
-                return mEventHandler.handleMotionEvent(ev);
+                return mEventHandler.handleMotionEvent(v, ev);
         }
     }
 
@@ -111,6 +127,6 @@
     }
 
     interface MotionEventHandler {
-        boolean handleMotionEvent(MotionEvent ev);
+        boolean handleMotionEvent(@Nullable View v, MotionEvent ev);
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
index 4e98f0c..941617d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
@@ -22,7 +22,9 @@
  * Callback called when receiving drag-resize or drag-move related input events.
  */
 public interface DragPositioningCallback {
-    @IntDef({CTRL_TYPE_UNDEFINED, CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM})
+    @IntDef(flag = true, value = {
+            CTRL_TYPE_UNDEFINED, CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM
+    })
     @interface CtrlType {}
 
     int CTRL_TYPE_UNDEFINED = 0;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
index 09e29bc..e32bd42 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -83,8 +83,6 @@
         // Make sure the new resizing destination in any direction falls within the stable bounds.
         // If not, set the bounds back to the old location that was valid to avoid conflicts with
         // some regions such as the gesture area.
-        displayController.getDisplayLayout(windowDecoration.mDisplay.getDisplayId())
-                .getStableBounds(stableBounds);
         if ((ctrlType & CTRL_TYPE_LEFT) != 0) {
             final int candidateLeft = repositionTaskBounds.left + (int) delta.x;
             repositionTaskBounds.left = (candidateLeft > stableBounds.left)
@@ -136,7 +134,7 @@
                 repositionTaskBounds.top);
     }
 
-    static void updateTaskBounds(Rect repositionTaskBounds, Rect taskBoundsAtDragStart,
+    private static void updateTaskBounds(Rect repositionTaskBounds, Rect taskBoundsAtDragStart,
             PointF repositionStartPoint, float x, float y) {
         final float deltaX = x - repositionStartPoint.x;
         final float deltaY = y - repositionStartPoint.y;
@@ -145,6 +143,23 @@
     }
 
     /**
+     * Updates repositionTaskBounds to the final bounds of the task after the drag is finished. If
+     * the bounds are outside of the stable bounds, they are shifted to place task at the top of the
+     * stable bounds.
+     */
+    static void onDragEnd(Rect repositionTaskBounds, Rect taskBoundsAtDragStart, Rect stableBounds,
+            PointF repositionStartPoint, float x, float y)  {
+        updateTaskBounds(repositionTaskBounds, taskBoundsAtDragStart, repositionStartPoint,
+                x, y);
+
+        // If task is outside of stable bounds (in the status bar area), shift the task down.
+        if (stableBounds.top > repositionTaskBounds.top) {
+            final int yShift =  stableBounds.top - repositionTaskBounds.top;
+            repositionTaskBounds.offset(0, yShift);
+        }
+    }
+
+    /**
      * Apply a bounds change to a task.
      * @param windowDecoration decor of task we are changing bounds for
      * @param taskBounds new bounds of this task
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 287d861..7c6fb99 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
@@ -18,8 +18,11 @@
 
 import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
+import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.WindowManager.LayoutParams.TYPE_INPUT_CONSUMER;
 
 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM;
 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT;
@@ -42,11 +45,14 @@
 import android.view.MotionEvent;
 import android.view.PointerIcon;
 import android.view.SurfaceControl;
+import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.WindowManagerGlobal;
 
 import com.android.internal.view.BaseIWindow;
 
+import java.util.function.Supplier;
+
 /**
  * An input event listener registered to InputDispatcher to receive input events on task edges and
  * and corners. Converts them to drag resize requests.
@@ -55,11 +61,11 @@
  */
 class DragResizeInputListener implements AutoCloseable {
     private static final String TAG = "DragResizeInputListener";
-
     private final IWindowSession mWindowSession = WindowManagerGlobal.getWindowSession();
     private final Handler mHandler;
     private final Choreographer mChoreographer;
     private final InputManager mInputManager;
+    private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier;
 
     private final int mDisplayId;
     private final BaseIWindow mFakeWindow;
@@ -69,10 +75,15 @@
     private final TaskResizeInputEventReceiver mInputEventReceiver;
     private final DragPositioningCallback mCallback;
 
+    private final SurfaceControl mInputSinkSurface;
+    private final BaseIWindow mFakeSinkWindow;
+    private final InputChannel mSinkInputChannel;
+
     private int mTaskWidth;
     private int mTaskHeight;
     private int mResizeHandleThickness;
     private int mCornerSize;
+    private int mTaskCornerRadius;
 
     private Rect mLeftTopCornerBounds;
     private Rect mRightTopCornerBounds;
@@ -87,15 +98,20 @@
             Handler handler,
             Choreographer choreographer,
             int displayId,
+            int taskCornerRadius,
             SurfaceControl decorationSurface,
-            DragPositioningCallback callback) {
+            DragPositioningCallback callback,
+            Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
+            Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier) {
         mInputManager = context.getSystemService(InputManager.class);
         mHandler = handler;
         mChoreographer = choreographer;
+        mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier;
         mDisplayId = displayId;
+        mTaskCornerRadius = taskCornerRadius;
         mDecorationSurface = decorationSurface;
-        // Use a fake window as the backing surface is a container layer and we don't want to create
-        // a buffer layer for it so we can't use ViewRootImpl.
+        // Use a fake window as the backing surface is a container layer, and we don't want to
+        // create a buffer layer for it, so we can't use ViewRootImpl.
         mFakeWindow = new BaseIWindow();
         mFakeWindow.setSession(mWindowSession);
         mFocusGrantToken = new Binder();
@@ -108,7 +124,7 @@
                     null /* hostInputToken */,
                     FLAG_NOT_FOCUSABLE,
                     PRIVATE_FLAG_TRUSTED_OVERLAY,
-                    0 /* inputFeatures */,
+                    INPUT_FEATURE_SPY,
                     TYPE_APPLICATION,
                     null /* windowToken */,
                     mFocusGrantToken,
@@ -123,15 +139,39 @@
         mCallback = callback;
         mDragDetector = new DragDetector(mInputEventReceiver);
         mDragDetector.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop());
+
+        mInputSinkSurface = surfaceControlBuilderSupplier.get()
+                .setName("TaskInputSink of " + decorationSurface)
+                .setContainerLayer()
+                .setParent(mDecorationSurface)
+                .build();
+        mSurfaceControlTransactionSupplier.get()
+                .setLayer(mInputSinkSurface, WindowDecoration.INPUT_SINK_Z_ORDER)
+                .show(mInputSinkSurface)
+                .apply();
+        mFakeSinkWindow = new BaseIWindow();
+        mSinkInputChannel = new InputChannel();
+        try {
+            mWindowSession.grantInputChannel(
+                    mDisplayId,
+                    mInputSinkSurface,
+                    mFakeSinkWindow,
+                    null /* hostInputToken */,
+                    FLAG_NOT_FOCUSABLE,
+                    0 /* privateFlags */,
+                    INPUT_FEATURE_NO_INPUT_CHANNEL,
+                    TYPE_INPUT_CONSUMER,
+                    null /* windowToken */,
+                    mFocusGrantToken,
+                    "TaskInputSink of " + decorationSurface,
+                    mSinkInputChannel);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
     }
 
     /**
-     * Updates geometry of this drag resize handler. Needs to be called every time there is a size
-     * change to notify the input event receiver it's ready to take the next input event. Otherwise
-     * it'll keep batching move events and the drag resize process is stalled.
-     *
-     * This is also used to update the touch regions of this handler every event dispatched here is
-     * a potential resize request.
+     * Updates the geometry (the touch region) of this drag resize handler.
      *
      * @param taskWidth The width of the task.
      * @param taskHeight The height of the task.
@@ -221,7 +261,35 @@
                     mDecorationSurface,
                     FLAG_NOT_FOCUSABLE,
                     PRIVATE_FLAG_TRUSTED_OVERLAY,
-                    0 /* inputFeatures */,
+                    INPUT_FEATURE_SPY,
+                    touchRegion);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+
+        mSurfaceControlTransactionSupplier.get()
+                .setWindowCrop(mInputSinkSurface, mTaskWidth, mTaskHeight)
+                .apply();
+        // The touch region of the TaskInputSink should be the touch region of this
+        // DragResizeInputHandler minus the task bounds. Pilfering events isn't enough to prevent
+        // input windows from handling down events, which will bring tasks in the back to front.
+        //
+        // Note not the entire touch region responds to both mouse and touchscreen events.
+        // Therefore, in the region that only responds to one of them, it would be a no-op to
+        // perform a gesture in the other type of events. We currently only have a mouse-only region
+        // out of the task bounds, and due to the roughness of touchscreen events, it's not a severe
+        // issue. However, were there touchscreen-only a region out of the task bounds, mouse
+        // gestures will become no-op in that region, even though the mouse gestures may appear to
+        // be performed on the input window behind the resize handle.
+        touchRegion.op(0, 0, mTaskWidth, mTaskHeight, Region.Op.DIFFERENCE);
+        try {
+            mWindowSession.updateInputChannel(
+                    mSinkInputChannel.getToken(),
+                    mDisplayId,
+                    mInputSinkSurface,
+                    FLAG_NOT_FOCUSABLE,
+                    0 /* privateFlags */,
+                    INPUT_FEATURE_NO_INPUT_CHANNEL,
                     touchRegion);
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
@@ -250,6 +318,16 @@
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
+
+        mSinkInputChannel.dispose();
+        try {
+            mWindowSession.remove(mFakeSinkWindow);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+        mSurfaceControlTransactionSupplier.get()
+                .remove(mInputSinkSurface)
+                .apply();
     }
 
     private class TaskResizeInputEventReceiver extends InputEventReceiver
@@ -258,6 +336,7 @@
         private final Runnable mConsumeBatchEventRunnable;
         private boolean mConsumeBatchEventScheduled;
         private boolean mShouldHandleEvents;
+        private int mLastCursorType = PointerIcon.TYPE_DEFAULT;
 
         private TaskResizeInputEventReceiver(
                 InputChannel inputChannel, Handler handler, Choreographer choreographer) {
@@ -303,7 +382,7 @@
         }
 
         @Override
-        public boolean handleMotionEvent(MotionEvent e) {
+        public boolean handleMotionEvent(View v, 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.
@@ -318,6 +397,8 @@
                         mShouldHandleEvents = isInResizeHandleBounds(x, y);
                     }
                     if (mShouldHandleEvents) {
+                        mInputManager.pilferPointers(mInputChannel.getToken());
+
                         mDragPointerId = e.getPointerId(0);
                         float rawX = e.getRawX(0);
                         float rawY = e.getRawY(0);
@@ -357,7 +438,6 @@
                     break;
                 }
                 case MotionEvent.ACTION_HOVER_EXIT:
-                    mInputManager.setPointerIconType(PointerIcon.TYPE_DEFAULT);
                     result = true;
                     break;
             }
@@ -383,19 +463,71 @@
         @DragPositioningCallback.CtrlType
         private int calculateResizeHandlesCtrlType(float x, float y) {
             int ctrlType = 0;
-            if (x < 0) {
+            // mTaskCornerRadius is only used in comparing with corner regions. Comparisons with
+            // sides will use the bounds specified in setGeometry and not go into task bounds.
+            if (x < mTaskCornerRadius) {
                 ctrlType |= CTRL_TYPE_LEFT;
             }
-            if (x > mTaskWidth) {
+            if (x > mTaskWidth - mTaskCornerRadius) {
                 ctrlType |= CTRL_TYPE_RIGHT;
             }
-            if (y < 0) {
+            if (y < mTaskCornerRadius) {
                 ctrlType |= CTRL_TYPE_TOP;
             }
-            if (y > mTaskHeight) {
+            if (y > mTaskHeight - mTaskCornerRadius) {
                 ctrlType |= CTRL_TYPE_BOTTOM;
             }
-            return ctrlType;
+            // Check distances from the center if it's in one of four corners.
+            if ((ctrlType & (CTRL_TYPE_LEFT | CTRL_TYPE_RIGHT)) != 0
+                    && (ctrlType & (CTRL_TYPE_TOP | CTRL_TYPE_BOTTOM)) != 0) {
+                return checkDistanceFromCenter(ctrlType, x, y);
+            }
+            // Otherwise, we should make sure we don't resize tasks inside task bounds.
+            return (x < 0 || y < 0 || x >= mTaskWidth || y >= mTaskHeight) ? ctrlType : 0;
+        }
+
+        // If corner input is not within appropriate distance of corner radius, do not use it.
+        // If input is not on a corner or is within valid distance, return ctrlType.
+        @DragPositioningCallback.CtrlType
+        private int checkDistanceFromCenter(@DragPositioningCallback.CtrlType int ctrlType,
+                float x, float y) {
+            int centerX;
+            int centerY;
+
+            // Determine center of rounded corner circle; this is simply the corner if radius is 0.
+            switch (ctrlType) {
+                case CTRL_TYPE_LEFT | CTRL_TYPE_TOP: {
+                    centerX = mTaskCornerRadius;
+                    centerY = mTaskCornerRadius;
+                    break;
+                }
+                case CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM: {
+                    centerX = mTaskCornerRadius;
+                    centerY = mTaskHeight - mTaskCornerRadius;
+                    break;
+                }
+                case CTRL_TYPE_RIGHT | CTRL_TYPE_TOP: {
+                    centerX = mTaskWidth - mTaskCornerRadius;
+                    centerY = mTaskCornerRadius;
+                    break;
+                }
+                case CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM: {
+                    centerX = mTaskWidth - mTaskCornerRadius;
+                    centerY = mTaskHeight - mTaskCornerRadius;
+                    break;
+                }
+                default: {
+                    throw new IllegalArgumentException("ctrlType should be complex, but it's 0x"
+                            + Integer.toHexString(ctrlType));
+                }
+            }
+            double distanceFromCenter = Math.hypot(x - centerX, y - centerY);
+
+            if (distanceFromCenter < mTaskCornerRadius + mResizeHandleThickness
+                    && distanceFromCenter >= mTaskCornerRadius) {
+                return ctrlType;
+            }
+            return 0;
         }
 
         @DragPositioningCallback.CtrlType
@@ -439,7 +571,19 @@
                     cursorType = PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW;
                     break;
             }
-            mInputManager.setPointerIconType(cursorType);
+            // Only update the cursor type to default once so that views behind the decor container
+            // layer that aren't in the active resizing regions have chances to update the cursor
+            // type. We would like to enforce the cursor type by setting the cursor type multilple
+            // times in active regions because we shouldn't allow the views behind to change it, as
+            // we'll pilfer the gesture initiated in this area. This is necessary because 1) we
+            // should allow the views behind regions only for touches to set the cursor type; and 2)
+            // there is a small region out of each rounded corner that's inside the task bounds,
+            // where views in the task can receive input events because we can't set touch regions
+            // of input sinks to have rounded corners.
+            if (mLastCursorType != cursorType || cursorType != PointerIcon.TYPE_DEFAULT) {
+                mInputManager.setPointerIconType(cursorType);
+                mLastCursorType = cursorType;
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
index 9082323..e1b6db5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -21,8 +21,6 @@
 import android.view.SurfaceControl;
 import android.window.WindowContainerTransaction;
 
-import androidx.annotation.Nullable;
-
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 
@@ -42,28 +40,29 @@
     private final Rect mTaskBoundsAtDragStart = new Rect();
     private final PointF mRepositionStartPoint = new PointF();
     private final Rect mRepositionTaskBounds = new Rect();
-    // If a task move (not resize) finishes in this region, the positioner will not attempt to
+    // If a task move (not resize) finishes with the positions y less than this value, do not
     // finalize the bounds there using WCT#setBounds
-    private final Rect mDisallowedAreaForEndBounds;
+    private final int mDisallowedAreaForEndBoundsHeight;
     private boolean mHasDragResized;
     private int mCtrlType;
 
     FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration,
-            DisplayController displayController, @Nullable Rect disallowedAreaForEndBounds) {
-        this(taskOrganizer, windowDecoration, displayController, disallowedAreaForEndBounds,
-                dragStartListener -> {}, SurfaceControl.Transaction::new);
+            DisplayController displayController, int disallowedAreaForEndBoundsHeight) {
+        this(taskOrganizer, windowDecoration, displayController, dragStartListener -> {},
+                SurfaceControl.Transaction::new, disallowedAreaForEndBoundsHeight);
     }
 
     FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration,
-            DisplayController displayController, @Nullable Rect disallowedAreaForEndBounds,
+            DisplayController displayController,
             DragPositioningCallbackUtility.DragStartListener dragStartListener,
-            Supplier<SurfaceControl.Transaction> supplier) {
+            Supplier<SurfaceControl.Transaction> supplier,
+            int disallowedAreaForEndBoundsHeight) {
         mTaskOrganizer = taskOrganizer;
         mWindowDecoration = windowDecoration;
         mDisplayController = displayController;
-        mDisallowedAreaForEndBounds = new Rect(disallowedAreaForEndBounds);
         mDragStartListener = dragStartListener;
         mTransactionSupplier = supplier;
+        mDisallowedAreaForEndBoundsHeight = disallowedAreaForEndBoundsHeight;
     }
 
     @Override
@@ -79,6 +78,10 @@
             mTaskOrganizer.applyTransaction(wct);
         }
         mRepositionTaskBounds.set(mTaskBoundsAtDragStart);
+        if (mStableBounds.isEmpty()) {
+            mDisplayController.getDisplayLayout(mWindowDecoration.mDisplay.getDisplayId())
+                    .getStableBounds(mStableBounds);
+        }
     }
 
     @Override
@@ -121,10 +124,10 @@
             }
             mTaskOrganizer.applyTransaction(wct);
         } else if (mCtrlType == CTRL_TYPE_UNDEFINED
-                && !mDisallowedAreaForEndBounds.contains((int) x, (int) y)) {
+                && y > mDisallowedAreaForEndBoundsHeight) {
             final WindowContainerTransaction wct = new WindowContainerTransaction();
-            DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds,
-                    mTaskBoundsAtDragStart, mRepositionStartPoint, x, y);
+            DragPositioningCallbackUtility.onDragEnd(mRepositionTaskBounds,
+                    mTaskBoundsAtDragStart, mStableBounds, mRepositionStartPoint, x, y);
             wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
             mTaskOrganizer.applyTransaction(wct);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
index 8277109..bfce72b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
@@ -102,11 +102,17 @@
     }
 
     /**
-     * Animate veil's alpha to 1, fading it in.
+     * Shows the veil surface/view.
+     *
+     * @param t the transaction to apply in sync with the veil draw
+     * @param parentSurface the surface that the veil should be a child of
+     * @param taskBounds the bounds of the task that owns the veil
+     * @param fadeIn if true, the veil will fade-in with an animation, if false, it will be shown
+     *               immediately
      */
-    public void showVeil(SurfaceControl parentSurface, Rect taskBounds) {
+    public void showVeil(SurfaceControl.Transaction t, SurfaceControl parentSurface,
+            Rect taskBounds, boolean fadeIn) {
         // Parent surface can change, ensure it is up to date.
-        SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
         if (!parentSurface.equals(mParentSurface)) {
             t.reparent(mVeilSurface, parentSurface);
             mParentSurface = parentSurface;
@@ -115,22 +121,36 @@
         int backgroundColorId = getBackgroundColorId();
         mViewHost.getView().setBackgroundColor(mContext.getColor(backgroundColorId));
 
-        final ValueAnimator animator = new ValueAnimator();
-        animator.setFloatValues(0f, 1f);
-        animator.setDuration(RESIZE_ALPHA_DURATION);
-        animator.addUpdateListener(animation -> {
-            t.setAlpha(mVeilSurface, animator.getAnimatedFraction());
-            t.apply();
-        });
-
         relayout(taskBounds, t);
-        t.show(mVeilSurface)
-                .addTransactionCommittedListener(mContext.getMainExecutor(), () -> animator.start())
-                .setAlpha(mVeilSurface, 0);
+        if (fadeIn) {
+            final ValueAnimator animator = new ValueAnimator();
+            animator.setFloatValues(0f, 1f);
+            animator.setDuration(RESIZE_ALPHA_DURATION);
+            animator.addUpdateListener(animation -> {
+                t.setAlpha(mVeilSurface, animator.getAnimatedFraction());
+                t.apply();
+            });
+
+            t.show(mVeilSurface)
+                    .addTransactionCommittedListener(
+                            mContext.getMainExecutor(), () -> animator.start())
+                    .setAlpha(mVeilSurface, 0);
+        } else {
+            // Show the veil immediately at full opacity.
+            t.show(mVeilSurface).setAlpha(mVeilSurface, 1);
+        }
         mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t);
     }
 
     /**
+     * Animate veil's alpha to 1, fading it in.
+     */
+    public void showVeil(SurfaceControl parentSurface, Rect taskBounds) {
+        SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
+        showVeil(t, parentSurface, taskBounds, true /* fadeIn */);
+    }
+
+    /**
      * Update veil bounds to match bounds changes.
      * @param newBounds bounds to update veil to.
      */
@@ -147,6 +167,16 @@
      */
     public void updateResizeVeil(Rect newBounds) {
         SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
+        updateResizeVeil(t, newBounds);
+    }
+
+    /**
+     * Calls relayout to update task and veil bounds.
+     *
+     * @param t a transaction to be applied in sync with the veil draw.
+     * @param newBounds bounds to update veil to.
+     */
+    public void updateResizeVeil(SurfaceControl.Transaction t, Rect newBounds) {
         relayout(newBounds, t);
         mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index 58c78e6..ae3b5eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -53,33 +53,33 @@
     private final Rect mTaskBoundsAtDragStart = new Rect();
     private final PointF mRepositionStartPoint = new PointF();
     private final Rect mRepositionTaskBounds = new Rect();
-    // If a task move (not resize) finishes in this region, the positioner will not attempt to
+    // If a task move (not resize) finishes with the positions y less than this value, do not
     // finalize the bounds there using WCT#setBounds
-    private final Rect mDisallowedAreaForEndBounds = new Rect();
+    private final int mDisallowedAreaForEndBoundsHeight;
     private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
     private int mCtrlType;
 
     public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
             DesktopModeWindowDecoration windowDecoration, DisplayController displayController,
-            Rect disallowedAreaForEndBounds,
             DragPositioningCallbackUtility.DragStartListener dragStartListener,
-            Transitions transitions) {
-        this(taskOrganizer, windowDecoration, displayController, disallowedAreaForEndBounds,
-                dragStartListener, SurfaceControl.Transaction::new, transitions);
+            Transitions transitions,
+            int disallowedAreaForEndBoundsHeight) {
+        this(taskOrganizer, windowDecoration, displayController, dragStartListener,
+                SurfaceControl.Transaction::new, transitions, disallowedAreaForEndBoundsHeight);
     }
 
     public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
             DesktopModeWindowDecoration windowDecoration, DisplayController displayController,
-            Rect disallowedAreaForEndBounds,
             DragPositioningCallbackUtility.DragStartListener dragStartListener,
-            Supplier<SurfaceControl.Transaction> supplier, Transitions transitions) {
+            Supplier<SurfaceControl.Transaction> supplier, Transitions transitions,
+            int disallowedAreaForEndBoundsHeight) {
         mTaskOrganizer = taskOrganizer;
         mDesktopWindowDecoration = windowDecoration;
         mDisplayController = displayController;
         mDragStartListener = dragStartListener;
-        mDisallowedAreaForEndBounds.set(disallowedAreaForEndBounds);
         mTransactionSupplier = supplier;
         mTransitions = transitions;
+        mDisallowedAreaForEndBoundsHeight = disallowedAreaForEndBoundsHeight;
     }
 
     @Override
@@ -98,6 +98,10 @@
         }
         mDragStartListener.onDragStart(mDesktopWindowDecoration.mTaskInfo.taskId);
         mRepositionTaskBounds.set(mTaskBoundsAtDragStart);
+        if (mStableBounds.isEmpty()) {
+            mDisplayController.getDisplayLayout(mDesktopWindowDecoration.mDisplay.getDisplayId())
+                    .getStableBounds(mStableBounds);
+        }
     }
 
     @Override
@@ -110,8 +114,7 @@
         } else if (mCtrlType == CTRL_TYPE_UNDEFINED) {
             final SurfaceControl.Transaction t = mTransactionSupplier.get();
             DragPositioningCallbackUtility.setPositionOnDrag(mDesktopWindowDecoration,
-                    mRepositionTaskBounds, mTaskBoundsAtDragStart, mRepositionStartPoint, t,
-                    x, y);
+                    mRepositionTaskBounds, mTaskBoundsAtDragStart, mRepositionStartPoint, t, x, y);
             t.apply();
         }
     }
@@ -138,9 +141,9 @@
                 // won't be called.
                 mDesktopWindowDecoration.hideResizeVeil();
             }
-        } else if (!mDisallowedAreaForEndBounds.contains((int) x, (int) y)) {
-            DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds,
-                    mTaskBoundsAtDragStart, mRepositionStartPoint, x, y);
+        } else if (y > mDisallowedAreaForEndBoundsHeight) {
+            DragPositioningCallbackUtility.onDragEnd(mRepositionTaskBounds,
+                    mTaskBoundsAtDragStart, mStableBounds, mRepositionStartPoint, x, y);
             DragPositioningCallbackUtility.applyTaskBoundsChange(new WindowContainerTransaction(),
                     mDesktopWindowDecoration, mRepositionTaskBounds, mTaskOrganizer);
         }
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 9f03d9a..ae1a3d9 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
@@ -22,6 +22,7 @@
 import android.window.TransitionInfo;
 
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.splitscreen.SplitScreenController;
 
 /**
  * The interface used by some {@link com.android.wm.shell.ShellTaskOrganizer.TaskListener} to help
@@ -39,6 +40,11 @@
     void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter);
 
     /**
+     * Sets the {@link SplitScreenController} if available.
+     */
+    void setSplitScreenController(SplitScreenController splitScreenController);
+
+    /**
      * Creates a window decoration for the given task. Can be {@code null} for Fullscreen tasks but
      * not Freeform ones.
      *
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 ac5ff20..0b0d9d5 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
@@ -64,6 +64,24 @@
         implements AutoCloseable {
 
     /**
+     * The Z-order of {@link #mCaptionContainerSurface}.
+     * <p>
+     * We use {@link #mDecorationContainerSurface} to define input window for task resizing; by
+     * layering it in front of {@link #mCaptionContainerSurface}, we can allow it to handle input
+     * prior to caption view itself, treating corner inputs as resize events rather than
+     * repositioning.
+     */
+    static final int CAPTION_LAYER_Z_ORDER = -1;
+    /**
+     * The Z-order of the task input sink in {@link DragPositioningCallback}.
+     * <p>
+     * This task input sink is used to prevent undesired dispatching of motion events out of task
+     * bounds; by layering it behind {@link #mCaptionContainerSurface}, we allow captions to handle
+     * input events first.
+     */
+    static final int INPUT_SINK_Z_ORDER = -2;
+
+    /**
      * System-wide context. Only used to create context with overridden configurations.
      */
     final Context mContext;
@@ -237,7 +255,9 @@
 
         final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
         final int captionWidth = taskBounds.width();
+
         startT.setWindowCrop(mCaptionContainerSurface, captionWidth, captionHeight)
+                .setLayer(mCaptionContainerSurface, CAPTION_LAYER_Z_ORDER)
                 .show(mCaptionContainerSurface);
 
         if (ViewRootImpl.CAPTION_ON_SHELL) {
@@ -248,6 +268,9 @@
             mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + captionHeight + params.mCaptionY;
             wct.addInsetsSource(mTaskInfo.token,
                     mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect);
+            wct.addInsetsSource(mTaskInfo.token,
+                    mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures(),
+                    mCaptionInsetsRect);
         } else {
             startT.hide(mCaptionContainerSurface);
         }
@@ -264,6 +287,7 @@
                 .setColor(mTaskSurface, mTmpColor)
                 .show(mTaskSurface);
         finishT.setPosition(mTaskSurface, taskPosition.x, taskPosition.y)
+                .setShadowRadius(mTaskSurface, shadowRadius)
                 .setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight);
         if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
             startT.setCornerRadius(mTaskSurface, params.mCornerRadius);
@@ -301,6 +325,10 @@
         }
     }
 
+    int getCaptionHeightId() {
+        return Resources.ID_NULL;
+    }
+
     /**
      * Obtains the {@link Display} instance for the display ID in {@link #mTaskInfo} if it exists or
      * registers {@link #mOnDisplaysChangedListener} if it doesn't.
@@ -345,6 +373,8 @@
         final WindowContainerTransaction wct = mWindowContainerTransactionSupplier.get();
         wct.removeInsetsSource(mTaskInfo.token,
                 mOwner, 0 /* index */, WindowInsets.Type.captionBar());
+        wct.removeInsetsSource(mTaskInfo.token,
+                mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures());
         mTaskOrganizer.applyTransaction(wct);
     }
 
@@ -413,6 +443,21 @@
                 mSurfaceControlTransactionSupplier);
     }
 
+    /**
+     * Adds caption inset source to a WCT
+     */
+    public void addCaptionInset(WindowContainerTransaction wct) {
+        final int captionHeightId = getCaptionHeightId();
+        if (!ViewRootImpl.CAPTION_ON_SHELL || captionHeightId == Resources.ID_NULL) {
+            return;
+        }
+
+        final int captionHeight = loadDimensionPixelSize(mContext.getResources(), captionHeightId);
+        final Rect captionInsets = new Rect(0, 0, 0, captionHeight);
+        wct.addInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, WindowInsets.Type.captionBar(),
+                captionInsets);
+    }
+
     static class RelayoutParams {
         RunningTaskInfo mRunningTaskInfo;
         int mLayoutResId;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt
index 514ea52..d293cf7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt
@@ -1,28 +1,35 @@
 package com.android.wm.shell.windowdecor.viewholder
 
 import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.content.Context
 import android.graphics.Color
 import android.view.View
+import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
 
 /**
  * Encapsulates the root [View] of a window decoration and its children to facilitate looking up
  * children (via findViewById) and updating to the latest data from [RunningTaskInfo].
  */
 internal abstract class DesktopModeWindowDecorationViewHolder(rootView: View) {
-    val context: Context = rootView.context
+  val context: Context = rootView.context
 
-    /**
-     * A signal to the view holder that new data is available and that the views should be updated
-     * to reflect it.
-     */
-    abstract fun bindData(taskInfo: RunningTaskInfo)
+  /**
+   * A signal to the view holder that new data is available and that the views should be updated to
+   * reflect it.
+   */
+  abstract fun bindData(taskInfo: RunningTaskInfo)
 
-    /**
-     * Whether the caption items should use the 'light' color variant so that there's good contrast
-     * with the caption background color.
-     */
-    protected fun shouldUseLightCaptionColors(taskInfo: RunningTaskInfo): Boolean {
-        return Color.valueOf(taskInfo.taskDescription.statusBarColor).luminance() < 0.5
+  /**
+   * Whether the caption items should use the 'light' color variant so that there's good contrast
+   * with the caption background color.
+   */
+  protected fun shouldUseLightCaptionColors(taskInfo: RunningTaskInfo): Boolean {
+    return if (Color.alpha(taskInfo.taskDescription.statusBarColor) != 0 &&
+        taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
+      Color.valueOf(taskInfo.taskDescription.statusBarColor).luminance() < 0.5
+    } else {
+      taskInfo.taskDescription.statusBarAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0
     }
+  }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp
index b6696c7..89747df 100644
--- a/libs/WindowManager/Shell/tests/flicker/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/Android.bp
@@ -23,14 +23,40 @@
     default_applicable_licenses: ["frameworks_base_license"],
 }
 
-android_test {
-    name: "WMShellFlickerTests",
+filegroup {
+    name: "WMShellFlickerTestsBase-src",
+    srcs: ["src/com/android/wm/shell/flicker/*.kt"],
+}
+
+filegroup {
+    name: "WMShellFlickerTestsBubbles-src",
+    srcs: ["src/com/android/wm/shell/flicker/bubble/*.kt"],
+}
+
+filegroup {
+    name: "WMShellFlickerTestsPip-src",
+    srcs: ["src/com/android/wm/shell/flicker/pip/*.kt"],
+}
+
+filegroup {
+    name: "WMShellFlickerTestsSplitScreen-src",
     srcs: [
-        "src/**/*.java",
-        "src/**/*.kt",
+        "src/com/android/wm/shell/flicker/splitscreen/*.kt",
+        "src/com/android/wm/shell/flicker/splitscreen/benchmark/*.kt",
     ],
-    manifest: "AndroidManifest.xml",
-    test_config: "AndroidTest.xml",
+}
+
+filegroup {
+    name: "WMShellFlickerServiceTests-src",
+    srcs: [
+        "src/com/android/wm/shell/flicker/service/**/*.kt",
+    ],
+}
+
+java_defaults {
+    name: "WMShellFlickerTestsDefault",
+    manifest: "manifests/AndroidManifest.xml",
+    test_config_template: "AndroidTestTemplate.xml",
     platform_apis: true,
     certificate: "platform",
     optimize: {
@@ -40,16 +66,83 @@
     libs: ["android.test.runner"],
     static_libs: [
         "androidx.test.ext.junit",
+        "flickertestapplib",
         "flickerlib",
-        "flickerlib-apphelpers",
         "flickerlib-helpers",
-        "truth-prebuilt",
-        "app-helpers-core",
+        "platform-test-annotations",
+        "wm-flicker-common-app-helpers",
+        "wm-flicker-common-assertions",
         "launcher-helper-lib",
         "launcher-aosp-tapl",
-        "wm-flicker-common-assertions",
-        "wm-flicker-common-app-helpers",
-        "platform-test-annotations",
-        "flickertestapplib",
+    ],
+    data: [
+        ":FlickerTestApp",
+        "trace_config/*",
+    ],
+}
+
+android_test {
+    name: "WMShellFlickerTestsOther",
+    defaults: ["WMShellFlickerTestsDefault"],
+    additional_manifests: ["manifests/AndroidManifestOther.xml"],
+    package_name: "com.android.wm.shell.flicker",
+    instrumentation_target_package: "com.android.wm.shell.flicker",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    exclude_srcs: [
+        ":WMShellFlickerTestsBubbles-src",
+        ":WMShellFlickerTestsPip-src",
+        ":WMShellFlickerTestsSplitScreen-src",
+        ":WMShellFlickerServiceTests-src",
+    ],
+}
+
+android_test {
+    name: "WMShellFlickerTestsBubbles",
+    defaults: ["WMShellFlickerTestsDefault"],
+    additional_manifests: ["manifests/AndroidManifestBubbles.xml"],
+    package_name: "com.android.wm.shell.flicker.bubbles",
+    instrumentation_target_package: "com.android.wm.shell.flicker.bubbles",
+    srcs: [
+        ":WMShellFlickerTestsBase-src",
+        ":WMShellFlickerTestsBubbles-src",
+    ],
+}
+
+android_test {
+    name: "WMShellFlickerTestsPip",
+    defaults: ["WMShellFlickerTestsDefault"],
+    additional_manifests: ["manifests/AndroidManifestPip.xml"],
+    package_name: "com.android.wm.shell.flicker.pip",
+    instrumentation_target_package: "com.android.wm.shell.flicker.pip",
+    srcs: [
+        ":WMShellFlickerTestsBase-src",
+        ":WMShellFlickerTestsPip-src",
+    ],
+}
+
+android_test {
+    name: "WMShellFlickerTestsSplitScreen",
+    defaults: ["WMShellFlickerTestsDefault"],
+    additional_manifests: ["manifests/AndroidManifestSplitScreen.xml"],
+    package_name: "com.android.wm.shell.flicker.splitscreen",
+    instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen",
+    srcs: [
+        ":WMShellFlickerTestsBase-src",
+        ":WMShellFlickerTestsSplitScreen-src",
+    ],
+}
+
+android_test {
+    name: "WMShellFlickerServiceTests",
+    defaults: ["WMShellFlickerTestsDefault"],
+    additional_manifests: ["manifests/AndroidManifestSplitScreen.xml"],
+    package_name: "com.android.wm.shell.flicker.service",
+    instrumentation_target_package: "com.android.wm.shell.flicker.service",
+    srcs: [
+        ":WMShellFlickerTestsBase-src",
+        ":WMShellFlickerServiceTests-src",
     ],
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml
deleted file mode 100644
index 4721741..0000000
--- a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.wm.shell.flicker">
-
-    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
-    <!-- Read and write traces from external storage -->
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <!-- Allow the test to write directly to /sdcard/ -->
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
-    <!-- Write secure settings -->
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
-    <!-- Capture screen contents -->
-    <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" />
-    <!-- Enable / Disable tracing !-->
-    <uses-permission android:name="android.permission.DUMP" />
-    <!-- Run layers trace -->
-    <uses-permission android:name="android.permission.HARDWARE_TEST"/>
-    <!-- Capture screen recording -->
-    <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/>
-    <!-- Workaround grant runtime permission exception from b/152733071 -->
-    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
-    <uses-permission android:name="android.permission.READ_LOGS"/>
-    <!-- Force-stop test apps -->
-    <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/>
-    <!-- Control test app's media session -->
-    <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/>
-    <!-- ATM.removeRootTasksWithActivityTypes() -->
-    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" />
-    <!-- Enable bubble notification-->
-    <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
-
-    <!-- Allow the test to write directly to /sdcard/ -->
-    <application android:requestLegacyExternalStorage="true" android:largeHeap="true">
-        <uses-library android:name="android.test.runner"/>
-
-        <service android:name=".NotificationListener"
-                 android:exported="true"
-                 android:label="WMShellTestsNotificationListenerService"
-                 android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
-            <intent-filter>
-                <action android:name="android.service.notification.NotificationListenerService" />
-            </intent-filter>
-        </service>
-    </application>
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.wm.shell.flicker"
-                     android:label="WindowManager Shell Flicker Tests">
-    </instrumentation>
-</manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
deleted file mode 100644
index b5937ae..0000000
--- a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- * Copyright 2020 Google Inc. All Rights Reserved.
- -->
-<configuration description="Runs WindowManager Shell Flicker Tests">
-    <option name="test-tag" value="FlickerTests" />
-    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
-        <!-- keeps the screen on during tests -->
-        <option name="screen-always-on" value="on" />
-        <!-- prevents the phone from restarting -->
-        <option name="force-skip-system-props" value="true" />
-        <!-- set WM tracing verbose level to all -->
-        <option name="run-command" value="cmd window tracing level all" />
-        <!-- set WM tracing to frame (avoid incomplete states) -->
-        <option name="run-command" value="cmd window tracing frame" />
-        <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests -->
-        <option name="run-command" value="pm disable com.google.android.internal.betterbug" />
-        <!-- ensure lock screen mode is swipe -->
-        <option name="run-command" value="locksettings set-disabled false" />
-        <!-- restart launcher to activate TAPL -->
-        <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" />
-        <!-- Ensure output directory is empty at the start -->
-        <option name="run-command" value="rm -rf /sdcard/flicker" />
-        <!-- Increase trace size: 20mb for WM and 80mb for SF -->
-        <option name="run-command" value="cmd window tracing size 20480" />
-        <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920" />
-    </target_preparer>
-    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
-        <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1" />
-        <option name="run-command" value="settings put system show_touches 1" />
-        <option name="run-command" value="settings put system pointer_location 1" />
-        <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard" />
-        <option name="teardown-command" value="settings delete system show_touches" />
-        <option name="teardown-command" value="settings delete system pointer_location" />
-        <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural" />
-    </target_preparer>
-    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="cleanup-apks" value="true"/>
-        <option name="test-file-name" value="WMShellFlickerTests.apk"/>
-        <option name="test-file-name" value="FlickerTestApp.apk" />
-    </target_preparer>
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
-        <option name="package" value="com.android.wm.shell.flicker"/>
-        <option name="shell-timeout" value="6600s" />
-        <option name="test-timeout" value="6000s" />
-        <option name="hidden-api-checks" value="false" />
-    </test>
-    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
-        <option name="directory-keys" value="/sdcard/flicker" />
-        <option name="collect-on-run-ended-only" value="true" />
-        <option name="clean-up" value="true" />
-    </metrics_collector>
-</configuration>
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml
new file mode 100644
index 0000000..991d7b5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml
@@ -0,0 +1,99 @@
+<?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.
+  -->
+<configuration description="Runs WindowManager Shell Flicker Tests {MODULE}">
+    <option name="test-tag" value="FlickerTests"/>
+    <!-- Needed for storing the perfetto trace files in the sdcard/test_results-->
+    <option name="isolated-storage" value="false"/>
+
+    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+        <!-- keeps the screen on during tests -->
+        <option name="screen-always-on" value="on"/>
+        <!-- prevents the phone from restarting -->
+        <option name="force-skip-system-props" value="true"/>
+        <!-- set WM tracing verbose level to all -->
+        <option name="run-command" value="cmd window tracing level all"/>
+        <!-- set WM tracing to frame (avoid incomplete states) -->
+        <option name="run-command" value="cmd window tracing frame"/>
+        <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests -->
+        <option name="run-command" value="pm disable com.google.android.internal.betterbug"/>
+        <!-- ensure lock screen mode is swipe -->
+        <option name="run-command" value="locksettings set-disabled false"/>
+        <!-- restart launcher to activate TAPL -->
+        <option name="run-command"
+                value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher"/>
+        <!-- Increase trace size: 20mb for WM and 80mb for SF -->
+        <option name="run-command" value="cmd window tracing size 20480"/>
+        <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920"/>
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="test-user-token" value="%TEST_USER%"/>
+        <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+        <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
+        <option name="run-command" value="settings put system show_touches 1"/>
+        <option name="run-command" value="settings put system pointer_location 1"/>
+        <option name="teardown-command"
+                value="settings delete secure show_ime_with_hard_keyboard"/>
+        <option name="teardown-command" value="settings delete system show_touches"/>
+        <option name="teardown-command" value="settings delete system pointer_location"/>
+        <option name="teardown-command"
+                value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/>
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="{MODULE}.apk"/>
+        <option name="test-file-name" value="FlickerTestApp.apk"/>
+    </target_preparer>
+    <!-- Needed for pushing the trace config file -->
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="push-file"
+                key="trace_config.textproto"
+                value="/data/misc/perfetto-traces/trace_config.textproto"
+        />
+        <!--Install the content provider automatically when we push some file in sdcard folder.-->
+        <!--Needed to avoid the installation during the test suite.-->
+        <option name="push-file" key="trace_config.textproto" value="/sdcard/sample.textproto"/>
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="{PACKAGE}"/>
+        <option name="shell-timeout" value="6600s"/>
+        <option name="test-timeout" value="6000s"/>
+        <option name="hidden-api-checks" value="false"/>
+        <option name="device-listeners" value="android.device.collectors.PerfettoListener"/>
+        <!-- PerfettoListener related arguments -->
+        <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true"/>
+        <option name="instrumentation-arg"
+                key="perfetto_config_file"
+                value="trace_config.textproto"
+        />
+        <option name="instrumentation-arg" key="per_run" value="true"/>
+    </test>
+    <!-- Needed for pulling the collected trace config on to the host -->
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="pull-pattern-keys" value="perfetto_file_path"/>
+        <option name="directory-keys"
+                value="/data/user/0/com.android.wm.shell.flicker/files"/>
+        <option name="directory-keys"
+                value="/data/user/0/com.android.wm.shell.flicker.bubbles/files"/>
+        <option name="directory-keys"
+                value="/data/user/0/com.android.server.wm.flicker.pip/files"/>
+        <option name="directory-keys"
+                value="/data/user/0/com.android.server.wm.flicker.splitscreen/files"/>
+        <option name="collect-on-run-ended-only" value="true"/>
+        <option name="clean-up" value="true"/>
+    </metrics_collector>
+</configuration>
diff --git a/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifest.xml
new file mode 100644
index 0000000..6a87de4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifest.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="com.android.wm.shell.flicker">
+
+    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
+    <!-- Read and write traces from external storage -->
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <!-- Allow the test to write directly to /sdcard/ -->
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <!-- Write secure settings -->
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <!-- Capture screen contents -->
+    <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" />
+    <!-- Enable / Disable tracing !-->
+    <uses-permission android:name="android.permission.DUMP" />
+    <!-- Run layers trace -->
+    <uses-permission android:name="android.permission.HARDWARE_TEST"/>
+    <!-- Capture screen recording -->
+    <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/>
+    <!-- Workaround grant runtime permission exception from b/152733071 -->
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
+    <uses-permission android:name="android.permission.READ_LOGS"/>
+    <!-- Force-stop test apps -->
+    <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/>
+    <!-- Control test app's media session -->
+    <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/>
+    <!-- ATM.removeRootTasksWithActivityTypes() -->
+    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" />
+    <!-- Enable bubble notification-->
+    <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
+
+    <!-- Allow the test to write directly to /sdcard/ -->
+    <application android:requestLegacyExternalStorage="true" android:largeHeap="true">
+        <uses-library android:name="android.test.runner"/>
+
+        <service android:name=".NotificationListener"
+                 android:exported="true"
+                 android:label="WMShellTestsNotificationListenerService"
+                 android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+            <intent-filter>
+                <action android:name="android.service.notification.NotificationListenerService" />
+            </intent-filter>
+        </service>
+
+        <!-- (b/197936012) Remove startup provider due to test timeout issue -->
+        <provider
+            android:name="androidx.startup.InitializationProvider"
+            android:authorities="${applicationId}.androidx-startup"
+            tools:node="remove" />
+    </application>
+</manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestBubbles.xml b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestBubbles.xml
new file mode 100644
index 0000000..437871f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestBubbles.xml
@@ -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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.wm.shell.flicker.bubble">
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.wm.shell.flicker.bubble"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
+</manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestOther.xml b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestOther.xml
new file mode 100644
index 0000000..cf642f6
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestOther.xml
@@ -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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.wm.shell.flicker">
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.wm.shell.flicker"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
+</manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestPip.xml b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestPip.xml
new file mode 100644
index 0000000..5a8155a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestPip.xml
@@ -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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.wm.shell.flicker.pip">
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.wm.shell.flicker.pip"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
+</manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestSplitScreen.xml b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestSplitScreen.xml
new file mode 100644
index 0000000..887d8db
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestSplitScreen.xml
@@ -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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.wm.shell.flicker.splitscreen">
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.wm.shell.flicker.splitscreen"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
+</manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt
index e06e074..0f3e0f5 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt
@@ -19,14 +19,14 @@
 import android.app.Instrumentation
 import android.tools.device.flicker.junit.FlickerBuilderProvider
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.tapl.LauncherInstrumentation
 
 abstract class BaseBenchmarkTest
 @JvmOverloads
 constructor(
-    protected open val flicker: FlickerTest,
+    protected open val flicker: LegacyFlickerTest,
     protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
     protected val tapl: LauncherInstrumentation = LauncherInstrumentation()
 ) {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
index c98c5a0..d2fe9fe 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
@@ -18,7 +18,7 @@
 
 import android.app.Instrumentation
 import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.tapl.LauncherInstrumentation
 
@@ -30,7 +30,7 @@
 abstract class BaseTest
 @JvmOverloads
 constructor(
-    override val flicker: FlickerTest,
+    override val flicker: LegacyFlickerTest,
     instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
     tapl: LauncherInstrumentation = LauncherInstrumentation()
 ) : BaseBenchmarkTest(flicker, instrumentation, tapl), ICommonAssertions
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
index 798cc95..9cc03a5 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
@@ -23,18 +23,18 @@
 import android.tools.common.flicker.subject.layers.LayerTraceEntrySubject
 import android.tools.common.flicker.subject.layers.LayersTraceSubject
 import android.tools.common.traces.component.IComponentMatcher
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.helpers.WindowUtils
 
-fun FlickerTest.appPairsDividerIsVisibleAtEnd() {
+fun LegacyFlickerTest.appPairsDividerIsVisibleAtEnd() {
     assertLayersEnd { this.isVisible(APP_PAIR_SPLIT_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTest.appPairsDividerIsInvisibleAtEnd() {
+fun LegacyFlickerTest.appPairsDividerIsInvisibleAtEnd() {
     assertLayersEnd { this.notContains(APP_PAIR_SPLIT_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTest.appPairsDividerBecomesVisible() {
+fun LegacyFlickerTest.appPairsDividerBecomesVisible() {
     assertLayers {
         this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
             .then()
@@ -42,7 +42,7 @@
     }
 }
 
-fun FlickerTest.splitScreenEntered(
+fun LegacyFlickerTest.splitScreenEntered(
     component1: IComponentMatcher,
     component2: IComponentMatcher,
     fromOtherApp: Boolean,
@@ -69,7 +69,7 @@
     splitScreenDividerIsVisibleAtEnd()
 }
 
-fun FlickerTest.splitScreenDismissed(
+fun LegacyFlickerTest.splitScreenDismissed(
     component1: IComponentMatcher,
     component2: IComponentMatcher,
     toHome: Boolean
@@ -87,27 +87,27 @@
     splitScreenDividerIsInvisibleAtEnd()
 }
 
-fun FlickerTest.splitScreenDividerIsVisibleAtStart() {
+fun LegacyFlickerTest.splitScreenDividerIsVisibleAtStart() {
     assertLayersStart { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTest.splitScreenDividerIsVisibleAtEnd() {
+fun LegacyFlickerTest.splitScreenDividerIsVisibleAtEnd() {
     assertLayersEnd { this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTest.splitScreenDividerIsInvisibleAtStart() {
+fun LegacyFlickerTest.splitScreenDividerIsInvisibleAtStart() {
     assertLayersStart { this.isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTest.splitScreenDividerIsInvisibleAtEnd() {
+fun LegacyFlickerTest.splitScreenDividerIsInvisibleAtEnd() {
     assertLayersEnd { this.isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTest.splitScreenDividerBecomesVisible() {
+fun LegacyFlickerTest.splitScreenDividerBecomesVisible() {
     layerBecomesVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
 }
 
-fun FlickerTest.splitScreenDividerBecomesInvisible() {
+fun LegacyFlickerTest.splitScreenDividerBecomesInvisible() {
     assertLayers {
         this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
             .then()
@@ -115,23 +115,23 @@
     }
 }
 
-fun FlickerTest.layerBecomesVisible(component: IComponentMatcher) {
+fun LegacyFlickerTest.layerBecomesVisible(component: IComponentMatcher) {
     assertLayers { this.isInvisible(component).then().isVisible(component) }
 }
 
-fun FlickerTest.layerBecomesInvisible(component: IComponentMatcher) {
+fun LegacyFlickerTest.layerBecomesInvisible(component: IComponentMatcher) {
     assertLayers { this.isVisible(component).then().isInvisible(component) }
 }
 
-fun FlickerTest.layerIsVisibleAtEnd(component: IComponentMatcher) {
+fun LegacyFlickerTest.layerIsVisibleAtEnd(component: IComponentMatcher) {
     assertLayersEnd { this.isVisible(component) }
 }
 
-fun FlickerTest.layerKeepVisible(component: IComponentMatcher) {
+fun LegacyFlickerTest.layerKeepVisible(component: IComponentMatcher) {
     assertLayers { this.isVisible(component) }
 }
 
-fun FlickerTest.splitAppLayerBoundsBecomesVisible(
+fun LegacyFlickerTest.splitAppLayerBoundsBecomesVisible(
     component: IComponentMatcher,
     landscapePosLeft: Boolean,
     portraitPosTop: Boolean
@@ -150,7 +150,7 @@
     }
 }
 
-fun FlickerTest.splitAppLayerBoundsBecomesVisibleByDrag(component: IComponentMatcher) {
+fun LegacyFlickerTest.splitAppLayerBoundsBecomesVisibleByDrag(component: IComponentMatcher) {
     assertLayers {
         this.notContains(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component), isOptional = true)
             .then()
@@ -161,7 +161,7 @@
     }
 }
 
-fun FlickerTest.splitAppLayerBoundsBecomesInvisible(
+fun LegacyFlickerTest.splitAppLayerBoundsBecomesInvisible(
     component: IComponentMatcher,
     landscapePosLeft: Boolean,
     portraitPosTop: Boolean
@@ -180,7 +180,7 @@
     }
 }
 
-fun FlickerTest.splitAppLayerBoundsIsVisibleAtEnd(
+fun LegacyFlickerTest.splitAppLayerBoundsIsVisibleAtEnd(
     component: IComponentMatcher,
     landscapePosLeft: Boolean,
     portraitPosTop: Boolean
@@ -195,7 +195,7 @@
     }
 }
 
-fun FlickerTest.splitAppLayerBoundsKeepVisible(
+fun LegacyFlickerTest.splitAppLayerBoundsKeepVisible(
     component: IComponentMatcher,
     landscapePosLeft: Boolean,
     portraitPosTop: Boolean
@@ -210,7 +210,7 @@
     }
 }
 
-fun FlickerTest.splitAppLayerBoundsChanges(
+fun LegacyFlickerTest.splitAppLayerBoundsChanges(
     component: IComponentMatcher,
     landscapePosLeft: Boolean,
     portraitPosTop: Boolean
@@ -304,7 +304,7 @@
     }
 }
 
-fun FlickerTest.appWindowBecomesVisible(component: IComponentMatcher) {
+fun LegacyFlickerTest.appWindowBecomesVisible(component: IComponentMatcher) {
     assertWm {
         this.isAppWindowInvisible(component)
             .then()
@@ -316,39 +316,39 @@
     }
 }
 
-fun FlickerTest.appWindowBecomesInvisible(component: IComponentMatcher) {
+fun LegacyFlickerTest.appWindowBecomesInvisible(component: IComponentMatcher) {
     assertWm { this.isAppWindowVisible(component).then().isAppWindowInvisible(component) }
 }
 
-fun FlickerTest.appWindowIsVisibleAtStart(component: IComponentMatcher) {
+fun LegacyFlickerTest.appWindowIsVisibleAtStart(component: IComponentMatcher) {
     assertWmStart { this.isAppWindowVisible(component) }
 }
 
-fun FlickerTest.appWindowIsVisibleAtEnd(component: IComponentMatcher) {
+fun LegacyFlickerTest.appWindowIsVisibleAtEnd(component: IComponentMatcher) {
     assertWmEnd { this.isAppWindowVisible(component) }
 }
 
-fun FlickerTest.appWindowIsInvisibleAtStart(component: IComponentMatcher) {
+fun LegacyFlickerTest.appWindowIsInvisibleAtStart(component: IComponentMatcher) {
     assertWmStart { this.isAppWindowInvisible(component) }
 }
 
-fun FlickerTest.appWindowIsInvisibleAtEnd(component: IComponentMatcher) {
+fun LegacyFlickerTest.appWindowIsInvisibleAtEnd(component: IComponentMatcher) {
     assertWmEnd { this.isAppWindowInvisible(component) }
 }
 
-fun FlickerTest.appWindowIsNotContainAtStart(component: IComponentMatcher) {
+fun LegacyFlickerTest.appWindowIsNotContainAtStart(component: IComponentMatcher) {
     assertWmStart { this.notContains(component) }
 }
 
-fun FlickerTest.appWindowKeepVisible(component: IComponentMatcher) {
+fun LegacyFlickerTest.appWindowKeepVisible(component: IComponentMatcher) {
     assertWm { this.isAppWindowVisible(component) }
 }
 
-fun FlickerTest.dockedStackDividerIsVisibleAtEnd() {
+fun LegacyFlickerTest.dockedStackDividerIsVisibleAtEnd() {
     assertLayersEnd { this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTest.dockedStackDividerBecomesVisible() {
+fun LegacyFlickerTest.dockedStackDividerBecomesVisible() {
     assertLayers {
         this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT)
             .then()
@@ -356,7 +356,7 @@
     }
 }
 
-fun FlickerTest.dockedStackDividerBecomesInvisible() {
+fun LegacyFlickerTest.dockedStackDividerBecomesInvisible() {
     assertLayers {
         this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT)
             .then()
@@ -364,11 +364,11 @@
     }
 }
 
-fun FlickerTest.dockedStackDividerNotExistsAtEnd() {
+fun LegacyFlickerTest.dockedStackDividerNotExistsAtEnd() {
     assertLayersEnd { this.notContains(DOCKED_STACK_DIVIDER_COMPONENT) }
 }
 
-fun FlickerTest.appPairsPrimaryBoundsIsVisibleAtEnd(
+fun LegacyFlickerTest.appPairsPrimaryBoundsIsVisibleAtEnd(
     rotation: Rotation,
     primaryComponent: IComponentMatcher
 ) {
@@ -380,7 +380,7 @@
     }
 }
 
-fun FlickerTest.dockedStackPrimaryBoundsIsVisibleAtEnd(
+fun LegacyFlickerTest.dockedStackPrimaryBoundsIsVisibleAtEnd(
     rotation: Rotation,
     primaryComponent: IComponentMatcher
 ) {
@@ -392,7 +392,7 @@
     }
 }
 
-fun FlickerTest.appPairsSecondaryBoundsIsVisibleAtEnd(
+fun LegacyFlickerTest.appPairsSecondaryBoundsIsVisibleAtEnd(
     rotation: Rotation,
     secondaryComponent: IComponentMatcher
 ) {
@@ -404,7 +404,7 @@
     }
 }
 
-fun FlickerTest.dockedStackSecondaryBoundsIsVisibleAtEnd(
+fun LegacyFlickerTest.dockedStackSecondaryBoundsIsVisibleAtEnd(
     rotation: Rotation,
     secondaryComponent: IComponentMatcher
 ) {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt
index 02d9a056..7b32901 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt
@@ -18,7 +18,7 @@
 
 import android.platform.test.annotations.Presubmit
 import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.navBarLayerIsVisibleAtStartAndEnd
 import com.android.server.wm.flicker.navBarLayerPositionAtStartAndEnd
@@ -32,7 +32,7 @@
 import org.junit.Test
 
 interface ICommonAssertions {
-    val flicker: FlickerTest
+    val flicker: LegacyFlickerTest
 
     /** Checks that all parts of the screen are covered during the transition */
     @Presubmit @Test fun entireScreenCovered() = flicker.entireScreenCovered()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt
new file mode 100644
index 0000000..fd56a6e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.flicker
+
+import android.app.Instrumentation
+import android.graphics.Point
+import android.os.SystemClock
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.common.traces.component.IComponentMatcher
+import android.tools.common.traces.component.IComponentNameMatcher
+import android.tools.device.apphelpers.StandardAppHelper
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import android.tools.device.traces.parsers.toFlickerComponent
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.ViewConfiguration
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
+import com.android.server.wm.flicker.helpers.NotificationAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.flicker.testapp.ActivityOptions.SplitScreen.Primary
+import org.junit.Assert.assertNotNull
+
+internal object SplitScreenUtils {
+    private const val TIMEOUT_MS = 3_000L
+    private const val DRAG_DURATION_MS = 1_000L
+    private const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
+    private const val DIVIDER_BAR = "docked_divider_handle"
+    private const val OVERVIEW_SNAPSHOT = "snapshot"
+    private const val GESTURE_STEP_MS = 16L
+    private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L
+    private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#")
+
+    private val notificationScrollerSelector: BySelector
+        get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER)
+    private val notificationContentSelector: BySelector
+        get() = By.text("Flicker Test Notification")
+    private val dividerBarSelector: BySelector
+        get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR)
+    private val overviewSnapshotSelector: BySelector
+        get() = By.res(LAUNCHER_UI_PACKAGE_NAME, OVERVIEW_SNAPSHOT)
+
+    fun getPrimary(instrumentation: Instrumentation): StandardAppHelper =
+        SimpleAppHelper(
+            instrumentation,
+            ActivityOptions.SplitScreen.Primary.LABEL,
+            ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()
+        )
+
+    fun getSecondary(instrumentation: Instrumentation): StandardAppHelper =
+        SimpleAppHelper(
+            instrumentation,
+            ActivityOptions.SplitScreen.Secondary.LABEL,
+            ActivityOptions.SplitScreen.Secondary.COMPONENT.toFlickerComponent()
+        )
+
+    fun getNonResizeable(instrumentation: Instrumentation): NonResizeableAppHelper =
+        NonResizeableAppHelper(instrumentation)
+
+    fun getSendNotification(instrumentation: Instrumentation): NotificationAppHelper =
+        NotificationAppHelper(instrumentation)
+
+    fun getIme(instrumentation: Instrumentation): ImeAppHelper = ImeAppHelper(instrumentation)
+
+    fun waitForSplitComplete(
+        wmHelper: WindowManagerStateHelper,
+        primaryApp: IComponentMatcher,
+        secondaryApp: IComponentMatcher,
+    ) {
+        wmHelper
+            .StateSyncBuilder()
+            .withWindowSurfaceAppeared(primaryApp)
+            .withWindowSurfaceAppeared(secondaryApp)
+            .withSplitDividerVisible()
+            .waitForAndVerify()
+    }
+
+    fun enterSplit(
+        wmHelper: WindowManagerStateHelper,
+        tapl: LauncherInstrumentation,
+        device: UiDevice,
+        primaryApp: StandardAppHelper,
+        secondaryApp: StandardAppHelper
+    ) {
+        primaryApp.launchViaIntent(wmHelper)
+        secondaryApp.launchViaIntent(wmHelper)
+        tapl.goHome()
+        wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
+        splitFromOverview(tapl, device)
+        waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+    }
+
+    fun enterSplitViaIntent(
+        wmHelper: WindowManagerStateHelper,
+        primaryApp: StandardAppHelper,
+        secondaryApp: StandardAppHelper
+    ) {
+        val stringExtras = mapOf(Primary.EXTRA_LAUNCH_ADJACENT to "true")
+        primaryApp.launchViaIntent(wmHelper, null, null, stringExtras)
+        waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+    }
+
+    fun splitFromOverview(tapl: LauncherInstrumentation, device: UiDevice) {
+        // Note: The initial split position in landscape is different between tablet and phone.
+        // In landscape, tablet will let the first app split to right side, and phone will
+        // split to left side.
+        if (tapl.isTablet) {
+            // TAPL's currentTask on tablet is sometimes not what we expected if the overview
+            // contains more than 3 task views. We need to use uiautomator directly to find the
+            // second task to split.
+            tapl.workspace.switchToOverview().overviewActions.clickSplit()
+            val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS)
+            if (snapshots == null || snapshots.size < 1) {
+                error("Fail to find a overview snapshot to split.")
+            }
+
+            // Find the second task in the upper right corner in split select mode by sorting
+            // 'left' in descending order and 'top' in ascending order.
+            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
+                t2.getVisibleBounds().left - t1.getVisibleBounds().left
+            }
+            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
+                t1.getVisibleBounds().top - t2.getVisibleBounds().top
+            }
+            snapshots[0].click()
+        } else {
+            tapl.workspace
+                .switchToOverview()
+                .currentTask
+                .tapMenu()
+                .tapSplitMenuItem()
+                .currentTask
+                .open()
+        }
+        SystemClock.sleep(TIMEOUT_MS)
+    }
+
+    fun dragFromNotificationToSplit(
+        instrumentation: Instrumentation,
+        device: UiDevice,
+        wmHelper: WindowManagerStateHelper
+    ) {
+        val displayBounds =
+            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
+                ?: error("Display not found")
+
+        // Pull down the notifications
+        device.swipe(
+            displayBounds.centerX(),
+            5,
+            displayBounds.centerX(),
+            displayBounds.bottom,
+            50 /* steps */
+        )
+        SystemClock.sleep(TIMEOUT_MS)
+
+        // Find the target notification
+        val notificationScroller =
+            device.wait(Until.findObject(notificationScrollerSelector), TIMEOUT_MS)
+                ?: error("Unable to find view $notificationScrollerSelector")
+        var notificationContent = notificationScroller.findObject(notificationContentSelector)
+
+        while (notificationContent == null) {
+            device.swipe(
+                displayBounds.centerX(),
+                displayBounds.centerY(),
+                displayBounds.centerX(),
+                displayBounds.centerY() - 150,
+                20 /* steps */
+            )
+            notificationContent = notificationScroller.findObject(notificationContentSelector)
+        }
+
+        // Drag to split
+        val dragStart = notificationContent.visibleCenter
+        val dragMiddle = Point(dragStart.x + 50, dragStart.y)
+        val dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4)
+        val downTime = SystemClock.uptimeMillis()
+
+        touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, dragStart)
+        // It needs a horizontal movement to trigger the drag
+        touchMove(
+            instrumentation,
+            downTime,
+            SystemClock.uptimeMillis(),
+            DRAG_DURATION_MS,
+            dragStart,
+            dragMiddle
+        )
+        touchMove(
+            instrumentation,
+            downTime,
+            SystemClock.uptimeMillis(),
+            DRAG_DURATION_MS,
+            dragMiddle,
+            dragEnd
+        )
+        // Wait for a while to start splitting
+        SystemClock.sleep(TIMEOUT_MS)
+        touch(
+            instrumentation,
+            MotionEvent.ACTION_UP,
+            downTime,
+            SystemClock.uptimeMillis(),
+            GESTURE_STEP_MS,
+            dragEnd
+        )
+        SystemClock.sleep(TIMEOUT_MS)
+    }
+
+    fun touch(
+        instrumentation: Instrumentation,
+        action: Int,
+        downTime: Long,
+        eventTime: Long,
+        duration: Long,
+        point: Point
+    ) {
+        val motionEvent =
+            MotionEvent.obtain(downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0)
+        motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN
+        instrumentation.uiAutomation.injectInputEvent(motionEvent, true)
+        motionEvent.recycle()
+        SystemClock.sleep(duration)
+    }
+
+    fun touchMove(
+        instrumentation: Instrumentation,
+        downTime: Long,
+        eventTime: Long,
+        duration: Long,
+        from: Point,
+        to: Point
+    ) {
+        val steps: Long = duration / GESTURE_STEP_MS
+        var currentTime = eventTime
+        var currentX = from.x.toFloat()
+        var currentY = from.y.toFloat()
+        val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat()
+        val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat()
+
+        for (i in 1..steps) {
+            val motionMove =
+                MotionEvent.obtain(
+                    downTime,
+                    currentTime,
+                    MotionEvent.ACTION_MOVE,
+                    currentX,
+                    currentY,
+                    0
+                )
+            motionMove.source = InputDevice.SOURCE_TOUCHSCREEN
+            instrumentation.uiAutomation.injectInputEvent(motionMove, true)
+            motionMove.recycle()
+
+            currentTime += GESTURE_STEP_MS
+            if (i == steps - 1) {
+                currentX = to.x.toFloat()
+                currentY = to.y.toFloat()
+            } else {
+                currentX += stepX
+                currentY += stepY
+            }
+            SystemClock.sleep(GESTURE_STEP_MS)
+        }
+    }
+
+    fun createShortcutOnHotseatIfNotExist(tapl: LauncherInstrumentation, appName: String) {
+        tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0))
+        val allApps = tapl.workspace.switchToAllApps()
+        allApps.freeze()
+        try {
+            allApps.getAppIcon(appName).dragToHotseat(0)
+        } finally {
+            allApps.unfreeze()
+        }
+    }
+
+    fun dragDividerToResizeAndWait(device: UiDevice, wmHelper: WindowManagerStateHelper) {
+        val displayBounds =
+            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
+                ?: error("Display not found")
+        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
+        dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3), 200)
+
+        wmHelper
+            .StateSyncBuilder()
+            .withWindowSurfaceDisappeared(SPLIT_DECOR_MANAGER)
+            .waitForAndVerify()
+    }
+
+    fun dragDividerToDismissSplit(
+        device: UiDevice,
+        wmHelper: WindowManagerStateHelper,
+        dragToRight: Boolean,
+        dragToBottom: Boolean
+    ) {
+        val displayBounds =
+            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
+                ?: error("Display not found")
+        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
+        dividerBar.drag(
+            Point(
+                if (dragToRight) {
+                    displayBounds.right
+                } else {
+                    displayBounds.left
+                },
+                if (dragToBottom) {
+                    displayBounds.bottom
+                } else {
+                    displayBounds.top
+                }
+            )
+        )
+    }
+
+    fun doubleTapDividerToSwitch(device: UiDevice) {
+        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
+        val interval =
+            (ViewConfiguration.getDoubleTapTimeout() + ViewConfiguration.getDoubleTapMinTime()) / 2
+        dividerBar.click()
+        SystemClock.sleep(interval.toLong())
+        dividerBar.click()
+    }
+
+    fun copyContentInSplit(
+        instrumentation: Instrumentation,
+        device: UiDevice,
+        sourceApp: IComponentNameMatcher,
+        destinationApp: IComponentNameMatcher,
+    ) {
+        // Copy text from sourceApp
+        val textView =
+            device.wait(
+                Until.findObject(By.res(sourceApp.packageName, "SplitScreenTest")),
+                TIMEOUT_MS
+            )
+        assertNotNull("Unable to find the TextView", textView)
+        textView.click(LONG_PRESS_TIME_MS)
+
+        val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS)
+        assertNotNull("Unable to find the copy button", copyBtn)
+        copyBtn.click()
+
+        // Paste text to destinationApp
+        val editText =
+            device.wait(
+                Until.findObject(By.res(destinationApp.packageName, "plain_text_input")),
+                TIMEOUT_MS
+            )
+        assertNotNull("Unable to find the EditText", editText)
+        editText.click(LONG_PRESS_TIME_MS)
+
+        val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS)
+        assertNotNull("Unable to find the paste button", pasteBtn)
+        pasteBtn.click()
+
+        // Verify text
+        if (!textView.text.contentEquals(editText.text)) {
+            error("Fail to copy content in split")
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
index 6178156..0f9579d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
@@ -20,21 +20,20 @@
 import android.system.helpers.CommandsHelper
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import com.android.server.wm.flicker.helpers.setRotation
+import android.tools.device.flicker.legacy.FlickerTestData
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import com.android.server.wm.flicker.helpers.LetterboxAppHelper
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import android.tools.device.flicker.legacy.IFlickerTestData
+import com.android.server.wm.flicker.helpers.setRotation
 import com.android.wm.shell.flicker.BaseTest
-import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
+import com.android.wm.shell.flicker.appWindowKeepVisible
 import com.android.wm.shell.flicker.layerKeepVisible
 import org.junit.After
 import org.junit.Assume
 import org.junit.Before
-import org.junit.runners.Parameterized
 
-abstract class BaseAppCompat(flicker: FlickerTest) : BaseTest(flicker) {
+abstract class BaseAppCompat(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     protected val context: Context = instrumentation.context
     protected val letterboxApp = LetterboxAppHelper(instrumentation)
     lateinit var cmdHelper: CommandsHelper
@@ -48,9 +47,7 @@
                 letterboxApp.launchViaIntent(wmHelper)
                 setEndRotation()
             }
-            teardown {
-                letterboxApp.exit(wmHelper)
-            }
+            teardown { letterboxApp.exit(wmHelper) }
         }
 
     @Before
@@ -58,12 +55,13 @@
         cmdHelper = CommandsHelper.getInstance(instrumentation)
         Assume.assumeTrue(tapl.isTablet && isIgnoreOrientationRequest())
         letterboxStyle = mapLetterboxStyle()
+        resetLetterboxStyle()
         setLetterboxEducationEnabled(false)
     }
 
     @After
     fun after() {
-        resetLetterboxEducationEnabled()
+        resetLetterboxStyle()
     }
 
     private fun mapLetterboxStyle(): HashMap<String, String> {
@@ -87,9 +85,8 @@
         return letterboxStyle
     }
 
-    private fun resetLetterboxEducationEnabled() {
-        val enabled = getLetterboxStyle().getValue("Is education enabled")
-        cmdHelper.executeShellCommand("wm set-letterbox-style --isEducationEnabled $enabled")
+    private fun resetLetterboxStyle() {
+        cmdHelper.executeShellCommand("wm reset-letterbox-style")
     }
 
     private fun setLetterboxEducationEnabled(enabled: Boolean) {
@@ -101,9 +98,9 @@
         return res != null && res.contains("true")
     }
 
-    fun IFlickerTestData.setStartRotation() = setRotation(flicker.scenario.startRotation)
+    fun FlickerTestData.setStartRotation() = setRotation(flicker.scenario.startRotation)
 
-    fun IFlickerTestData.setEndRotation() = setRotation(flicker.scenario.endRotation)
+    fun FlickerTestData.setEndRotation() = setRotation(flicker.scenario.endRotation)
 
     /** Checks that app entering letterboxed state have rounded corners */
     fun assertLetterboxAppAtStartHasRoundedCorners() {
@@ -126,25 +123,21 @@
         flicker.appWindowIsVisibleAtEnd(letterboxApp)
     }
 
+    fun assertLetterboxAppKeepVisible() {
+        assertLetterboxAppWindowKeepVisible()
+        assertLetterboxAppLayerKeepVisible()
+    }
+
     fun assertAppLetterboxedAtEnd() =
-            flicker.assertLayersEnd { isVisible(ComponentNameMatcher.LETTERBOX) }
+        flicker.assertLayersEnd { isVisible(ComponentNameMatcher.LETTERBOX) }
 
     fun assertAppLetterboxedAtStart() =
-            flicker.assertLayersStart { isVisible(ComponentNameMatcher.LETTERBOX) }
+        flicker.assertLayersStart { isVisible(ComponentNameMatcher.LETTERBOX) }
+
+    fun assertAppStaysLetterboxed() =
+        flicker.assertLayers { isVisible(ComponentNameMatcher.LETTERBOX) }
 
     fun assertLetterboxAppLayerKeepVisible() = flicker.layerKeepVisible(letterboxApp)
 
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.rotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.rotationTests()
-        }
-    }
+    fun assertLetterboxAppWindowKeepVisible() = flicker.appWindowKeepVisible(letterboxApp)
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt
index c2141a3..a7bd258 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt
@@ -17,10 +17,12 @@
 package com.android.wm.shell.flicker.appcompat
 
 import android.platform.test.annotations.Postsubmit
+import android.tools.common.flicker.assertions.FlickerTest
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -45,7 +47,7 @@
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-class OpenAppInSizeCompatModeTest(flicker: FlickerTest) : BaseAppCompat(flicker) {
+class OpenAppInSizeCompatModeTest(flicker: LegacyFlickerTest) : BaseAppCompat(flicker) {
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
@@ -70,9 +72,7 @@
     @Test
     fun letterboxedAppHasRoundedCorners() = assertLetterboxAppAtEndHasRoundedCorners()
 
-    @Postsubmit
-    @Test
-    fun appIsLetterboxedAtEnd() = assertAppLetterboxedAtEnd()
+    @Postsubmit @Test fun appIsLetterboxedAtEnd() = assertAppLetterboxedAtEnd()
 
     /**
      * Checks that the [ComponentNameMatcher.ROTATION] layer appears during the transition, doesn't
@@ -90,4 +90,18 @@
                 .isInvisible(ComponentNameMatcher.ROTATION)
         }
     }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.rotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTest> {
+            return LegacyFlickerTestFactory.rotationTests()
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt
new file mode 100644
index 0000000..d3f3c5b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt
@@ -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.flicker.appcompat
+
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.RequiresDevice
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.common.datatypes.Rect
+import android.tools.common.flicker.assertions.FlickerTest
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test quick switching to letterboxed app from launcher
+ *
+ * To run this test: `atest WMShellFlickerTestsOther:QuickSwitchLauncherToLetterboxAppTest`
+ *
+ * Actions:
+ * ```
+ *     Launch a letterboxed app
+ *     Navigate home to show launcher
+ *     Swipe right from the bottom of the screen to quick switch back to the app
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class QuickSwitchLauncherToLetterboxAppTest(flicker: LegacyFlickerTest) :
+    BaseAppCompat(flicker) {
+
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit = {
+        setup {
+            tapl.setExpectedRotationCheckEnabled(false)
+
+            tapl.setExpectedRotation(flicker.scenario.startRotation.value)
+
+            letterboxApp.launchViaIntent(wmHelper)
+            tapl.goHome()
+            wmHelper
+                .StateSyncBuilder()
+                .withHomeActivityVisible()
+                .withWindowSurfaceDisappeared(letterboxApp)
+                .waitForAndVerify()
+
+            startDisplayBounds =
+                wmHelper.currentState.layerState.physicalDisplayBounds ?: error("Display not found")
+        }
+        transitions {
+            tapl.workspace.quickSwitchToPreviousApp()
+            wmHelper
+                .StateSyncBuilder()
+                .withFullScreenApp(letterboxApp)
+                .withNavOrTaskBarVisible()
+                .withStatusBarVisible()
+                .waitForAndVerify()
+        }
+        teardown { letterboxApp.exit(wmHelper) }
+    }
+
+    /**
+     * Checks that [letterboxApp] is the top window at the end of the transition once we have fully
+     * quick switched from the launcher back to the [letterboxApp].
+     */
+    @Postsubmit
+    @Test
+    fun endsWithAppBeingOnTop() {
+        flicker.assertWmEnd { this.isAppWindowOnTop(letterboxApp) }
+    }
+
+    /** Checks that the transition starts with the home activity being tagged as visible. */
+    @Postsubmit
+    @Test
+    fun startsWithHomeActivityFlaggedVisible() {
+        flicker.assertWmStart { this.isHomeActivityVisible() }
+    }
+
+    /**
+     * Checks that the transition starts with the [ComponentNameMatcher.LAUNCHER] windows
+     * filling/covering exactly display size
+     */
+    @Postsubmit
+    @Test
+    fun startsWithLauncherWindowsCoverFullScreen() {
+        flicker.assertWmStart {
+            this.visibleRegion(ComponentNameMatcher.LAUNCHER).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Checks that the transition starts with the [ComponentNameMatcher.LAUNCHER] layers
+     * filling/covering exactly the display size.
+     */
+    @Postsubmit
+    @Test
+    fun startsWithLauncherLayersCoverFullScreen() {
+        flicker.assertLayersStart {
+            this.visibleRegion(ComponentNameMatcher.LAUNCHER).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /**
+     * Checks that the transition starts with the [ComponentNameMatcher.LAUNCHER] being the top
+     * window.
+     */
+    @Postsubmit
+    @Test
+    fun startsWithLauncherBeingOnTop() {
+        flicker.assertWmStart { this.isAppWindowOnTop(ComponentNameMatcher.LAUNCHER) }
+    }
+
+    /**
+     * Checks that the transition ends with the home activity being flagged as not visible. By this
+     * point we should have quick switched away from the launcher back to the [letterboxApp].
+     */
+    @Postsubmit
+    @Test
+    fun endsWithHomeActivityFlaggedInvisible() {
+        flicker.assertWmEnd { this.isHomeActivityInvisible() }
+    }
+
+    /**
+     * Checks that [letterboxApp]'s window starts off invisible and becomes visible at some point
+     * before the end of the transition and then stays visible until the end of the transition.
+     */
+    @Postsubmit
+    @Test
+    fun appWindowBecomesAndStaysVisible() {
+        flicker.assertWm {
+            this.isAppWindowInvisible(letterboxApp).then().isAppWindowVisible(letterboxApp)
+        }
+    }
+
+    /**
+     * Checks that [letterboxApp]'s layer starts off invisible and becomes visible at some point
+     * before the end of the transition and then stays visible until the end of the transition.
+     */
+    @Postsubmit
+    @Test
+    fun appLayerBecomesAndStaysVisible() {
+        flicker.assertLayers { this.isInvisible(letterboxApp).then().isVisible(letterboxApp) }
+    }
+
+    /**
+     * Checks that the [ComponentNameMatcher.LAUNCHER] window starts off visible and becomes
+     * invisible at some point before the end of the transition and then stays invisible until the
+     * end of the transition.
+     */
+    @Postsubmit
+    @Test
+    fun launcherWindowBecomesAndStaysInvisible() {
+        flicker.assertWm {
+            this.isAppWindowOnTop(ComponentNameMatcher.LAUNCHER)
+                .then()
+                .isAppWindowNotOnTop(ComponentNameMatcher.LAUNCHER)
+        }
+    }
+
+    /**
+     * Checks that the [ComponentNameMatcher.LAUNCHER] layer starts off visible and becomes
+     * invisible at some point before the end of the transition and then stays invisible until the
+     * end of the transition.
+     */
+    @Postsubmit
+    @Test
+    fun launcherLayerBecomesAndStaysInvisible() {
+        flicker.assertLayers {
+            this.isVisible(ComponentNameMatcher.LAUNCHER)
+                .then()
+                .isInvisible(ComponentNameMatcher.LAUNCHER)
+        }
+    }
+
+    /**
+     * Checks that the [ComponentNameMatcher.LAUNCHER] window is visible at least until the app
+     * window is visible. Ensures that at any point, either the launcher or [letterboxApp] windows
+     * are at least partially visible.
+     */
+    @Postsubmit
+    @Test
+    fun appWindowIsVisibleOnceLauncherWindowIsInvisible() {
+        flicker.assertWm {
+            this.isAppWindowOnTop(ComponentNameMatcher.LAUNCHER)
+                .then()
+                .isAppWindowVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
+                .then()
+                .isAppWindowVisible(letterboxApp)
+        }
+    }
+
+    /**
+     * Checks that the [ComponentNameMatcher.LAUNCHER] layer is visible at least until the app layer
+     * is visible. Ensures that at any point, either the launcher or [letterboxApp] layers are at
+     * least partially visible.
+     */
+    @Postsubmit
+    @Test
+    fun appLayerIsVisibleOnceLauncherLayerIsInvisible() {
+        flicker.assertLayers {
+            this.isVisible(ComponentNameMatcher.LAUNCHER)
+                .then()
+                .isVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
+                .then()
+                .isVisible(letterboxApp)
+        }
+    }
+
+    /**
+     * Checks that the [ComponentNameMatcher.LETTERBOX] layer is visible as soon as the
+     * [letterboxApp] layer is visible at the end of the transition once we have fully quick
+     * switched from the launcher back to the [letterboxApp].
+     */
+    @Postsubmit
+    @Test
+    fun appAndLetterboxLayersBothVisibleOnceLauncherIsInvisible() {
+        flicker.assertLayers {
+            this.isVisible(ComponentNameMatcher.LAUNCHER)
+                .then()
+                .isVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
+                .then()
+                .isVisible(letterboxApp)
+                .isVisible(ComponentNameMatcher.LETTERBOX)
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+    }
+
+    companion object {
+        /** {@inheritDoc} */
+        private var startDisplayBounds = Rect.EMPTY
+
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTest> {
+            return LegacyFlickerTestFactory.nonRotationTests(
+                supportedNavigationModes = listOf(NavBar.MODE_GESTURAL),
+                supportedRotations = listOf(Rotation.ROTATION_90)
+            )
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt
new file mode 100644
index 0000000..e875aae
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.flicker.appcompat
+
+import android.platform.test.annotations.Postsubmit
+import android.tools.common.Rotation
+import android.tools.common.flicker.assertions.FlickerTest
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.device.helpers.WindowUtils
+import androidx.test.filters.RequiresDevice
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching a fixed portrait letterboxed app in landscape and repositioning to the right.
+ *
+ * To run this test: `atest WMShellFlickerTests:RepositionFixedPortraitAppTest` Actions:
+ *
+ *  ```
+ *      Launch a fixed portrait app in landscape to letterbox app
+ *      Double tap to the right to reposition app and wait for app to move
+ *  ```
+ *
+ * Notes:
+ *
+ *  ```
+ *      Some default assertions (e.g., nav bar, status bar and screen covered)
+ *      are inherited [BaseTest]
+ *  ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+class RepositionFixedPortraitAppTest(flicker: LegacyFlickerTest) : BaseAppCompat(flicker) {
+
+    val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation).bounds
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            setup {
+                setStartRotation()
+                letterboxApp.launchViaIntent(wmHelper)
+            }
+            transitions {
+                letterboxApp.repositionHorizontally(displayBounds, true)
+                letterboxApp.waitForAppToMoveHorizontallyTo(wmHelper, displayBounds, true)
+            }
+            teardown {
+                letterboxApp.repositionHorizontally(displayBounds, false)
+                letterboxApp.exit(wmHelper)
+            }
+        }
+
+    @Postsubmit
+    @Test
+    fun letterboxedAppHasRoundedCorners() = assertLetterboxAppAtEndHasRoundedCorners()
+
+    @Postsubmit @Test fun letterboxAppLayerKeepVisible() = assertLetterboxAppLayerKeepVisible()
+
+    @Postsubmit @Test fun appStaysLetterboxed() = assertAppStaysLetterboxed()
+
+    @Postsubmit @Test fun appKeepVisible() = assertLetterboxAppKeepVisible()
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTest> {
+            return LegacyFlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(Rotation.ROTATION_90)
+            )
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt
index b0e1a42..a18a144 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt
@@ -17,9 +17,11 @@
 package com.android.wm.shell.flicker.appcompat
 
 import android.platform.test.annotations.Postsubmit
+import android.tools.common.flicker.assertions.FlickerTest
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.helpers.WindowUtils
 import androidx.test.filters.RequiresDevice
 import org.junit.Test
@@ -46,7 +48,7 @@
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-class RestartAppInSizeCompatModeTest(flicker: FlickerTest) : BaseAppCompat(flicker) {
+class RestartAppInSizeCompatModeTest(flicker: LegacyFlickerTest) : BaseAppCompat(flicker) {
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
@@ -69,13 +71,9 @@
         }
     }
 
-    @Postsubmit
-    @Test
-    fun appLayerKeepVisible() = assertLetterboxAppLayerKeepVisible()
+    @Postsubmit @Test fun appLayerKeepVisible() = assertLetterboxAppLayerKeepVisible()
 
-    @Postsubmit
-    @Test
-    fun appIsLetterboxedAtStart() = assertAppLetterboxedAtStart()
+    @Postsubmit @Test fun appIsLetterboxedAtStart() = assertAppLetterboxedAtStart()
 
     @Postsubmit
     @Test
@@ -88,4 +86,18 @@
         val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.endRotation)
         flicker.assertLayersEnd { visibleRegion(letterboxApp).coversAtMost(displayBounds) }
     }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.rotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTest> {
+            return LegacyFlickerTestFactory.rotationTests()
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
index bab81d7..5c7d1d8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
@@ -23,9 +23,9 @@
 import android.os.ServiceManager
 import android.tools.common.Rotation
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import android.tools.device.flicker.legacy.IFlickerTestData
+import android.tools.device.flicker.legacy.FlickerTestData
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.helpers.SYSTEMUI_PACKAGE
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.UiObject2
@@ -35,7 +35,7 @@
 import org.junit.runners.Parameterized
 
 /** Base configurations for Bubble flicker tests */
-abstract class BaseBubbleScreen(flicker: FlickerTest) : BaseTest(flicker) {
+abstract class BaseBubbleScreen(flicker: LegacyFlickerTest) : BaseTest(flicker) {
 
     protected val context: Context = instrumentation.context
     protected val testApp = LaunchBubbleHelper(instrumentation)
@@ -72,28 +72,31 @@
                     uid,
                     NotificationManager.BUBBLE_PREFERENCE_NONE
                 )
-                testApp.exit()
+                device.wait(
+                    Until.gone(By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)),
+                    FIND_OBJECT_TIMEOUT
+                )
+                testApp.exit(wmHelper)
             }
 
             extraSpec(this)
         }
     }
 
-    protected fun IFlickerTestData.waitAndGetAddBubbleBtn(): UiObject2? =
+    protected fun FlickerTestData.waitAndGetAddBubbleBtn(): UiObject2? =
         device.wait(Until.findObject(By.text("Add Bubble")), FIND_OBJECT_TIMEOUT)
-    protected fun IFlickerTestData.waitAndGetCancelAllBtn(): UiObject2? =
+    protected fun FlickerTestData.waitAndGetCancelAllBtn(): UiObject2? =
         device.wait(Until.findObject(By.text("Cancel All Bubble")), FIND_OBJECT_TIMEOUT)
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
 
-        const val FIND_OBJECT_TIMEOUT = 2000L
+        const val FIND_OBJECT_TIMEOUT = 4000L
         const val SYSTEM_UI_PACKAGE = SYSTEMUI_PACKAGE
         const val BUBBLE_RES_NAME = "bubble_view"
     }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
index 2474ecf..bc565bc 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
@@ -21,7 +21,7 @@
 import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.filters.RequiresDevice
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.UiObject2
@@ -44,7 +44,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FlakyTest(bugId = 217777115)
-open class ChangeActiveActivityFromBubbleTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
+open class ChangeActiveActivityFromBubbleTest(flicker: LegacyFlickerTest) :
+    BaseBubbleScreen(flicker) {
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
         get() = buildTransition {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTestCfArm.kt
index bdfdad5..abc6b9f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTestCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTestCfArm.kt
@@ -17,11 +17,11 @@
 package com.android.wm.shell.flicker.bubble
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class ChangeActiveActivityFromBubbleTestCfArm(flicker: FlickerTest) :
+open class ChangeActiveActivityFromBubbleTestCfArm(flicker: LegacyFlickerTest) :
     ChangeActiveActivityFromBubbleTest(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt
index 8474ce0..3f28ae8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt
@@ -19,9 +19,10 @@
 import android.content.Context
 import android.graphics.Point
 import android.platform.test.annotations.Presubmit
+import android.tools.common.flicker.subject.layers.LayersTraceSubject
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.util.DisplayMetrics
 import android.view.WindowManager
 import androidx.test.filters.RequiresDevice
@@ -44,7 +45,7 @@
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class DragToDismissBubbleScreenTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
+open class DragToDismissBubbleScreenTest(flicker: LegacyFlickerTest) : BaseBubbleScreen(flicker) {
 
     private val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
     private val displaySize = DisplayMetrics()
@@ -73,4 +74,14 @@
     open fun testAppIsAlwaysVisible() {
         flicker.assertLayers { this.isVisible(testApp) }
     }
+
+    @Presubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+        flicker.assertLayers {
+            this.visibleLayersShownMoreThanOneConsecutiveEntry(
+                LayersTraceSubject.VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS + listOf(testApp)
+            )
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTestCfArm.kt
index 62fa7b4..ee55eca 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTestCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTestCfArm.kt
@@ -17,11 +17,11 @@
 package com.android.wm.shell.flicker.bubble
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-class DragToDismissBubbleScreenTestCfArm(flicker: FlickerTest) :
+class DragToDismissBubbleScreenTestCfArm(flicker: LegacyFlickerTest) :
     DragToDismissBubbleScreenTest(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt
index 889e177..26aca18 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt
@@ -21,7 +21,7 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.view.WindowInsets
 import android.view.WindowManager
 import androidx.test.filters.RequiresDevice
@@ -48,7 +48,8 @@
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-class OpenActivityFromBubbleOnLocksreenTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
+class OpenActivityFromBubbleOnLocksreenTest(flicker: LegacyFlickerTest) :
+    BaseBubbleScreen(flicker) {
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
@@ -80,7 +81,7 @@
                 instrumentation.uiAutomation.syncInputTransactions()
                 val showBubble =
                     device.wait(
-                        Until.findObject(By.res("com.android.systemui", "bubble_view")),
+                        Until.findObject(By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)),
                         FIND_OBJECT_TIMEOUT
                     )
                 showBubble?.click() ?: error("Bubble notify not found")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt
index 07ba4133..5085394 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt
@@ -19,7 +19,7 @@
 import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.filters.RequiresDevice
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
@@ -42,7 +42,7 @@
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class OpenActivityFromBubbleTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
+open class OpenActivityFromBubbleTest(flicker: LegacyFlickerTest) : BaseBubbleScreen(flicker) {
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTestCfArm.kt
index 6c61710..6a46d23 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTestCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTestCfArm.kt
@@ -17,10 +17,11 @@
 package com.android.wm.shell.flicker.bubble
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-class OpenActivityFromBubbleTestCfArm(flicker: FlickerTest) : OpenActivityFromBubbleTest(flicker)
+class OpenActivityFromBubbleTestCfArm(flicker: LegacyFlickerTest) :
+    OpenActivityFromBubbleTest(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt
index 29f76d0..a926bb7 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt
@@ -19,7 +19,7 @@
 import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.filters.RequiresDevice
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
@@ -41,7 +41,7 @@
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class SendBubbleNotificationTest(flicker: FlickerTest) : BaseBubbleScreen(flicker) {
+open class SendBubbleNotificationTest(flicker: LegacyFlickerTest) : BaseBubbleScreen(flicker) {
 
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
@@ -55,6 +55,7 @@
                     FIND_OBJECT_TIMEOUT
                 )
                     ?: error("No bubbles found")
+                device.waitForIdle()
             }
         }
 
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTestCfArm.kt
index e323ebf..a401cb4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTestCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTestCfArm.kt
@@ -17,11 +17,11 @@
 package com.android.wm.shell.flicker.bubble
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-open class SendBubbleNotificationTestCfArm(flicker: FlickerTest) :
+open class SendBubbleNotificationTestCfArm(flicker: LegacyFlickerTest) :
     SendBubbleNotificationTest(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt
index f7ce870..8bd44c3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt
@@ -16,26 +16,19 @@
 
 package com.android.wm.shell.flicker.pip
 
-import android.app.Instrumentation
-import android.os.SystemClock
 import android.platform.test.annotations.Presubmit
 import android.tools.common.NavBar
 import android.tools.common.Rotation
-import android.tools.device.apphelpers.StandardAppHelper
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.helpers.WindowUtils
 import android.tools.device.traces.parsers.toFlickerComponent
 import androidx.test.filters.RequiresDevice
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.BySelector
-import androidx.test.uiautomator.UiObject2
-import androidx.test.uiautomator.Until
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.testapp.ActivityOptions
-import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
+import com.android.wm.shell.flicker.SplitScreenUtils
 import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -69,17 +62,16 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class AutoEnterPipFromSplitScreenOnGoToHomeTest(flicker: FlickerTest) :
-        AutoEnterPipOnGoToHomeTest(flicker) {
+class AutoEnterPipFromSplitScreenOnGoToHomeTest(flicker: LegacyFlickerTest) :
+    AutoEnterPipOnGoToHomeTest(flicker) {
     private val portraitDisplayBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0)
     /** Second app used to enter split screen mode */
-    protected val secondAppForSplitScreen = getSplitScreenApp(instrumentation)
-    fun getSplitScreenApp(instrumentation: Instrumentation): StandardAppHelper =
-            SimpleAppHelper(
-                    instrumentation,
-                    ActivityOptions.SplitScreen.Primary.LABEL,
-                    ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()
-            )
+    private val secondAppForSplitScreen =
+        SimpleAppHelper(
+            instrumentation,
+            ActivityOptions.SplitScreen.Primary.LABEL,
+            ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()
+        )
 
     /** Defines the transition used to run the test */
     override val transition: FlickerBuilder.() -> Unit
@@ -88,14 +80,7 @@
                 secondAppForSplitScreen.launchViaIntent(wmHelper)
                 pipApp.launchViaIntent(wmHelper)
                 tapl.goHome()
-                enterSplitScreen()
-                // wait until split screen is established
-                wmHelper
-                        .StateSyncBuilder()
-                        .withWindowSurfaceAppeared(pipApp)
-                        .withWindowSurfaceAppeared(secondAppForSplitScreen)
-                        .withSplitDividerVisible()
-                        .waitForAndVerify()
+                SplitScreenUtils.enterSplit(wmHelper, tapl, device, pipApp, secondAppForSplitScreen)
                 pipApp.enableAutoEnterForPipActivity()
             }
             teardown {
@@ -107,46 +92,6 @@
             transitions { tapl.goHome() }
         }
 
-    // TODO(b/285400227) merge the code in a common utility - this is copied from SplitScreenUtils
-    private val TIMEOUT_MS = 3_000L
-    private val overviewSnapshotSelector: BySelector
-        get() = By.res(LAUNCHER_UI_PACKAGE_NAME, "snapshot")
-    private fun enterSplitScreen() {
-        // Note: The initial split position in landscape is different between tablet and phone.
-        // In landscape, tablet will let the first app split to right side, and phone will
-        // split to left side.
-        if (tapl.isTablet) {
-            // TAPL's currentTask on tablet is sometimes not what we expected if the overview
-            // contains more than 3 task views. We need to use uiautomator directly to find the
-            // second task to split.
-            tapl.workspace.switchToOverview().overviewActions.clickSplit()
-            val snapshots = tapl.device.wait(Until.findObjects(overviewSnapshotSelector),
-                    TIMEOUT_MS)
-            if (snapshots == null || snapshots.size < 1) {
-                error("Fail to find a overview snapshot to split.")
-            }
-
-            // Find the second task in the upper right corner in split select mode by sorting
-            // 'left' in descending order and 'top' in ascending order.
-            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
-                t2.getVisibleBounds().left - t1.getVisibleBounds().left
-            }
-            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
-                t1.getVisibleBounds().top - t2.getVisibleBounds().top
-            }
-            snapshots[0].click()
-        } else {
-            tapl.workspace
-                    .switchToOverview()
-                    .currentTask
-                    .tapMenu()
-                    .tapSplitMenuItem()
-                    .currentTask
-                    .open()
-        }
-        SystemClock.sleep(TIMEOUT_MS)
-    }
-
     @Presubmit
     @Test
     override fun pipOverlayLayerAppearThenDisappear() {
@@ -190,11 +135,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
-                    // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
-                    supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
+                // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+                supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
index b95732e..2f7a25e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
@@ -19,7 +19,7 @@
 import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.filters.RequiresDevice
 import org.junit.Assume
 import org.junit.FixMethodOrder
@@ -53,10 +53,9 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class AutoEnterPipOnGoToHomeTest(flicker: FlickerTest) : EnterPipViaAppUiButtonTest(flicker) {
-    override val thisTransition: FlickerBuilder.() -> Unit = {
-        transitions { tapl.goHome() }
-    }
+open class AutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) :
+    EnterPipViaAppUiButtonTest(flicker) {
+    override val thisTransition: FlickerBuilder.() -> Unit = { transitions { tapl.goHome() } }
 
     override val defaultEnterPip: FlickerBuilder.() -> Unit = {
         setup {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt
index afcc172..68bc9a2 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt
@@ -20,7 +20,7 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.filters.RequiresDevice
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -53,7 +53,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ClosePipBySwipingDownTest(flicker: FlickerTest) : ClosePipTransition(flicker) {
+open class ClosePipBySwipingDownTest(flicker: LegacyFlickerTest) : ClosePipTransition(flicker) {
     override val thisTransition: FlickerBuilder.() -> Unit = {
         transitions {
             val pipRegion = wmHelper.getWindowRegion(pipApp).bounds
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTestCfArm.kt
index 02f6010..7a66889 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTestCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTestCfArm.kt
@@ -18,8 +18,8 @@
 
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -28,20 +28,20 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ClosePipBySwipingDownTestCfArm(flicker: FlickerTest) : ClosePipBySwipingDownTest(flicker) {
+class ClosePipBySwipingDownTestCfArm(flicker: LegacyFlickerTest) :
+    ClosePipBySwipingDownTest(flicker) {
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
-         * and navigation modes.
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring repetitions, screen
+         * orientation and navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt
index e52b71e..a17144b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipTransition.kt
@@ -20,14 +20,14 @@
 import android.tools.common.Rotation
 import android.tools.common.traces.component.ComponentNameMatcher.Companion.LAUNCHER
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import com.android.server.wm.flicker.helpers.setRotation
 import org.junit.Test
 import org.junit.runners.Parameterized
 
 /** Base class for exiting pip (closing pip window) without returning to the app */
-abstract class ClosePipTransition(flicker: FlickerTest) : PipTransition(flicker) {
+abstract class ClosePipTransition(flicker: LegacyFlickerTest) : PipTransition(flicker) {
     override val thisTransition: FlickerBuilder.() -> Unit = {
         setup { this.setRotation(flicker.scenario.startRotation) }
         teardown { this.setRotation(Rotation.ROTATION_0) }
@@ -74,15 +74,14 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
-         * and navigation modes.
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring repetitions, screen
+         * orientation and navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt
index 86fe583..dc48696 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt
@@ -19,7 +19,7 @@
 import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.filters.RequiresDevice
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -53,7 +53,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ClosePipWithDismissButtonTest(flicker: FlickerTest) : ClosePipTransition(flicker) {
+open class ClosePipWithDismissButtonTest(flicker: LegacyFlickerTest) : ClosePipTransition(flicker) {
     override val thisTransition: FlickerBuilder.() -> Unit = {
         transitions { pipApp.closePipWindow(wmHelper) }
     }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTestCfArm.kt
index 05262fe..718b14b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTestCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTestCfArm.kt
@@ -18,8 +18,8 @@
 
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -28,21 +28,20 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ClosePipWithDismissButtonTestCfArm(flicker: FlickerTest) :
+open class ClosePipWithDismissButtonTestCfArm(flicker: LegacyFlickerTest) :
     ClosePipWithDismissButtonTest(flicker) {
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
-         * and navigation modes.
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring repetitions, screen
+         * orientation and navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
index 01d67cc..5e39262 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
@@ -19,7 +19,7 @@
 import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.filters.RequiresDevice
 import org.junit.Assume
 import org.junit.FixMethodOrder
@@ -44,10 +44,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class EnterPipOnUserLeaveHintTest(flicker: FlickerTest) : EnterPipTransition(flicker) {
-    override val thisTransition: FlickerBuilder.() -> Unit = {
-        transitions { tapl.goHome() }
-    }
+open class EnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) : EnterPipTransition(flicker) {
+    override val thisTransition: FlickerBuilder.() -> Unit = { transitions { tapl.goHome() } }
 
     override val defaultEnterPip: FlickerBuilder.() -> Unit = {
         setup {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt
index 90f99c0..2b3e76a 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt
@@ -17,7 +17,7 @@
 package com.android.wm.shell.flicker.pip
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -27,4 +27,5 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterPipOnUserLeaveHintTestCfArm(flicker: FlickerTest) : EnterPipOnUserLeaveHintTest(flicker)
+class EnterPipOnUserLeaveHintTestCfArm(flicker: LegacyFlickerTest) :
+    EnterPipOnUserLeaveHintTest(flicker)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
index 5480144..ec35837 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
@@ -21,11 +21,12 @@
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
+import android.tools.common.flicker.assertions.FlickerTest
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.helpers.WindowUtils
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.entireScreenCovered
@@ -68,15 +69,13 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class EnterPipToOtherOrientation(flicker: FlickerTest) : PipTransition(flicker) {
+open class EnterPipToOtherOrientation(flicker: LegacyFlickerTest) : PipTransition(flicker) {
     private val testApp = FixedOrientationAppHelper(instrumentation)
     private val startingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_90)
     private val endingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0)
 
     override val thisTransition: FlickerBuilder.() -> Unit = {
-        teardown {
-            testApp.exit(wmHelper)
-        }
+        teardown { testApp.exit(wmHelper) }
         transitions {
             // Enter PiP, and assert that the PiP is within bounds now that the device is back
             // in portrait
@@ -95,14 +94,13 @@
         setup {
             // Launch a portrait only app on the fullscreen stack
             testApp.launchViaIntent(
-                    wmHelper,
-                    stringExtras = mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_PORTRAIT.toString())
+                wmHelper,
+                stringExtras = mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_PORTRAIT.toString())
             )
             // Launch the PiP activity fixed as landscape, but don't enter PiP
             pipApp.launchViaIntent(
-                    wmHelper,
-                    stringExtras =
-                    mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())
+                wmHelper,
+                stringExtras = mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())
             )
         }
     }
@@ -207,13 +205,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+            return LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
         }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationCfArm.kt
index 5841666..9264219 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationCfArm.kt
@@ -17,9 +17,10 @@
 package com.android.wm.shell.flicker.pip
 
 import android.tools.common.Rotation
+import android.tools.common.flicker.assertions.FlickerTest
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -29,19 +30,19 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class EnterPipToOtherOrientationCfArm(flicker: FlickerTest) :
+open class EnterPipToOtherOrientationCfArm(flicker: LegacyFlickerTest) :
     EnterPipToOtherOrientation(flicker) {
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+            return LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
         }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt
index cdbdb85..6d20740 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTransition.kt
@@ -20,16 +20,14 @@
 import android.tools.common.Rotation
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.Test
 import org.junit.runners.Parameterized
 
-abstract class EnterPipTransition(flicker: FlickerTest) : PipTransition(flicker) {
+abstract class EnterPipTransition(flicker: LegacyFlickerTest) : PipTransition(flicker) {
     override val defaultEnterPip: FlickerBuilder.() -> Unit = {
-        setup {
-            pipApp.launchViaIntent(wmHelper)
-        }
+        setup { pipApp.launchViaIntent(wmHelper) }
     }
 
     /** Checks [pipApp] window remains visible throughout the animation */
@@ -126,15 +124,14 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
-         * and navigation modes.
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring repetitions, screen
+         * orientation and navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt
index 95725b6..76c811c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt
@@ -18,7 +18,7 @@
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.filters.RequiresDevice
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
@@ -50,7 +50,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class EnterPipViaAppUiButtonTest(flicker: FlickerTest) : EnterPipTransition(flicker) {
+open class EnterPipViaAppUiButtonTest(flicker: LegacyFlickerTest) : EnterPipTransition(flicker) {
     override val thisTransition: FlickerBuilder.() -> Unit = {
         transitions { pipApp.clickEnterPipButton(wmHelper) }
     }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTestCfArm.kt
index 4390f0b..78e8049 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTestCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTestCfArm.kt
@@ -18,8 +18,8 @@
 
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -28,20 +28,20 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterPipViaAppUiButtonTestCfArm(flicker: FlickerTest) : EnterPipViaAppUiButtonTest(flicker) {
+class EnterPipViaAppUiButtonTestCfArm(flicker: LegacyFlickerTest) :
+    EnterPipViaAppUiButtonTest(flicker) {
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
-         * and navigation modes.
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring repetitions, screen
+         * orientation and navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt
index 5ac9829..dfffba8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt
@@ -19,14 +19,14 @@
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
 import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import org.junit.Test
 import org.junit.runners.Parameterized
 
 /** Base class for pip expand tests */
-abstract class ExitPipToAppTransition(flicker: FlickerTest) : PipTransition(flicker) {
+abstract class ExitPipToAppTransition(flicker: LegacyFlickerTest) : PipTransition(flicker) {
     protected val testApp = SimpleAppHelper(instrumentation)
 
     /**
@@ -130,15 +130,14 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt
index 0b3d16a..b80b748 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt
@@ -18,7 +18,7 @@
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.filters.RequiresDevice
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
@@ -52,7 +52,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ExitPipToAppViaExpandButtonTest(flicker: FlickerTest) : ExitPipToAppTransition(flicker) {
+open class ExitPipToAppViaExpandButtonTest(flicker: LegacyFlickerTest) :
+    ExitPipToAppTransition(flicker) {
     override val thisTransition: FlickerBuilder.() -> Unit = {
         setup {
             // launch an app behind the pip one
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTestCfArm.kt
index eccb85d..e25c0d6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTestCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTestCfArm.kt
@@ -18,8 +18,8 @@
 
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -28,21 +28,20 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ExitPipToAppViaExpandButtonTestCfArm(flicker: FlickerTest) :
+class ExitPipToAppViaExpandButtonTestCfArm(flicker: LegacyFlickerTest) :
     ExitPipToAppViaExpandButtonTest(flicker) {
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt
index bb2d40b..f003ed8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt
@@ -18,7 +18,7 @@
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.filters.RequiresDevice
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
@@ -51,7 +51,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ExitPipToAppViaIntentTest(flicker: FlickerTest) : ExitPipToAppTransition(flicker) {
+open class ExitPipToAppViaIntentTest(flicker: LegacyFlickerTest) : ExitPipToAppTransition(flicker) {
     override val thisTransition: FlickerBuilder.() -> Unit = {
         setup {
             // launch an app behind the pip one
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTestCfArm.kt
index 6ab6a1f..be19f3c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTestCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTestCfArm.kt
@@ -18,8 +18,8 @@
 
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -28,20 +28,20 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ExitPipToAppViaIntentTestCfArm(flicker: FlickerTest) : ExitPipToAppViaIntentTest(flicker) {
+class ExitPipToAppViaIntentTestCfArm(flicker: LegacyFlickerTest) :
+    ExitPipToAppViaIntentTest(flicker) {
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
-         * and navigation modes.
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring repetitions, screen
+         * orientation and navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
index fd16b6e..a1d3a11 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
@@ -21,8 +21,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -55,7 +55,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ExpandPipOnDoubleClickTest(flicker: FlickerTest) : PipTransition(flicker) {
+open class ExpandPipOnDoubleClickTest(flicker: LegacyFlickerTest) : PipTransition(flicker) {
     override val thisTransition: FlickerBuilder.() -> Unit = {
         transitions { pipApp.doubleClickPipWindow(wmHelper) }
     }
@@ -142,15 +142,14 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTestTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTestTestCfArm.kt
index c096234..3095cac 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTestTestCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTestTestCfArm.kt
@@ -18,8 +18,8 @@
 
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -28,21 +28,20 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ExpandPipOnDoubleClickTestTestCfArm(flicker: FlickerTest) :
+class ExpandPipOnDoubleClickTestTestCfArm(flicker: LegacyFlickerTest) :
     ExpandPipOnDoubleClickTest(flicker) {
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
index 253aa4c..8c8d280 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt
@@ -20,8 +20,8 @@
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -34,7 +34,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ExpandPipOnPinchOpenTest(flicker: FlickerTest) : PipTransition(flicker) {
+open class ExpandPipOnPinchOpenTest(flicker: LegacyFlickerTest) : PipTransition(flicker) {
     override val thisTransition: FlickerBuilder.() -> Unit = {
         transitions { pipApp.pinchOpenPipWindow(wmHelper, 0.25f, 30) }
     }
@@ -55,15 +55,14 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTestCfArm.kt
index e064bf2..1a1ce68 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTestCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTestCfArm.kt
@@ -18,8 +18,8 @@
 
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -28,20 +28,20 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ExpandPipOnPinchOpenTestCfArm(flicker: FlickerTest) : ExpandPipOnPinchOpenTest(flicker) {
+class ExpandPipOnPinchOpenTestCfArm(flicker: LegacyFlickerTest) :
+    ExpandPipOnPinchOpenTest(flicker) {
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt
index 094060f..4f88184 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt
@@ -19,7 +19,7 @@
 import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.Direction
 import org.junit.FixMethodOrder
@@ -55,7 +55,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class MovePipDownOnShelfHeightChange(flicker: FlickerTest) : MovePipShelfHeightTransition(flicker) {
+class MovePipDownOnShelfHeightChange(flicker: LegacyFlickerTest) :
+    MovePipShelfHeightTransition(flicker) {
     override val thisTransition: FlickerBuilder.() -> Unit = {
         teardown { testApp.exit(wmHelper) }
         transitions { testApp.launchViaIntent(wmHelper) }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt
index ff51c27..dffc822 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt
@@ -18,11 +18,12 @@
 
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
+import android.tools.common.flicker.assertions.FlickerTest
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.helpers.WindowUtils
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.helpers.ImeAppHelper
@@ -38,7 +39,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class MovePipOnImeVisibilityChangeTest(flicker: FlickerTest) : PipTransition(flicker) {
+open class MovePipOnImeVisibilityChangeTest(flicker: LegacyFlickerTest) : PipTransition(flicker) {
     private val imeApp = ImeAppHelper(instrumentation)
 
     override val thisTransition: FlickerBuilder.() -> Unit = {
@@ -80,7 +81,7 @@
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+            return LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
         }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTestCfArm.kt
index d3d77d2..63292a4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTestCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTestCfArm.kt
@@ -17,9 +17,10 @@
 package com.android.wm.shell.flicker.pip
 
 import android.tools.common.Rotation
+import android.tools.common.flicker.assertions.FlickerTest
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -28,7 +29,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class MovePipOnImeVisibilityChangeTestCfArm(flicker: FlickerTest) :
+class MovePipOnImeVisibilityChangeTestCfArm(flicker: LegacyFlickerTest) :
     MovePipOnImeVisibilityChangeTest(flicker) {
     companion object {
         private const val TAG_IME_VISIBLE = "imeIsVisible"
@@ -36,7 +37,7 @@
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+            return LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
         }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
index 109354a..9a2fa09 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
@@ -19,15 +19,15 @@
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
 import android.tools.common.flicker.subject.region.RegionSubject
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper
 import com.android.wm.shell.flicker.Direction
 import org.junit.Test
 import org.junit.runners.Parameterized
 
 /** Base class for pip tests with Launcher shelf height change */
-abstract class MovePipShelfHeightTransition(flicker: FlickerTest) : PipTransition(flicker) {
+abstract class MovePipShelfHeightTransition(flicker: LegacyFlickerTest) : PipTransition(flicker) {
     protected val testApp = FixedOrientationAppHelper(instrumentation)
 
     /** Checks [pipApp] window remains visible throughout the animation */
@@ -111,15 +111,14 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt
index 27b061b..afb4af6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt
@@ -19,7 +19,7 @@
 import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.Direction
 import org.junit.FixMethodOrder
@@ -55,14 +55,13 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class MovePipUpOnShelfHeightChangeTest(flicker: FlickerTest) :
+open class MovePipUpOnShelfHeightChangeTest(flicker: LegacyFlickerTest) :
     MovePipShelfHeightTransition(flicker) {
-    override val thisTransition: FlickerBuilder.() -> Unit =
-        {
-            setup { testApp.launchViaIntent(wmHelper) }
-            transitions { tapl.pressHome() }
-            teardown { testApp.exit(wmHelper) }
-        }
+    override val thisTransition: FlickerBuilder.() -> Unit = {
+        setup { testApp.launchViaIntent(wmHelper) }
+        transitions { tapl.pressHome() }
+        teardown { testApp.exit(wmHelper) }
+    }
 
     /** Checks that the visible region of [pipApp] window always moves up during the animation. */
     @Presubmit @Test fun pipWindowMovesUp() = pipWindowMoves(Direction.UP)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt
index 9f81ba8..7085d55 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt
@@ -20,8 +20,8 @@
 import android.platform.test.annotations.RequiresDevice
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -34,7 +34,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class PipDragTest(flicker: FlickerTest) : PipTransition(flicker) {
+class PipDragTest(flicker: LegacyFlickerTest) : PipTransition(flicker) {
     private var isDraggedLeft: Boolean = true
 
     override val thisTransition: FlickerBuilder.() -> Unit = {
@@ -81,13 +81,11 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt
index 9fe9f52..2b87766 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt
@@ -21,8 +21,8 @@
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.helpers.setRotation
@@ -38,7 +38,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class PipDragThenSnapTest(flicker: FlickerTest) : PipTransition(flicker) {
+class PipDragThenSnapTest(flicker: LegacyFlickerTest) : PipTransition(flicker) {
     // represents the direction in which the pip window should be snapping
     private var willSnapRight: Boolean = true
 
@@ -99,15 +99,14 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt
index 60bf5ff..adc5ee3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt
@@ -21,8 +21,8 @@
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -36,7 +36,7 @@
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @FlakyTest(bugId = 270677470)
-class PipPinchInTest(flicker: FlickerTest) : PipTransition(flicker) {
+class PipPinchInTest(flicker: LegacyFlickerTest) : PipTransition(flicker) {
     override val thisTransition: FlickerBuilder.() -> Unit = {
         transitions { pipApp.pinchInPipWindow(wmHelper, 0.4f, 30) }
     }
@@ -57,15 +57,14 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
index 17a178f..096af39 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt
@@ -22,7 +22,7 @@
 import android.tools.common.Rotation
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
 import android.tools.device.helpers.WindowUtils
 import com.android.server.wm.flicker.helpers.PipAppHelper
@@ -32,7 +32,7 @@
 import com.google.common.truth.Truth
 import org.junit.Test
 
-abstract class PipTransition(flicker: FlickerTest) : BaseTest(flicker) {
+abstract class PipTransition(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     protected val pipApp = PipAppHelper(instrumentation)
     protected val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
     protected val broadcastActionTrigger = BroadcastActionTrigger(instrumentation)
@@ -78,16 +78,16 @@
     /** Defines the default method of entering PiP */
     protected open val defaultEnterPip: FlickerBuilder.() -> Unit = {
         setup {
-            pipApp.launchViaIntentAndWaitForPip(wmHelper,
-                    stringExtras = mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true"))
+            pipApp.launchViaIntentAndWaitForPip(
+                wmHelper,
+                stringExtras = mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true")
+            )
         }
     }
 
     /** Defines the default teardown required to clean up after the test */
     protected open val defaultTeardown: FlickerBuilder.() -> Unit = {
-        teardown {
-            pipApp.exit(wmHelper)
-        }
+        teardown { pipApp.exit(wmHelper) }
     }
 
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt
index c618e5a..c315e74 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt
@@ -21,10 +21,11 @@
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
+import android.tools.common.flicker.assertions.FlickerTest
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.helpers.WindowUtils
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.testapp.ActivityOptions
@@ -46,7 +47,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class SetRequestedOrientationWhilePinned(flicker: FlickerTest) : PipTransition(flicker) {
+open class SetRequestedOrientationWhilePinned(flicker: LegacyFlickerTest) : PipTransition(flicker) {
     private val startingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0)
     private val endingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_90)
 
@@ -69,20 +70,19 @@
         setup {
             // Launch the PiP activity fixed as landscape.
             pipApp.launchViaIntent(
-                    wmHelper,
-                    stringExtras =
-                    mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())
+                wmHelper,
+                stringExtras = mapOf(EXTRA_FIXED_ORIENTATION to ORIENTATION_LANDSCAPE.toString())
             )
             // Enter PiP.
             broadcastActionTrigger.doAction(ActivityOptions.Pip.ACTION_ENTER_PIP)
             // System bar may fade out during fixed rotation.
             wmHelper
-                    .StateSyncBuilder()
-                    .withPipShown()
-                    .withRotation(Rotation.ROTATION_0)
-                    .withNavOrTaskBarVisible()
-                    .withStatusBarVisible()
-                    .waitForAndVerify()
+                .StateSyncBuilder()
+                .withPipShown()
+                .withRotation(Rotation.ROTATION_0)
+                .withNavOrTaskBarVisible()
+                .withStatusBarVisible()
+                .waitForAndVerify()
         }
     }
 
@@ -150,7 +150,7 @@
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+            return LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
         }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt
index 43d6c8f..0ff9cff 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt
@@ -17,10 +17,11 @@
 package com.android.wm.shell.flicker.pip
 
 import android.platform.test.annotations.Presubmit
+import android.tools.common.flicker.assertions.FlickerTest
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.helpers.WindowUtils
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
@@ -58,7 +59,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ShowPipAndRotateDisplay(flicker: FlickerTest) : PipTransition(flicker) {
+open class ShowPipAndRotateDisplay(flicker: LegacyFlickerTest) : PipTransition(flicker) {
     private val testApp = SimpleAppHelper(instrumentation)
     private val screenBoundsStart = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
     private val screenBoundsEnd = WindowUtils.getDisplayBounds(flicker.scenario.endRotation)
@@ -154,13 +155,13 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
-         * and navigation modes.
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring repetitions, screen
+         * orientation and navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.rotationTests()
+            return LegacyFlickerTestFactory.rotationTests()
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplayCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplayCfArm.kt
index b7a2c47..2516471 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplayCfArm.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplayCfArm.kt
@@ -16,9 +16,10 @@
 
 package com.android.wm.shell.flicker.pip
 
+import android.tools.common.flicker.assertions.FlickerTest
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -27,18 +28,18 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ShowPipAndRotateDisplayCfArm(flicker: FlickerTest) : ShowPipAndRotateDisplay(flicker) {
+class ShowPipAndRotateDisplayCfArm(flicker: LegacyFlickerTest) : ShowPipAndRotateDisplay(flicker) {
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation
-         * and navigation modes.
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring repetitions, screen
+         * orientation and navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.rotationTests()
+            return LegacyFlickerTestFactory.rotationTests()
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/SplitScreenUtils.kt
new file mode 100644
index 0000000..e640dc4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/SplitScreenUtils.kt
@@ -0,0 +1,387 @@
+/*
+ * 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.flicker.service.splitscreen
+
+import android.app.Instrumentation
+import android.graphics.Point
+import android.os.SystemClock
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.common.traces.component.IComponentMatcher
+import android.tools.common.traces.component.IComponentNameMatcher
+import android.tools.device.apphelpers.StandardAppHelper
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import android.tools.device.traces.parsers.toFlickerComponent
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.ViewConfiguration
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
+import com.android.server.wm.flicker.helpers.NotificationAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
+import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
+import org.junit.Assert.assertNotNull
+
+object SplitScreenUtils {
+    private const val TIMEOUT_MS = 3_000L
+    private const val DRAG_DURATION_MS = 1_000L
+    private const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
+    private const val DIVIDER_BAR = "docked_divider_handle"
+    private const val OVERVIEW_SNAPSHOT = "snapshot"
+    private const val GESTURE_STEP_MS = 16L
+    private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L
+    private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#")
+
+    private val notificationScrollerSelector: BySelector
+        get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER)
+    private val notificationContentSelector: BySelector
+        get() = By.text("Flicker Test Notification")
+    private val dividerBarSelector: BySelector
+        get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR)
+    private val overviewSnapshotSelector: BySelector
+        get() = By.res(LAUNCHER_UI_PACKAGE_NAME, OVERVIEW_SNAPSHOT)
+
+    fun getPrimary(instrumentation: Instrumentation): StandardAppHelper =
+        SimpleAppHelper(
+            instrumentation,
+            ActivityOptions.SplitScreen.Primary.LABEL,
+            ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()
+        )
+
+    fun getSecondary(instrumentation: Instrumentation): StandardAppHelper =
+        SimpleAppHelper(
+            instrumentation,
+            ActivityOptions.SplitScreen.Secondary.LABEL,
+            ActivityOptions.SplitScreen.Secondary.COMPONENT.toFlickerComponent()
+        )
+
+    fun getNonResizeable(instrumentation: Instrumentation): NonResizeableAppHelper =
+        NonResizeableAppHelper(instrumentation)
+
+    fun getSendNotification(instrumentation: Instrumentation): NotificationAppHelper =
+        NotificationAppHelper(instrumentation)
+
+    fun getIme(instrumentation: Instrumentation): ImeAppHelper = ImeAppHelper(instrumentation)
+
+    fun waitForSplitComplete(
+        wmHelper: WindowManagerStateHelper,
+        primaryApp: IComponentMatcher,
+        secondaryApp: IComponentMatcher,
+    ) {
+        wmHelper
+            .StateSyncBuilder()
+            .withWindowSurfaceAppeared(primaryApp)
+            .withWindowSurfaceAppeared(secondaryApp)
+            .withSplitDividerVisible()
+            .waitForAndVerify()
+    }
+
+    fun enterSplit(
+        wmHelper: WindowManagerStateHelper,
+        tapl: LauncherInstrumentation,
+        device: UiDevice,
+        primaryApp: StandardAppHelper,
+        secondaryApp: StandardAppHelper
+    ) {
+        primaryApp.launchViaIntent(wmHelper)
+        secondaryApp.launchViaIntent(wmHelper)
+        tapl.goHome()
+        wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
+        splitFromOverview(tapl, device)
+        waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+    }
+
+    fun splitFromOverview(tapl: LauncherInstrumentation, device: UiDevice) {
+        // Note: The initial split position in landscape is different between tablet and phone.
+        // In landscape, tablet will let the first app split to right side, and phone will
+        // split to left side.
+        if (tapl.isTablet) {
+            // TAPL's currentTask on tablet is sometimes not what we expected if the overview
+            // contains more than 3 task views. We need to use uiautomator directly to find the
+            // second task to split.
+            tapl.workspace.switchToOverview().overviewActions.clickSplit()
+            val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS)
+            if (snapshots == null || snapshots.size < 1) {
+                error("Fail to find a overview snapshot to split.")
+            }
+
+            // Find the second task in the upper right corner in split select mode by sorting
+            // 'left' in descending order and 'top' in ascending order.
+            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
+                t2.getVisibleBounds().left - t1.getVisibleBounds().left
+            }
+            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
+                t1.getVisibleBounds().top - t2.getVisibleBounds().top
+            }
+            snapshots[0].click()
+        } else {
+            tapl.workspace
+                .switchToOverview()
+                .currentTask
+                .tapMenu()
+                .tapSplitMenuItem()
+                .currentTask
+                .open()
+        }
+        SystemClock.sleep(TIMEOUT_MS)
+    }
+
+    fun enterSplitViaIntent(
+        wmHelper: WindowManagerStateHelper,
+        primaryApp: StandardAppHelper,
+        secondaryApp: StandardAppHelper
+    ) {
+        val stringExtras =
+            mapOf(ActivityOptions.SplitScreen.Primary.EXTRA_LAUNCH_ADJACENT to "true")
+        primaryApp.launchViaIntent(wmHelper, null, null, stringExtras)
+        SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+    }
+
+    fun dragFromNotificationToSplit(
+        instrumentation: Instrumentation,
+        device: UiDevice,
+        wmHelper: WindowManagerStateHelper
+    ) {
+        val displayBounds =
+            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
+                ?: error("Display not found")
+
+        // Pull down the notifications
+        device.swipe(
+            displayBounds.centerX(),
+            5,
+            displayBounds.centerX(),
+            displayBounds.bottom,
+            50 /* steps */
+        )
+        SystemClock.sleep(TIMEOUT_MS)
+
+        // Find the target notification
+        val notificationScroller =
+            device.wait(Until.findObject(notificationScrollerSelector), TIMEOUT_MS)
+                ?: error("Unable to find view $notificationScrollerSelector")
+        var notificationContent = notificationScroller.findObject(notificationContentSelector)
+
+        while (notificationContent == null) {
+            device.swipe(
+                displayBounds.centerX(),
+                displayBounds.centerY(),
+                displayBounds.centerX(),
+                displayBounds.centerY() - 150,
+                20 /* steps */
+            )
+            notificationContent = notificationScroller.findObject(notificationContentSelector)
+        }
+
+        // Drag to split
+        val dragStart = notificationContent.visibleCenter
+        val dragMiddle = Point(dragStart.x + 50, dragStart.y)
+        val dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4)
+        val downTime = SystemClock.uptimeMillis()
+
+        touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, dragStart)
+        // It needs a horizontal movement to trigger the drag
+        touchMove(
+            instrumentation,
+            downTime,
+            SystemClock.uptimeMillis(),
+            DRAG_DURATION_MS,
+            dragStart,
+            dragMiddle
+        )
+        touchMove(
+            instrumentation,
+            downTime,
+            SystemClock.uptimeMillis(),
+            DRAG_DURATION_MS,
+            dragMiddle,
+            dragEnd
+        )
+        // Wait for a while to start splitting
+        SystemClock.sleep(TIMEOUT_MS)
+        touch(
+            instrumentation,
+            MotionEvent.ACTION_UP,
+            downTime,
+            SystemClock.uptimeMillis(),
+            GESTURE_STEP_MS,
+            dragEnd
+        )
+        SystemClock.sleep(TIMEOUT_MS)
+    }
+
+    fun touch(
+        instrumentation: Instrumentation,
+        action: Int,
+        downTime: Long,
+        eventTime: Long,
+        duration: Long,
+        point: Point
+    ) {
+        val motionEvent =
+            MotionEvent.obtain(downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0)
+        motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN
+        instrumentation.uiAutomation.injectInputEvent(motionEvent, true)
+        motionEvent.recycle()
+        SystemClock.sleep(duration)
+    }
+
+    fun touchMove(
+        instrumentation: Instrumentation,
+        downTime: Long,
+        eventTime: Long,
+        duration: Long,
+        from: Point,
+        to: Point
+    ) {
+        val steps: Long = duration / GESTURE_STEP_MS
+        var currentTime = eventTime
+        var currentX = from.x.toFloat()
+        var currentY = from.y.toFloat()
+        val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat()
+        val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat()
+
+        for (i in 1..steps) {
+            val motionMove =
+                MotionEvent.obtain(
+                    downTime,
+                    currentTime,
+                    MotionEvent.ACTION_MOVE,
+                    currentX,
+                    currentY,
+                    0
+                )
+            motionMove.source = InputDevice.SOURCE_TOUCHSCREEN
+            instrumentation.uiAutomation.injectInputEvent(motionMove, true)
+            motionMove.recycle()
+
+            currentTime += GESTURE_STEP_MS
+            if (i == steps - 1) {
+                currentX = to.x.toFloat()
+                currentY = to.y.toFloat()
+            } else {
+                currentX += stepX
+                currentY += stepY
+            }
+            SystemClock.sleep(GESTURE_STEP_MS)
+        }
+    }
+
+    fun createShortcutOnHotseatIfNotExist(tapl: LauncherInstrumentation, appName: String) {
+        tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0))
+        val allApps = tapl.workspace.switchToAllApps()
+        allApps.freeze()
+        try {
+            allApps.getAppIcon(appName).dragToHotseat(0)
+        } finally {
+            allApps.unfreeze()
+        }
+    }
+
+    fun dragDividerToResizeAndWait(device: UiDevice, wmHelper: WindowManagerStateHelper) {
+        val displayBounds =
+            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
+                ?: error("Display not found")
+        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
+        dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3), 200)
+
+        wmHelper
+            .StateSyncBuilder()
+            .withWindowSurfaceDisappeared(SPLIT_DECOR_MANAGER)
+            .waitForAndVerify()
+    }
+
+    fun dragDividerToDismissSplit(
+        device: UiDevice,
+        wmHelper: WindowManagerStateHelper,
+        dragToRight: Boolean,
+        dragToBottom: Boolean
+    ) {
+        val displayBounds =
+            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
+                ?: error("Display not found")
+        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
+        dividerBar.drag(
+            Point(
+                if (dragToRight) {
+                    displayBounds.width * 4 / 5
+                } else {
+                    displayBounds.width * 1 / 5
+                },
+                if (dragToBottom) {
+                    displayBounds.height * 4 / 5
+                } else {
+                    displayBounds.height * 1 / 5
+                }
+            )
+        )
+    }
+
+    fun doubleTapDividerToSwitch(device: UiDevice) {
+        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
+        val interval =
+            (ViewConfiguration.getDoubleTapTimeout() + ViewConfiguration.getDoubleTapMinTime()) / 2
+        dividerBar.click()
+        SystemClock.sleep(interval.toLong())
+        dividerBar.click()
+    }
+
+    fun copyContentInSplit(
+        instrumentation: Instrumentation,
+        device: UiDevice,
+        sourceApp: IComponentNameMatcher,
+        destinationApp: IComponentNameMatcher,
+    ) {
+        // Copy text from sourceApp
+        val textView =
+            device.wait(
+                Until.findObject(By.res(sourceApp.packageName, "SplitScreenTest")),
+                TIMEOUT_MS
+            )
+        assertNotNull("Unable to find the TextView", textView)
+        textView.click(LONG_PRESS_TIME_MS)
+
+        val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS)
+        assertNotNull("Unable to find the copy button", copyBtn)
+        copyBtn.click()
+
+        // Paste text to destinationApp
+        val editText =
+            device.wait(
+                Until.findObject(By.res(destinationApp.packageName, "plain_text_input")),
+                TIMEOUT_MS
+            )
+        assertNotNull("Unable to find the EditText", editText)
+        editText.click(LONG_PRESS_TIME_MS)
+
+        val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS)
+        assertNotNull("Unable to find the paste button", pasteBtn)
+        pasteBtn.click()
+
+        // Verify text
+        if (!textView.text.contentEquals(editText.text)) {
+            error("Fail to copy content in split")
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..566adec
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit
+import org.junit.Test
+
+@RequiresDevice
+class CopyContentInSplitGesturalNavLandscapeBenchmark : CopyContentInSplit(Rotation.ROTATION_90) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun copyContentInSplit() = super.copyContentInSplit()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..92b6227
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit
+import org.junit.Test
+
+@RequiresDevice
+class CopyContentInSplitGesturalNavPortraitBenchmark : CopyContentInSplit(Rotation.ROTATION_0) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun copyContentInSplit() = super.copyContentInSplit()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..e6d56b5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByDivider
+import org.junit.Test
+
+@RequiresDevice
+class DismissSplitScreenByDividerGesturalNavLandscapeBenchmark :
+    DismissSplitScreenByDivider(Rotation.ROTATION_90) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun dismissSplitScreenByDivider() = super.dismissSplitScreenByDivider()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..6752c58
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByDivider
+import org.junit.Test
+
+@RequiresDevice
+class DismissSplitScreenByDividerGesturalNavPortraitBenchmark :
+    DismissSplitScreenByDivider(Rotation.ROTATION_0) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun dismissSplitScreenByDivider() = super.dismissSplitScreenByDivider()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..7c9ab99
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByGoHome
+import org.junit.Test
+
+@RequiresDevice
+class DismissSplitScreenByGoHomeGesturalNavLandscapeBenchmark :
+    DismissSplitScreenByGoHome(Rotation.ROTATION_90) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun dismissSplitScreenByGoHome() = super.dismissSplitScreenByGoHome()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..4b79571
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByGoHome
+import org.junit.Test
+
+@RequiresDevice
+class DismissSplitScreenByGoHomeGesturalNavPortraitBenchmark :
+    DismissSplitScreenByGoHome(Rotation.ROTATION_0) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun dismissSplitScreenByGoHome() = super.dismissSplitScreenByGoHome()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..0495079
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.DragDividerToResize
+import org.junit.Test
+
+@RequiresDevice
+class DragDividerToResizeGesturalNavLandscapeBenchmark : DragDividerToResize(Rotation.ROTATION_90) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun dragDividerToResize() = super.dragDividerToResize()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..71ef48b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.DragDividerToResize
+import org.junit.Test
+
+@RequiresDevice
+class DragDividerToResizeGesturalNavPortraitBenchmark : DragDividerToResize(Rotation.ROTATION_0) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun dragDividerToResize() = super.dragDividerToResize()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..c78729c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromAllApps
+import org.junit.Test
+
+@RequiresDevice
+class EnterSplitScreenByDragFromAllAppsGesturalNavLandscapeBenchmark :
+    EnterSplitScreenByDragFromAllApps(Rotation.ROTATION_90) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun enterSplitScreenByDragFromAllApps() = super.enterSplitScreenByDragFromAllApps()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..30bce2f6
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromAllApps
+import org.junit.Test
+
+@RequiresDevice
+class EnterSplitScreenByDragFromAllAppsGesturalNavPortraitBenchmark :
+    EnterSplitScreenByDragFromAllApps(Rotation.ROTATION_0) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun enterSplitScreenByDragFromAllApps() = super.enterSplitScreenByDragFromAllApps()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..b33ea7c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromNotification
+import org.junit.Test
+
+@RequiresDevice
+class EnterSplitScreenByDragFromNotificationGesturalNavLandscapeBenchmark :
+    EnterSplitScreenByDragFromNotification(Rotation.ROTATION_90) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun enterSplitScreenByDragFromNotification() =
+        super.enterSplitScreenByDragFromNotification()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..07a86a5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromNotification
+import org.junit.Test
+
+@RequiresDevice
+class EnterSplitScreenByDragFromNotificationGesturalNavPortraitBenchmark :
+    EnterSplitScreenByDragFromNotification(Rotation.ROTATION_0) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun enterSplitScreenByDragFromNotification() =
+        super.enterSplitScreenByDragFromNotification()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..9a1d127
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromShortcut
+import org.junit.Test
+
+@RequiresDevice
+class EnterSplitScreenByDragFromShortcutGesturalNavLandscapeBenchmark :
+    EnterSplitScreenByDragFromShortcut(Rotation.ROTATION_90) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun enterSplitScreenByDragFromShortcut() = super.enterSplitScreenByDragFromShortcut()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..266e268
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromShortcut
+import org.junit.Test
+
+@RequiresDevice
+class EnterSplitScreenByDragFromShortcutGesturalNavPortraitBenchmark :
+    EnterSplitScreenByDragFromShortcut(Rotation.ROTATION_0) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun enterSplitScreenByDragFromShortcut() = super.enterSplitScreenByDragFromShortcut()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..83fc30b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromTaskbar
+import org.junit.Test
+
+@RequiresDevice
+class EnterSplitScreenByDragFromTaskbarGesturalNavLandscapeBenchmark :
+    EnterSplitScreenByDragFromTaskbar(Rotation.ROTATION_90) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun enterSplitScreenByDragFromTaskbar() = super.enterSplitScreenByDragFromTaskbar()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..b2f1929
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromTaskbar
+import org.junit.Test
+
+@RequiresDevice
+class EnterSplitScreenByDragFromTaskbarGesturalNavPortraitBenchmark :
+    EnterSplitScreenByDragFromTaskbar(Rotation.ROTATION_0) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun enterSplitScreenByDragFromTaskbar() = super.enterSplitScreenByDragFromTaskbar()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..dae92dd
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenFromOverview
+import org.junit.Test
+
+@RequiresDevice
+class EnterSplitScreenFromOverviewGesturalNavLandscapeBenchmark :
+    EnterSplitScreenFromOverview(Rotation.ROTATION_90) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun enterSplitScreenFromOverview() = super.enterSplitScreenFromOverview()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..732047b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenFromOverview
+import org.junit.Test
+
+@RequiresDevice
+class EnterSplitScreenFromOverviewGesturalNavPortraitBenchmark :
+    EnterSplitScreenFromOverview(Rotation.ROTATION_0) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun enterSplitScreenFromOverview() = super.enterSplitScreenFromOverview()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..1de7efd
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchAppByDoubleTapDivider
+import org.junit.Test
+
+@RequiresDevice
+class SwitchAppByDoubleTapDividerGesturalNavLandscapeBenchmark :
+    SwitchAppByDoubleTapDivider(Rotation.ROTATION_90) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun switchAppByDoubleTapDivider() = super.switchAppByDoubleTapDivider()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..1a046aa
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchAppByDoubleTapDivider
+import org.junit.Test
+
+@RequiresDevice
+class SwitchAppByDoubleTapDividerGesturalNavPortraitBenchmark :
+    SwitchAppByDoubleTapDivider(Rotation.ROTATION_0) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun switchAppByDoubleTapDivider() = super.switchAppByDoubleTapDivider()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..6e88f0e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromAnotherApp
+import org.junit.Test
+
+@RequiresDevice
+class SwitchBackToSplitFromAnotherAppGesturalNavLandscapeBenchmark :
+    SwitchBackToSplitFromAnotherApp(Rotation.ROTATION_90) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun switchBackToSplitFromAnotherApp() = super.switchBackToSplitFromAnotherApp()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..d26a29c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromAnotherApp
+import org.junit.Test
+
+@RequiresDevice
+class SwitchBackToSplitFromAnotherAppGesturalNavPortraitBenchmark :
+    SwitchBackToSplitFromAnotherApp(Rotation.ROTATION_0) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun switchBackToSplitFromAnotherApp() = super.switchBackToSplitFromAnotherApp()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..4a552b0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromHome
+import org.junit.Test
+
+@RequiresDevice
+class SwitchBackToSplitFromHomeGesturalNavLandscapeBenchmark :
+    SwitchBackToSplitFromHome(Rotation.ROTATION_90) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun switchBackToSplitFromHome() = super.switchBackToSplitFromHome()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..b7376ea
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromHome
+import org.junit.Test
+
+@RequiresDevice
+class SwitchBackToSplitFromHomeGesturalNavPortraitBenchmark :
+    SwitchBackToSplitFromHome(Rotation.ROTATION_0) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun switchBackToSplitFromHome() = super.switchBackToSplitFromHome()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..b2d05e4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromRecent
+import org.junit.Test
+
+@RequiresDevice
+class SwitchBackToSplitFromRecentGesturalNavLandscapeBenchmark :
+    SwitchBackToSplitFromRecent(Rotation.ROTATION_90) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun switchBackToSplitFromRecent() = super.switchBackToSplitFromRecent()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..6de31b1
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromRecent
+import org.junit.Test
+
+@RequiresDevice
+class SwitchBackToSplitFromRecentGesturalNavPortraitBenchmark :
+    SwitchBackToSplitFromRecent(Rotation.ROTATION_0) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun switchBackToSplitFromRecent() = super.switchBackToSplitFromRecent()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..aab18a6
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBetweenSplitPairs
+import org.junit.Test
+
+@RequiresDevice
+class SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark :
+    SwitchBetweenSplitPairs(Rotation.ROTATION_90) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun switchBetweenSplitPairs() = super.switchBetweenSplitPairs()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..b074f2c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.Rotation
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBetweenSplitPairs
+import org.junit.Test
+
+@RequiresDevice
+class SwitchBetweenSplitPairsGesturalNavPortraitBenchmark :
+    SwitchBetweenSplitPairs(Rotation.ROTATION_0) {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun switchBetweenSplitPairs() = super.switchBetweenSplitPairs()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark.kt
new file mode 100644
index 0000000..c402aa4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.UnlockKeyguardToSplitScreen
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+@RequiresDevice
+@RunWith(BlockJUnit4ClassRunner::class)
+class UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark : UnlockKeyguardToSplitScreen() {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun unlockKeyguardToSplitScreen() = super.unlockKeyguardToSplitScreen()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavPortraitBenchmark.kt
new file mode 100644
index 0000000..840401c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavPortraitBenchmark.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.wm.shell.flicker.service.splitscreen.benchmark
+
+import android.platform.test.annotations.PlatinumTest
+import android.platform.test.annotations.Presubmit
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.UnlockKeyguardToSplitScreen
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+@RequiresDevice
+@RunWith(BlockJUnit4ClassRunner::class)
+class UnlockKeyguardToSplitScreenGesturalNavPortraitBenchmark : UnlockKeyguardToSplitScreen() {
+    @PlatinumTest(focusArea = "sysui")
+    @Presubmit
+    @Test
+    override fun unlockKeyguardToSplitScreen() = super.unlockKeyguardToSplitScreen()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/CopyContentInSplitGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/CopyContentInSplitGesturalNavLandscape.kt
new file mode 100644
index 0000000..a5c5122
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/CopyContentInSplitGesturalNavLandscape.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class CopyContentInSplitGesturalNavLandscape : CopyContentInSplit(Rotation.ROTATION_90) {
+    @ExpectedScenarios([]) @Test override fun copyContentInSplit() = super.copyContentInSplit()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/CopyContentInSplitGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/CopyContentInSplitGesturalNavPortrait.kt
new file mode 100644
index 0000000..092fb67
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/CopyContentInSplitGesturalNavPortrait.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class CopyContentInSplitGesturalNavPortrait : CopyContentInSplit(Rotation.ROTATION_0) {
+    @ExpectedScenarios([]) @Test override fun copyContentInSplit() = super.copyContentInSplit()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByDividerGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByDividerGesturalNavLandscape.kt
new file mode 100644
index 0000000..8cb25fe
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByDividerGesturalNavLandscape.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByDivider
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class DismissSplitScreenByDividerGesturalNavLandscape :
+    DismissSplitScreenByDivider(Rotation.ROTATION_90) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_EXIT"])
+    @Test
+    override fun dismissSplitScreenByDivider() = super.dismissSplitScreenByDivider()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByDividerGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByDividerGesturalNavPortrait.kt
new file mode 100644
index 0000000..fa1be63
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByDividerGesturalNavPortrait.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByDivider
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class DismissSplitScreenByDividerGesturalNavPortrait :
+    DismissSplitScreenByDivider(Rotation.ROTATION_0) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_EXIT"])
+    @Test
+    override fun dismissSplitScreenByDivider() = super.dismissSplitScreenByDivider()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByGoHomeGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByGoHomeGesturalNavLandscape.kt
new file mode 100644
index 0000000..aa35237
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByGoHomeGesturalNavLandscape.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByGoHome
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class DismissSplitScreenByGoHomeGesturalNavLandscape :
+    DismissSplitScreenByGoHome(Rotation.ROTATION_90) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_EXIT"])
+    @Test
+    override fun dismissSplitScreenByGoHome() = super.dismissSplitScreenByGoHome()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByGoHomeGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByGoHomeGesturalNavPortrait.kt
new file mode 100644
index 0000000..e195360
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByGoHomeGesturalNavPortrait.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByGoHome
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class DismissSplitScreenByGoHomeGesturalNavPortrait :
+    DismissSplitScreenByGoHome(Rotation.ROTATION_0) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_EXIT"])
+    @Test
+    override fun dismissSplitScreenByGoHome() = super.dismissSplitScreenByGoHome()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DragDividerToResizeGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DragDividerToResizeGesturalNavLandscape.kt
new file mode 100644
index 0000000..c1b3aad
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DragDividerToResizeGesturalNavLandscape.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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.DragDividerToResize
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class DragDividerToResizeGesturalNavLandscape : DragDividerToResize(Rotation.ROTATION_90) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_RESIZE"])
+    @Test
+    override fun dragDividerToResize() = super.dragDividerToResize()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DragDividerToResizeGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DragDividerToResizeGesturalNavPortrait.kt
new file mode 100644
index 0000000..c6e2e85
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DragDividerToResizeGesturalNavPortrait.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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.DragDividerToResize
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class DragDividerToResizeGesturalNavPortrait : DragDividerToResize(Rotation.ROTATION_0) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_RESIZE"])
+    @Test
+    override fun dragDividerToResize() = super.dragDividerToResize()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt
new file mode 100644
index 0000000..5f771c7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromAllApps
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class EnterSplitScreenByDragFromAllAppsGesturalNavLandscape :
+    EnterSplitScreenByDragFromAllApps(Rotation.ROTATION_90) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_ENTER"])
+    @Test
+    override fun enterSplitScreenByDragFromAllApps() = super.enterSplitScreenByDragFromAllApps()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt
new file mode 100644
index 0000000..729a401
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromAllApps
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class EnterSplitScreenByDragFromAllAppsGesturalNavPortrait :
+    EnterSplitScreenByDragFromAllApps(Rotation.ROTATION_0) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_ENTER"])
+    @Test
+    override fun enterSplitScreenByDragFromAllApps() = super.enterSplitScreenByDragFromAllApps()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt
new file mode 100644
index 0000000..6e4cf9f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromNotification
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class EnterSplitScreenByDragFromNotificationGesturalNavLandscape :
+    EnterSplitScreenByDragFromNotification(Rotation.ROTATION_90) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_ENTER"])
+    @Test
+    override fun enterSplitScreenByDragFromNotification() =
+        super.enterSplitScreenByDragFromNotification()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt
new file mode 100644
index 0000000..cc28702
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromNotification
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class EnterSplitScreenByDragFromNotificationGesturalNavPortrait :
+    EnterSplitScreenByDragFromNotification(Rotation.ROTATION_0) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_ENTER"])
+    @Test
+    override fun enterSplitScreenByDragFromNotification() =
+        super.enterSplitScreenByDragFromNotification()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt
new file mode 100644
index 0000000..736604f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromShortcut
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class EnterSplitScreenByDragFromShortcutGesturalNavLandscape :
+    EnterSplitScreenByDragFromShortcut(Rotation.ROTATION_90) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_ENTER"])
+    @Test
+    override fun enterSplitScreenByDragFromShortcut() = super.enterSplitScreenByDragFromShortcut()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt
new file mode 100644
index 0000000..8df8dfa
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromShortcut
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class EnterSplitScreenByDragFromShortcutGesturalNavPortrait :
+    EnterSplitScreenByDragFromShortcut(Rotation.ROTATION_0) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_ENTER"])
+    @Test
+    override fun enterSplitScreenByDragFromShortcut() = super.enterSplitScreenByDragFromShortcut()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt
new file mode 100644
index 0000000..378f055
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromTaskbar
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class EnterSplitScreenByDragFromTaskbarGesturalNavLandscape :
+    EnterSplitScreenByDragFromTaskbar(Rotation.ROTATION_90) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_ENTER"])
+    @Test
+    override fun enterSplitScreenByDragFromTaskbar() = super.enterSplitScreenByDragFromTaskbar()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt
new file mode 100644
index 0000000..b33d262
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromTaskbar
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class EnterSplitScreenByDragFromTaskbarGesturalNavPortrait :
+    EnterSplitScreenByDragFromTaskbar(Rotation.ROTATION_0) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_ENTER"])
+    @Test
+    override fun enterSplitScreenByDragFromTaskbar() = super.enterSplitScreenByDragFromTaskbar()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenFromOverviewGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenFromOverviewGesturalNavLandscape.kt
new file mode 100644
index 0000000..b1d3858
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenFromOverviewGesturalNavLandscape.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenFromOverview
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class EnterSplitScreenFromOverviewGesturalNavLandscape :
+    EnterSplitScreenFromOverview(Rotation.ROTATION_90) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_ENTER"])
+    @Test
+    override fun enterSplitScreenFromOverview() = super.enterSplitScreenFromOverview()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenFromOverviewGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenFromOverviewGesturalNavPortrait.kt
new file mode 100644
index 0000000..6d824c7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenFromOverviewGesturalNavPortrait.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenFromOverview
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class EnterSplitScreenFromOverviewGesturalNavPortrait :
+    EnterSplitScreenFromOverview(Rotation.ROTATION_0) {
+
+    @ExpectedScenarios(["SPLIT_SCREEN_ENTER"])
+    @Test
+    override fun enterSplitScreenFromOverview() = super.enterSplitScreenFromOverview()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt
new file mode 100644
index 0000000..f1d3d0c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchAppByDoubleTapDivider
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SwitchAppByDoubleTapDividerGesturalNavLandscape :
+    SwitchAppByDoubleTapDivider(Rotation.ROTATION_90) {
+
+    @ExpectedScenarios([])
+    @Test
+    override fun switchAppByDoubleTapDivider() = super.switchAppByDoubleTapDivider()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt
new file mode 100644
index 0000000..a867bac
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchAppByDoubleTapDivider
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SwitchAppByDoubleTapDividerGesturalNavPortrait :
+    SwitchAppByDoubleTapDivider(Rotation.ROTATION_0) {
+
+    @ExpectedScenarios([])
+    @Test
+    override fun switchAppByDoubleTapDivider() = super.switchAppByDoubleTapDivider()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt
new file mode 100644
index 0000000..76247ba
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromAnotherApp
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SwitchBackToSplitFromAnotherAppGesturalNavLandscape :
+    SwitchBackToSplitFromAnotherApp(Rotation.ROTATION_90) {
+
+    @ExpectedScenarios(["QUICKSWITCH"])
+    @Test
+    override fun switchBackToSplitFromAnotherApp() = super.switchBackToSplitFromAnotherApp()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt
new file mode 100644
index 0000000..e179da8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromAnotherApp
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SwitchBackToSplitFromAnotherAppGesturalNavPortrait :
+    SwitchBackToSplitFromAnotherApp(Rotation.ROTATION_0) {
+
+    @ExpectedScenarios(["QUICKSWITCH"])
+    @Test
+    override fun switchBackToSplitFromAnotherApp() = super.switchBackToSplitFromAnotherApp()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromHomeGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromHomeGesturalNavLandscape.kt
new file mode 100644
index 0000000..20f554f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromHomeGesturalNavLandscape.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromHome
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SwitchBackToSplitFromHomeGesturalNavLandscape :
+    SwitchBackToSplitFromHome(Rotation.ROTATION_90) {
+
+    @ExpectedScenarios(["QUICKSWITCH"])
+    @Test
+    override fun switchBackToSplitFromHome() = super.switchBackToSplitFromHome()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromHomeGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromHomeGesturalNavPortrait.kt
new file mode 100644
index 0000000..f7776ee
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromHomeGesturalNavPortrait.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromHome
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SwitchBackToSplitFromHomeGesturalNavPortrait :
+    SwitchBackToSplitFromHome(Rotation.ROTATION_0) {
+
+    @ExpectedScenarios(["QUICKSWITCH"])
+    @Test
+    override fun switchBackToSplitFromHome() = super.switchBackToSplitFromHome()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromRecentGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromRecentGesturalNavLandscape.kt
new file mode 100644
index 0000000..00f6073
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromRecentGesturalNavLandscape.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromRecent
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SwitchBackToSplitFromRecentGesturalNavLandscape :
+    SwitchBackToSplitFromRecent(Rotation.ROTATION_90) {
+
+    @ExpectedScenarios(["QUICKSWITCH"])
+    @Test
+    override fun switchBackToSplitFromRecent() = super.switchBackToSplitFromRecent()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromRecentGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromRecentGesturalNavPortrait.kt
new file mode 100644
index 0000000..b3340e7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromRecentGesturalNavPortrait.kt
@@ -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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromRecent
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SwitchBackToSplitFromRecentGesturalNavPortrait :
+    SwitchBackToSplitFromRecent(Rotation.ROTATION_0) {
+
+    @ExpectedScenarios(["QUICKSWITCH"])
+    @Test
+    override fun switchBackToSplitFromRecent() = super.switchBackToSplitFromRecent()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBetweenSplitPairsGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBetweenSplitPairsGesturalNavLandscape.kt
new file mode 100644
index 0000000..3da61e5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBetweenSplitPairsGesturalNavLandscape.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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBetweenSplitPairs
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SwitchBetweenSplitPairsGesturalNavLandscape : SwitchBetweenSplitPairs(Rotation.ROTATION_90) {
+
+    @ExpectedScenarios(["QUICKSWITCH"])
+    @Test
+    override fun switchBetweenSplitPairs() = super.switchBetweenSplitPairs()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBetweenSplitPairsGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBetweenSplitPairsGesturalNavPortrait.kt
new file mode 100644
index 0000000..627ae18
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBetweenSplitPairsGesturalNavPortrait.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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.Rotation
+import android.tools.common.flicker.FlickerConfig
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.common.flicker.annotation.FlickerConfigProvider
+import android.tools.common.flicker.config.FlickerConfig
+import android.tools.common.flicker.config.FlickerServiceConfig
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBetweenSplitPairs
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class SwitchBetweenSplitPairsGesturalNavPortrait : SwitchBetweenSplitPairs(Rotation.ROTATION_0) {
+
+    @ExpectedScenarios(["QUICKSWITCH"])
+    @Test
+    override fun switchBetweenSplitPairs() = super.switchBetweenSplitPairs()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt
new file mode 100644
index 0000000..7cbc1c3
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/UnlockKeyguardToSplitScreenGesturalNavLandscape.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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.UnlockKeyguardToSplitScreen
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class UnlockKeyguardToSplitScreenGesturalNavLandscape : UnlockKeyguardToSplitScreen() {
+
+    @ExpectedScenarios(["QUICKSWITCH"])
+    @Test
+    override fun unlockKeyguardToSplitScreen() = super.unlockKeyguardToSplitScreen()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt
new file mode 100644
index 0000000..2eb81e0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/flicker/UnlockKeyguardToSplitScreenGesturalNavPortrait.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.wm.shell.flicker.service.splitscreen.flicker
+
+import android.tools.common.flicker.annotation.ExpectedScenarios
+import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.splitscreen.scenarios.UnlockKeyguardToSplitScreen
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class UnlockKeyguardToSplitScreenGesturalNavPortrait : UnlockKeyguardToSplitScreen() {
+
+    @ExpectedScenarios(["QUICKSWITCH"])
+    @Test
+    override fun unlockKeyguardToSplitScreen() = super.unlockKeyguardToSplitScreen()
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt
new file mode 100644
index 0000000..76ad6b9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt
@@ -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.wm.shell.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class CopyContentInSplit
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+    private val textEditApp = SplitScreenUtils.getIme(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { rotation })
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+
+        SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, textEditApp)
+    }
+
+    @Test
+    open fun copyContentInSplit() {
+        SplitScreenUtils.copyContentInSplit(instrumentation, device, primaryApp, textEditApp)
+    }
+
+    @After
+    fun teardown() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByDivider.kt
new file mode 100644
index 0000000..25182b4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByDivider.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.wm.shell.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class DismissSplitScreenByDivider
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { rotation })
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+
+        SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
+    }
+
+    @Test
+    open fun dismissSplitScreenByDivider() {
+        if (tapl.isTablet) {
+            SplitScreenUtils.dragDividerToDismissSplit(
+                device,
+                wmHelper,
+                dragToRight = false,
+                dragToBottom = true
+            )
+        } else {
+            SplitScreenUtils.dragDividerToDismissSplit(
+                device,
+                wmHelper,
+                dragToRight = true,
+                dragToBottom = true
+            )
+        }
+        wmHelper.StateSyncBuilder().withFullScreenApp(secondaryApp).waitForAndVerify()
+    }
+
+    @After
+    fun teardown() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByGoHome.kt
new file mode 100644
index 0000000..000b628
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByGoHome.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.wm.shell.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class DismissSplitScreenByGoHome
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { rotation })
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+
+        SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
+    }
+
+    @Test
+    open fun dismissSplitScreenByGoHome() {
+        tapl.goHome()
+        wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
+    }
+
+    @After
+    fun teardown() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DragDividerToResize.kt
new file mode 100644
index 0000000..dd9ff3c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DragDividerToResize.kt
@@ -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.wm.shell.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class DragDividerToResize
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { rotation })
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+
+        SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
+    }
+
+    @Test
+    open fun dragDividerToResize() {
+        SplitScreenUtils.dragDividerToResizeAndWait(device, wmHelper)
+    }
+
+    @After
+    fun teardown() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt
new file mode 100644
index 0000000..4bbb9aa
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.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.wm.shell.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class EnterSplitScreenByDragFromAllApps
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { rotation })
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+
+        tapl.goHome()
+        primaryApp.launchViaIntent(wmHelper)
+    }
+
+    @Test
+    open fun enterSplitScreenByDragFromAllApps() {
+        tapl.launchedAppState.taskbar
+            .openAllApps()
+            .getAppIcon(secondaryApp.appName)
+            .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`)
+        SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+    }
+
+    @After
+    fun teardown() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt
new file mode 100644
index 0000000..a2b7526
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.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.wm.shell.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class EnterSplitScreenByDragFromNotification
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+    private val sendNotificationApp = SplitScreenUtils.getSendNotification(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { rotation })
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+
+        // Send a notification
+        sendNotificationApp.launchViaIntent(wmHelper)
+        sendNotificationApp.postNotification(wmHelper)
+        tapl.goHome()
+        primaryApp.launchViaIntent(wmHelper)
+    }
+
+    @Test
+    open fun enterSplitScreenByDragFromNotification() {
+        SplitScreenUtils.dragFromNotificationToSplit(instrumentation, device, wmHelper)
+        SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, sendNotificationApp)
+    }
+
+    @After
+    fun teardown() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+        sendNotificationApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt
new file mode 100644
index 0000000..1ccd813
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.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.wm.shell.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class EnterSplitScreenByDragFromShortcut
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { rotation })
+
+    @Before
+    fun setup() {
+        Assume.assumeTrue(tapl.isTablet)
+
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+
+        tapl.goHome()
+        SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName)
+        primaryApp.launchViaIntent(wmHelper)
+    }
+
+    @Test
+    open fun enterSplitScreenByDragFromShortcut() {
+        tapl.launchedAppState.taskbar
+            .getAppIcon(secondaryApp.appName)
+            .openDeepShortcutMenu()
+            .getMenuItem("Split Screen Secondary Activity")
+            .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`)
+        SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+
+        // TODO: Do we want this check in here? Add to the other tests?
+        //        flicker.splitScreenEntered(
+        //                primaryApp,
+        //                secondaryApp,
+        //                fromOtherApp = false,
+        //                appExistAtStart = false
+        //        )
+    }
+
+    @After
+    fun teardwon() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt
new file mode 100644
index 0000000..664786b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.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.wm.shell.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class EnterSplitScreenByDragFromTaskbar
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { rotation })
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+
+        tapl.goHome()
+        SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName)
+        primaryApp.launchViaIntent(wmHelper)
+    }
+
+    @Test
+    open fun enterSplitScreenByDragFromTaskbar() {
+        tapl.launchedAppState.taskbar
+            .getAppIcon(secondaryApp.appName)
+            .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`)
+        SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+    }
+
+    @After
+    fun teardown() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt
new file mode 100644
index 0000000..88fd084
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.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.wm.shell.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class EnterSplitScreenFromOverview
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { rotation })
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+
+        primaryApp.launchViaIntent(wmHelper)
+        secondaryApp.launchViaIntent(wmHelper)
+        tapl.goHome()
+        wmHelper
+            .StateSyncBuilder()
+            .withAppTransitionIdle()
+            .withHomeActivityVisible()
+            .waitForAndVerify()
+    }
+
+    @Test
+    open fun enterSplitScreenFromOverview() {
+        SplitScreenUtils.splitFromOverview(tapl, device)
+        SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+    }
+
+    @After
+    fun teardown() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SplitScreenUtils.kt
new file mode 100644
index 0000000..83a18e8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SplitScreenUtils.kt
@@ -0,0 +1,422 @@
+/*
+ * 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.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.graphics.Point
+import android.os.SystemClock
+import android.platform.test.rule.NavigationModeRule
+import android.platform.test.rule.PressHomeRule
+import android.platform.test.rule.UnlockScreenRule
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.common.traces.component.IComponentMatcher
+import android.tools.common.traces.component.IComponentNameMatcher
+import android.tools.device.apphelpers.MessagingAppHelper
+import android.tools.device.apphelpers.StandardAppHelper
+import android.tools.device.flicker.rules.ChangeDisplayOrientationRule
+import android.tools.device.flicker.rules.LaunchAppRule
+import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import android.tools.device.traces.parsers.toFlickerComponent
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.ViewConfiguration
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
+import com.android.server.wm.flicker.helpers.NotificationAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
+import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
+import org.junit.Assert.assertNotNull
+import org.junit.rules.RuleChain
+
+object SplitScreenUtils {
+    private const val TIMEOUT_MS = 3_000L
+    private const val DRAG_DURATION_MS = 1_000L
+    private const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
+    private const val DIVIDER_BAR = "docked_divider_handle"
+    private const val OVERVIEW_SNAPSHOT = "snapshot"
+    private const val GESTURE_STEP_MS = 16L
+    private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L
+    private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#")
+
+    private val notificationScrollerSelector: BySelector
+        get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER)
+    private val notificationContentSelector: BySelector
+        get() = By.text("Flicker Test Notification")
+    private val dividerBarSelector: BySelector
+        get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR)
+    private val overviewSnapshotSelector: BySelector
+        get() = By.res(LAUNCHER_UI_PACKAGE_NAME, OVERVIEW_SNAPSHOT)
+
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    fun testSetupRule(navigationMode: () -> NavBar, rotation: () -> Rotation): RuleChain {
+        return RuleChain.outerRule(UnlockScreenRule())
+            .around(
+                NavigationModeRule(
+                    navigationMode().value,
+                    /* changeNavigationModeAfterTest */ false
+                )
+            )
+            .around(
+                LaunchAppRule(MessagingAppHelper(instrumentation), clearCacheAfterParsing = false)
+            )
+            .around(RemoveAllTasksButHomeRule())
+            .around(
+                ChangeDisplayOrientationRule(
+                    rotation(),
+                    resetOrientationAfterTest = false,
+                    clearCacheAfterParsing = false
+                )
+            )
+            .around(PressHomeRule())
+    }
+
+    fun getPrimary(instrumentation: Instrumentation): StandardAppHelper =
+        SimpleAppHelper(
+            instrumentation,
+            ActivityOptions.SplitScreen.Primary.LABEL,
+            ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()
+        )
+
+    fun getSecondary(instrumentation: Instrumentation): StandardAppHelper =
+        SimpleAppHelper(
+            instrumentation,
+            ActivityOptions.SplitScreen.Secondary.LABEL,
+            ActivityOptions.SplitScreen.Secondary.COMPONENT.toFlickerComponent()
+        )
+
+    fun getNonResizeable(instrumentation: Instrumentation): NonResizeableAppHelper =
+        NonResizeableAppHelper(instrumentation)
+
+    fun getSendNotification(instrumentation: Instrumentation): NotificationAppHelper =
+        NotificationAppHelper(instrumentation)
+
+    fun getIme(instrumentation: Instrumentation): ImeAppHelper = ImeAppHelper(instrumentation)
+
+    fun waitForSplitComplete(
+        wmHelper: WindowManagerStateHelper,
+        primaryApp: IComponentMatcher,
+        secondaryApp: IComponentMatcher,
+    ) {
+        wmHelper
+            .StateSyncBuilder()
+            .withWindowSurfaceAppeared(primaryApp)
+            .withWindowSurfaceAppeared(secondaryApp)
+            .withSplitDividerVisible()
+            .waitForAndVerify()
+    }
+
+    fun enterSplit(
+        wmHelper: WindowManagerStateHelper,
+        tapl: LauncherInstrumentation,
+        device: UiDevice,
+        primaryApp: StandardAppHelper,
+        secondaryApp: StandardAppHelper
+    ) {
+        primaryApp.launchViaIntent(wmHelper)
+        secondaryApp.launchViaIntent(wmHelper)
+        tapl.goHome()
+        wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
+        splitFromOverview(tapl, device)
+        waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+    }
+
+    fun splitFromOverview(tapl: LauncherInstrumentation, device: UiDevice) {
+        // Note: The initial split position in landscape is different between tablet and phone.
+        // In landscape, tablet will let the first app split to right side, and phone will
+        // split to left side.
+        if (tapl.isTablet) {
+            // TAPL's currentTask on tablet is sometimes not what we expected if the overview
+            // contains more than 3 task views. We need to use uiautomator directly to find the
+            // second task to split.
+            tapl.workspace.switchToOverview().overviewActions.clickSplit()
+            val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS)
+            if (snapshots == null || snapshots.size < 1) {
+                error("Fail to find a overview snapshot to split.")
+            }
+
+            // Find the second task in the upper right corner in split select mode by sorting
+            // 'left' in descending order and 'top' in ascending order.
+            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
+                t2.getVisibleBounds().left - t1.getVisibleBounds().left
+            }
+            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
+                t1.getVisibleBounds().top - t2.getVisibleBounds().top
+            }
+            snapshots[0].click()
+        } else {
+            tapl.workspace
+                .switchToOverview()
+                .currentTask
+                .tapMenu()
+                .tapSplitMenuItem()
+                .currentTask
+                .open()
+        }
+        SystemClock.sleep(TIMEOUT_MS)
+    }
+
+    fun enterSplitViaIntent(
+        wmHelper: WindowManagerStateHelper,
+        primaryApp: StandardAppHelper,
+        secondaryApp: StandardAppHelper
+    ) {
+        val stringExtras =
+            mapOf(ActivityOptions.SplitScreen.Primary.EXTRA_LAUNCH_ADJACENT to "true")
+        primaryApp.launchViaIntent(wmHelper, null, null, stringExtras)
+        SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+    }
+
+    fun dragFromNotificationToSplit(
+        instrumentation: Instrumentation,
+        device: UiDevice,
+        wmHelper: WindowManagerStateHelper
+    ) {
+        val displayBounds =
+            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
+                ?: error("Display not found")
+
+        // Pull down the notifications
+        device.swipe(
+            displayBounds.centerX(),
+            5,
+            displayBounds.centerX(),
+            displayBounds.bottom,
+            50 /* steps */
+        )
+        SystemClock.sleep(TIMEOUT_MS)
+
+        // Find the target notification
+        val notificationScroller =
+            device.wait(Until.findObject(notificationScrollerSelector), TIMEOUT_MS)
+                ?: error("Unable to find view $notificationScrollerSelector")
+        var notificationContent = notificationScroller.findObject(notificationContentSelector)
+
+        while (notificationContent == null) {
+            device.swipe(
+                displayBounds.centerX(),
+                displayBounds.centerY(),
+                displayBounds.centerX(),
+                displayBounds.centerY() - 150,
+                20 /* steps */
+            )
+            notificationContent = notificationScroller.findObject(notificationContentSelector)
+        }
+
+        // Drag to split
+        val dragStart = notificationContent.visibleCenter
+        val dragMiddle = Point(dragStart.x + 50, dragStart.y)
+        val dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4)
+        val downTime = SystemClock.uptimeMillis()
+
+        touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, dragStart)
+        // It needs a horizontal movement to trigger the drag
+        touchMove(
+            instrumentation,
+            downTime,
+            SystemClock.uptimeMillis(),
+            DRAG_DURATION_MS,
+            dragStart,
+            dragMiddle
+        )
+        touchMove(
+            instrumentation,
+            downTime,
+            SystemClock.uptimeMillis(),
+            DRAG_DURATION_MS,
+            dragMiddle,
+            dragEnd
+        )
+        // Wait for a while to start splitting
+        SystemClock.sleep(TIMEOUT_MS)
+        touch(
+            instrumentation,
+            MotionEvent.ACTION_UP,
+            downTime,
+            SystemClock.uptimeMillis(),
+            GESTURE_STEP_MS,
+            dragEnd
+        )
+        SystemClock.sleep(TIMEOUT_MS)
+    }
+
+    fun touch(
+        instrumentation: Instrumentation,
+        action: Int,
+        downTime: Long,
+        eventTime: Long,
+        duration: Long,
+        point: Point
+    ) {
+        val motionEvent =
+            MotionEvent.obtain(downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0)
+        motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN
+        instrumentation.uiAutomation.injectInputEvent(motionEvent, true)
+        motionEvent.recycle()
+        SystemClock.sleep(duration)
+    }
+
+    fun touchMove(
+        instrumentation: Instrumentation,
+        downTime: Long,
+        eventTime: Long,
+        duration: Long,
+        from: Point,
+        to: Point
+    ) {
+        val steps: Long = duration / GESTURE_STEP_MS
+        var currentTime = eventTime
+        var currentX = from.x.toFloat()
+        var currentY = from.y.toFloat()
+        val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat()
+        val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat()
+
+        for (i in 1..steps) {
+            val motionMove =
+                MotionEvent.obtain(
+                    downTime,
+                    currentTime,
+                    MotionEvent.ACTION_MOVE,
+                    currentX,
+                    currentY,
+                    0
+                )
+            motionMove.source = InputDevice.SOURCE_TOUCHSCREEN
+            instrumentation.uiAutomation.injectInputEvent(motionMove, true)
+            motionMove.recycle()
+
+            currentTime += GESTURE_STEP_MS
+            if (i == steps - 1) {
+                currentX = to.x.toFloat()
+                currentY = to.y.toFloat()
+            } else {
+                currentX += stepX
+                currentY += stepY
+            }
+            SystemClock.sleep(GESTURE_STEP_MS)
+        }
+    }
+
+    fun createShortcutOnHotseatIfNotExist(tapl: LauncherInstrumentation, appName: String) {
+        tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0))
+        val allApps = tapl.workspace.switchToAllApps()
+        allApps.freeze()
+        try {
+            allApps.getAppIcon(appName).dragToHotseat(0)
+        } finally {
+            allApps.unfreeze()
+        }
+    }
+
+    fun dragDividerToResizeAndWait(device: UiDevice, wmHelper: WindowManagerStateHelper) {
+        val displayBounds =
+            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
+                ?: error("Display not found")
+        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
+        dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3), 200)
+
+        wmHelper
+            .StateSyncBuilder()
+            .withWindowSurfaceDisappeared(SPLIT_DECOR_MANAGER)
+            .waitForAndVerify()
+    }
+
+    fun dragDividerToDismissSplit(
+        device: UiDevice,
+        wmHelper: WindowManagerStateHelper,
+        dragToRight: Boolean,
+        dragToBottom: Boolean
+    ) {
+        val displayBounds =
+            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
+                ?: error("Display not found")
+        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
+        dividerBar.drag(
+            Point(
+                if (dragToRight) {
+                    displayBounds.width * 4 / 5
+                } else {
+                    displayBounds.width * 1 / 5
+                },
+                if (dragToBottom) {
+                    displayBounds.height * 4 / 5
+                } else {
+                    displayBounds.height * 1 / 5
+                }
+            )
+        )
+    }
+
+    fun doubleTapDividerToSwitch(device: UiDevice) {
+        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
+        val interval =
+            (ViewConfiguration.getDoubleTapTimeout() + ViewConfiguration.getDoubleTapMinTime()) / 2
+        dividerBar.click()
+        SystemClock.sleep(interval.toLong())
+        dividerBar.click()
+    }
+
+    fun copyContentInSplit(
+        instrumentation: Instrumentation,
+        device: UiDevice,
+        sourceApp: IComponentNameMatcher,
+        destinationApp: IComponentNameMatcher,
+    ) {
+        // Copy text from sourceApp
+        val textView =
+            device.wait(
+                Until.findObject(By.res(sourceApp.packageName, "SplitScreenTest")),
+                TIMEOUT_MS
+            )
+        assertNotNull("Unable to find the TextView", textView)
+        textView.click(LONG_PRESS_TIME_MS)
+
+        val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS)
+        assertNotNull("Unable to find the copy button", copyBtn)
+        copyBtn.click()
+
+        // Paste text to destinationApp
+        val editText =
+            device.wait(
+                Until.findObject(By.res(destinationApp.packageName, "plain_text_input")),
+                TIMEOUT_MS
+            )
+        assertNotNull("Unable to find the EditText", editText)
+        editText.click(LONG_PRESS_TIME_MS)
+
+        val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS)
+        assertNotNull("Unable to find the paste button", pasteBtn)
+        pasteBtn.click()
+
+        // Verify text
+        if (!textView.text.contentEquals(editText.text)) {
+            error("Fail to copy content in split")
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
new file mode 100644
index 0000000..e5501f4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.graphics.Point
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.helpers.WindowUtils
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class SwitchAppByDoubleTapDivider
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { rotation })
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+        tapl.workspace.switchToOverview().dismissAllTasks()
+
+        SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
+    }
+
+    @Test
+    open fun switchAppByDoubleTapDivider() {
+        SplitScreenUtils.doubleTapDividerToSwitch(device)
+        wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
+
+        waitForLayersToSwitch(wmHelper)
+        waitForWindowsToSwitch(wmHelper)
+    }
+
+    @After
+    fun teardown() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+    }
+
+    private fun waitForWindowsToSwitch(wmHelper: WindowManagerStateHelper) {
+        wmHelper
+            .StateSyncBuilder()
+            .add("appWindowsSwitched") {
+                val primaryAppWindow =
+                    it.wmState.visibleWindows.firstOrNull { window ->
+                        primaryApp.windowMatchesAnyOf(window)
+                    }
+                        ?: return@add false
+                val secondaryAppWindow =
+                    it.wmState.visibleWindows.firstOrNull { window ->
+                        secondaryApp.windowMatchesAnyOf(window)
+                    }
+                        ?: return@add false
+
+                if (isLandscape(rotation)) {
+                    return@add if (isTablet()) {
+                        secondaryAppWindow.frame.right <= primaryAppWindow.frame.left
+                    } else {
+                        primaryAppWindow.frame.right <= secondaryAppWindow.frame.left
+                    }
+                } else {
+                    return@add if (isTablet()) {
+                        primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top
+                    } else {
+                        primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top
+                    }
+                }
+            }
+            .waitForAndVerify()
+    }
+
+    private fun waitForLayersToSwitch(wmHelper: WindowManagerStateHelper) {
+        wmHelper
+            .StateSyncBuilder()
+            .add("appLayersSwitched") {
+                val primaryAppLayer =
+                    it.layerState.visibleLayers.firstOrNull { window ->
+                        primaryApp.layerMatchesAnyOf(window)
+                    }
+                        ?: return@add false
+                val secondaryAppLayer =
+                    it.layerState.visibleLayers.firstOrNull { window ->
+                        secondaryApp.layerMatchesAnyOf(window)
+                    }
+                        ?: return@add false
+
+                val primaryVisibleRegion = primaryAppLayer.visibleRegion?.bounds ?: return@add false
+                val secondaryVisibleRegion =
+                    secondaryAppLayer.visibleRegion?.bounds ?: return@add false
+
+                if (isLandscape(rotation)) {
+                    return@add if (isTablet()) {
+                        secondaryVisibleRegion.right <= primaryVisibleRegion.left
+                    } else {
+                        primaryVisibleRegion.right <= secondaryVisibleRegion.left
+                    }
+                } else {
+                    return@add if (isTablet()) {
+                        primaryVisibleRegion.bottom <= secondaryVisibleRegion.top
+                    } else {
+                        primaryVisibleRegion.bottom <= secondaryVisibleRegion.top
+                    }
+                }
+            }
+            .waitForAndVerify()
+    }
+
+    private fun isLandscape(rotation: Rotation): Boolean {
+        val displayBounds = WindowUtils.getDisplayBounds(rotation)
+        return displayBounds.width > displayBounds.height
+    }
+
+    private fun isTablet(): Boolean {
+        val sizeDp: Point = device.displaySizeDp
+        val LARGE_SCREEN_DP_THRESHOLD = 600
+        return sizeDp.x >= LARGE_SCREEN_DP_THRESHOLD && sizeDp.y >= LARGE_SCREEN_DP_THRESHOLD
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt
new file mode 100644
index 0000000..b3f1e87
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.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.wm.shell.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class SwitchBackToSplitFromAnotherApp
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+    private val thirdApp = SplitScreenUtils.getNonResizeable(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { rotation })
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+
+        SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
+
+        thirdApp.launchViaIntent(wmHelper)
+        wmHelper.StateSyncBuilder().withWindowSurfaceAppeared(thirdApp).waitForAndVerify()
+    }
+
+    @Test
+    open fun switchBackToSplitFromAnotherApp() {
+        tapl.launchedAppState.quickSwitchToPreviousApp()
+        SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+    }
+
+    @After
+    fun teardown() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt
new file mode 100644
index 0000000..d112116
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.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.wm.shell.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class SwitchBackToSplitFromHome
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { rotation })
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+
+        SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
+
+        tapl.goHome()
+        wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
+    }
+
+    @Test
+    open fun switchBackToSplitFromHome() {
+        tapl.workspace.quickSwitchToPreviousApp()
+        SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+    }
+
+    @After
+    fun teardown() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt
new file mode 100644
index 0000000..9ab924c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.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.wm.shell.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class SwitchBackToSplitFromRecent
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { rotation })
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+
+        SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
+
+        tapl.goHome()
+        wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
+    }
+
+    @Test
+    open fun switchBackToSplitFromRecent() {
+        tapl.workspace.switchToOverview().currentTask.open()
+        SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+    }
+
+    @After
+    fun teardown() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt
new file mode 100644
index 0000000..b694dfa
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.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.wm.shell.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class SwitchBetweenSplitPairs
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+    private val thirdApp = SplitScreenUtils.getIme(instrumentation)
+    private val fourthApp = SplitScreenUtils.getSendNotification(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { rotation })
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+
+        SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
+        SplitScreenUtils.enterSplit(wmHelper, tapl, device, thirdApp, fourthApp)
+        SplitScreenUtils.waitForSplitComplete(wmHelper, thirdApp, fourthApp)
+    }
+
+    @Test
+    open fun switchBetweenSplitPairs() {
+        tapl.launchedAppState.quickSwitchToPreviousApp()
+        SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+    }
+
+    @After
+    fun teardown() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+        thirdApp.exit(wmHelper)
+        fourthApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt
new file mode 100644
index 0000000..f78b788
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.flicker.service.splitscreen.scenarios
+
+import android.app.Instrumentation
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.device.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class UnlockKeyguardToSplitScreen {
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    private val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule =
+        SplitScreenUtils.testSetupRule({ NavBar.MODE_GESTURAL }, { Rotation.ROTATION_0 })
+
+    @Before
+    fun setup() {
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(Rotation.ROTATION_0.value)
+
+        SplitScreenUtils.enterSplitViaIntent(wmHelper, primaryApp, secondaryApp)
+    }
+
+    @Test
+    open fun unlockKeyguardToSplitScreen() {
+        device.sleep()
+        wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
+        device.wakeUp()
+        device.pressMenu()
+        wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
+    }
+
+    @After
+    fun teardown() {
+        primaryApp.exit(wmHelper)
+        secondaryApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index d1f0980..1d4c4d2 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -17,24 +17,19 @@
 package com.android.wm.shell.flicker.splitscreen
 
 import android.platform.test.annotations.FlakyTest
-import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.common.traces.component.EdgeExtensionComponentMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
 import com.android.wm.shell.flicker.appWindowKeepVisible
 import com.android.wm.shell.flicker.layerKeepVisible
 import com.android.wm.shell.flicker.splitAppLayerBoundsKeepVisible
-import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart
 import com.android.wm.shell.flicker.splitscreen.benchmark.CopyContentInSplitBenchmark
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -45,13 +40,13 @@
 /**
  * Test copy content from the left to the right side of the split-screen.
  *
- * To run this test: `atest WMShellFlickerTests:CopyContentInSplit`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:CopyContentInSplit`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CopyContentInSplit(override val flicker: FlickerTest) :
+class CopyContentInSplit(override val flicker: LegacyFlickerTest) :
     CopyContentInSplitBenchmark(flicker), ICommonAssertions {
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -60,21 +55,6 @@
             thisTransition(this)
         }
 
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    override fun cujCompleted() {
-        flicker.appWindowIsVisibleAtStart(primaryApp)
-        flicker.appWindowIsVisibleAtStart(textEditApp)
-        flicker.splitScreenDividerIsVisibleAtStart()
-
-        flicker.appWindowIsVisibleAtEnd(primaryApp)
-        flicker.appWindowIsVisibleAtEnd(textEditApp)
-        flicker.splitScreenDividerIsVisibleAtEnd()
-
-        // The validation of copied text is already done in SplitScreenUtils.copyContentInSplit()
-    }
-
     @Presubmit
     @Test
     fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
@@ -136,8 +116,6 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
index 4505b99..0b8f109 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
@@ -21,7 +21,7 @@
 import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.helpers.WindowUtils
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
@@ -41,13 +41,13 @@
 /**
  * Test dismiss split screen by dragging the divider bar.
  *
- * To run this test: `atest WMShellFlickerTests:DismissSplitScreenByDivider`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:DismissSplitScreenByDivider`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class DismissSplitScreenByDivider(override val flicker: FlickerTest) :
+class DismissSplitScreenByDivider(override val flicker: LegacyFlickerTest) :
     DismissSplitScreenByDividerBenchmark(flicker), ICommonAssertions {
 
     override val transition: FlickerBuilder.() -> Unit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
index e05b221..38d4b40 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
@@ -20,8 +20,8 @@
 import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
 import com.android.wm.shell.flicker.appWindowBecomesInvisible
@@ -38,13 +38,13 @@
 /**
  * Test dismiss split screen by go home.
  *
- * To run this test: `atest WMShellFlickerTests:DismissSplitScreenByGoHome`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:DismissSplitScreenByGoHome`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class DismissSplitScreenByGoHome(override val flicker: FlickerTest) :
+class DismissSplitScreenByGoHome(override val flicker: LegacyFlickerTest) :
     DismissSplitScreenByGoHomeBenchmark(flicker), ICommonAssertions {
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -154,8 +154,6 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
index 63c5d14..0d967eb 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
@@ -17,22 +17,17 @@
 package com.android.wm.shell.flicker.splitscreen
 
 import android.platform.test.annotations.FlakyTest
-import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
 import com.android.wm.shell.flicker.appWindowKeepVisible
 import com.android.wm.shell.flicker.layerKeepVisible
 import com.android.wm.shell.flicker.splitAppLayerBoundsChanges
-import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart
 import com.android.wm.shell.flicker.splitscreen.benchmark.DragDividerToResizeBenchmark
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -43,13 +38,13 @@
 /**
  * Test resize split by dragging the divider bar.
  *
- * To run this test: `atest WMShellFlickerTests:DragDividerToResize`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:DragDividerToResize`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class DragDividerToResize(override val flicker: FlickerTest) :
+class DragDividerToResize(override val flicker: LegacyFlickerTest) :
     DragDividerToResizeBenchmark(flicker), ICommonAssertions {
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -58,19 +53,6 @@
             thisTransition(this)
         }
 
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    override fun cujCompleted() {
-        flicker.appWindowIsVisibleAtStart(primaryApp)
-        flicker.appWindowIsVisibleAtStart(secondaryApp)
-        flicker.splitScreenDividerIsVisibleAtStart()
-
-        flicker.appWindowIsVisibleAtEnd(primaryApp)
-        flicker.appWindowIsVisibleAtEnd(secondaryApp)
-        flicker.splitScreenDividerIsVisibleAtEnd()
-    }
-
     @Presubmit
     @Test
     fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
@@ -127,8 +109,6 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
index e558686..05c0480 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
@@ -22,8 +22,8 @@
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
@@ -45,13 +45,13 @@
  * Test enter split screen by dragging app icon from all apps. This test is only for large screen
  * devices.
  *
- * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromAllApps`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:EnterSplitScreenByDragFromAllApps`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterSplitScreenByDragFromAllApps(override val flicker: FlickerTest) :
+class EnterSplitScreenByDragFromAllApps(override val flicker: LegacyFlickerTest) :
     EnterSplitScreenByDragFromAllAppsBenchmark(flicker), ICommonAssertions {
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -160,11 +160,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
index ab8ecc5..3a75fa6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
@@ -22,8 +22,8 @@
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
@@ -44,13 +44,13 @@
  * Test enter split screen by dragging app icon from notification. This test is only for large
  * screen devices.
  *
- * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromNotification`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:EnterSplitScreenByDragFromNotification`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterSplitScreenByDragFromNotification(override val flicker: FlickerTest) :
+class EnterSplitScreenByDragFromNotification(override val flicker: LegacyFlickerTest) :
     EnterSplitScreenByDragFromNotificationBenchmark(flicker), ICommonAssertions {
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
@@ -162,11 +162,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
index 516ca97..6d73f92 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
@@ -21,8 +21,8 @@
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
@@ -41,13 +41,13 @@
 /**
  * Test enter split screen by dragging a shortcut. This test is only for large screen devices.
  *
- * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromShortcut`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:EnterSplitScreenByDragFromShortcut`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterSplitScreenByDragFromShortcut(override val flicker: FlickerTest) :
+class EnterSplitScreenByDragFromShortcut(override val flicker: LegacyFlickerTest) :
     EnterSplitScreenByDragFromShortcutBenchmark(flicker), ICommonAssertions {
 
     override val transition: FlickerBuilder.() -> Unit
@@ -105,11 +105,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
index 4af7e24..15cae69 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
@@ -22,8 +22,8 @@
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
@@ -45,13 +45,13 @@
  * Test enter split screen by dragging app icon from taskbar. This test is only for large screen
  * devices.
  *
- * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromTaskbar`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:EnterSplitScreenByDragFromTaskbar`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterSplitScreenByDragFromTaskbar(override val flicker: FlickerTest) :
+class EnterSplitScreenByDragFromTaskbar(override val flicker: LegacyFlickerTest) :
     EnterSplitScreenByDragFromTaskbarBenchmark(flicker), ICommonAssertions {
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
@@ -163,10 +163,9 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
index faad9e8..90399fc 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
@@ -20,8 +20,8 @@
 import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
 import com.android.wm.shell.flicker.appWindowBecomesVisible
@@ -40,13 +40,13 @@
 /**
  * Test enter split screen from Overview.
  *
- * To run this test: `atest WMShellFlickerTests:EnterSplitScreenFromOverview`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:EnterSplitScreenFromOverview`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class EnterSplitScreenFromOverview(override val flicker: FlickerTest) :
+class EnterSplitScreenFromOverview(override val flicker: LegacyFlickerTest) :
     EnterSplitScreenFromOverviewBenchmark(flicker), ICommonAssertions {
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -106,8 +106,6 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
index 195b73a..d3434a5 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
@@ -18,11 +18,12 @@
 
 import android.content.Context
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.wm.shell.flicker.BaseBenchmarkTest
+import com.android.wm.shell.flicker.SplitScreenUtils
 
-abstract class SplitScreenBase(flicker: FlickerTest) : BaseBenchmarkTest(flicker) {
+abstract class SplitScreenBase(flicker: LegacyFlickerTest) : BaseBenchmarkTest(flicker) {
     protected val context: Context = instrumentation.context
     protected val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
     protected val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
deleted file mode 100644
index 1063dfd..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
+++ /dev/null
@@ -1,376 +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.wm.shell.flicker.splitscreen
-
-import android.app.Instrumentation
-import android.graphics.Point
-import android.os.SystemClock
-import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.common.traces.component.IComponentMatcher
-import android.tools.common.traces.component.IComponentNameMatcher
-import android.tools.device.apphelpers.StandardAppHelper
-import android.tools.device.traces.parsers.WindowManagerStateHelper
-import android.tools.device.traces.parsers.toFlickerComponent
-import android.view.InputDevice
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.BySelector
-import androidx.test.uiautomator.UiDevice
-import androidx.test.uiautomator.UiObject2
-import androidx.test.uiautomator.Until
-import com.android.launcher3.tapl.LauncherInstrumentation
-import com.android.server.wm.flicker.helpers.ImeAppHelper
-import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
-import com.android.server.wm.flicker.helpers.NotificationAppHelper
-import com.android.server.wm.flicker.helpers.SimpleAppHelper
-import com.android.server.wm.flicker.testapp.ActivityOptions
-import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
-import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
-import org.junit.Assert.assertNotNull
-
-internal object SplitScreenUtils {
-    private const val TIMEOUT_MS = 3_000L
-    private const val DRAG_DURATION_MS = 1_000L
-    private const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
-    private const val DIVIDER_BAR = "docked_divider_handle"
-    private const val OVERVIEW_SNAPSHOT = "snapshot"
-    private const val GESTURE_STEP_MS = 16L
-    private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L
-    private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#")
-
-    private val notificationScrollerSelector: BySelector
-        get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER)
-    private val notificationContentSelector: BySelector
-        get() = By.text("Flicker Test Notification")
-    private val dividerBarSelector: BySelector
-        get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR)
-    private val overviewSnapshotSelector: BySelector
-        get() = By.res(LAUNCHER_UI_PACKAGE_NAME, OVERVIEW_SNAPSHOT)
-
-    fun getPrimary(instrumentation: Instrumentation): StandardAppHelper =
-        SimpleAppHelper(
-            instrumentation,
-            ActivityOptions.SplitScreen.Primary.LABEL,
-            ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()
-        )
-
-    fun getSecondary(instrumentation: Instrumentation): StandardAppHelper =
-        SimpleAppHelper(
-            instrumentation,
-            ActivityOptions.SplitScreen.Secondary.LABEL,
-            ActivityOptions.SplitScreen.Secondary.COMPONENT.toFlickerComponent()
-        )
-
-    fun getNonResizeable(instrumentation: Instrumentation): NonResizeableAppHelper =
-        NonResizeableAppHelper(instrumentation)
-
-    fun getSendNotification(instrumentation: Instrumentation): NotificationAppHelper =
-        NotificationAppHelper(instrumentation)
-
-    fun getIme(instrumentation: Instrumentation): ImeAppHelper = ImeAppHelper(instrumentation)
-
-    fun waitForSplitComplete(
-        wmHelper: WindowManagerStateHelper,
-        primaryApp: IComponentMatcher,
-        secondaryApp: IComponentMatcher,
-    ) {
-        wmHelper
-            .StateSyncBuilder()
-            .withWindowSurfaceAppeared(primaryApp)
-            .withWindowSurfaceAppeared(secondaryApp)
-            .withSplitDividerVisible()
-            .waitForAndVerify()
-    }
-
-    fun enterSplit(
-        wmHelper: WindowManagerStateHelper,
-        tapl: LauncherInstrumentation,
-        device: UiDevice,
-        primaryApp: StandardAppHelper,
-        secondaryApp: StandardAppHelper
-    ) {
-        primaryApp.launchViaIntent(wmHelper)
-        secondaryApp.launchViaIntent(wmHelper)
-        tapl.goHome()
-        wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
-        splitFromOverview(tapl, device)
-        waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
-    }
-
-    fun splitFromOverview(tapl: LauncherInstrumentation, device: UiDevice) {
-        // Note: The initial split position in landscape is different between tablet and phone.
-        // In landscape, tablet will let the first app split to right side, and phone will
-        // split to left side.
-        if (tapl.isTablet) {
-            // TAPL's currentTask on tablet is sometimes not what we expected if the overview
-            // contains more than 3 task views. We need to use uiautomator directly to find the
-            // second task to split.
-            tapl.workspace.switchToOverview().overviewActions.clickSplit()
-            val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS)
-            if (snapshots == null || snapshots.size < 1) {
-                error("Fail to find a overview snapshot to split.")
-            }
-
-            // Find the second task in the upper right corner in split select mode by sorting
-            // 'left' in descending order and 'top' in ascending order.
-            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
-                t2.getVisibleBounds().left - t1.getVisibleBounds().left
-            }
-            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
-                t1.getVisibleBounds().top - t2.getVisibleBounds().top
-            }
-            snapshots[0].click()
-        } else {
-            tapl.workspace
-                .switchToOverview()
-                .currentTask
-                .tapMenu()
-                .tapSplitMenuItem()
-                .currentTask
-                .open()
-        }
-        SystemClock.sleep(TIMEOUT_MS)
-    }
-
-    fun dragFromNotificationToSplit(
-        instrumentation: Instrumentation,
-        device: UiDevice,
-        wmHelper: WindowManagerStateHelper
-    ) {
-        val displayBounds =
-            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
-                ?: error("Display not found")
-
-        // Pull down the notifications
-        device.swipe(
-            displayBounds.centerX(),
-            5,
-            displayBounds.centerX(),
-            displayBounds.bottom,
-            50 /* steps */
-        )
-        SystemClock.sleep(TIMEOUT_MS)
-
-        // Find the target notification
-        val notificationScroller =
-            device.wait(Until.findObject(notificationScrollerSelector), TIMEOUT_MS)
-                ?: error("Unable to find view $notificationScrollerSelector")
-        var notificationContent = notificationScroller.findObject(notificationContentSelector)
-
-        while (notificationContent == null) {
-            device.swipe(
-                displayBounds.centerX(),
-                displayBounds.centerY(),
-                displayBounds.centerX(),
-                displayBounds.centerY() - 150,
-                20 /* steps */
-            )
-            notificationContent = notificationScroller.findObject(notificationContentSelector)
-        }
-
-        // Drag to split
-        val dragStart = notificationContent.visibleCenter
-        val dragMiddle = Point(dragStart.x + 50, dragStart.y)
-        val dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4)
-        val downTime = SystemClock.uptimeMillis()
-
-        touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, dragStart)
-        // It needs a horizontal movement to trigger the drag
-        touchMove(
-            instrumentation,
-            downTime,
-            SystemClock.uptimeMillis(),
-            DRAG_DURATION_MS,
-            dragStart,
-            dragMiddle
-        )
-        touchMove(
-            instrumentation,
-            downTime,
-            SystemClock.uptimeMillis(),
-            DRAG_DURATION_MS,
-            dragMiddle,
-            dragEnd
-        )
-        // Wait for a while to start splitting
-        SystemClock.sleep(TIMEOUT_MS)
-        touch(
-            instrumentation,
-            MotionEvent.ACTION_UP,
-            downTime,
-            SystemClock.uptimeMillis(),
-            GESTURE_STEP_MS,
-            dragEnd
-        )
-        SystemClock.sleep(TIMEOUT_MS)
-    }
-
-    fun touch(
-        instrumentation: Instrumentation,
-        action: Int,
-        downTime: Long,
-        eventTime: Long,
-        duration: Long,
-        point: Point
-    ) {
-        val motionEvent =
-            MotionEvent.obtain(downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0)
-        motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN
-        instrumentation.uiAutomation.injectInputEvent(motionEvent, true)
-        motionEvent.recycle()
-        SystemClock.sleep(duration)
-    }
-
-    fun touchMove(
-        instrumentation: Instrumentation,
-        downTime: Long,
-        eventTime: Long,
-        duration: Long,
-        from: Point,
-        to: Point
-    ) {
-        val steps: Long = duration / GESTURE_STEP_MS
-        var currentTime = eventTime
-        var currentX = from.x.toFloat()
-        var currentY = from.y.toFloat()
-        val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat()
-        val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat()
-
-        for (i in 1..steps) {
-            val motionMove =
-                MotionEvent.obtain(
-                    downTime,
-                    currentTime,
-                    MotionEvent.ACTION_MOVE,
-                    currentX,
-                    currentY,
-                    0
-                )
-            motionMove.source = InputDevice.SOURCE_TOUCHSCREEN
-            instrumentation.uiAutomation.injectInputEvent(motionMove, true)
-            motionMove.recycle()
-
-            currentTime += GESTURE_STEP_MS
-            if (i == steps - 1) {
-                currentX = to.x.toFloat()
-                currentY = to.y.toFloat()
-            } else {
-                currentX += stepX
-                currentY += stepY
-            }
-            SystemClock.sleep(GESTURE_STEP_MS)
-        }
-    }
-
-    fun createShortcutOnHotseatIfNotExist(tapl: LauncherInstrumentation, appName: String) {
-        tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0))
-        val allApps = tapl.workspace.switchToAllApps()
-        allApps.freeze()
-        try {
-            allApps.getAppIcon(appName).dragToHotseat(0)
-        } finally {
-            allApps.unfreeze()
-        }
-    }
-
-    fun dragDividerToResizeAndWait(device: UiDevice, wmHelper: WindowManagerStateHelper) {
-        val displayBounds =
-            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
-                ?: error("Display not found")
-        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
-        dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3), 200)
-
-        wmHelper
-            .StateSyncBuilder()
-            .withWindowSurfaceDisappeared(SPLIT_DECOR_MANAGER)
-            .waitForAndVerify()
-    }
-
-    fun dragDividerToDismissSplit(
-        device: UiDevice,
-        wmHelper: WindowManagerStateHelper,
-        dragToRight: Boolean,
-        dragToBottom: Boolean
-    ) {
-        val displayBounds =
-            wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
-                ?: error("Display not found")
-        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
-        dividerBar.drag(
-            Point(
-                if (dragToRight) {
-                    displayBounds.width * 4 / 5
-                } else {
-                    displayBounds.width * 1 / 5
-                },
-                if (dragToBottom) {
-                    displayBounds.height * 4 / 5
-                } else {
-                    displayBounds.height * 1 / 5
-                }
-            )
-        )
-    }
-
-    fun doubleTapDividerToSwitch(device: UiDevice) {
-        val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
-        val interval =
-            (ViewConfiguration.getDoubleTapTimeout() + ViewConfiguration.getDoubleTapMinTime()) / 2
-        dividerBar.click()
-        SystemClock.sleep(interval.toLong())
-        dividerBar.click()
-    }
-
-    fun copyContentInSplit(
-        instrumentation: Instrumentation,
-        device: UiDevice,
-        sourceApp: IComponentNameMatcher,
-        destinationApp: IComponentNameMatcher,
-    ) {
-        // Copy text from sourceApp
-        val textView =
-            device.wait(
-                Until.findObject(By.res(sourceApp.packageName, "SplitScreenTest")),
-                TIMEOUT_MS
-            )
-        assertNotNull("Unable to find the TextView", textView)
-        textView.click(LONG_PRESS_TIME_MS)
-
-        val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS)
-        assertNotNull("Unable to find the copy button", copyBtn)
-        copyBtn.click()
-
-        // Paste text to destinationApp
-        val editText =
-            device.wait(
-                Until.findObject(By.res(destinationApp.packageName, "plain_text_input")),
-                TIMEOUT_MS
-            )
-        assertNotNull("Unable to find the EditText", editText)
-        editText.click(LONG_PRESS_TIME_MS)
-
-        val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS)
-        assertNotNull("Unable to find the paste button", pasteBtn)
-        pasteBtn.click()
-
-        // Verify text
-        if (!textView.text.contentEquals(editText.text)) {
-            error("Fail to copy content in split")
-        }
-    }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
index 03b8a75..f236c2d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
@@ -16,24 +16,20 @@
 
 package com.android.wm.shell.flicker.splitscreen
 
-import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
 import com.android.wm.shell.flicker.layerIsVisibleAtEnd
 import com.android.wm.shell.flicker.layerKeepVisible
 import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart
 import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchAppByDoubleTapDividerBenchmark
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -44,13 +40,13 @@
 /**
  * Test double tap the divider bar to switch the two apps.
  *
- * To run this test: `atest WMShellFlickerTests:SwitchAppByDoubleTapDivider`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:SwitchAppByDoubleTapDivider`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class SwitchAppByDoubleTapDivider(override val flicker: FlickerTest) :
+class SwitchAppByDoubleTapDivider(override val flicker: LegacyFlickerTest) :
     SwitchAppByDoubleTapDividerBenchmark(flicker), ICommonAssertions {
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -59,19 +55,6 @@
             thisTransition(this)
         }
 
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    override fun cujCompleted() {
-        flicker.appWindowIsVisibleAtStart(primaryApp)
-        flicker.appWindowIsVisibleAtStart(secondaryApp)
-        flicker.splitScreenDividerIsVisibleAtStart()
-
-        flicker.appWindowIsVisibleAtEnd(primaryApp)
-        flicker.appWindowIsVisibleAtEnd(secondaryApp)
-        flicker.splitScreenDividerIsVisibleAtEnd()
-    }
-
     @Presubmit
     @Test
     fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
@@ -120,11 +103,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
index 078d95d..a406009 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
@@ -21,8 +21,8 @@
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
 import com.android.wm.shell.flicker.appWindowBecomesVisible
@@ -39,13 +39,13 @@
 /**
  * Test quick switch to split pair from another app.
  *
- * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromAnotherApp`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:SwitchBackToSplitFromAnotherApp`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class SwitchBackToSplitFromAnotherApp(override val flicker: FlickerTest) :
+class SwitchBackToSplitFromAnotherApp(override val flicker: LegacyFlickerTest) :
     SwitchBackToSplitFromAnotherAppBenchmark(flicker), ICommonAssertions {
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -149,11 +149,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
index 7c84243..251bd10 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
@@ -21,8 +21,8 @@
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
 import com.android.wm.shell.flicker.appWindowBecomesVisible
@@ -39,13 +39,13 @@
 /**
  * Test quick switch to split pair from home.
  *
- * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromHome`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:SwitchBackToSplitFromHome`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class SwitchBackToSplitFromHome(override val flicker: FlickerTest) :
+class SwitchBackToSplitFromHome(override val flicker: LegacyFlickerTest) :
     SwitchBackToSplitFromHomeBenchmark(flicker), ICommonAssertions {
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -149,11 +149,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
index 7c46d3e..1dd45fe 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
@@ -21,8 +21,8 @@
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
 import com.android.wm.shell.flicker.appWindowBecomesVisible
@@ -39,13 +39,13 @@
 /**
  * Test switch back to split pair from recent.
  *
- * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromRecent`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:SwitchBackToSplitFromRecent`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class SwitchBackToSplitFromRecent(override val flicker: FlickerTest) :
+class SwitchBackToSplitFromRecent(override val flicker: LegacyFlickerTest) :
     SwitchBackToSplitFromRecentBenchmark(flicker), ICommonAssertions {
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -149,11 +149,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
index 674ba40..8aaa98a 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
@@ -17,26 +17,20 @@
 package com.android.wm.shell.flicker.splitscreen
 
 import android.platform.test.annotations.FlakyTest
-import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.appWindowBecomesInvisible
 import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.appWindowIsInvisibleAtEnd
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
 import com.android.wm.shell.flicker.layerBecomesInvisible
 import com.android.wm.shell.flicker.layerBecomesVisible
 import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
 import com.android.wm.shell.flicker.splitAppLayerBoundsSnapToDivider
-import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart
 import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBetweenSplitPairsBenchmark
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -47,13 +41,13 @@
 /**
  * Test quick switch between two split pairs.
  *
- * To run this test: `atest WMShellFlickerTests:SwitchBetweenSplitPairs`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:SwitchBetweenSplitPairs`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class SwitchBetweenSplitPairs(override val flicker: FlickerTest) :
+class SwitchBetweenSplitPairs(override val flicker: LegacyFlickerTest) :
     SwitchBetweenSplitPairsBenchmark(flicker), ICommonAssertions {
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -62,21 +56,6 @@
             thisTransition(this)
         }
 
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    override fun cujCompleted() {
-        flicker.appWindowIsVisibleAtStart(thirdApp)
-        flicker.appWindowIsVisibleAtStart(fourthApp)
-        flicker.splitScreenDividerIsVisibleAtStart()
-
-        flicker.appWindowIsVisibleAtEnd(primaryApp)
-        flicker.appWindowIsVisibleAtEnd(secondaryApp)
-        flicker.appWindowIsInvisibleAtEnd(thirdApp)
-        flicker.appWindowIsInvisibleAtEnd(fourthApp)
-        flicker.splitScreenDividerIsVisibleAtEnd()
-    }
-
     @Presubmit
     @Test
     fun splitScreenDividerInvisibleAtMiddle() =
@@ -223,8 +202,6 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
index 676c150..994d6cb 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
@@ -21,8 +21,8 @@
 import android.tools.common.flicker.subject.region.RegionSubject
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.wm.shell.flicker.ICommonAssertions
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
@@ -37,21 +37,21 @@
 import org.junit.runners.Parameterized
 
 /**
- * Test unlocking insecure keyguard to back to split screen tasks and verify the transition behavior.
+ * Test unlocking insecure keyguard to back to split screen tasks and verify the transition
+ * behavior.
  *
- * To run this test: `atest WMShellFlickerTests:UnlockKeyguardToSplitScreen`
+ * To run this test: `atest WMShellFlickerTestsSplitScreen:UnlockKeyguardToSplitScreen`
  */
 @RequiresDevice
 @Postsubmit
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class UnlockKeyguardToSplitScreen(override val flicker: FlickerTest) :
-        UnlockKeyguardToSplitScreenBenchmark(flicker), ICommonAssertions {
+class UnlockKeyguardToSplitScreen(override val flicker: LegacyFlickerTest) :
+    UnlockKeyguardToSplitScreenBenchmark(flicker), ICommonAssertions {
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
         get() = {
-            defaultSetup(this)
             defaultTeardown(this)
             thisTransition(this)
         }
@@ -65,33 +65,35 @@
 
     @Test
     fun primaryAppBoundsIsVisibleAtEnd() =
-            flicker.splitAppLayerBoundsIsVisibleAtEnd(
-                    primaryApp,
-                    landscapePosLeft = false,
-                    portraitPosTop = false
-            )
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
+            primaryApp,
+            landscapePosLeft = false,
+            portraitPosTop = false
+        )
 
     @Test
     fun secondaryAppBoundsIsVisibleAtEnd() =
-            flicker.splitAppLayerBoundsIsVisibleAtEnd(
-                    secondaryApp,
-                    landscapePosLeft = true,
-                    portraitPosTop = true
-            )
+        flicker.splitAppLayerBoundsIsVisibleAtEnd(
+            secondaryApp,
+            landscapePosLeft = true,
+            portraitPosTop = true
+        )
 
-    @Test
-    fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp)
+    @Test fun primaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(primaryApp)
 
-    @Test
-    fun secondaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(secondaryApp)
+    @Test fun secondaryAppWindowIsVisibleAtEnd() = flicker.appWindowIsVisibleAtEnd(secondaryApp)
 
     @Test
     fun notOverlapsForPrimaryAndSecondaryAppLayers() {
         flicker.assertLayers {
             this.invoke("notOverlapsForPrimaryAndSecondaryLayers") {
-                val primaryAppRegions = it.subjects.filter { subject ->
-                    subject.name.contains(primaryApp.toLayerName()) && subject.isVisible
-                }.mapNotNull { primaryApp -> primaryApp.layer.visibleRegion }.toTypedArray()
+                val primaryAppRegions =
+                    it.subjects
+                        .filter { subject ->
+                            subject.name.contains(primaryApp.toLayerName()) && subject.isVisible
+                        }
+                        .mapNotNull { primaryApp -> primaryApp.layer.visibleRegion }
+                        .toTypedArray()
 
                 val primaryAppRegionArea = RegionSubject(primaryAppRegions, it.timestamp)
                 it.visibleRegion(secondaryApp).notOverlaps(primaryAppRegionArea.region)
@@ -102,10 +104,9 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
-                    supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
+                supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt
index c3c5f88..d9d22de 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt
@@ -16,18 +16,15 @@
 
 package com.android.wm.shell.flicker.splitscreen.benchmark
 
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.FixMethodOrder
-import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
@@ -36,7 +33,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CopyContentInSplitBenchmark(override val flicker: FlickerTest) :
+abstract class CopyContentInSplitBenchmark(override val flicker: LegacyFlickerTest) :
     SplitScreenBase(flicker) {
     protected val textEditApp = SplitScreenUtils.getIme(instrumentation)
     protected val magnifierLayer = ComponentNameMatcher("", "magnifier surface bbq wrapper#")
@@ -54,26 +51,9 @@
             }
         }
 
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            withoutTracing(this)
-            defaultSetup(this)
-            defaultTeardown(this)
-            thisTransition(this)
-        }
-
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    open fun cujCompleted() {
-        // The validation of copied text is already done in SplitScreenUtils.copyContentInSplit()
-    }
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt
index 37cd18f..7e8d60b4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt
@@ -16,18 +16,14 @@
 
 package com.android.wm.shell.flicker.splitscreen.benchmark
 
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.splitScreenDismissed
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.FixMethodOrder
-import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
@@ -36,7 +32,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class DismissSplitScreenByDividerBenchmark(flicker: FlickerTest) : SplitScreenBase(flicker) {
+abstract class DismissSplitScreenByDividerBenchmark(override val flicker: LegacyFlickerTest) :
+    SplitScreenBase(flicker) {
     protected val thisTransition: FlickerBuilder.() -> Unit
         get() = {
             setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) }
@@ -60,24 +57,9 @@
             }
         }
 
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            withoutTracing(this)
-            defaultSetup(this)
-            defaultTeardown(this)
-            thisTransition(this)
-        }
-
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    fun cujCompleted() = flicker.splitScreenDismissed(primaryApp, secondaryApp, toHome = false)
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt
index 0ec6dc9..770e032 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt
@@ -16,18 +16,14 @@
 
 package com.android.wm.shell.flicker.splitscreen.benchmark
 
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.splitScreenDismissed
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.FixMethodOrder
-import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
@@ -36,7 +32,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class DismissSplitScreenByGoHomeBenchmark(override val flicker: FlickerTest) :
+abstract class DismissSplitScreenByGoHomeBenchmark(override val flicker: LegacyFlickerTest) :
     SplitScreenBase(flicker) {
     protected val thisTransition: FlickerBuilder.() -> Unit
         get() = {
@@ -47,24 +43,9 @@
             }
         }
 
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            withoutTracing(this)
-            defaultSetup(this)
-            defaultTeardown(this)
-            thisTransition(this)
-        }
-
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    fun cujCompleted() = flicker.splitScreenDismissed(primaryApp, secondaryApp, toHome = true)
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt
index 190e2e7..46570fd 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt
@@ -16,19 +16,16 @@
 
 package com.android.wm.shell.flicker.splitscreen.benchmark
 
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
-import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
@@ -37,7 +34,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class DragDividerToResizeBenchmark(override val flicker: FlickerTest) :
+abstract class DragDividerToResizeBenchmark(override val flicker: LegacyFlickerTest) :
     SplitScreenBase(flicker) {
     protected val thisTransition: FlickerBuilder.() -> Unit
         get() = {
@@ -45,32 +42,14 @@
             transitions { SplitScreenUtils.dragDividerToResizeAndWait(device, wmHelper) }
         }
 
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            withoutTracing(this)
-            defaultSetup(this)
-            defaultTeardown(this)
-            thisTransition(this)
-        }
-
     @Before
     fun before() {
         Assume.assumeTrue(tapl.isTablet || !flicker.scenario.isLandscapeOrSeascapeAtStart)
     }
 
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    open fun cujCompleted() {
-        // TODO(b/246490534): Add validation for resized app after withAppTransitionIdle is
-        // robust enough to get the correct end state.
-    }
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt
index 3a1d1a4..5c3d4ff 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt
@@ -16,21 +16,17 @@
 
 package com.android.wm.shell.flicker.splitscreen.benchmark
 
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
-import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
@@ -39,7 +35,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class EnterSplitScreenByDragFromAllAppsBenchmark(override val flicker: FlickerTest) :
+abstract class EnterSplitScreenByDragFromAllAppsBenchmark(override val flicker: LegacyFlickerTest) :
     SplitScreenBase(flicker) {
 
     protected val thisTransition: FlickerBuilder.() -> Unit
@@ -57,38 +53,18 @@
             }
         }
 
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            withoutTracing(this)
-            defaultSetup(this)
-            defaultTeardown(this)
-            thisTransition(this)
-        }
-
     @Before
     fun before() {
         Assume.assumeTrue(tapl.isTablet)
     }
 
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    fun cujCompleted() =
-        flicker.splitScreenEntered(
-            primaryApp,
-            secondaryApp,
-            fromOtherApp = false,
-            appExistAtStart = false
-        )
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt
index 2033b7d..6b122c6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt
@@ -16,21 +16,17 @@
 
 package com.android.wm.shell.flicker.splitscreen.benchmark
 
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
-import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
@@ -39,8 +35,9 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class EnterSplitScreenByDragFromNotificationBenchmark(override val flicker: FlickerTest) :
-    SplitScreenBase(flicker) {
+abstract class EnterSplitScreenByDragFromNotificationBenchmark(
+    override val flicker: LegacyFlickerTest
+) : SplitScreenBase(flicker) {
     protected val sendNotificationApp = SplitScreenUtils.getSendNotification(instrumentation)
     protected val thisTransition: FlickerBuilder.() -> Unit
         get() = {
@@ -58,21 +55,6 @@
             teardown { sendNotificationApp.exit(wmHelper) }
         }
 
-    /** {@inheritDoc} */
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            withoutTracing(this)
-            defaultSetup(this)
-            defaultTeardown(this)
-            thisTransition(this)
-        }
-
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    fun cujCompleted() =
-        flicker.splitScreenEntered(primaryApp, sendNotificationApp, fromOtherApp = false)
-
     @Before
     fun before() {
         Assume.assumeTrue(tapl.isTablet)
@@ -81,11 +63,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt
index b7a7110..78f9bab 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt
@@ -16,21 +16,17 @@
 
 package com.android.wm.shell.flicker.splitscreen.benchmark
 
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
-import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
@@ -39,8 +35,9 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class EnterSplitScreenByDragFromShortcutBenchmark(flicker: FlickerTest) :
-    SplitScreenBase(flicker) {
+abstract class EnterSplitScreenByDragFromShortcutBenchmark(
+    override val flicker: LegacyFlickerTest
+) : SplitScreenBase(flicker) {
     @Before
     fun before() {
         Assume.assumeTrue(tapl.isTablet)
@@ -62,33 +59,13 @@
         }
     }
 
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            withoutTracing(this)
-            defaultSetup(this)
-            defaultTeardown(this)
-            thisTransition(this)
-        }
-
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    fun cujCompleted() =
-        flicker.splitScreenEntered(
-            primaryApp,
-            secondaryApp,
-            fromOtherApp = false,
-            appExistAtStart = false
-        )
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt
index b1ce62f..78907f0 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt
@@ -16,21 +16,17 @@
 
 package com.android.wm.shell.flicker.splitscreen.benchmark
 
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.Assume
 import org.junit.Before
 import org.junit.FixMethodOrder
-import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
@@ -39,7 +35,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class EnterSplitScreenByDragFromTaskbarBenchmark(override val flicker: FlickerTest) :
+abstract class EnterSplitScreenByDragFromTaskbarBenchmark(override val flicker: LegacyFlickerTest) :
     SplitScreenBase(flicker) {
     protected val thisTransition: FlickerBuilder.() -> Unit
         get() = {
@@ -56,26 +52,6 @@
             }
         }
 
-    /** {@inheritDoc} */
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            withoutTracing(this)
-            defaultSetup(this)
-            defaultTeardown(this)
-            thisTransition(this)
-        }
-
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    fun cujCompleted() =
-        flicker.splitScreenEntered(
-            primaryApp,
-            secondaryApp,
-            fromOtherApp = false,
-            appExistAtStart = false
-        )
-
     @Before
     fun before() {
         Assume.assumeTrue(tapl.isTablet)
@@ -84,10 +60,9 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt
index 14f0745..2c91e84 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt
@@ -16,18 +16,14 @@
 
 package com.android.wm.shell.flicker.splitscreen.benchmark
 
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.FixMethodOrder
-import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
@@ -36,7 +32,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class EnterSplitScreenFromOverviewBenchmark(override val flicker: FlickerTest) :
+abstract class EnterSplitScreenFromOverviewBenchmark(override val flicker: LegacyFlickerTest) :
     SplitScreenBase(flicker) {
     protected val thisTransition: FlickerBuilder.() -> Unit
         get() = {
@@ -56,24 +52,9 @@
             }
         }
 
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            withoutTracing(this)
-            defaultSetup(this)
-            defaultTeardown(this)
-            thisTransition(this)
-        }
-
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt
index 65fb135..fa09c2e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt
@@ -16,21 +16,18 @@
 
 package com.android.wm.shell.flicker.splitscreen.benchmark
 
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
 import android.tools.common.NavBar
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.helpers.WindowUtils
 import android.tools.device.traces.parsers.WindowManagerStateHelper
 import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.FixMethodOrder
-import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
@@ -39,7 +36,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class SwitchAppByDoubleTapDividerBenchmark(override val flicker: FlickerTest) :
+abstract class SwitchAppByDoubleTapDividerBenchmark(override val flicker: LegacyFlickerTest) :
     SplitScreenBase(flicker) {
     protected val thisTransition: FlickerBuilder.() -> Unit
         get() = {
@@ -53,14 +50,6 @@
             }
         }
 
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            withoutTracing(this)
-            defaultSetup(this)
-            defaultTeardown(this)
-            thisTransition(this)
-        }
-
     private fun waitForWindowsToSwitch(wmHelper: WindowManagerStateHelper) {
         wmHelper
             .StateSyncBuilder()
@@ -134,22 +123,13 @@
         return displayBounds.width > displayBounds.height
     }
 
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    open fun cujCompleted() {
-        // TODO(b/246490534): Add validation for switched app after withAppTransitionIdle is
-        // robust enough to get the correct end state.
-    }
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt
index b333aba..ff22006 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt
@@ -16,19 +16,15 @@
 
 package com.android.wm.shell.flicker.splitscreen.benchmark
 
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.FixMethodOrder
-import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
@@ -37,7 +33,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class SwitchBackToSplitFromAnotherAppBenchmark(override val flicker: FlickerTest) :
+abstract class SwitchBackToSplitFromAnotherAppBenchmark(override val flicker: LegacyFlickerTest) :
     SplitScreenBase(flicker) {
     private val thirdApp = SplitScreenUtils.getNonResizeable(instrumentation)
 
@@ -55,27 +51,13 @@
             }
         }
 
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            withoutTracing(this)
-            defaultSetup(this)
-            defaultTeardown(this)
-            thisTransition(this)
-        }
-
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt
index a27540e..5787b02 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt
@@ -16,19 +16,15 @@
 
 package com.android.wm.shell.flicker.splitscreen.benchmark
 
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.FixMethodOrder
-import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
@@ -37,7 +33,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class SwitchBackToSplitFromHomeBenchmark(override val flicker: FlickerTest) :
+abstract class SwitchBackToSplitFromHomeBenchmark(override val flicker: LegacyFlickerTest) :
     SplitScreenBase(flicker) {
     protected val thisTransition: FlickerBuilder.() -> Unit
         get() = {
@@ -53,27 +49,13 @@
             }
         }
 
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            withoutTracing(this)
-            defaultSetup(this)
-            defaultTeardown(this)
-            thisTransition(this)
-        }
-
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt
index 18bf4ff..b2d5091 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt
@@ -16,19 +16,15 @@
 
 package com.android.wm.shell.flicker.splitscreen.benchmark
 
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.splitScreenEntered
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.FixMethodOrder
-import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
@@ -37,7 +33,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class SwitchBackToSplitFromRecentBenchmark(override val flicker: FlickerTest) :
+abstract class SwitchBackToSplitFromRecentBenchmark(override val flicker: LegacyFlickerTest) :
     SplitScreenBase(flicker) {
     protected val thisTransition: FlickerBuilder.() -> Unit
         get() = {
@@ -53,27 +49,13 @@
             }
         }
 
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            withoutTracing(this)
-            defaultSetup(this)
-            defaultTeardown(this)
-            thisTransition(this)
-        }
-
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit
-    @Test
-    fun cujCompleted() = flicker.splitScreenEntered(primaryApp, secondaryApp, fromOtherApp = true)
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt
index c5fe61e..f234e46 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt
@@ -16,17 +16,14 @@
 
 package com.android.wm.shell.flicker.splitscreen.benchmark
 
-import android.platform.test.annotations.PlatinumTest
-import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.FixMethodOrder
-import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
@@ -35,7 +32,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class SwitchBetweenSplitPairsBenchmark(override val flicker: FlickerTest) :
+abstract class SwitchBetweenSplitPairsBenchmark(override val flicker: LegacyFlickerTest) :
     SplitScreenBase(flicker) {
     protected val thirdApp = SplitScreenUtils.getIme(instrumentation)
     protected val fourthApp = SplitScreenUtils.getSendNotification(instrumentation)
@@ -64,14 +61,9 @@
             thisTransition(this)
         }
 
-    @PlatinumTest(focusArea = "sysui")
-    @Presubmit @Test open fun cujCompleted() {}
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt
index 5f16e5b..61c3679 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt
@@ -19,11 +19,11 @@
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.SplitScreenUtils
 import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
-import com.android.wm.shell.flicker.splitscreen.SplitScreenUtils
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -33,11 +33,11 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class UnlockKeyguardToSplitScreenBenchmark(override val flicker: FlickerTest) :
-        SplitScreenBase(flicker) {
+abstract class UnlockKeyguardToSplitScreenBenchmark(override val flicker: LegacyFlickerTest) :
+    SplitScreenBase(flicker) {
     protected val thisTransition: FlickerBuilder.() -> Unit
         get() = {
-            setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) }
+            setup { SplitScreenUtils.enterSplitViaIntent(wmHelper, primaryApp, secondaryApp) }
             transitions {
                 device.sleep()
                 wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
@@ -47,21 +47,12 @@
             }
         }
 
-    /** {@inheritDoc} */
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            defaultSetup(this)
-            defaultTeardown(this)
-            thisTransition(this)
-        }
-
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
-                    supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
+                supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/trace_config/trace_config.textproto
new file mode 100644
index 0000000..406ada9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/trace_config/trace_config.textproto
@@ -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.
+
+# proto-message: TraceConfig
+
+# Enable periodic flushing of the trace buffer into the output file.
+write_into_file: true
+
+# Writes the userspace buffer into the file every 1s.
+file_write_period_ms: 2500
+
+# See b/126487238 - we need to guarantee ordering of events.
+flush_period_ms: 30000
+
+# The trace buffers needs to be big enough to hold |file_write_period_ms| of
+# trace data. The trace buffer sizing depends on the number of trace categories
+# enabled and the device activity.
+
+# RSS events
+buffers: {
+  size_kb: 63488
+  fill_policy: RING_BUFFER
+}
+
+data_sources {
+  config {
+    name: "linux.process_stats"
+    target_buffer: 0
+    # polled per-process memory counters and process/thread names.
+    # If you don't want the polled counters, remove the "process_stats_config"
+    # section, but keep the data source itself as it still provides on-demand
+    # thread/process naming for ftrace data below.
+    process_stats_config {
+      scan_all_processes_on_start: true
+    }
+  }
+}
+
+data_sources: {
+  config {
+    name: "linux.ftrace"
+    ftrace_config {
+      ftrace_events: "ftrace/print"
+      ftrace_events: "task/task_newtask"
+      ftrace_events: "task/task_rename"
+      atrace_categories: "ss"
+      atrace_categories: "wm"
+      atrace_categories: "am"
+      atrace_categories: "aidl"
+      atrace_categories: "input"
+      atrace_categories: "binder_driver"
+      atrace_categories: "sched_process_exit"
+      atrace_apps: "com.android.server.wm.flicker.testapp"
+      atrace_apps: "com.android.systemui"
+      atrace_apps: "com.android.wm.shell.flicker"
+      atrace_apps: "com.android.wm.shell.flicker.other"
+      atrace_apps: "com.android.wm.shell.flicker.bubbles"
+      atrace_apps: "com.android.wm.shell.flicker.pip"
+      atrace_apps: "com.android.wm.shell.flicker.splitscreen"
+      atrace_apps: "com.google.android.apps.nexuslauncher"
+    }
+  }
+}
+
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/BubbleOverflowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/BubbleOverflowTest.java
index 8278c67..0dc16f4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/BubbleOverflowTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/BubbleOverflowTest.java
@@ -64,18 +64,27 @@
     }
 
     @Test
-    public void test_initialize() {
+    public void test_initialize_forStack() {
         assertThat(mOverflow.getExpandedView()).isNull();
 
-        mOverflow.initialize(mBubbleController);
+        mOverflow.initialize(mBubbleController, /* forBubbleBar= */ false);
 
         assertThat(mOverflow.getExpandedView()).isNotNull();
         assertThat(mOverflow.getExpandedView().getBubbleKey()).isEqualTo(BubbleOverflow.KEY);
+        assertThat(mOverflow.getBubbleBarExpandedView()).isNull();
+    }
+
+    @Test
+    public void test_initialize_forBubbleBar() {
+        mOverflow.initialize(mBubbleController, /* forBubbleBar= */ true);
+
+        assertThat(mOverflow.getBubbleBarExpandedView()).isNotNull();
+        assertThat(mOverflow.getExpandedView()).isNull();
     }
 
     @Test
     public void test_cleanUpExpandedState() {
-        mOverflow.createExpandedView();
+        mOverflow.initialize(mBubbleController, /* forBubbleBar= */ false);
         assertThat(mOverflow.getExpandedView()).isNotNull();
 
         mOverflow.cleanUpExpandedState();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/MockToken.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/MockToken.java
new file mode 100644
index 0000000..a97c19f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/MockToken.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.os.IBinder;
+import android.window.WindowContainerToken;
+
+/**
+ * {@link WindowContainerToken} wrapper that supports a mock binder
+ */
+public class MockToken {
+    private final WindowContainerToken mToken;
+
+    public MockToken() {
+        mToken = mock(WindowContainerToken.class);
+        IBinder binder = mock(IBinder.class);
+        when(mToken.asBinder()).thenReturn(binder);
+    }
+
+    public WindowContainerToken token() {
+        return mToken;
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt
new file mode 100644
index 0000000..6d9d62d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.bubbles
+
+import android.app.ActivityTaskManager
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutInfo
+import android.util.SparseArray
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.bubbles.storage.BubbleEntity
+import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
+import com.android.wm.shell.common.ShellExecutor
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+
+class BubbleDataRepositoryTest : ShellTestCase() {
+
+    private val user0BubbleEntities = listOf(
+        BubbleEntity(
+            userId = 0,
+            packageName = "com.example.messenger",
+            shortcutId = "shortcut-1",
+            key = "0k1",
+            desiredHeight = 120,
+            desiredHeightResId = 0,
+            title = null,
+            taskId = 1,
+            locus = null,
+            isDismissable = true
+        ),
+        BubbleEntity(
+            userId = 10,
+            packageName = "com.example.chat",
+            shortcutId = "alice and bob",
+            key = "0k2",
+            desiredHeight = 0,
+            desiredHeightResId = 16537428,
+            title = "title",
+            taskId = 2,
+            locus = null
+        ),
+        BubbleEntity(
+            userId = 0,
+            packageName = "com.example.messenger",
+            shortcutId = "shortcut-2",
+            key = "0k3",
+            desiredHeight = 120,
+            desiredHeightResId = 0,
+            title = null,
+            taskId = ActivityTaskManager.INVALID_TASK_ID,
+            locus = null
+        )
+    )
+
+    private val user1BubbleEntities = listOf(
+        BubbleEntity(
+            userId = 1,
+            packageName = "com.example.messenger",
+            shortcutId = "shortcut-1",
+            key = "1k1",
+            desiredHeight = 120,
+            desiredHeightResId = 0,
+            title = null,
+            taskId = 3,
+            locus = null,
+            isDismissable = true
+        ),
+        BubbleEntity(
+            userId = 12,
+            packageName = "com.example.chat",
+            shortcutId = "alice and bob",
+            key = "1k2",
+            desiredHeight = 0,
+            desiredHeightResId = 16537428,
+            title = "title",
+            taskId = 4,
+            locus = null
+        ),
+        BubbleEntity(
+            userId = 1,
+            packageName = "com.example.messenger",
+            shortcutId = "shortcut-2",
+            key = "1k3",
+            desiredHeight = 120,
+            desiredHeightResId = 0,
+            title = null,
+            taskId = ActivityTaskManager.INVALID_TASK_ID,
+            locus = null
+        ),
+        BubbleEntity(
+            userId = 12,
+            packageName = "com.example.chat",
+            shortcutId = "alice",
+            key = "1k4",
+            desiredHeight = 0,
+            desiredHeightResId = 16537428,
+            title = "title",
+            taskId = 5,
+            locus = null
+        )
+    )
+
+    private val mainExecutor = mock(ShellExecutor::class.java)
+    private val launcherApps = mock(LauncherApps::class.java)
+
+    private val persistedBubbles = SparseArray<List<BubbleEntity>>()
+
+    private lateinit var dataRepository: BubbleDataRepository
+    private lateinit var persistentRepository: BubblePersistentRepository
+
+    @Before
+    fun setup() {
+        persistentRepository = spy(BubblePersistentRepository(mContext))
+        dataRepository = BubbleDataRepository(launcherApps, mainExecutor, persistentRepository)
+
+        // Add the bubbles to the persistent repository
+        persistedBubbles.put(0, user0BubbleEntities)
+        persistedBubbles.put(1, user1BubbleEntities)
+        persistentRepository.persistsToDisk(persistedBubbles)
+    }
+
+    @After
+    fun teardown() {
+        // Clean up any persisted bubbles for the next run
+        persistentRepository.persistsToDisk(SparseArray())
+    }
+
+    @Test
+    fun testLoadBubbles_invalidParent() {
+        val activeUserIds = listOf(10, 1, 12) // Missing user 0 in persistedBubbles
+        dataRepository.loadBubbles(1, activeUserIds) {
+            // Verify that user 0 has been removed from the persisted list
+            val entitiesByUser = persistentRepository.readFromDisk()
+            assertThat(entitiesByUser.get(0)).isNull()
+        }
+    }
+
+    @Test
+    fun testLoadBubbles_invalidChild() {
+        val activeUserIds = listOf(0, 10, 1) // Missing user 1's child user 12
+        dataRepository.loadBubbles(1, activeUserIds) {
+            // Build a list to compare against
+            val user1BubblesWithoutUser12 = mutableListOf<Bubble>()
+            val user1EntitiesWithoutUser12 = mutableListOf<BubbleEntity>()
+            for (entity in user1BubbleEntities) {
+                if (entity.userId != 12) {
+                    user1BubblesWithoutUser12.add(entity.toBubble())
+                    user1EntitiesWithoutUser12.add(entity)
+                }
+            }
+
+            // Verify that user 12 has been removed from the persisted list
+            val entitiesByUser = persistentRepository.readFromDisk()
+            assertThat(entitiesByUser.get(1)).isEqualTo(user1EntitiesWithoutUser12)
+        }
+    }
+
+    private fun BubbleEntity.toBubble(): Bubble {
+        return Bubble(
+            key,
+            mock(ShortcutInfo::class.java),
+            desiredHeight,
+            desiredHeightResId,
+            title,
+            taskId,
+            locus,
+            isDismissable,
+            mainExecutor,
+            mock(Bubbles.BubbleMetadataFlagListener::class.java)
+        )
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java
new file mode 100644
index 0000000..d38b848
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.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.wm.shell.bubbles.bar;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.drawable.ColorDrawable;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.core.content.ContextCompat;
+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;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class BubbleBarHandleViewTest extends ShellTestCase {
+    private BubbleBarHandleView mHandleView;
+
+    @Before
+    public void setup() {
+        mHandleView = new BubbleBarHandleView(mContext);
+    }
+
+    @Test
+    public void testUpdateHandleColor_lightBg() {
+        mHandleView.updateHandleColor(false /* isRegionDark */, false /* animated */);
+
+        assertTrue(mHandleView.getClipToOutline());
+        assertTrue(mHandleView.getBackground() instanceof ColorDrawable);
+        ColorDrawable bgDrawable = (ColorDrawable) mHandleView.getBackground();
+        assertEquals(bgDrawable.getColor(),
+                ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_handle_dark));
+    }
+
+    @Test
+    public void testUpdateHandleColor_darkBg() {
+        mHandleView.updateHandleColor(true /* isRegionDark */, false /* animated */);
+
+        assertTrue(mHandleView.getClipToOutline());
+        assertTrue(mHandleView.getBackground() instanceof ColorDrawable);
+        ColorDrawable bgDrawable = (ColorDrawable) mHandleView.getBackground();
+        assertEquals(bgDrawable.getColor(),
+                ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_handle_light));
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/LaunchAdjacentControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/LaunchAdjacentControllerTest.kt
new file mode 100644
index 0000000..9dc816b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/LaunchAdjacentControllerTest.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.wm.shell.common
+
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.window.WindowContainerTransaction
+import android.window.WindowContainerTransaction.HierarchyOp
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.MockToken
+import com.android.wm.shell.ShellTestCase
+import com.google.common.truth.Truth.assertThat
+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.any
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class LaunchAdjacentControllerTest : ShellTestCase() {
+
+    private lateinit var controller: LaunchAdjacentController
+
+    @Mock private lateinit var syncQueue: SyncTransactionQueue
+
+    @Before
+    fun setUp() {
+        controller = LaunchAdjacentController(syncQueue)
+    }
+
+    @Test
+    fun newInstance_enabledByDefault() {
+        assertThat(controller.launchAdjacentEnabled).isTrue()
+    }
+
+    @Test
+    fun setLaunchAdjacentRoot_launchAdjacentEnabled_setsFlagRoot() {
+        val token = MockToken().token()
+        controller.setLaunchAdjacentRoot(token)
+        val wct = getLatestTransactionOrFail()
+        assertThat(wct.getSetLaunchAdjacentFlagRootContainer()).isEqualTo(token.asBinder())
+    }
+
+    @Test
+    fun setLaunchAdjacentRoot_launchAdjacentDisabled_doesNotUpdateFlagRoot() {
+        val token = MockToken().token()
+        controller.launchAdjacentEnabled = false
+        controller.setLaunchAdjacentRoot(token)
+        verify(syncQueue, never()).queue(any())
+    }
+
+    @Test
+    fun clearLaunchAdjacentRoot_launchAdjacentEnabled_clearsFlagRoot() {
+        val token = MockToken().token()
+        controller.setLaunchAdjacentRoot(token)
+        controller.clearLaunchAdjacentRoot()
+        val wct = getLatestTransactionOrFail()
+        assertThat(wct.getClearLaunchAdjacentFlagRootContainer()).isEqualTo(token.asBinder())
+    }
+
+    @Test
+    fun clearLaunchAdjacentRoot_launchAdjacentDisabled_clearsFlagRoot() {
+        val token = MockToken().token()
+        controller.setLaunchAdjacentRoot(token)
+        controller.launchAdjacentEnabled = false
+        clearInvocations(syncQueue)
+
+        controller.clearLaunchAdjacentRoot()
+        val wct = getLatestTransactionOrFail()
+        assertThat(wct.getClearLaunchAdjacentFlagRootContainer()).isEqualTo(token.asBinder())
+    }
+
+    @Test
+    fun setLaunchAdjacentEnabled_wasDisabledWithContainerSet_setsFlagRoot() {
+        val token = MockToken().token()
+        controller.setLaunchAdjacentRoot(token)
+        controller.launchAdjacentEnabled = false
+        clearInvocations(syncQueue)
+
+        controller.launchAdjacentEnabled = true
+        val wct = getLatestTransactionOrFail()
+        assertThat(wct.getSetLaunchAdjacentFlagRootContainer()).isEqualTo(token.asBinder())
+    }
+
+    @Test
+    fun setLaunchAdjacentEnabled_containerNotSet_doesNotUpdateFlagRoot() {
+        controller.launchAdjacentEnabled = false
+        controller.launchAdjacentEnabled = true
+        verify(syncQueue, never()).queue(any())
+    }
+
+    @Test
+    fun setLaunchAdjacentEnabled_multipleTimes_setsFlagRootOnce() {
+        val token = MockToken().token()
+        controller.setLaunchAdjacentRoot(token)
+        controller.launchAdjacentEnabled = true
+        controller.launchAdjacentEnabled = true
+        // Only execute once
+        verify(syncQueue).queue(any())
+    }
+
+    @Test
+    fun setLaunchAdjacentDisabled_containerSet_clearsFlagRoot() {
+        val token = MockToken().token()
+        controller.setLaunchAdjacentRoot(token)
+        controller.launchAdjacentEnabled = false
+        val wct = getLatestTransactionOrFail()
+        assertThat(wct.getClearLaunchAdjacentFlagRootContainer()).isEqualTo(token.asBinder())
+    }
+
+    @Test
+    fun setLaunchAdjacentDisabled_containerNotSet_doesNotUpdateFlagRoot() {
+        controller.launchAdjacentEnabled = false
+        verify(syncQueue, never()).queue(any())
+    }
+
+    @Test
+    fun setLaunchAdjacentDisabled_multipleTimes_setsFlagRootOnce() {
+        val token = MockToken().token()
+        controller.setLaunchAdjacentRoot(token)
+        clearInvocations(syncQueue)
+        controller.launchAdjacentEnabled = false
+        controller.launchAdjacentEnabled = false
+        // Only execute once
+        verify(syncQueue).queue(any())
+    }
+
+    private fun getLatestTransactionOrFail(): WindowContainerTransaction {
+        val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(syncQueue, atLeastOnce()).queue(arg.capture())
+        return arg.allValues.last().also { assertThat(it).isNotNull() }
+    }
+}
+
+private fun WindowContainerTransaction.getSetLaunchAdjacentFlagRootContainer(): IBinder {
+    return hierarchyOps
+        // Find the operation with the correct type
+        .filter { op -> op.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT }
+        // For set flag root operation, toTop is false
+        .filter { op -> !op.toTop }
+        .map { it.container }
+        .first()
+}
+
+private fun WindowContainerTransaction.getClearLaunchAdjacentFlagRootContainer(): IBinder {
+    return hierarchyOps
+        // Find the operation with the correct type
+        .filter { op -> op.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT }
+        // For clear flag root operation, toTop is true
+        .filter { op -> op.toTop }
+        .map { it.container }
+        .first()
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java
new file mode 100644
index 0000000..145c8f0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java
@@ -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.wm.shell.common.split;
+
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+
+import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CURSOR_HOVER_STATES_ENABLED;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.provider.DeviceConfig;
+import android.view.InputDevice;
+import android.view.InsetsState;
+import android.view.MotionEvent;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayImeController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for {@link DividerView} */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DividerViewTest extends ShellTestCase {
+    private @Mock SplitWindowManager.ParentContainerCallbacks mCallbacks;
+    private @Mock SplitLayout.SplitLayoutHandler mSplitLayoutHandler;
+    private @Mock DisplayController mDisplayController;
+    private @Mock DisplayImeController mDisplayImeController;
+    private @Mock ShellTaskOrganizer mTaskOrganizer;
+    private SplitLayout mSplitLayout;
+    private DividerView mDividerView;
+
+    @Before
+    @UiThreadTest
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        Configuration configuration = getConfiguration();
+        mSplitLayout = new SplitLayout("TestSplitLayout", mContext, configuration,
+                mSplitLayoutHandler, mCallbacks, mDisplayController, mDisplayImeController,
+                mTaskOrganizer, SplitLayout.PARALLAX_NONE);
+        SplitWindowManager splitWindowManager = new SplitWindowManager("TestSplitWindowManager",
+                mContext,
+                configuration, mCallbacks);
+        splitWindowManager.init(mSplitLayout, new InsetsState());
+        mDividerView = spy((DividerView) splitWindowManager.getDividerView());
+    }
+
+    @Test
+    @UiThreadTest
+    public void testHoverDividerView() {
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CURSOR_HOVER_STATES_ENABLED,
+                "true", false);
+
+        Rect dividerBounds = mSplitLayout.getDividerBounds();
+        int x = dividerBounds.centerX();
+        int y = dividerBounds.centerY();
+        long downTime = SystemClock.uptimeMillis();
+        mDividerView.onHoverEvent(getMotionEvent(downTime, MotionEvent.ACTION_HOVER_ENTER, x, y));
+
+        verify(mDividerView, times(1)).setHovering();
+
+        mDividerView.onHoverEvent(getMotionEvent(downTime, MotionEvent.ACTION_HOVER_EXIT, x, y));
+
+        verify(mDividerView, times(1)).releaseHovering();
+
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CURSOR_HOVER_STATES_ENABLED,
+                "false", false);
+    }
+
+    private static MotionEvent getMotionEvent(long eventTime, int action, float x, float y) {
+        MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
+        properties.id = 0;
+        properties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+
+        MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+        coords.pressure = 1;
+        coords.size = 1;
+        coords.x = x;
+        coords.y = y;
+
+        return MotionEvent.obtain(eventTime, eventTime, action, 1,
+                new MotionEvent.PointerProperties[]{properties},
+                new MotionEvent.PointerCoords[]{coords}, 0, 0, 1.0f, 1.0f, 0, 0,
+                InputDevice.SOURCE_TOUCHSCREEN, 0);
+    }
+
+    private static Configuration getConfiguration() {
+        final Configuration configuration = new Configuration();
+        configuration.unset();
+        configuration.orientation = ORIENTATION_LANDSCAPE;
+        configuration.windowConfiguration.setRotation(0);
+        configuration.windowConfiguration.setBounds(new Rect(0, 0, 1080, 2160));
+        return configuration;
+    }
+}
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 d6387ee..605a762 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
@@ -59,6 +59,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.wm.shell.MockToken;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
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 3bc2f0e..3fe78ef 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
@@ -129,6 +129,18 @@
     }
 
     @Test
+    fun addListener_notifiesStashed() {
+        repo.setStashed(DEFAULT_DISPLAY, true)
+        val listener = TestVisibilityListener()
+        val executor = TestShellExecutor()
+        repo.addVisibleTasksListener(listener, executor)
+        executor.flushAll()
+
+        assertThat(listener.stashedOnDefaultDisplay).isTrue()
+        assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1)
+    }
+
+    @Test
     fun addListener_tasksOnDifferentDisplay_doesNotNotify() {
         repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = true)
         val listener = TestVisibilityListener()
@@ -313,6 +325,65 @@
         assertThat(tasks.first()).isEqualTo(6)
     }
 
+    @Test
+    fun setStashed_stateIsUpdatedForTheDisplay() {
+        repo.setStashed(DEFAULT_DISPLAY, true)
+        assertThat(repo.isStashed(DEFAULT_DISPLAY)).isTrue()
+        assertThat(repo.isStashed(SECOND_DISPLAY)).isFalse()
+
+        repo.setStashed(DEFAULT_DISPLAY, false)
+        assertThat(repo.isStashed(DEFAULT_DISPLAY)).isFalse()
+    }
+
+    @Test
+    fun setStashed_notifyListener() {
+        val listener = TestVisibilityListener()
+        val executor = TestShellExecutor()
+        repo.addVisibleTasksListener(listener, executor)
+        repo.setStashed(DEFAULT_DISPLAY, true)
+        executor.flushAll()
+        assertThat(listener.stashedOnDefaultDisplay).isTrue()
+        assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1)
+
+        repo.setStashed(DEFAULT_DISPLAY, false)
+        executor.flushAll()
+        assertThat(listener.stashedOnDefaultDisplay).isFalse()
+        assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(2)
+    }
+
+    @Test
+    fun setStashed_secondCallDoesNotNotify() {
+        val listener = TestVisibilityListener()
+        val executor = TestShellExecutor()
+        repo.addVisibleTasksListener(listener, executor)
+        repo.setStashed(DEFAULT_DISPLAY, true)
+        repo.setStashed(DEFAULT_DISPLAY, true)
+        executor.flushAll()
+        assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1)
+    }
+
+    @Test
+    fun setStashed_tracksPerDisplay() {
+        val listener = TestVisibilityListener()
+        val executor = TestShellExecutor()
+        repo.addVisibleTasksListener(listener, executor)
+
+        repo.setStashed(DEFAULT_DISPLAY, true)
+        executor.flushAll()
+        assertThat(listener.stashedOnDefaultDisplay).isTrue()
+        assertThat(listener.stashedOnSecondaryDisplay).isFalse()
+
+        repo.setStashed(SECOND_DISPLAY, true)
+        executor.flushAll()
+        assertThat(listener.stashedOnDefaultDisplay).isTrue()
+        assertThat(listener.stashedOnSecondaryDisplay).isTrue()
+
+        repo.setStashed(DEFAULT_DISPLAY, false)
+        executor.flushAll()
+        assertThat(listener.stashedOnDefaultDisplay).isFalse()
+        assertThat(listener.stashedOnSecondaryDisplay).isTrue()
+    }
+
     class TestListener : DesktopModeTaskRepository.ActiveTasksListener {
         var activeChangesOnDefaultDisplay = 0
         var activeChangesOnSecondaryDisplay = 0
@@ -332,6 +403,12 @@
         var visibleChangesOnDefaultDisplay = 0
         var visibleChangesOnSecondaryDisplay = 0
 
+        var stashedOnDefaultDisplay = false
+        var stashedOnSecondaryDisplay = false
+
+        var stashedChangesOnDefaultDisplay = 0
+        var stashedChangesOnSecondaryDisplay = 0
+
         override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {
             when (displayId) {
                 DEFAULT_DISPLAY -> {
@@ -345,6 +422,20 @@
                 else -> fail("Visible task listener received unexpected display id: $displayId")
             }
         }
+
+        override fun onStashedChanged(displayId: Int, stashed: Boolean) {
+            when (displayId) {
+                DEFAULT_DISPLAY -> {
+                    stashedOnDefaultDisplay = stashed
+                    stashedChangesOnDefaultDisplay++
+                }
+                SECOND_DISPLAY -> {
+                    stashedOnSecondaryDisplay = stashed
+                    stashedChangesOnDefaultDisplay++
+                }
+                else -> fail("Visible task listener received unexpected display id: $displayId")
+            }
+        }
     }
 
     companion object {
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 1335ebf..1477cf7 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
@@ -39,17 +39,20 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
 import com.android.dx.mockito.inline.extended.ExtendedMockito.never
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.wm.shell.MockToken
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 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.LaunchAdjacentController
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask
+import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
@@ -77,6 +80,7 @@
 class DesktopTasksControllerTest : ShellTestCase() {
 
     @Mock lateinit var testExecutor: ShellExecutor
+    @Mock lateinit var shellCommandHandler: ShellCommandHandler
     @Mock lateinit var shellController: ShellController
     @Mock lateinit var displayController: DisplayController
     @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer
@@ -85,12 +89,16 @@
     @Mock lateinit var transitions: Transitions
     @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler
     @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler
+    @Mock lateinit var mToggleResizeDesktopTaskTransitionHandler:
+            ToggleResizeDesktopTaskTransitionHandler
+    @Mock lateinit var launchAdjacentController: LaunchAdjacentController
 
     private lateinit var mockitoSession: StaticMockitoSession
     private lateinit var controller: DesktopTasksController
     private lateinit var shellInit: ShellInit
     private lateinit var desktopModeTaskRepository: DesktopModeTaskRepository
 
+    private val shellExecutor = TestShellExecutor()
     // Mock running tasks are registered here so we can get the list from mock shell task organizer
     private val runningTasks = mutableListOf<RunningTaskInfo>()
 
@@ -114,6 +122,7 @@
         return DesktopTasksController(
             context,
             shellInit,
+            shellCommandHandler,
             shellController,
             displayController,
             shellTaskOrganizer,
@@ -122,8 +131,10 @@
             transitions,
             enterDesktopTransitionHandler,
             exitDesktopTransitionHandler,
+            mToggleResizeDesktopTaskTransitionHandler,
             desktopModeTaskRepository,
-            TestShellExecutor()
+            launchAdjacentController,
+            shellExecutor
         )
     }
 
@@ -262,8 +273,9 @@
     }
 
     @Test
-    fun moveToDesktop() {
+    fun moveToDesktop_displayFullscreen_windowingModeSetToFreeform() {
         val task = setUpFullscreenTask()
+        task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN
         controller.moveToDesktop(task)
         val wct = getLatestWct(expectTransition = TRANSIT_CHANGE)
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
@@ -271,6 +283,16 @@
     }
 
     @Test
+    fun moveToDesktop_displayFreeform_windowingModeSetToUndefined() {
+        val task = setUpFullscreenTask()
+        task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM
+        controller.moveToDesktop(task)
+        val wct = getLatestWct(expectTransition = TRANSIT_CHANGE)
+        assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+                .isEqualTo(WINDOWING_MODE_UNDEFINED)
+    }
+
+    @Test
     fun moveToDesktop_nonExistentTask_doesNothing() {
         controller.moveToDesktop(999)
         verifyWCTNotExecuted()
@@ -317,12 +339,23 @@
     }
 
     @Test
-    fun moveToFullscreen() {
+    fun moveToFullscreen_displayFullscreen_windowingModeSetToUndefined() {
         val task = setUpFreeformTask()
+        task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN
         controller.moveToFullscreen(task)
         val wct = getLatestWct(expectTransition = TRANSIT_CHANGE)
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
-            .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED)
+    }
+
+    @Test
+    fun moveToFullscreen_displayFreeform_windowingModeSetToFullscreen() {
+        val task = setUpFreeformTask()
+        task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM
+        controller.moveToFullscreen(task)
+        val wct = getLatestWct(expectTransition = TRANSIT_CHANGE)
+        assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+                .isEqualTo(WINDOWING_MODE_FULLSCREEN)
     }
 
     @Test
@@ -345,6 +378,18 @@
     }
 
     @Test
+    fun moveTaskToFront_postsWctWithReorderOp() {
+        val task1 = setUpFreeformTask()
+        setUpFreeformTask()
+
+        controller.moveTaskToFront(task1)
+
+        val wct = getLatestWct(expectTransition = TRANSIT_TO_FRONT)
+        assertThat(wct.hierarchyOps).hasSize(1)
+        wct.assertReorderAt(index = 0, task1)
+    }
+
+    @Test
     fun moveToNextDisplay_noOtherDisplays() {
         whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY))
         val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -451,6 +496,27 @@
     }
 
     @Test
+    fun handleRequest_fullscreenTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() {
+        assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+        val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY)
+        markTaskHidden(stashedFreeformTask)
+
+        val fullscreenTask = createFullscreenTask(DEFAULT_DISPLAY)
+
+        controller.stashDesktopApps(DEFAULT_DISPLAY)
+
+        val result = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+        assertThat(result).isNotNull()
+        result!!.assertReorderSequence(stashedFreeformTask, fullscreenTask)
+        assertThat(result.changes[fullscreenTask.token.asBinder()]?.windowingMode)
+                .isEqualTo(WINDOWING_MODE_FREEFORM)
+
+        // Stashed state should be cleared
+        assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse()
+    }
+
+    @Test
     fun handleRequest_freeformTask_freeformVisible_returnNull() {
         assumeTrue(ENABLE_SHELL_TRANSITIONS)
 
@@ -501,6 +567,25 @@
     }
 
     @Test
+    fun handleRequest_freeformTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() {
+        assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+        val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY)
+        markTaskHidden(stashedFreeformTask)
+
+        val freeformTask = createFreeformTask(DEFAULT_DISPLAY)
+
+        controller.stashDesktopApps(DEFAULT_DISPLAY)
+
+        val result = controller.handleRequest(Binder(), createTransition(freeformTask))
+        assertThat(result).isNotNull()
+        result?.assertReorderSequence(stashedFreeformTask, freeformTask)
+
+        // Stashed state should be cleared
+        assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse()
+    }
+
+    @Test
     fun handleRequest_notOpenOrToFrontTransition_returnNull() {
         assumeTrue(ENABLE_SHELL_TRANSITIONS)
 
@@ -539,6 +624,46 @@
         assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
     }
 
+    @Test
+    fun stashDesktopApps_stateUpdates() {
+        controller.stashDesktopApps(DEFAULT_DISPLAY)
+
+        assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isTrue()
+        assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isFalse()
+    }
+
+    @Test
+    fun hideStashedDesktopApps_stateUpdates() {
+        desktopModeTaskRepository.setStashed(DEFAULT_DISPLAY, true)
+        desktopModeTaskRepository.setStashed(SECOND_DISPLAY, true)
+        controller.hideStashedDesktopApps(DEFAULT_DISPLAY)
+
+        assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse()
+        // Check that second display is not affected
+        assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isTrue()
+    }
+
+    @Test
+    fun desktopTasksVisibilityChange_visible_setLaunchAdjacentDisabled() {
+        val task = setUpFreeformTask()
+        clearInvocations(launchAdjacentController)
+
+        markTaskVisible(task)
+        shellExecutor.flushAll()
+        verify(launchAdjacentController).launchAdjacentEnabled = false
+    }
+
+    @Test
+    fun desktopTasksVisibilityChange_invisible_setLaunchAdjacentEnabled() {
+        val task = setUpFreeformTask()
+        markTaskVisible(task)
+        clearInvocations(launchAdjacentController)
+
+        markTaskHidden(task)
+        shellExecutor.flushAll()
+        verify(launchAdjacentController).launchAdjacentEnabled = true
+    }
+
     private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
         val task = createFreeformTask(displayId)
         whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
index cf1ff32..29a757c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
@@ -22,6 +22,7 @@
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
 import android.view.Display.DEFAULT_DISPLAY
+import com.android.wm.shell.MockToken
 import com.android.wm.shell.TestRunningTaskInfoBuilder
 
 class DesktopTestHelpers {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/MockToken.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/MockToken.java
deleted file mode 100644
index 09d474d..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/MockToken.java
+++ /dev/null
@@ -1,40 +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.wm.shell.desktopmode;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.os.IBinder;
-import android.window.WindowContainerToken;
-
-/**
- * {@link WindowContainerToken} wrapper that supports a mock binder
- */
-class MockToken {
-    private final WindowContainerToken mToken;
-
-    MockToken() {
-        mToken = mock(WindowContainerToken.class);
-        IBinder binder = mock(IBinder.class);
-        when(mToken.asBinder()).thenReturn(binder);
-    }
-
-    WindowContainerToken token() {
-        return mToken;
-    }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
index 68cb57c..b1befc4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
@@ -41,6 +41,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Optional;
+
 /** Tests for {@link MainStage} */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -61,7 +63,7 @@
         MockitoAnnotations.initMocks(this);
         mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
         mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks,
-                mSyncQueue, mSurfaceSession, mIconProvider);
+                mSyncQueue, mSurfaceSession, mIconProvider, Optional.empty());
         mMainStage.onTaskAppeared(mRootTaskInfo, mRootLeash);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
index 3b42a48..549bd3f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
@@ -46,6 +46,8 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
 
+import java.util.Optional;
+
 /** Tests for {@link SideStage} */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -66,7 +68,7 @@
         MockitoAnnotations.initMocks(this);
         mRootTask = new TestRunningTaskInfoBuilder().build();
         mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks,
-                mSyncQueue, mSurfaceSession, mIconProvider);
+                mSyncQueue, mSurfaceSession, mIconProvider, Optional.empty());
         mSideStage.onTaskAppeared(mRootTask, mRootLeash);
     }
 
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 fb17d87..e8a1e91 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
@@ -61,6 +61,7 @@
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.LaunchAdjacentController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
@@ -71,6 +72,7 @@
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.sysui.ShellSharedConstants;
 import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -102,6 +104,8 @@
     @Mock IconProvider mIconProvider;
     @Mock StageCoordinator mStageCoordinator;
     @Mock RecentTasksController mRecentTasks;
+    @Mock LaunchAdjacentController mLaunchAdjacentController;
+    @Mock WindowDecorViewModel mWindowDecorViewModel;
     @Captor ArgumentCaptor<Intent> mIntentCaptor;
 
     private ShellController mShellController;
@@ -117,7 +121,8 @@
                 mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
                 mRootTDAOrganizer, mDisplayController, mDisplayImeController,
                 mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
-                mIconProvider, mRecentTasks, mMainExecutor, mStageCoordinator));
+                mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel,
+                mMainExecutor, mStageCoordinator));
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index 4e446c6..a3009a5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -31,12 +31,14 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.LaunchAdjacentController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.split.SplitLayout;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
 import java.util.Optional;
 
@@ -76,10 +78,13 @@
                 DisplayInsetsController insetsController, SplitLayout splitLayout,
                 Transitions transitions, TransactionPool transactionPool,
                 ShellExecutor mainExecutor,
-                Optional<RecentTasksController> recentTasks) {
+                Optional<RecentTasksController> recentTasks,
+                LaunchAdjacentController launchAdjacentController,
+                Optional<WindowDecorViewModel> windowDecorViewModel) {
             super(context, displayId, syncQueue, taskOrganizer, mainStage,
                     sideStage, displayController, imeController, insetsController, splitLayout,
-                    transitions, transactionPool, mainExecutor, recentTasks);
+                    transitions, transactionPool, mainExecutor, recentTasks,
+                    launchAdjacentController, windowDecorViewModel);
 
             // Prepare root task for testing.
             mRootTask = new TestRunningTaskInfoBuilder().build();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index 44c76de..5efd9ad 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -70,6 +70,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.LaunchAdjacentController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
@@ -77,6 +78,7 @@
 import com.android.wm.shell.common.split.SplitLayout;
 import com.android.wm.shell.transition.DefaultMixedHandler;
 import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -101,7 +103,9 @@
     @Mock private Transitions mTransitions;
     @Mock private SurfaceSession mSurfaceSession;
     @Mock private IconProvider mIconProvider;
+    @Mock private WindowDecorViewModel mWindowDecorViewModel;
     @Mock private ShellExecutor mMainExecutor;
+    @Mock private LaunchAdjacentController mLaunchAdjacentController;
     @Mock private DefaultMixedHandler mMixedHandler;
     private SplitLayout mSplitLayout;
     private MainStage mMainStage;
@@ -123,16 +127,17 @@
         mSplitLayout = SplitTestUtils.createMockSplitLayout();
         mMainStage = spy(new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
                 StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
-                mIconProvider));
+                mIconProvider, Optional.of(mWindowDecorViewModel)));
         mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
         mSideStage = spy(new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
                 StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
-                mIconProvider));
+                mIconProvider, Optional.of(mWindowDecorViewModel)));
         mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
         mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
                 mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController,
                 mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions,
-                mTransactionPool, mMainExecutor, Optional.empty());
+                mTransactionPool, mMainExecutor, Optional.empty(),
+                mLaunchAdjacentController, Optional.empty());
         mStageCoordinator.setMixedHandler(mMixedHandler);
         mSplitScreenTransitions = mStageCoordinator.getSplitTransitions();
         doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class))
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 2dcdc74..e59d09c 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
@@ -65,6 +65,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.LaunchAdjacentController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
@@ -107,6 +108,8 @@
     private DisplayInsetsController mDisplayInsetsController;
     @Mock
     private TransactionPool mTransactionPool;
+    @Mock
+    private LaunchAdjacentController mLaunchAdjacentController;
 
     private final Rect mBounds1 = new Rect(10, 20, 30, 40);
     private final Rect mBounds2 = new Rect(5, 10, 15, 20);
@@ -130,7 +133,7 @@
         mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
                 mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController,
                 mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool,
-                mMainExecutor, Optional.empty()));
+                mMainExecutor, Optional.empty(), mLaunchAdjacentController, Optional.empty()));
         mDividerLeash = new SurfaceControl.Builder(mSurfaceSession).setName("fakeDivider").build();
 
         when(mSplitLayout.getBounds1()).thenReturn(mBounds1);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
index 1a1bebd..946a7ef 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
@@ -43,6 +43,7 @@
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -52,6 +53,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Optional;
+
 /**
  * Tests for {@link StageTaskListener}
  * Build/Install/Run:
@@ -71,6 +74,8 @@
     private SyncTransactionQueue mSyncQueue;
     @Mock
     private IconProvider mIconProvider;
+    @Mock
+    private WindowDecorViewModel mWindowDecorViewModel;
     @Captor
     private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor;
     private SurfaceSession mSurfaceSession = new SurfaceSession();
@@ -89,7 +94,8 @@
                 mCallbacks,
                 mSyncQueue,
                 mSurfaceSession,
-                mIconProvider);
+                mIconProvider,
+                Optional.of(mWindowDecorViewModel));
         mRootTask = new TestRunningTaskInfoBuilder().build();
         mRootTask.parentTaskId = INVALID_TASK_ID;
         mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
@@ -155,7 +161,7 @@
         childTask.supportsMultiWindow = false;
 
         mStageTaskListener.onTaskInfoChanged(childTask);
-        verify(mCallbacks).onNoLongerSupportMultiWindow();
+        verify(mCallbacks).onNoLongerSupportMultiWindow(childTask);
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
index 1b38956..50435a0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
@@ -40,6 +40,7 @@
 import android.app.ActivityOptions;
 import android.app.PendingIntent;
 import android.content.Context;
+import android.graphics.Insets;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.testing.AndroidTestingRunner;
@@ -563,4 +564,47 @@
         mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash);
         verify(mTaskViewTaskController, never()).cleanUpPendingTask();
     }
+
+    @Test
+    public void testSetCaptionInsets_noTaskInitially() {
+        assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+        Rect insets = new Rect(0, 400, 0, 0);
+        mTaskView.setCaptionInsets(Insets.of(insets));
+        mTaskView.onComputeInternalInsets(new ViewTreeObserver.InternalInsetsInfo());
+
+        verify(mTaskViewTaskController).applyCaptionInsetsIfNeeded();
+        verify(mOrganizer, never()).applyTransaction(any());
+
+        mTaskView.surfaceCreated(mock(SurfaceHolder.class));
+        reset(mOrganizer);
+        reset(mTaskViewTaskController);
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        mTaskViewTaskController.prepareOpenAnimation(true /* newTask */,
+                new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+                mLeash, wct);
+        mTaskView.onComputeInternalInsets(new ViewTreeObserver.InternalInsetsInfo());
+
+        verify(mTaskViewTaskController).applyCaptionInsetsIfNeeded();
+        verify(mOrganizer).applyTransaction(any());
+    }
+
+    @Test
+    public void testSetCaptionInsets_withTask() {
+        assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+        mTaskView.surfaceCreated(mock(SurfaceHolder.class));
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        mTaskViewTaskController.prepareOpenAnimation(true /* newTask */,
+                new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo,
+                mLeash, wct);
+        reset(mTaskViewTaskController);
+        reset(mOrganizer);
+
+        Rect insets = new Rect(0, 400, 0, 0);
+        mTaskView.setCaptionInsets(Insets.of(insets));
+        mTaskView.onComputeInternalInsets(new ViewTreeObserver.InternalInsetsInfo());
+        verify(mTaskViewTaskController).applyCaptionInsetsIfNeeded();
+        verify(mOrganizer).applyTransaction(any());
+    }
 }
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 41bab95..adc2a6f 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
@@ -53,7 +53,6 @@
 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 com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
@@ -82,7 +81,6 @@
     @Mock private ShellTaskOrganizer mTaskOrganizer;
     @Mock private DisplayController mDisplayController;
     @Mock private DisplayLayout mDisplayLayout;
-    @Mock private SplitScreenController mSplitScreenController;
     @Mock private SyncTransactionQueue mSyncQueue;
     @Mock private DesktopModeController mDesktopModeController;
     @Mock private DesktopTasksController mDesktopTasksController;
@@ -92,6 +90,7 @@
     @Mock private DesktopModeWindowDecorViewModel.InputMonitorFactory mMockInputMonitorFactory;
     @Mock private Supplier<SurfaceControl.Transaction> mTransactionFactory;
     @Mock private SurfaceControl.Transaction mTransaction;
+    @Mock private Display mDisplay;
     private final List<InputManager> mMockInputManagers = new ArrayList<>();
 
     private DesktopModeWindowDecorViewModel mDesktopModeWindowDecorViewModel;
@@ -111,7 +110,6 @@
                 mTransitions,
                 Optional.of(mDesktopModeController),
                 Optional.of(mDesktopTasksController),
-                Optional.of(mSplitScreenController),
                 mDesktopModeWindowDecorFactory,
                 mMockInputMonitorFactory,
                 mTransactionFactory
@@ -129,6 +127,9 @@
         final InputChannel[] inputChannels = InputChannel.openInputChannelPair(TAG);
         inputChannels[0].dispose();
         when(mInputMonitor.getInputChannel()).thenReturn(inputChannels[1]);
+
+        mDesktopModeWindowDecoration.mDisplay = mDisplay;
+        doReturn(Display.DEFAULT_DISPLAY).when(mDisplay).getDisplayId();
     }
 
     @Test
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
index 8f84008..3fbab0f 100644
--- 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
@@ -55,7 +55,7 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        `when`(eventHandler.handleMotionEvent(any())).thenReturn(true)
+        `when`(eventHandler.handleMotionEvent(any(), any())).thenReturn(true)
 
         dragDetector = DragDetector(eventHandler)
         dragDetector.setTouchSlop(SLOP)
@@ -72,13 +72,13 @@
     @Test
     fun testNoMove_passesDownAndUp() {
         assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
-        verify(eventHandler).handleMotionEvent(argThat {
+        verify(eventHandler).handleMotionEvent(any(), 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 {
+        verify(eventHandler).handleMotionEvent(any(), argThat {
             return@argThat it.action == MotionEvent.ACTION_UP && it.x == X && it.y == Y &&
                     it.source == InputDevice.SOURCE_TOUCHSCREEN
         })
@@ -86,12 +86,12 @@
 
     @Test
     fun testMoveInSlop_touch_passesDownAndUp() {
-        `when`(eventHandler.handleMotionEvent(argThat {
+        `when`(eventHandler.handleMotionEvent(any(), argThat {
             return@argThat it.action == MotionEvent.ACTION_DOWN
         })).thenReturn(false)
 
         assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
-        verify(eventHandler).handleMotionEvent(argThat {
+        verify(eventHandler).handleMotionEvent(any(), argThat {
             return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
                     it.source == InputDevice.SOURCE_TOUCHSCREEN
         })
@@ -99,12 +99,12 @@
         val newX = X + SLOP - 1
         assertFalse(
                 dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y)))
-        verify(eventHandler, never()).handleMotionEvent(argThat {
+        verify(eventHandler, never()).handleMotionEvent(any(), argThat {
             return@argThat it.action == MotionEvent.ACTION_MOVE
         })
 
         assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_UP, newX, Y)))
-        verify(eventHandler).handleMotionEvent(argThat {
+        verify(eventHandler).handleMotionEvent(any(), argThat {
             return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y &&
                     it.source == InputDevice.SOURCE_TOUCHSCREEN
         })
@@ -112,13 +112,13 @@
 
     @Test
     fun testMoveInSlop_mouse_passesDownMoveAndUp() {
-        `when`(eventHandler.handleMotionEvent(argThat {
+        `when`(eventHandler.handleMotionEvent(any(), argThat {
             it.action == MotionEvent.ACTION_DOWN
         })).thenReturn(false)
 
         assertFalse(dragDetector.onMotionEvent(
                 createMotionEvent(MotionEvent.ACTION_DOWN, isTouch = false)))
-        verify(eventHandler).handleMotionEvent(argThat {
+        verify(eventHandler).handleMotionEvent(any(), argThat {
             return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
                     it.source == InputDevice.SOURCE_MOUSE
         })
@@ -126,14 +126,14 @@
         val newX = X + SLOP - 1
         assertTrue(dragDetector.onMotionEvent(
                 createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y, isTouch = false)))
-        verify(eventHandler).handleMotionEvent(argThat {
+        verify(eventHandler).handleMotionEvent(any(), 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 {
+        verify(eventHandler).handleMotionEvent(any(), argThat {
             return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y &&
                     it.source == InputDevice.SOURCE_MOUSE
         })
@@ -141,25 +141,25 @@
 
     @Test
     fun testMoveBeyondSlop_passesDownMoveAndUp() {
-        `when`(eventHandler.handleMotionEvent(argThat {
+        `when`(eventHandler.handleMotionEvent(any(), argThat {
             it.action == MotionEvent.ACTION_DOWN
         })).thenReturn(false)
 
         assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
-        verify(eventHandler).handleMotionEvent(argThat {
+        verify(eventHandler).handleMotionEvent(any(), 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 {
+        verify(eventHandler).handleMotionEvent(any(), 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 {
+        verify(eventHandler).handleMotionEvent(any(), argThat {
             return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y &&
                     it.source == InputDevice.SOURCE_TOUCHSCREEN
         })
@@ -167,12 +167,12 @@
 
     @Test
     fun testPassesHoverEnter() {
-        `when`(eventHandler.handleMotionEvent(argThat {
+        `when`(eventHandler.handleMotionEvent(any(), argThat {
             it.action == MotionEvent.ACTION_HOVER_ENTER
         })).thenReturn(false)
 
         assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_ENTER)))
-        verify(eventHandler).handleMotionEvent(argThat {
+        verify(eventHandler).handleMotionEvent(any(), argThat {
             return@argThat it.action == MotionEvent.ACTION_HOVER_ENTER && it.x == X && it.y == Y
         })
     }
@@ -180,7 +180,7 @@
     @Test
     fun testPassesHoverMove() {
         assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_MOVE)))
-        verify(eventHandler).handleMotionEvent(argThat {
+        verify(eventHandler).handleMotionEvent(any(), argThat {
             return@argThat it.action == MotionEvent.ACTION_HOVER_MOVE && it.x == X && it.y == Y
         })
     }
@@ -188,7 +188,7 @@
     @Test
     fun testPassesHoverExit() {
         assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_EXIT)))
-        verify(eventHandler).handleMotionEvent(argThat {
+        verify(eventHandler).handleMotionEvent(any(), argThat {
             return@argThat it.action == MotionEvent.ACTION_HOVER_EXIT && it.x == X && it.y == Y
         })
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index 69604dd..6f0599a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -22,6 +22,7 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
 import org.mockito.Mockito.any
 import org.mockito.Mockito.argThat
@@ -71,16 +72,6 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        taskPositioner =
-            FluidResizeTaskPositioner(
-                mockShellTaskOrganizer,
-                mockWindowDecoration,
-                mockDisplayController,
-                DISALLOWED_AREA_FOR_END_BOUNDS,
-                mockDragStartListener,
-                mockTransactionFactory
-        )
-
         whenever(taskToken.asBinder()).thenReturn(taskBinder)
         whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout)
         whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI)
@@ -101,6 +92,15 @@
         }
         mockWindowDecoration.mDisplay = mockDisplay
         whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID }
+
+        taskPositioner = FluidResizeTaskPositioner(
+                mockShellTaskOrganizer,
+                mockWindowDecoration,
+                mockDisplayController,
+                mockDragStartListener,
+                mockTransactionFactory,
+                DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT
+        )
     }
 
     @Test
@@ -544,7 +544,7 @@
         )
 
         val newX = STARTING_BOUNDS.right.toFloat() + 5
-        val newY = STARTING_BOUNDS.top.toFloat() + 5
+        val newY = DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT.toFloat() - 1
         taskPositioner.onDragPositioningMove(
                 newX,
                 newY
@@ -614,6 +614,38 @@
         })
     }
 
+    @Test
+    fun testDragResize_drag_taskPositionedInStableBounds() {
+        taskPositioner.onDragPositioningStart(
+                CTRL_TYPE_UNDEFINED, // drag
+                STARTING_BOUNDS.left.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        val newX = STARTING_BOUNDS.left.toFloat()
+        val newY = STABLE_BOUNDS.top.toFloat() - 5
+        taskPositioner.onDragPositioningMove(
+                newX,
+                newY
+        )
+        verify(mockTransaction).setPosition(any(), eq(newX), eq(newY))
+
+        taskPositioner.onDragPositioningEnd(
+                newX,
+                newY
+        )
+        // Verify task's top bound is set to stable bounds top since dragged outside stable bounds
+        // but not in disallowed end bounds area.
+        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 ==
+                        STABLE_BOUNDS.top
+            }
+        })
+    }
+
     companion object {
         private const val TASK_ID = 5
         private const val MIN_WIDTH = 10
@@ -622,10 +654,11 @@
         private const val DEFAULT_MIN = 40
         private const val DISPLAY_ID = 1
         private const val NAVBAR_HEIGHT = 50
+        private const val CAPTION_HEIGHT = 50
+        private const val DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT = 10
         private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600)
-        private val STARTING_BOUNDS = Rect(0, 0, 100, 100)
+        private val STARTING_BOUNDS = Rect(100, 100, 200, 200)
         private val STABLE_INSETS = Rect(0, 50, 0, 0)
-        private val DISALLOWED_AREA_FOR_END_BOUNDS = Rect(0, 0, 300, 300)
         private val DISALLOWED_RESIZE_AREA = Rect(
                 DISPLAY_BOUNDS.left,
                 DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT,
@@ -633,7 +666,7 @@
                 DISPLAY_BOUNDS.bottom)
         private val STABLE_BOUNDS = Rect(
                 DISPLAY_BOUNDS.left,
-                DISPLAY_BOUNDS.top,
+                DISPLAY_BOUNDS.top + CAPTION_HEIGHT,
                 DISPLAY_BOUNDS.right,
                 DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT
         )
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 4147dd8..3465ddd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -89,17 +89,6 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        taskPositioner =
-            VeiledResizeTaskPositioner(
-                mockShellTaskOrganizer,
-                mockDesktopWindowDecoration,
-                mockDisplayController,
-                DISALLOWED_AREA_FOR_END_BOUNDS,
-                mockDragStartListener,
-                mockTransactionFactory,
-                mockTransitions
-            )
-
         whenever(taskToken.asBinder()).thenReturn(taskBinder)
         whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout)
         whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI)
@@ -119,6 +108,17 @@
         }
         mockDesktopWindowDecoration.mDisplay = mockDisplay
         whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID }
+
+        taskPositioner =
+                VeiledResizeTaskPositioner(
+                        mockShellTaskOrganizer,
+                        mockDesktopWindowDecoration,
+                        mockDisplayController,
+                        mockDragStartListener,
+                        mockTransactionFactory,
+                        mockTransitions,
+                        DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT
+                )
     }
 
     @Test
@@ -269,7 +269,7 @@
         )
 
         val newX = STARTING_BOUNDS.left.toFloat() + 5
-        val newY = STARTING_BOUNDS.top.toFloat() + 5
+        val newY = DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT.toFloat() - 1
         taskPositioner.onDragPositioningMove(
                 newX,
                 newY
@@ -334,6 +334,38 @@
         })
     }
 
+    @Test
+    fun testDragResize_drag_taskPositionedInStableBounds() {
+        taskPositioner.onDragPositioningStart(
+                CTRL_TYPE_UNDEFINED, // drag
+                STARTING_BOUNDS.left.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        val newX = STARTING_BOUNDS.left.toFloat()
+        val newY = STABLE_BOUNDS.top.toFloat() - 5
+        taskPositioner.onDragPositioningMove(
+                newX,
+                newY
+        )
+        verify(mockTransaction).setPosition(any(), eq(newX), eq(newY))
+
+        taskPositioner.onDragPositioningEnd(
+                newX,
+                newY
+        )
+        // Verify task's top bound is set to stable bounds top since dragged outside stable bounds
+        // but not in disallowed end bounds area.
+        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 ==
+                        STABLE_BOUNDS.top
+            }
+        })
+    }
+
     companion object {
         private const val TASK_ID = 5
         private const val MIN_WIDTH = 10
@@ -342,12 +374,13 @@
         private const val DEFAULT_MIN = 40
         private const val DISPLAY_ID = 1
         private const val NAVBAR_HEIGHT = 50
+        private const val CAPTION_HEIGHT = 50
+        private const val DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT = 10
         private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600)
-        private val STARTING_BOUNDS = Rect(0, 0, 100, 100)
-        private val DISALLOWED_AREA_FOR_END_BOUNDS = Rect(0, 0, 50, 50)
+        private val STARTING_BOUNDS = Rect(100, 100, 200, 200)
         private val STABLE_BOUNDS = Rect(
             DISPLAY_BOUNDS.left,
-            DISPLAY_BOUNDS.top,
+            DISPLAY_BOUNDS.top + CAPTION_HEIGHT,
             DISPLAY_BOUNDS.right,
             DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT
         )
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 5a2326b..7fc1c99 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
@@ -57,7 +57,6 @@
 import android.window.WindowContainerTransaction;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
@@ -316,7 +315,8 @@
         releaseOrder.verify(t).remove(captionContainerSurface);
         releaseOrder.verify(t).remove(decorContainerSurface);
         releaseOrder.verify(t).apply();
-        verify(mMockWindowContainerTransaction)
+        // Expect to remove two insets sources, the caption insets and the mandatory gesture insets.
+        verify(mMockWindowContainerTransaction, Mockito.times(2))
                 .removeInsetsSource(eq(taskInfo.token), any(), anyInt(), anyInt());
     }
 
@@ -410,15 +410,17 @@
         verify(additionalWindowSurfaceBuilder).build();
         verify(mMockSurfaceControlAddWindowT).setPosition(additionalWindowSurface, 0, 0);
         final int width = WindowDecoration.loadDimensionPixelSize(
-                mContext.getResources(), mCaptionMenuWidthId);
+                windowDecor.mDecorWindowContext.getResources(), mCaptionMenuWidthId);
         final int height = WindowDecoration.loadDimensionPixelSize(
-                mContext.getResources(), mRelayoutParams.mCaptionHeightId);
+                windowDecor.mDecorWindowContext.getResources(), mRelayoutParams.mCaptionHeightId);
         verify(mMockSurfaceControlAddWindowT).setWindowCrop(additionalWindowSurface, width, height);
-        final int shadowRadius = WindowDecoration.loadDimensionPixelSize(mContext.getResources(),
+        final int shadowRadius = WindowDecoration.loadDimensionPixelSize(
+                windowDecor.mDecorWindowContext.getResources(),
                 mCaptionMenuShadowRadiusId);
         verify(mMockSurfaceControlAddWindowT)
                 .setShadowRadius(additionalWindowSurface, shadowRadius);
-        final int cornerRadius = WindowDecoration.loadDimensionPixelSize(mContext.getResources(),
+        final int cornerRadius = WindowDecoration.loadDimensionPixelSize(
+                windowDecor.mDecorWindowContext.getResources(),
                 mCaptionMenuCornerRadiusId);
         verify(mMockSurfaceControlAddWindowT)
                 .setCornerRadius(additionalWindowSurface, cornerRadius);
@@ -513,8 +515,7 @@
 
     private TestWindowDecoration createWindowDecoration(
             ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) {
-        return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(),
-                mMockDisplayController, mMockShellTaskOrganizer,
+        return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer,
                 taskInfo, testSurface,
                 new MockObjectSupplier<>(mMockSurfaceControlBuilders,
                         () -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))),
diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp
index 00919dc..f71e728 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.cpp
+++ b/libs/hwui/pipeline/skia/ShaderCache.cpp
@@ -79,7 +79,7 @@
 
 void ShaderCache::initShaderDiskCache(const void* identity, ssize_t size) {
     ATRACE_NAME("initShaderDiskCache");
-    std::lock_guard<std::mutex> lock(mMutex);
+    std::lock_guard lock(mMutex);
 
     // Emulators can switch between different renders either as part of config
     // or snapshot migration. Also, program binaries may not work well on some
@@ -92,7 +92,7 @@
 }
 
 void ShaderCache::setFilename(const char* filename) {
-    std::lock_guard<std::mutex> lock(mMutex);
+    std::lock_guard lock(mMutex);
     mFilename = filename;
 }
 
@@ -104,7 +104,7 @@
 sk_sp<SkData> ShaderCache::load(const SkData& key) {
     ATRACE_NAME("ShaderCache::load");
     size_t keySize = key.size();
-    std::lock_guard<std::mutex> lock(mMutex);
+    std::lock_guard lock(mMutex);
     if (!mInitialized) {
         return nullptr;
     }
@@ -181,13 +181,18 @@
             auto key = sIDKey;
             set(mBlobCache.get(), &key, sizeof(key), mIDHash.data(), mIDHash.size());
         }
+        // The most straightforward way to make ownership shared
+        mMutex.unlock();
+        mMutex.lock_shared();
         mBlobCache->writeToFile();
+        mMutex.unlock_shared();
+        mMutex.lock();
     }
 }
 
 void ShaderCache::store(const SkData& key, const SkData& data, const SkString& /*description*/) {
     ATRACE_NAME("ShaderCache::store");
-    std::lock_guard<std::mutex> lock(mMutex);
+    std::lock_guard lock(mMutex);
     mNumShadersCachedInRam++;
     ATRACE_FORMAT("HWUI RAM cache: %d shaders", mNumShadersCachedInRam);
 
@@ -229,7 +234,7 @@
         mSavePending = true;
         std::thread deferredSaveThread([this]() {
             usleep(mDeferredSaveDelayMs * 1000);  // milliseconds to microseconds
-            std::lock_guard<std::mutex> lock(mMutex);
+            std::lock_guard lock(mMutex);
             // Store file on disk if there a new shader or Vulkan pipeline cache size changed.
             if (mCacheDirty || mNewPipelineCacheSize != mOldPipelineCacheSize) {
                 saveToDiskLocked();
@@ -245,11 +250,12 @@
 
 void ShaderCache::onVkFrameFlushed(GrDirectContext* context) {
     {
-        std::lock_guard<std::mutex> lock(mMutex);
-
+        mMutex.lock_shared();
         if (!mInitialized || !mTryToStorePipelineCache) {
+            mMutex.unlock_shared();
             return;
         }
+        mMutex.unlock_shared();
     }
     mInStoreVkPipelineInProgress = true;
     context->storeVkPipelineCacheData();
diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h
index f5506d6..2f91c77 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.h
+++ b/libs/hwui/pipeline/skia/ShaderCache.h
@@ -19,8 +19,10 @@
 #include <GrContextOptions.h>
 #include <SkRefCnt.h>
 #include <cutils/compiler.h>
+#include <ftl/shared_mutex.h>
+#include <utils/Mutex.h>
+
 #include <memory>
-#include <mutex>
 #include <string>
 #include <vector>
 
@@ -99,20 +101,20 @@
      * this will do so, loading the serialized cache contents from disk if
      * possible.
      */
-    BlobCache* getBlobCacheLocked();
+    BlobCache* getBlobCacheLocked() REQUIRES(mMutex);
 
     /**
      * "validateCache" updates the cache to match the given identity.  If the
      * cache currently has the wrong identity, all entries in the cache are cleared.
      */
-    bool validateCache(const void* identity, ssize_t size);
+    bool validateCache(const void* identity, ssize_t size) REQUIRES(mMutex);
 
     /**
-     * "saveToDiskLocked" attemps to save the current contents of the cache to
+     * "saveToDiskLocked" attempts to save the current contents of the cache to
      * disk. If the identity hash exists, we will insert the identity hash into
      * the cache for next validation.
      */
-    void saveToDiskLocked();
+    void saveToDiskLocked() REQUIRES(mMutex);
 
     /**
      * "mInitialized" indicates whether the ShaderCache is in the initialized
@@ -122,7 +124,7 @@
      * the load and store methods will return without performing any cache
      * operations.
      */
-    bool mInitialized = false;
+    bool mInitialized GUARDED_BY(mMutex) = false;
 
     /**
      * "mBlobCache" is the cache in which the key/value blob pairs are stored.  It
@@ -131,7 +133,7 @@
      * The blob cache contains the Android build number. We treat version mismatches as an empty
      * cache (logic implemented in BlobCache::unflatten).
      */
-    std::unique_ptr<FileBlobCache> mBlobCache;
+    std::unique_ptr<FileBlobCache> mBlobCache GUARDED_BY(mMutex);
 
     /**
      * "mFilename" is the name of the file for storing cache contents in between
@@ -140,7 +142,7 @@
      * empty string indicates that the cache should not be saved to or restored
      * from disk.
      */
-    std::string mFilename;
+    std::string mFilename GUARDED_BY(mMutex);
 
     /**
      * "mIDHash" is the current identity hash for the cache validation. It is
@@ -149,7 +151,7 @@
      * indicates that cache validation is not performed, and the hash should
      * not be stored on disk.
      */
-    std::vector<uint8_t> mIDHash;
+    std::vector<uint8_t> mIDHash GUARDED_BY(mMutex);
 
     /**
      * "mSavePending" indicates whether or not a deferred save operation is
@@ -159,7 +161,7 @@
      * contents to disk, unless mDeferredSaveDelayMs is 0 in which case saving
      * is disabled.
      */
-    bool mSavePending = false;
+    bool mSavePending GUARDED_BY(mMutex) = false;
 
     /**
      *  "mObservedBlobValueSize" is the maximum value size observed by the cache reading function.
@@ -174,16 +176,16 @@
     unsigned int mDeferredSaveDelayMs = 4 * 1000;
 
     /**
-     * "mMutex" is the mutex used to prevent concurrent access to the member
+     * "mMutex" is the shared mutex used to prevent concurrent access to the member
      * variables. It must be locked whenever the member variables are accessed.
      */
-    mutable std::mutex mMutex;
+    mutable ftl::SharedMutex mMutex;
 
     /**
      *  If set to "true", the next call to onVkFrameFlushed, will invoke
      * GrCanvas::storeVkPipelineCacheData. This does not guarantee that data will be stored on disk.
      */
-    bool mTryToStorePipelineCache = true;
+    bool mTryToStorePipelineCache GUARDED_BY(mMutex) = true;
 
     /**
      * This flag is used by "ShaderCache::store" to distinguish between shader data and
@@ -195,16 +197,16 @@
      *  "mNewPipelineCacheSize" has the size of the new Vulkan pipeline cache data. It is used
      *  to prevent unnecessary disk writes, if the pipeline cache size has not changed.
      */
-    size_t mNewPipelineCacheSize = -1;
+    size_t mNewPipelineCacheSize GUARDED_BY(mMutex) = -1;
     /**
      *  "mOldPipelineCacheSize" has the size of the Vulkan pipeline cache data stored on disk.
      */
-    size_t mOldPipelineCacheSize = -1;
+    size_t mOldPipelineCacheSize GUARDED_BY(mMutex) = -1;
 
     /**
      *  "mCacheDirty" is true when there is new shader cache data, which is not saved to disk.
      */
-    bool mCacheDirty = false;
+    bool mCacheDirty GUARDED_BY(mMutex) = false;
 
     /**
      * "sCache" is the singleton ShaderCache object.
@@ -221,7 +223,7 @@
      * interesting to keep track of how many shaders are stored in RAM. This
      * class provides a convenient entry point for that.
      */
-    int mNumShadersCachedInRam = 0;
+    int mNumShadersCachedInRam GUARDED_BY(mMutex) = 0;
 
     friend class ShaderCacheTestUtils;  // used for unit testing
 };
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 16b35ff..a5518eb 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -405,8 +405,17 @@
 
     // If the previous frame was dropped we don't need to hold onto it, so
     // just keep using the previous frame's structure instead
-    if (!wasSkipped(mCurrentFrameInfo)) {
+    if (wasSkipped(mCurrentFrameInfo)) {
+        // Use the oldest skipped frame in case we skip more than a single frame
+        if (!mSkippedFrameInfo) {
+            mSkippedFrameInfo.emplace();
+            mSkippedFrameInfo->vsyncId =
+                mCurrentFrameInfo->get(FrameInfoIndex::FrameTimelineVsyncId);
+            mSkippedFrameInfo->startTime = mCurrentFrameInfo->get(FrameInfoIndex::FrameStartTime);
+        }
+    } else {
         mCurrentFrameInfo = mJankTracker.startFrame();
+        mSkippedFrameInfo.reset();
     }
 
     mCurrentFrameInfo->importUiThreadInfo(uiFrameInfo);
@@ -602,10 +611,18 @@
         if (vsyncId != UiFrameInfoBuilder::INVALID_VSYNC_ID) {
             const auto inputEventId =
                     static_cast<int32_t>(mCurrentFrameInfo->get(FrameInfoIndex::InputEventId));
-            native_window_set_frame_timeline_info(
-                    mNativeSurface->getNativeWindow(), frameCompleteNr, vsyncId, inputEventId,
-                    mCurrentFrameInfo->get(FrameInfoIndex::FrameStartTime),
-                    solelyTextureViewUpdates);
+            const ANativeWindowFrameTimelineInfo ftl = {
+                    .frameNumber = frameCompleteNr,
+                    .frameTimelineVsyncId = vsyncId,
+                    .inputEventId = inputEventId,
+                    .startTimeNanos = mCurrentFrameInfo->get(FrameInfoIndex::FrameStartTime),
+                    .useForRefreshRateSelection = solelyTextureViewUpdates,
+                    .skippedFrameVsyncId = mSkippedFrameInfo ? mSkippedFrameInfo->vsyncId
+                                                             : UiFrameInfoBuilder::INVALID_VSYNC_ID,
+                    .skippedFrameStartTimeNanos =
+                            mSkippedFrameInfo ? mSkippedFrameInfo->startTime : 0,
+            };
+            native_window_set_frame_timeline_info(mNativeSurface->getNativeWindow(), ftl);
         }
     }
 
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index 5219b57..32ac5af 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -366,6 +366,12 @@
 
     ColorMode mColorMode = ColorMode::Default;
     float mTargetSdrHdrRatio = 1.f;
+
+    struct SkippedFrameInfo {
+        int64_t vsyncId;
+        int64_t startTime;
+    };
+    std::optional<SkippedFrameInfo> mSkippedFrameInfo;
 };
 
 } /* namespace renderthread */
diff --git a/libs/hwui/tests/unit/ShaderCacheTests.cpp b/libs/hwui/tests/unit/ShaderCacheTests.cpp
index 7bcd45c..9aa2e1d 100644
--- a/libs/hwui/tests/unit/ShaderCacheTests.cpp
+++ b/libs/hwui/tests/unit/ShaderCacheTests.cpp
@@ -49,7 +49,7 @@
      */
     static void reinitializeAllFields(ShaderCache& cache) {
         ShaderCache newCache = ShaderCache();
-        std::lock_guard<std::mutex> lock(cache.mMutex);
+        std::lock_guard lock(cache.mMutex), newLock(newCache.mMutex);
         // By order of declaration
         cache.mInitialized = newCache.mInitialized;
         cache.mBlobCache.reset(nullptr);
@@ -72,7 +72,7 @@
      * 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);
+        std::lock_guard lock(cache.mMutex);
         cache.mDeferredSaveDelayMs = saveDelayMs;
     }
 
@@ -81,7 +81,7 @@
      * Next call to "initShaderDiskCache" will load again the in-memory cache from disk.
      */
     static void terminate(ShaderCache& cache, bool saveContent) {
-        std::lock_guard<std::mutex> lock(cache.mMutex);
+        std::lock_guard lock(cache.mMutex);
         if (saveContent) {
             cache.saveToDiskLocked();
         }
@@ -93,6 +93,7 @@
      */
     template <typename T>
     static bool validateCache(ShaderCache& cache, std::vector<T> hash) {
+        std::lock_guard lock(cache.mMutex);
         return cache.validateCache(hash.data(), hash.size() * sizeof(T));
     }
 
@@ -108,7 +109,7 @@
      */
     static void waitForPendingSave(ShaderCache& cache, const int timeoutMs = 50) {
         {
-            std::lock_guard<std::mutex> lock(cache.mMutex);
+            std::lock_guard lock(cache.mMutex);
             ASSERT_TRUE(cache.mSavePending);
         }
         bool saving = true;
@@ -123,7 +124,7 @@
             usleep(delayMicroseconds);
             elapsedMilliseconds += (float)delayMicroseconds / 1000;
 
-            std::lock_guard<std::mutex> lock(cache.mMutex);
+            std::lock_guard lock(cache.mMutex);
             saving = cache.mSavePending;
         }
     }
diff --git a/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl b/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl
index 2231ce1..e46d34e 100644
--- a/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl
+++ b/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl
@@ -17,9 +17,22 @@
 package android.media.projection;
 
 import android.media.projection.MediaProjectionInfo;
+import android.view.ContentRecordingSession;
 
 /** {@hide} */
 oneway interface IMediaProjectionWatcherCallback {
     void onStart(in MediaProjectionInfo info);
     void onStop(in MediaProjectionInfo info);
+    /**
+     * Called when the {@link ContentRecordingSession} was set for the current media
+     * projection.
+     *
+     * @param info    always present and contains information about the media projection host.
+     * @param session the recording session for the current media projection. Can be
+     *                {@code null} when the recording will stop.
+     */
+    void onRecordingSessionSet(
+        in MediaProjectionInfo info,
+        in @nullable ContentRecordingSession session
+    );
 }
diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java
index 5703c42..5a68c53 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -29,6 +29,7 @@
 import android.os.ServiceManager;
 import android.util.ArrayMap;
 import android.util.Log;
+import android.view.ContentRecordingSession;
 import android.view.Surface;
 
 import java.util.Map;
@@ -300,7 +301,22 @@
     /** @hide */
     public static abstract class Callback {
         public abstract void onStart(MediaProjectionInfo info);
+
         public abstract void onStop(MediaProjectionInfo info);
+
+        /**
+         * Called when the {@link ContentRecordingSession} was set for the current media
+         * projection.
+         *
+         * @param info    always present and contains information about the media projection host.
+         * @param session the recording session for the current media projection. Can be
+         *                {@code null} when the recording will stop.
+         */
+        public void onRecordingSessionSet(
+                @NonNull MediaProjectionInfo info,
+                @Nullable ContentRecordingSession session
+        ) {
+        }
     }
 
     /** @hide */
@@ -335,5 +351,13 @@
                 }
             });
         }
+
+        @Override
+        public void onRecordingSessionSet(
+                @NonNull final MediaProjectionInfo info,
+                @Nullable final ContentRecordingSession session
+        ) {
+            mHandler.post(() -> mCallback.onRecordingSessionSet(info, session));
+        }
     }
 }
diff --git a/packages/CarrierDefaultApp/assets/slice_purchase_test.html b/packages/CarrierDefaultApp/assets/slice_purchase_test.html
index 67d2184..ad18a9d 100644
--- a/packages/CarrierDefaultApp/assets/slice_purchase_test.html
+++ b/packages/CarrierDefaultApp/assets/slice_purchase_test.html
@@ -20,7 +20,7 @@
     <meta charset="UTF-8">
     <meta name="description" content="
     This is a HTML page that calls and verifies responses from the @JavascriptInterface functions of
-    SlicePurchaseWebInterface. Test slice purchase application behavior using ADB shell commands and
+    DataBoostWebServiceFlow. Test slice purchase application behavior using ADB shell commands and
     the APIs below:
 
     FROM TERMINAL:
@@ -65,8 +65,8 @@
     <p id="requested_capability"></p>
 
     <h2>Notify purchase successful</h2>
-    <button type="button" onclick="testNotifyPurchaseSuccessful(60000)">
-        Notify purchase successful for 1 minute
+    <button type="button" onclick="testNotifyPurchaseSuccessful()">
+        Notify purchase successful
     </button>
     <p id="purchase_successful"></p>
 
@@ -75,5 +75,13 @@
         Notify purchase failed
     </button>
     <p id="purchase_failed"></p>
+
+    <h2>Dismiss flow</h2>
+    <button type="button" onclick="testDismissFlow()">
+        Dismiss flow
+    </button>
+    <p id="dismiss_flow"></p>
+
+    <h2>Test <a href="http://www.google.com">hyperlink</a></h2>
 </body>
 </html>
diff --git a/packages/CarrierDefaultApp/assets/slice_purchase_test.js b/packages/CarrierDefaultApp/assets/slice_purchase_test.js
index 02c4fea..be397a1 100644
--- a/packages/CarrierDefaultApp/assets/slice_purchase_test.js
+++ b/packages/CarrierDefaultApp/assets/slice_purchase_test.js
@@ -15,19 +15,25 @@
  */
 
 function testGetRequestedCapability() {
-    let capability = SlicePurchaseWebInterface.getRequestedCapability();
+    let capability = DataBoostWebServiceFlow.getRequestedCapability();
     document.getElementById("requested_capability").innerHTML =
             "Premium capability requested: " + capability;
 }
 
-function testNotifyPurchaseSuccessful(duration_ms_long = 0) {
-    SlicePurchaseWebInterface.notifyPurchaseSuccessful(duration_ms_long);
+function testNotifyPurchaseSuccessful() {
+    DataBoostWebServiceFlow.notifyPurchaseSuccessful();
     document.getElementById("purchase_successful").innerHTML =
-            "Notified purchase success for duration: " + duration_ms_long;
+            "Notified purchase successful.";
 }
 
 function testNotifyPurchaseFailed(failure_code = 0, failure_reason = "unknown") {
-    SlicePurchaseWebInterface.notifyPurchaseFailed(failure_code, failure_reason);
+    DataBoostWebServiceFlow.notifyPurchaseFailed(failure_code, failure_reason);
     document.getElementById("purchase_failed").innerHTML =
             "Notified purchase failed.";
 }
+
+function testDismissFlow() {
+    DataBoostWebServiceFlow.dismissFlow();
+    document.getElementById("dismiss_flow").innerHTML =
+            "Called dismiss flow.";
+}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/DataBoostWebServiceFlow.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/DataBoostWebServiceFlow.java
new file mode 100644
index 0000000..4500a22
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/DataBoostWebServiceFlow.java
@@ -0,0 +1,108 @@
+/*
+ * 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.carrierdefaultapp;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import android.webkit.JavascriptInterface;
+
+import com.android.phone.slice.SlicePurchaseController;
+
+/**
+ * Data boost web service flow interface allowing carrier websites to send responses back to the
+ * slice purchase application using JavaScript.
+ */
+public class DataBoostWebServiceFlow {
+    @NonNull SlicePurchaseActivity mActivity;
+
+    public DataBoostWebServiceFlow(@NonNull SlicePurchaseActivity activity) {
+        mActivity = activity;
+    }
+
+    /**
+     * Interface method allowing the carrier website to get the premium capability
+     * that was requested to purchase.
+     *
+     * This can be called using the JavaScript below:
+     * <script type="text/javascript">
+     *     function getRequestedCapability(duration) {
+     *         DataBoostWebServiceFlow.getRequestedCapability();
+     *     }
+     * </script>
+     */
+    @JavascriptInterface
+    @TelephonyManager.PremiumCapability public int getRequestedCapability() {
+        return mActivity.mCapability;
+    }
+
+    /**
+     * Interface method allowing the carrier website to notify the slice purchase application of
+     * a successful premium capability purchase and the duration for which the premium capability is
+     * purchased.
+     *
+     * This can be called using the JavaScript below:
+     * <script type="text/javascript">
+     *     function notifyPurchaseSuccessful() {
+     *         DataBoostWebServiceFlow.notifyPurchaseSuccessful();
+     *     }
+     * </script>
+     */
+    @JavascriptInterface
+    public void notifyPurchaseSuccessful() {
+        mActivity.onPurchaseSuccessful();
+    }
+
+    /**
+     * Interface method allowing the carrier website to notify the slice purchase application of
+     * a failed premium capability purchase.
+     *
+     * This can be called using the JavaScript below:
+     * <script type="text/javascript">
+     *     function notifyPurchaseFailed(failure_code = 0, failure_reason = "unknown") {
+     *         DataBoostWebServiceFlow.notifyPurchaseFailed();
+     *     }
+     * </script>
+     *
+     * @param failureCode The failure code.
+     * @param failureReason If the failure code is
+     *                      {@link SlicePurchaseController#FAILURE_CODE_UNKNOWN},
+     *                      the human-readable reason for failure.
+     */
+    @JavascriptInterface
+    public void notifyPurchaseFailed(@SlicePurchaseController.FailureCode int failureCode,
+            @Nullable String failureReason) {
+        mActivity.onPurchaseFailed(failureCode, failureReason);
+    }
+
+    /**
+     * Interface method allowing the carrier website to notify the slice purchase application that
+     * the service flow ended prematurely. This can be due to user action, an error in the
+     * web sheet logic, or an error on the network side.
+     *
+     * This can be called using the JavaScript below:
+     * <script type="text/javascript">
+     *     function dismissFlow() {
+     *         DataBoostWebServiceFlow.dismissFlow();
+     *     }
+     * </script>
+     */
+    @JavascriptInterface
+    public void dismissFlow() {
+        mActivity.onDismissFlow();
+    }
+}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
index 946185a..b100980 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
@@ -27,26 +27,26 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.KeyEvent;
+import android.webkit.CookieManager;
 import android.webkit.WebView;
+import android.webkit.WebViewClient;
 
 import com.android.phone.slice.SlicePurchaseController;
 
 import java.net.URL;
-import java.util.concurrent.TimeUnit;
 
 /**
  * Activity that launches when the user clicks on the performance boost notification.
  * This will open a {@link WebView} for the carrier website to allow the user to complete the
  * premium capability purchase.
  * The carrier website can get the requested premium capability using the JavaScript interface
- * method {@code SlicePurchaseWebInterface.getRequestedCapability()}.
+ * method {@code DataBoostWebServiceFlow.getRequestedCapability()}.
  * If the purchase is successful, the carrier website shall notify the slice purchase application
  * using the JavaScript interface method
- * {@code SlicePurchaseWebInterface.notifyPurchaseSuccessful(duration)}, where {@code duration} is
- * the optional duration of the performance boost.
+ * {@code DataBoostWebServiceFlow.notifyPurchaseSuccessful()}.
  * If the purchase was not successful, the carrier website shall notify the slice purchase
  * application using the JavaScript interface method
- * {@code SlicePurchaseWebInterface.notifyPurchaseFailed(code, reason)}, where {@code code} is the
+ * {@code DataBoostWebServiceFlow.notifyPurchaseFailed(code, reason)}, where {@code code} is the
  * {@link SlicePurchaseController.FailureCode} indicating the reason for failure and {@code reason}
  * is the human-readable reason for failure if the failure code is
  * {@link SlicePurchaseController#FAILURE_CODE_UNKNOWN}.
@@ -114,19 +114,17 @@
             return;
         }
 
-        // Create and configure WebView
-        setupWebView();
+        // Clear any cookies that might be persisted from previous sessions before loading WebView
+        CookieManager.getInstance().removeAllCookies(value -> {
+            setupWebView();
+        });
     }
 
-    protected void onPurchaseSuccessful(long duration) {
+    protected void onPurchaseSuccessful() {
         logd("onPurchaseSuccessful: Carrier website indicated successfully purchased premium "
-                + "capability " + TelephonyManager.convertPremiumCapabilityToString(mCapability)
-                + (duration > 0 ? " for " + TimeUnit.MILLISECONDS.toMinutes(duration) + " minutes."
-                : "."));
-        Intent intent = new Intent();
-        intent.putExtra(SlicePurchaseController.EXTRA_PURCHASE_DURATION, duration);
-        SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(mApplicationContext,
-                mIntent, SlicePurchaseController.EXTRA_INTENT_SUCCESS, intent);
+                + "capability " + TelephonyManager.convertPremiumCapabilityToString(mCapability));
+        SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
+                mIntent, SlicePurchaseController.EXTRA_INTENT_SUCCESS);
         finishAndRemoveTask();
     }
 
@@ -143,6 +141,14 @@
         finishAndRemoveTask();
     }
 
+    protected void onDismissFlow() {
+        logd("onDismissFlow: Dismiss flow called while purchasing premium capability "
+                + TelephonyManager.convertPremiumCapabilityToString(mCapability));
+        SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
+                mIntent, SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED);
+        finishAndRemoveTask();
+    }
+
     @Override
     public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
         // Pressing back in the WebView will go to the previous page instead of closing
@@ -173,12 +179,13 @@
     private void setupWebView() {
         // Create WebView
         mWebView = new WebView(this);
+        mWebView.setWebViewClient(new WebViewClient());
 
         // Enable JavaScript for the carrier purchase website to send results back to
         //  the slice purchase application.
         mWebView.getSettings().setJavaScriptEnabled(true);
         mWebView.addJavascriptInterface(
-                new SlicePurchaseWebInterface(this), "SlicePurchaseWebInterface");
+                new DataBoostWebServiceFlow(this), "DataBoostWebServiceFlow");
 
         // Display WebView
         setContentView(mWebView);
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseWebInterface.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseWebInterface.java
deleted file mode 100644
index 8547898..0000000
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseWebInterface.java
+++ /dev/null
@@ -1,93 +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.carrierdefaultapp;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.telephony.TelephonyManager;
-import android.webkit.JavascriptInterface;
-
-import com.android.phone.slice.SlicePurchaseController;
-
-/**
- * Slice purchase web interface class allowing carrier websites to send responses back to the
- * slice purchase application using JavaScript.
- */
-public class SlicePurchaseWebInterface {
-    @NonNull SlicePurchaseActivity mActivity;
-
-    public SlicePurchaseWebInterface(@NonNull SlicePurchaseActivity activity) {
-        mActivity = activity;
-    }
-
-    /**
-     * Interface method allowing the carrier website to get the premium capability
-     * that was requested to purchase.
-     *
-     * This can be called using the JavaScript below:
-     * <script type="text/javascript">
-     *     function getRequestedCapability(duration) {
-     *         SlicePurchaseWebInterface.getRequestedCapability();
-     *     }
-     * </script>
-     */
-    @JavascriptInterface
-    @TelephonyManager.PremiumCapability public int getRequestedCapability() {
-        return mActivity.mCapability;
-    }
-
-    /**
-     * Interface method allowing the carrier website to notify the slice purchase application of
-     * a successful premium capability purchase and the duration for which the premium capability is
-     * purchased.
-     *
-     * This can be called using the JavaScript below:
-     * <script type="text/javascript">
-     *     function notifyPurchaseSuccessful(duration_ms_long = 0) {
-     *         SlicePurchaseWebInterface.notifyPurchaseSuccessful(duration_ms_long);
-     *     }
-     * </script>
-     *
-     * @param duration The duration for which the premium capability is purchased in milliseconds.
-     */
-    @JavascriptInterface
-    public void notifyPurchaseSuccessful(long duration) {
-        mActivity.onPurchaseSuccessful(duration);
-    }
-
-    /**
-     * Interface method allowing the carrier website to notify the slice purchase application of
-     * a failed premium capability purchase.
-     *
-     * This can be called using the JavaScript below:
-     * <script type="text/javascript">
-     *     function notifyPurchaseFailed(failure_code = 0, failure_reason = "unknown") {
-     *         SlicePurchaseWebInterface.notifyPurchaseFailed();
-     *     }
-     * </script>
-     *
-     * @param failureCode The failure code.
-     * @param failureReason If the failure code is
-     *                      {@link SlicePurchaseController#FAILURE_CODE_UNKNOWN},
-     *                      the human-readable reason for failure.
-     */
-    @JavascriptInterface
-    public void notifyPurchaseFailed(@SlicePurchaseController.FailureCode int failureCode,
-            @Nullable String failureReason) {
-        mActivity.onPurchaseFailed(failureCode, failureReason);
-    }
-}
diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java
index daf0b42..cc103fa 100644
--- a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java
+++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java
@@ -53,7 +53,9 @@
     private static final int PHONE_ID = 0;
 
     @Mock PendingIntent mPendingIntent;
+    @Mock PendingIntent mSuccessfulIntent;
     @Mock PendingIntent mCanceledIntent;
+    @Mock PendingIntent mRequestFailedIntent;
     @Mock CarrierConfigManager mCarrierConfigManager;
     @Mock NotificationManager mNotificationManager;
     @Mock PersistableBundle mPersistableBundle;
@@ -107,25 +109,28 @@
         doReturn(true).when(mCanceledIntent).isBroadcast();
         doReturn(mCanceledIntent).when(spiedIntent).getParcelableExtra(
                 eq(SlicePurchaseController.EXTRA_INTENT_CANCELED), eq(PendingIntent.class));
+        doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mSuccessfulIntent).getCreatorPackage();
+        doReturn(true).when(mSuccessfulIntent).isBroadcast();
+        doReturn(mSuccessfulIntent).when(spiedIntent).getParcelableExtra(
+                eq(SlicePurchaseController.EXTRA_INTENT_SUCCESS), eq(PendingIntent.class));
+        doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mRequestFailedIntent)
+                .getCreatorPackage();
+        doReturn(true).when(mRequestFailedIntent).isBroadcast();
+        doReturn(mRequestFailedIntent).when(spiedIntent).getParcelableExtra(
+                eq(SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED), eq(PendingIntent.class));
 
         mSlicePurchaseActivity = startActivity(spiedIntent, null, null);
     }
 
     @Test
     public void testOnPurchaseSuccessful() throws Exception {
-        int duration = 5 * 60 * 1000; // 5 minutes
-        int invalidDuration = -1;
-        mSlicePurchaseActivity.onPurchaseSuccessful(duration);
-        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
-        verify(mPendingIntent).send(eq(mContext), eq(0), intentCaptor.capture());
-        Intent intent = intentCaptor.getValue();
-        assertEquals(duration, intent.getLongExtra(
-                SlicePurchaseController.EXTRA_PURCHASE_DURATION, invalidDuration));
+        mSlicePurchaseActivity.onPurchaseSuccessful();
+        verify(mSuccessfulIntent).send();
     }
 
     @Test
     public void testOnPurchaseFailed() throws Exception {
-        int failureCode = SlicePurchaseController.FAILURE_CODE_SERVER_UNREACHABLE;
+        int failureCode = SlicePurchaseController.FAILURE_CODE_CARRIER_URL_UNAVAILABLE;
         String failureReason = "Server unreachable";
         mSlicePurchaseActivity.onPurchaseFailed(failureCode, failureReason);
         ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -142,4 +147,10 @@
         mSlicePurchaseActivity.onDestroy();
         verify(mCanceledIntent).send();
     }
+
+    @Test
+    public void testOnDismissFlow() throws Exception {
+        mSlicePurchaseActivity.onDismissFlow();
+        verify(mRequestFailedIntent).send();
+    }
 }
diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
index aae30df..a0b3469 100644
--- a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
+++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
@@ -144,7 +144,7 @@
                         android:visibility="gone"
                         android:duplicateParentState="true"
                         android:clickable="false"
-                        android:text="@string/consent_no" />
+                        android:text="@string/consent_cancel" />
 
                 </LinearLayout>
 
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index 2502bbf..7a6fad4 100644
--- a/packages/CompanionDeviceManager/res/values/strings.xml
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -20,7 +20,7 @@
     <string name="app_label">Companion Device Manager</string>
 
     <!-- Title of the device association confirmation dialog. -->
-    <string name="confirmation_title">Allow &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt; to access &lt;strong&gt;<xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g>&lt;/strong&gt;?</string>
+    <string name="confirmation_title">Allow the app &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt; to access &lt;strong&gt;<xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g>&lt;/strong&gt;?</string>
 
     <!-- ================= DEVICE_PROFILE_WATCH and null profile ================= -->
 
@@ -28,13 +28,13 @@
     <string name="profile_name_watch">watch</string>
 
     <!-- Title of the device selection dialog. -->
-    <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%2$s</xliff:g>&lt;/strong&gt;</string>
+    <string name="chooser_title_non_profile">Choose a device to be managed by &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt;</string>
 
-    <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] -->
-    <string name="summary_watch">This app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to sync info, like the name of someone calling, interact with your notifications and access your Phone, SMS, Contacts, Calendar, Call logs and Nearby devices permissions.</string>
+    <!-- Tile of the multiple devices' dialog. -->
+    <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to set up</string>
 
-    <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile for singleDevice(type) [CHAR LIMIT=NONE] -->
-    <string name="summary_watch_single_device">This app will be allowed to sync info, like the name of someone calling, and access these permissions on your <xliff:g id="device_type" example="phone">%1$s</xliff:g></string>
+    <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile [CHAR LIMIT=NONE] -->
+    <string name="summary_watch">This app will be allowed to sync info, like the name of someone calling, and access these permissions on your <xliff:g id="device_name" example="phone">%1$s</xliff:g></string>
 
     <!-- ================= DEVICE_PROFILE_GLASSES ================= -->
 
@@ -42,13 +42,10 @@
     <string name="confirmation_title_glasses">Allow &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt; to manage &lt;strong&gt;<xliff:g id="device_name" example="Glasses">%2$s</xliff:g>&lt;/strong&gt;?</string>
 
     <!-- The name of the "glasses" device type [CHAR LIMIT=30] -->
-    <string name="profile_name_glasses">glasses</string>
+    <string name="profile_name_glasses">device</string>
 
-    <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile (type) [CHAR LIMIT=NONE] -->
-    <string name="summary_glasses_multi_device">This app is needed to manage <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Microphone and Nearby devices permissions.</string>
-
-    <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile for singleDevice(type) [CHAR LIMIT=NONE] -->
-    <string name="summary_glasses_single_device">This app will be allowed to access these permissions on your <xliff:g id="device_type" example="phone">%1$s</xliff:g></string>
+    <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile [CHAR LIMIT=NONE] -->
+    <string name="summary_glasses">This app will be allowed to access these permissions on your <xliff:g id="device_name" example="phone">%1$s</xliff:g></string>
 
     <!-- ================= DEVICE_PROFILE_APP_STREAMING ================= -->
 
@@ -97,9 +94,6 @@
     <string name="profile_name_generic">device</string>
 
     <!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] -->
-    <string name="summary_generic_single_device">This app will be able to sync info, like the name of someone calling, between your phone and <xliff:g id="device_name" example="My Watch">%1$s</xliff:g></string>
-
-    <!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] -->
     <string name="summary_generic">This app will be able to sync info, like the name of someone calling, between your phone and the chosen device</string>
 
     <!-- ================= Buttons ================= -->
@@ -110,6 +104,9 @@
     <!-- Negative button for the device-app association consent dialog [CHAR LIMIT=30] -->
     <string name="consent_no">Don\u2019t allow</string>
 
+    <!-- Cancel button for the device chooser dialog [CHAR LIMIT=30] -->
+    <string name="consent_cancel">Cancel</string>
+
     <!-- Back button for the helper consent dialog [CHAR LIMIT=30] -->
     <string name="consent_back">Back</string>
 
diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml
index e85190b..222877b 100644
--- a/packages/CompanionDeviceManager/res/values/styles.xml
+++ b/packages/CompanionDeviceManager/res/values/styles.xml
@@ -69,11 +69,13 @@
 
     <style name="PositiveButton"
            parent="@android:style/Widget.Material.Button.Borderless.Colored">
-        <item name="android:layout_width">300dp</item>
-        <item name="android:layout_height">56dp</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
         <item name="android:layout_marginBottom">2dp</item>
         <item name="android:textAllCaps">false</item>
         <item name="android:textSize">14sp</item>
+        <item name="android:layout_marginStart">32dp</item>
+        <item name="android:layout_marginEnd">32dp</item>
         <item name="android:textColor">@android:color/system_neutral1_900</item>
         <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item>
         <item name="android:background">@drawable/btn_positive_bottom</item>
@@ -81,11 +83,13 @@
 
     <style name="NegativeButton"
            parent="@android:style/Widget.Material.Button.Borderless.Colored">
-        <item name="android:layout_width">300dp</item>
-        <item name="android:layout_height">56dp</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
         <item name="android:layout_marginTop">2dp</item>
         <item name="android:textAllCaps">false</item>
         <item name="android:textSize">14sp</item>
+        <item name="android:layout_marginStart">32dp</item>
+        <item name="android:layout_marginEnd">32dp</item>
         <item name="android:textColor">@android:color/system_neutral1_900</item>
         <item name="android:layout_marginTop">4dp</item>
         <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item>
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index 4154029..97016f5 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -27,10 +27,8 @@
 
 import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState;
 import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState.FINISHED_TIMEOUT;
-import static com.android.companiondevicemanager.CompanionDeviceResources.MULTI_DEVICES_SUMMARIES;
 import static com.android.companiondevicemanager.CompanionDeviceResources.PERMISSION_TYPES;
 import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILES_NAME;
-import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILES_NAME_MULTI;
 import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILE_ICON;
 import static com.android.companiondevicemanager.CompanionDeviceResources.SUMMARIES;
 import static com.android.companiondevicemanager.CompanionDeviceResources.SUPPORTED_PROFILES;
@@ -121,6 +119,9 @@
     private IAssociationRequestCallback mAppCallback;
     private ResultReceiver mCdmServiceReceiver;
 
+    // Present for application's name.
+    private CharSequence mAppLabel;
+
     // Always present widgets.
     private TextView mTitle;
     private TextView mSummary;
@@ -165,8 +166,7 @@
     private @Nullable RecyclerView mDeviceListRecyclerView;
     private @Nullable DeviceListAdapter mDeviceAdapter;
 
-
-    // The recycler view is only shown for selfManaged and singleDevice  association request.
+    // The recycler view is shown for non-null profile association request.
     private @Nullable RecyclerView mPermissionListRecyclerView;
     private @Nullable PermissionListAdapter mPermissionListAdapter;
 
@@ -178,8 +178,6 @@
     // onActivityResult() after the association is created.
     private @Nullable DeviceFilterPair<?> mSelectedDevice;
 
-    private @Nullable List<Integer> mPermissionTypes;
-
     private LinearLayoutManager mPermissionsLayoutManager = new LinearLayoutManager(this);
 
     @Override
@@ -302,6 +300,8 @@
 
         setContentView(R.layout.activity_confirmation);
 
+        mAppLabel = appLabel;
+
         mConstraintList = findViewById(R.id.constraint_list);
         mAssociationConfirmationDialog = findViewById(R.id.association_confirmation);
         mVendorHeader = findViewById(R.id.vendor_header);
@@ -322,7 +322,6 @@
 
         mMultipleDeviceSpinner = findViewById(R.id.spinner_multiple_device);
         mSingleDeviceSpinner = findViewById(R.id.spinner_single_device);
-        mDeviceAdapter = new DeviceListAdapter(this, this::onListItemClick);
 
         mPermissionListRecyclerView = findViewById(R.id.permission_list);
         mPermissionListAdapter = new PermissionListAdapter(this);
@@ -468,8 +467,6 @@
             throw new RuntimeException("Unsupported profile " + deviceProfile);
         }
 
-        mPermissionTypes = new ArrayList<>();
-
         try {
             vendorIcon = getVendorHeaderIcon(this, packageName, userId);
             vendorName = getVendorHeaderName(this, packageName, userId);
@@ -486,17 +483,13 @@
         }
 
         title = getHtmlFromResources(this, TITLES.get(deviceProfile), deviceName);
-        mPermissionTypes.addAll(PERMISSION_TYPES.get(deviceProfile));
+        setupPermissionList(deviceProfile);
 
         // Summary is not needed for selfManaged dialog.
         mSummary.setVisibility(View.GONE);
-
-        setupPermissionList();
-
         mTitle.setText(title);
         mVendorHeaderName.setText(vendorName);
         mVendorHeader.setVisibility(View.VISIBLE);
-        mVendorHeader.setVisibility(View.VISIBLE);
         mProfileIcon.setVisibility(View.GONE);
         mDeviceListRecyclerView.setVisibility(View.GONE);
         // Top and bottom borders should be gone for selfManaged dialog.
@@ -509,7 +502,9 @@
 
         final String deviceProfile = mRequest.getDeviceProfile();
 
-        mPermissionTypes = new ArrayList<>();
+        if (!SUPPORTED_PROFILES.contains(deviceProfile)) {
+            throw new RuntimeException("Unsupported profile " + deviceProfile);
+        }
 
         CompanionDeviceDiscoveryService.getScanResult().observe(this,
                 deviceFilterPairs -> updateSingleDeviceUi(
@@ -529,75 +524,40 @@
         if (deviceFilterPairs.isEmpty()) return;
 
         mSelectedDevice = requireNonNull(deviceFilterPairs.get(0));
-        // No need to show user consent dialog if it is a singleDevice
-        // and isSkipPrompt(true) AssociationRequest.
-        // See AssociationRequestsProcessor#mayAssociateWithoutPrompt.
-        if (mRequest.isSkipPrompt()) {
-            mSingleDeviceSpinner.setVisibility(View.GONE);
-            onUserSelectedDevice(mSelectedDevice);
-            return;
-        }
 
-        final String deviceName = mSelectedDevice.getDisplayName();
-        final Spanned title;
-        final Spanned summary;
-        final Drawable profileIcon;
+        final Drawable profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile));
 
-        if (!SUPPORTED_PROFILES.contains(deviceProfile)) {
-            throw new RuntimeException("Unsupported profile " + deviceProfile);
-        }
+        updatePermissionUi();
 
-        if (deviceProfile == null) {
-            summary = getHtmlFromResources(this, SUMMARIES.get(null), deviceName);
-            mConstraintList.setVisibility(View.GONE);
-        } else {
-            summary = getHtmlFromResources(
-                    this, SUMMARIES.get(deviceProfile), getString(R.string.device_type));
-            mPermissionTypes.addAll(PERMISSION_TYPES.get(deviceProfile));
-            setupPermissionList();
-        }
-
-        title = getHtmlFromResources(this, TITLES.get(deviceProfile), appLabel, deviceName);
-        profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile));
-
-        mTitle.setText(title);
-        mSummary.setText(summary);
         mProfileIcon.setImageDrawable(profileIcon);
-        mSingleDeviceSpinner.setVisibility(View.GONE);
         mAssociationConfirmationDialog.setVisibility(View.VISIBLE);
+        mSingleDeviceSpinner.setVisibility(View.GONE);
     }
 
     private void initUiForMultipleDevices(CharSequence appLabel) {
         if (DEBUG) Log.i(TAG, "initUiFor_MultipleDevices()");
 
-        final String deviceProfile = mRequest.getDeviceProfile();
-
-        final String profileName;
-        final String profileNameMulti;
-        final Spanned summary;
         final Drawable profileIcon;
-        final int summaryResourceId;
+        final Spanned title;
+        final String deviceProfile = mRequest.getDeviceProfile();
 
         if (!SUPPORTED_PROFILES.contains(deviceProfile)) {
             throw new RuntimeException("Unsupported profile " + deviceProfile);
         }
 
-        profileName = getString(PROFILES_NAME.get(deviceProfile));
-        profileNameMulti = getString(PROFILES_NAME_MULTI.get(deviceProfile));
         profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile));
-        summaryResourceId = MULTI_DEVICES_SUMMARIES.get(deviceProfile);
 
         if (deviceProfile == null) {
-            summary = getHtmlFromResources(this, summaryResourceId);
+            title = getHtmlFromResources(this, R.string.chooser_title_non_profile, appLabel);
+            mButtonNotAllowMultipleDevices.setText(R.string.consent_no);
         } else {
-            summary = getHtmlFromResources(this, summaryResourceId, profileName, appLabel);
+            title = getHtmlFromResources(this,
+                    R.string.chooser_title, getString(PROFILES_NAME.get(deviceProfile)));
         }
 
-        final Spanned title = getHtmlFromResources(
-                this, R.string.chooser_title, profileNameMulti, appLabel);
+        mDeviceAdapter = new DeviceListAdapter(this, this::onDeviceClicked);
 
         mTitle.setText(title);
-        mSummary.setText(summary);
         mProfileIcon.setImageDrawable(profileIcon);
 
         mDeviceListRecyclerView.setAdapter(mDeviceAdapter);
@@ -613,6 +573,7 @@
                     mDeviceAdapter.setDevices(deviceFilterPairs);
                 });
 
+        mSummary.setVisibility(View.GONE);
         // "Remove" consent button: users would need to click on the list item.
         mButtonAllow.setVisibility(View.GONE);
         mButtonNotAllow.setVisibility(View.GONE);
@@ -623,11 +584,9 @@
         mMultipleDeviceSpinner.setVisibility(View.VISIBLE);
     }
 
-    private void onListItemClick(int position) {
-        if (DEBUG) Log.d(TAG, "onListItemClick() " + position);
-
+    private void onDeviceClicked(int position) {
         final DeviceFilterPair<?> selectedDevice = mDeviceAdapter.getItem(position);
-
+        // To prevent double tap on the selected device.
         if (mSelectedDevice != null) {
             if (DEBUG) Log.w(TAG, "Already selected.");
             return;
@@ -637,7 +596,47 @@
 
         mSelectedDevice = requireNonNull(selectedDevice);
 
-        onUserSelectedDevice(selectedDevice);
+        Log.d(TAG, "onDeviceClicked(): " + mSelectedDevice.toShortString());
+
+        updatePermissionUi();
+
+        mSummary.setVisibility(View.VISIBLE);
+        mButtonAllow.setVisibility(View.VISIBLE);
+        mButtonNotAllow.setVisibility(View.VISIBLE);
+        mDeviceListRecyclerView.setVisibility(View.GONE);
+        mNotAllowMultipleDevicesLayout.setVisibility(View.GONE);
+    }
+
+    private void updatePermissionUi() {
+        final String deviceProfile = mRequest.getDeviceProfile();
+        final int summaryResourceId = SUMMARIES.get(deviceProfile);
+        final String remoteDeviceName = mSelectedDevice.getDisplayName();
+        final Spanned title = getHtmlFromResources(
+                this, TITLES.get(deviceProfile), mAppLabel, remoteDeviceName);
+        final Spanned summary;
+
+        // No need to show permission consent dialog if it is a isSkipPrompt(true)
+        // AssociationRequest. See AssociationRequestsProcessor#mayAssociateWithoutPrompt.
+        if (mRequest.isSkipPrompt()) {
+            mSingleDeviceSpinner.setVisibility(View.GONE);
+            onUserSelectedDevice(mSelectedDevice);
+            return;
+        }
+
+        if (deviceProfile == null && mRequest.isSingleDevice()) {
+            summary = getHtmlFromResources(this, summaryResourceId, remoteDeviceName);
+            mConstraintList.setVisibility(View.GONE);
+        } else if (deviceProfile == null) {
+            onUserSelectedDevice(mSelectedDevice);
+            return;
+        } else {
+            summary = getHtmlFromResources(
+                    this, summaryResourceId, getString(R.string.device_type));
+            setupPermissionList(deviceProfile);
+        }
+
+        mTitle.setText(title);
+        mSummary.setText(summary);
     }
 
     private void onPositiveButtonClick(View v) {
@@ -680,8 +679,9 @@
     // initiate the layoutManager for the recyclerview, add listeners for monitoring the scrolling
     // and when mPermissionListRecyclerView is fully populated.
     // Lastly, disable the Allow and Don't allow buttons.
-    private void setupPermissionList() {
-        mPermissionListAdapter.setPermissionType(mPermissionTypes);
+    private void setupPermissionList(String deviceProfile) {
+        final List<Integer> permissionTypes = new ArrayList<>(PERMISSION_TYPES.get(deviceProfile));
+        mPermissionListAdapter.setPermissionType(permissionTypes);
         mPermissionListRecyclerView.setAdapter(mPermissionListAdapter);
         mPermissionListRecyclerView.setLayoutManager(mPermissionsLayoutManager);
 
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
index 7aed139..060c032 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
@@ -86,21 +86,11 @@
     static final Map<String, Integer> SUMMARIES;
     static {
         final Map<String, Integer> map = new ArrayMap<>();
-        map.put(DEVICE_PROFILE_WATCH, R.string.summary_watch_single_device);
-        map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses_single_device);
-        map.put(null, R.string.summary_generic_single_device);
-
-        SUMMARIES = unmodifiableMap(map);
-    }
-
-    static final Map<String, Integer> MULTI_DEVICES_SUMMARIES;
-    static {
-        final Map<String, Integer> map = new ArrayMap<>();
         map.put(DEVICE_PROFILE_WATCH, R.string.summary_watch);
-        map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses_multi_device);
+        map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses);
         map.put(null, R.string.summary_generic);
 
-        MULTI_DEVICES_SUMMARIES = unmodifiableMap(map);
+        SUMMARIES = unmodifiableMap(map);
     }
 
     static final Map<String, Integer> PROFILES_NAME;
diff --git a/packages/EasterEgg/res/values/q_puzzles.xml b/packages/EasterEgg/res/values/q_puzzles.xml
index 7c2eff1..2a477f5 100644
--- a/packages/EasterEgg/res/values/q_puzzles.xml
+++ b/packages/EasterEgg/res/values/q_puzzles.xml
@@ -44,7 +44,6 @@
 
         <item>android:drawable/ic_audio_alarm</item>
         <item>android:drawable/ic_audio_alarm_mute</item>
-        <item>android:drawable/ic_bluetooth_share_icon</item>
         <item>android:drawable/ic_bt_headphones_a2dp</item>
         <item>android:drawable/ic_bt_headset_hfp</item>
         <item>android:drawable/ic_bt_hearing_aid</item>
diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml
index a2118fa..c52fde6 100644
--- a/packages/PackageInstaller/res/values/strings.xml
+++ b/packages/PackageInstaller/res/values/strings.xml
@@ -38,7 +38,11 @@
     <!-- Message for updating an existing app [CHAR LIMIT=NONE] -->
     <string name="install_confirm_question_update">Do you want to update this app?</string>
     <!-- Message for updating an existing app with update owner reminder [CHAR LIMIT=NONE] -->
-    <string name="install_confirm_question_update_owner_reminder">Update this app from <xliff:g id="new_update_owner">%1$s</xliff:g>?\n\nThis app normally receives updates from <xliff:g id="existing_update_owner">%2$s</xliff:g>. By updating from a different source, you may receive future updates from any source on your phone. App functionality may change.</string>
+    <string name="install_confirm_question_update_owner_reminder" product="tablet">Update this app from <xliff:g id="new_update_owner">%1$s</xliff:g>?\n\nThis app normally receives updates from <xliff:g id="existing_update_owner">%2$s</xliff:g>. By updating from a different source, you may receive future updates from any source on your tablet. App functionality may change.</string>
+    <!-- Message for updating an existing app with update owner reminder [CHAR LIMIT=NONE] -->
+    <string name="install_confirm_question_update_owner_reminder" product="tv">Update this app from <xliff:g id="new_update_owner">%1$s</xliff:g>?\n\nThis app normally receives updates from <xliff:g id="existing_update_owner">%2$s</xliff:g>. By updating from a different source, you may receive future updates from any source on your TV. App functionality may change.</string>
+    <!-- Message for updating an existing app with update owner reminder [CHAR LIMIT=NONE] -->
+    <string name="install_confirm_question_update_owner_reminder" product="default">Update this app from <xliff:g id="new_update_owner">%1$s</xliff:g>?\n\nThis app normally receives updates from <xliff:g id="existing_update_owner">%2$s</xliff:g>. By updating from a different source, you may receive future updates from any source on your phone. App functionality may change.</string>
     <!-- [CHAR LIMIT=100] -->
     <string name="install_failed">App not installed.</string>
     <!-- Reason displayed when installation fails because the package was blocked
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java b/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java
index 7b17cbd..19d74b3 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java
@@ -16,6 +16,8 @@
 
 package com.android.packageinstaller;
 
+import static android.content.Intent.CATEGORY_LAUNCHER;
+
 import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_STAGED_SESSION_ID;
 
 import android.app.Activity;
@@ -45,6 +47,9 @@
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         setResult(resultCode, data);
         finish();
+        if (data != null && data.hasCategory(CATEGORY_LAUNCHER)) {
+            startActivity(data);
+        }
     }
 
     @Override
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
index 73c03a5..ff991d2 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
@@ -17,7 +17,6 @@
 package com.android.packageinstaller;
 
 import android.app.Activity;
-import android.content.ActivityNotFoundException;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
@@ -123,11 +122,7 @@
         Button launchButton = mAlert.getButton(DialogInterface.BUTTON_POSITIVE);
         if (enabled) {
             launchButton.setOnClickListener(view -> {
-                try {
-                    startActivity(mLaunchIntent);
-                } catch (ActivityNotFoundException | SecurityException e) {
-                    Log.e(LOG_TAG, "Could not start activity", e);
-                }
+                setResult(Activity.RESULT_OK, mLaunchIntent);
                 finish();
             });
         } else {
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index d154156..e071c11 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -16,8 +16,9 @@
 */
 package com.android.packageinstaller;
 
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
 import static android.content.Intent.FLAG_ACTIVITY_NO_HISTORY;
-import static android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT;
+import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
 
 import android.Manifest;
@@ -789,7 +790,8 @@
             }
             new Handler(Looper.getMainLooper()).postDelayed(() -> {
                 if (!isDestroyed()) {
-                    startActivity(getIntent().addFlags(FLAG_ACTIVITY_REORDER_TO_FRONT));
+                    startActivity(getIntent().addFlags(
+                            FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP));
                 }
             }, 500);
 
diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle
deleted file mode 100644
index e68ef85..0000000
--- a/packages/SettingsLib/Spa/build.gradle
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
-buildscript {
-    ext {
-        BUILD_TOOLS_VERSION = "30.0.3"
-        MIN_SDK = 21
-        TARGET_SDK = 33
-        jetpack_compose_version = '1.4.0-beta01'
-        jetpack_compose_compiler_version = '1.4.4'
-    }
-}
-plugins {
-    id 'com.android.application' version '8.0.0' apply false
-    id 'com.android.library' version '8.0.0' apply false
-    id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
-}
-subprojects {
-    tasks.withType(KotlinCompile).configureEach {
-        kotlinOptions {
-            jvmTarget = "17"
-            freeCompilerArgs = ["-Xjvm-default=all"]
-        }
-    }
-}
diff --git a/packages/SettingsLib/Spa/build.gradle.kts b/packages/SettingsLib/Spa/build.gradle.kts
new file mode 100644
index 0000000..e76139f
--- /dev/null
+++ b/packages/SettingsLib/Spa/build.gradle.kts
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import com.android.build.gradle.BaseExtension
+import com.android.build.gradle.api.AndroidBasePlugin
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+    alias(libs.plugins.android.application) apply false
+    alias(libs.plugins.android.library) apply false
+    alias(libs.plugins.kotlin.android) apply false
+}
+
+allprojects {
+    extra["jetpackComposeVersion"] = "1.6.0-alpha01"
+}
+
+subprojects {
+    plugins.withType<AndroidBasePlugin> {
+        configure<BaseExtension> {
+            compileSdkVersion(34)
+
+            defaultConfig {
+                minSdk = 21
+                targetSdk = 34
+            }
+
+            compileOptions {
+                sourceCompatibility = JavaVersion.VERSION_17
+                targetCompatibility = JavaVersion.VERSION_17
+            }
+        }
+    }
+
+    afterEvaluate {
+        plugins.withType<AndroidBasePlugin> {
+            configure<BaseExtension> {
+                if (buildFeatures.compose == true) {
+                    composeOptions {
+                        kotlinCompilerExtensionVersion = "1.4.4"
+                    }
+                }
+            }
+        }
+    }
+
+    tasks.withType<KotlinCompile> {
+        kotlinOptions {
+            jvmTarget = "17"
+            freeCompilerArgs = listOf("-Xjvm-default=all")
+        }
+    }
+}
diff --git a/packages/SettingsLib/Spa/gallery/Android.bp b/packages/SettingsLib/Spa/gallery/Android.bp
index bc083c9..e59e982 100644
--- a/packages/SettingsLib/Spa/gallery/Android.bp
+++ b/packages/SettingsLib/Spa/gallery/Android.bp
@@ -28,6 +28,6 @@
         "androidx.compose.runtime_runtime",
     ],
     kotlincflags: ["-Xjvm-default=all"],
-    platform_apis: true,
+    sdk_version: "current",
     min_sdk_version: "31",
 }
diff --git a/packages/SettingsLib/Spa/gallery/build.gradle b/packages/SettingsLib/Spa/gallery/build.gradle
deleted file mode 100644
index 212aa7b..0000000
--- a/packages/SettingsLib/Spa/gallery/build.gradle
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-plugins {
-    id 'com.android.application'
-    id 'kotlin-android'
-}
-
-android {
-    namespace 'com.android.settingslib.spa.gallery'
-    compileSdk TARGET_SDK
-    buildToolsVersion = BUILD_TOOLS_VERSION
-
-    defaultConfig {
-        applicationId "com.android.settingslib.spa.gallery"
-        minSdk MIN_SDK
-        targetSdk TARGET_SDK
-        versionCode 1
-        versionName "1.0"
-    }
-
-    sourceSets {
-        main {
-            kotlin {
-                srcDir "src"
-            }
-            res.srcDirs = ["res"]
-            manifest.srcFile "AndroidManifest.xml"
-        }
-    }
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_17
-        targetCompatibility JavaVersion.VERSION_17
-    }
-    buildFeatures {
-        compose true
-    }
-    composeOptions {
-        kotlinCompilerExtensionVersion jetpack_compose_compiler_version
-    }
-}
-
-dependencies {
-    implementation project(":spa")
-}
diff --git a/packages/SettingsLib/Spa/gallery/build.gradle.kts b/packages/SettingsLib/Spa/gallery/build.gradle.kts
new file mode 100644
index 0000000..a1151a5
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/build.gradle.kts
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+    alias(libs.plugins.android.application)
+    alias(libs.plugins.kotlin.android)
+}
+
+android {
+    namespace = "com.android.settingslib.spa.gallery"
+
+    defaultConfig {
+        applicationId = "com.android.settingslib.spa.gallery"
+        versionCode = 1
+        versionName = "1.0"
+    }
+
+    sourceSets {
+        sourceSets.getByName("main") {
+            kotlin.setSrcDirs(listOf("src"))
+            res.setSrcDirs(listOf("res"))
+            manifest.srcFile("AndroidManifest.xml")
+        }
+    }
+    buildFeatures {
+        compose = true
+    }
+}
+
+dependencies {
+    implementation(project(":spa"))
+}
diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml
new file mode 100644
index 0000000..9a16df8
--- /dev/null
+++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml
@@ -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.
+#
+
+[versions]
+agp = "8.0.2"
+dexmaker-mockito = "2.28.3"
+kotlin = "1.8.10"
+truth = "1.1"
+
+[libraries]
+dexmaker-mockito = { module = "com.linkedin.dexmaker:dexmaker-mockito", version.ref = "dexmaker-mockito" }
+truth = { module = "com.google.truth:truth", version.ref = "truth" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
index ed85e33..33f49e3 100644
--- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
+++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
@@ -14,9 +14,8 @@
 # limitations under the License.
 #
 
-#Thu Jul 14 10:36:06 CST 2022
 distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
 distributionPath=wrapper/dists
 zipStorePath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
diff --git a/packages/SettingsLib/Spa/settings.gradle b/packages/SettingsLib/Spa/settings.gradle
deleted file mode 100644
index 1c5a1ce..0000000
--- a/packages/SettingsLib/Spa/settings.gradle
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-pluginManagement {
-    repositories {
-        gradlePluginPortal()
-        google()
-        mavenCentral()
-    }
-}
-dependencyResolutionManagement {
-    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
-    repositories {
-        google()
-        mavenCentral()
-        maven { url "https://jitpack.io"}
-    }
-}
-rootProject.name = "SpaLib"
-include ':spa'
-include ':gallery'
-include ':testutils'
diff --git a/packages/SettingsLib/Spa/settings.gradle.kts b/packages/SettingsLib/Spa/settings.gradle.kts
new file mode 100644
index 0000000..9909781
--- /dev/null
+++ b/packages/SettingsLib/Spa/settings.gradle.kts
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+pluginManagement {
+    repositories {
+        google()
+        mavenCentral()
+        gradlePluginPortal()
+    }
+}
+dependencyResolutionManagement {
+    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+    rulesMode.set(RulesMode.FAIL_ON_PROJECT_RULES)
+
+    repositories {
+        google()
+        mavenCentral()
+        maven {
+            url = uri("https://jitpack.io")
+            content {
+                includeGroup("com.github.PhilJay")
+            }
+        }
+    }
+}
+rootProject.name = "SpaLib"
+include(":spa")
+include(":gallery")
+include(":testutils")
diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp
index 139f3e1..79f8c46 100644
--- a/packages/SettingsLib/Spa/spa/Android.bp
+++ b/packages/SettingsLib/Spa/spa/Android.bp
@@ -43,6 +43,7 @@
     kotlincflags: [
         "-Xjvm-default=all",
     ],
+    sdk_version: "current",
     min_sdk_version: "31",
 }
 
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
deleted file mode 100644
index a591366..0000000
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-plugins {
-    id 'com.android.library'
-    id 'kotlin-android'
-}
-
-android {
-    namespace 'com.android.settingslib.spa'
-    compileSdk TARGET_SDK
-    buildToolsVersion = BUILD_TOOLS_VERSION
-
-    defaultConfig {
-        minSdk MIN_SDK
-        targetSdk TARGET_SDK
-
-        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-    }
-
-    sourceSets {
-        main {
-            kotlin {
-                srcDir "src"
-            }
-            res.srcDirs = ["res"]
-            manifest.srcFile "AndroidManifest.xml"
-        }
-        androidTest {
-            kotlin {
-                srcDir "../tests/src"
-            }
-            res.srcDirs = ["../tests/res"]
-            manifest.srcFile "../tests/AndroidManifest.xml"
-        }
-    }
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_17
-        targetCompatibility JavaVersion.VERSION_17
-    }
-    buildFeatures {
-        compose true
-    }
-    composeOptions {
-        kotlinCompilerExtensionVersion jetpack_compose_compiler_version
-    }
-    buildTypes {
-        debug {
-            testCoverageEnabled = true
-        }
-    }
-}
-
-dependencies {
-    api "androidx.appcompat:appcompat:1.7.0-alpha02"
-    api "androidx.slice:slice-builders:1.1.0-alpha02"
-    api "androidx.slice:slice-core:1.1.0-alpha02"
-    api "androidx.slice:slice-view:1.1.0-alpha02"
-    api "androidx.compose.material3:material3:1.1.0-alpha06"
-    api "androidx.compose.material:material-icons-extended:$jetpack_compose_version"
-    api "androidx.compose.runtime:runtime-livedata:$jetpack_compose_version"
-    api "androidx.compose.ui:ui-tooling-preview:$jetpack_compose_version"
-    api "androidx.lifecycle:lifecycle-livedata-ktx"
-    api "androidx.lifecycle:lifecycle-runtime-compose"
-    api "androidx.navigation:navigation-compose:2.6.0-alpha08"
-    api "com.github.PhilJay:MPAndroidChart:v3.1.0-alpha"
-    api "com.google.android.material:material:1.7.0-alpha03"
-    debugApi "androidx.compose.ui:ui-tooling:$jetpack_compose_version"
-    implementation "com.airbnb.android:lottie-compose:5.2.0"
-
-    androidTestImplementation project(":testutils")
-    androidTestImplementation 'androidx.lifecycle:lifecycle-runtime-testing'
-    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:2.28.3"
-}
-
-task coverageReport(type: JacocoReport, dependsOn: "connectedDebugAndroidTest") {
-    group = "Reporting"
-    description = "Generate Jacoco coverage reports after running tests."
-
-    sourceDirectories.from = files("src")
-    classDirectories.from = fileTree(
-            dir: "$buildDir/tmp/kotlin-classes/debug",
-            excludes: [
-                    "com/android/settingslib/spa/debug/**",
-
-                    // Excludes files forked from AndroidX.
-                    "com/android/settingslib/spa/widget/scaffold/CustomizedAppBar*",
-                    "com/android/settingslib/spa/widget/scaffold/TopAppBarColors*",
-
-                    // Excludes files forked from Accompanist.
-                    "com/android/settingslib/spa/framework/compose/DrawablePainter*",
-
-                    // Excludes inline functions, which is not covered in Jacoco reports.
-                    "com/android/settingslib/spa/framework/util/Collections*",
-                    "com/android/settingslib/spa/framework/util/Flows*",
-
-                    // Excludes debug functions
-                    "com/android/settingslib/spa/framework/compose/TimeMeasurer*",
-
-                    // Excludes slice demo presenter & provider
-                    "com/android/settingslib/spa/slice/presenter/Demo*",
-                    "com/android/settingslib/spa/slice/provider/Demo*",
-            ],
-    )
-    executionData.from = fileTree(dir: "$buildDir/outputs/code_coverage/debugAndroidTest/connected")
-}
diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts
new file mode 100644
index 0000000..188e7f6
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/build.gradle.kts
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+    alias(libs.plugins.android.library)
+    alias(libs.plugins.kotlin.android)
+    jacoco
+}
+
+val jetpackComposeVersion: String? by extra
+
+android {
+    namespace = "com.android.settingslib.spa"
+
+    defaultConfig {
+        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    sourceSets {
+        sourceSets.getByName("main") {
+            kotlin.setSrcDirs(listOf("src"))
+            res.setSrcDirs(listOf("res"))
+            manifest.srcFile("AndroidManifest.xml")
+        }
+        sourceSets.getByName("androidTest") {
+            kotlin.setSrcDirs(listOf("../tests/src"))
+            res.setSrcDirs(listOf("../tests/res"))
+            manifest.srcFile("../tests/AndroidManifest.xml")
+        }
+    }
+    buildFeatures {
+        compose = true
+    }
+    buildTypes {
+        getByName("debug") {
+            enableAndroidTestCoverage = true
+        }
+    }
+}
+
+dependencies {
+    api("androidx.appcompat:appcompat:1.7.0-alpha02")
+    api("androidx.slice:slice-builders:1.1.0-alpha02")
+    api("androidx.slice:slice-core:1.1.0-alpha02")
+    api("androidx.slice:slice-view:1.1.0-alpha02")
+    api("androidx.compose.material3:material3:1.2.0-alpha03")
+    api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion")
+    api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion")
+    api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion")
+    api("androidx.lifecycle:lifecycle-livedata-ktx")
+    api("androidx.lifecycle:lifecycle-runtime-compose")
+    api("androidx.navigation:navigation-compose:2.7.0-beta01")
+    api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha")
+    api("com.google.android.material:material:1.7.0-alpha03")
+    debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion")
+    implementation("com.airbnb.android:lottie-compose:5.2.0")
+
+    androidTestImplementation(project(":testutils"))
+    androidTestImplementation(libs.dexmaker.mockito)
+}
+
+tasks.register<JacocoReport>("coverageReport") {
+    group = "Reporting"
+    description = "Generate Jacoco coverage reports after running tests."
+    dependsOn("connectedDebugAndroidTest")
+    sourceDirectories.setFrom(files("src"))
+    classDirectories.setFrom(
+        fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/debug")) {
+            setExcludes(
+                listOf(
+                    "com/android/settingslib/spa/debug/**",
+
+                    // Excludes files forked from AndroidX.
+                    "com/android/settingslib/spa/widget/scaffold/CustomizedAppBar*",
+                    "com/android/settingslib/spa/widget/scaffold/TopAppBarColors*",
+
+                    // Excludes files forked from Accompanist.
+                    "com/android/settingslib/spa/framework/compose/DrawablePainter*",
+
+                    // Excludes inline functions, which is not covered in Jacoco reports.
+                    "com/android/settingslib/spa/framework/util/Collections*",
+                    "com/android/settingslib/spa/framework/util/Flows*",
+
+                    // Excludes debug functions
+                    "com/android/settingslib/spa/framework/compose/TimeMeasurer*",
+
+                    // Excludes slice demo presenter & provider
+                    "com/android/settingslib/spa/slice/presenter/Demo*",
+                    "com/android/settingslib/spa/slice/provider/Demo*",
+                )
+            )
+        }
+    )
+    executionData.setFrom(
+        fileTree(layout.buildDirectory.dir("outputs/code_coverage/debugAndroidTest/connected"))
+    )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
index afce16c..08e3a27 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalAnimationApi::class)
-
 package com.android.settingslib.spa.framework
 
 import android.content.Intent
@@ -24,20 +22,17 @@
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.annotation.VisibleForTesting
-import androidx.compose.animation.AnimatedContentTransitionScope
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.unit.IntOffset
 import androidx.core.view.WindowCompat
 import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
 import com.android.settingslib.spa.R
 import com.android.settingslib.spa.framework.common.LogCategory
 import com.android.settingslib.spa.framework.common.NullPageProvider
@@ -46,12 +41,10 @@
 import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 import com.android.settingslib.spa.framework.common.createSettingsPage
-import com.android.settingslib.spa.framework.compose.AnimatedNavHost
 import com.android.settingslib.spa.framework.compose.LocalNavController
 import com.android.settingslib.spa.framework.compose.NavControllerWrapperImpl
-import com.android.settingslib.spa.framework.compose.composable
+import com.android.settingslib.spa.framework.compose.animatedComposable
 import com.android.settingslib.spa.framework.compose.localNavController
-import com.android.settingslib.spa.framework.compose.rememberAnimatedNavController
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.framework.util.PageLogger
 import com.android.settingslib.spa.framework.util.getDestination
@@ -108,7 +101,7 @@
     isPageEnabled: (SettingsPage) -> Boolean,
     initialIntent: Intent?,
 ) {
-    val navController = rememberAnimatedNavController()
+    val navController = rememberNavController()
     CompositionLocalProvider(navController.localNavController()) {
         val controller = LocalNavController.current as NavControllerWrapperImpl
         controller.NavContent(sppRepository.getAllProviders()) { page ->
@@ -133,41 +126,15 @@
     allProvider: Collection<SettingsPageProvider>,
     content: @Composable (SettingsPage) -> Unit,
 ) {
-    AnimatedNavHost(
+    NavHost(
         navController = navController,
         startDestination = NullPageProvider.name,
     ) {
-        val slideEffect = tween<IntOffset>(durationMillis = 300)
-        val fadeEffect = tween<Float>(durationMillis = 300)
         composable(NullPageProvider.name) {}
         for (spp in allProvider) {
-            composable(
+            animatedComposable(
                 route = spp.name + spp.parameter.navRoute(),
                 arguments = spp.parameter,
-                enterTransition = {
-                    slideIntoContainer(
-                        AnimatedContentTransitionScope.SlideDirection.Start,
-                        animationSpec = slideEffect
-                    ) + fadeIn(animationSpec = fadeEffect)
-                },
-                exitTransition = {
-                    slideOutOfContainer(
-                        AnimatedContentTransitionScope.SlideDirection.Start,
-                        animationSpec = slideEffect
-                    ) + fadeOut(animationSpec = fadeEffect)
-                },
-                popEnterTransition = {
-                    slideIntoContainer(
-                        AnimatedContentTransitionScope.SlideDirection.End,
-                        animationSpec = slideEffect
-                    ) + fadeIn(animationSpec = fadeEffect)
-                },
-                popExitTransition = {
-                    slideOutOfContainer(
-                        AnimatedContentTransitionScope.SlideDirection.End,
-                        animationSpec = slideEffect
-                    ) + fadeOut(animationSpec = fadeEffect)
-                },
             ) { navBackStackEntry ->
                 val page = remember { spp.createSettingsPage(navBackStackEntry.arguments) }
                 content(page)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedComposeNavigator.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedComposeNavigator.kt
deleted file mode 100644
index 930a83f..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedComposeNavigator.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.framework.compose
-
-import androidx.compose.animation.AnimatedVisibilityScope
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.navigation.NavBackStackEntry
-import androidx.navigation.NavDestination
-import androidx.navigation.NavOptions
-import androidx.navigation.Navigator
-
-/**
- * Navigator that navigates through [Composable]s. Every destination using this Navigator must
- * set a valid [Composable] by setting it directly on an instantiated [Destination] or calling
- * [composable].
- */
-@ExperimentalAnimationApi
-@Navigator.Name("animatedComposable")
-public class AnimatedComposeNavigator : Navigator<AnimatedComposeNavigator.Destination>() {
-    internal val transitionsInProgress get() = state.transitionsInProgress
-
-    internal val backStack get() = state.backStack
-
-    internal val isPop = mutableStateOf(false)
-
-    override fun navigate(
-        entries: List<NavBackStackEntry>,
-        navOptions: NavOptions?,
-        navigatorExtras: Extras?
-    ) {
-        entries.forEach { entry ->
-            state.pushWithTransition(entry)
-        }
-        isPop.value = false
-    }
-
-    override fun createDestination(): Destination {
-        return Destination(this, content = { })
-    }
-
-    override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
-        state.popWithTransition(popUpTo, savedState)
-        isPop.value = true
-    }
-
-    internal fun markTransitionComplete(entry: NavBackStackEntry) {
-        state.markTransitionComplete(entry)
-    }
-
-    /**
-     * NavDestination specific to [AnimatedComposeNavigator]
-     */
-    @ExperimentalAnimationApi
-    @NavDestination.ClassType(Composable::class)
-    public class Destination(
-        navigator: AnimatedComposeNavigator,
-        internal val content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit
-    ) : NavDestination(navigator)
-
-    internal companion object {
-        internal const val NAME = "animatedComposable"
-    }
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt
new file mode 100644
index 0000000..192b125
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.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.settingslib.spa.framework.compose
+
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.animation.AnimatedContentTransitionScope
+import androidx.compose.animation.core.FastOutLinearInEasing
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.IntOffset
+import androidx.navigation.NamedNavArgument
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDeepLink
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+
+/**
+ * Add the [Composable] to the [NavGraphBuilder] with animation
+ *
+ * @param route route for the destination
+ * @param arguments list of arguments to associate with destination
+ * @param deepLinks list of deep links to associate with the destinations
+ * @param content composable for the destination
+ */
+internal fun NavGraphBuilder.animatedComposable(
+    route: String,
+    arguments: List<NamedNavArgument> = emptyList(),
+    deepLinks: List<NavDeepLink> = emptyList(),
+    content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
+) = composable(
+    route = route,
+    arguments = arguments,
+    deepLinks = deepLinks,
+    enterTransition = {
+        slideIntoContainer(
+            towards = AnimatedContentTransitionScope.SlideDirection.Start,
+            animationSpec = slideInEffect,
+            initialOffset = offsetFunc,
+        ) + fadeIn(animationSpec = fadeInEffect)
+    },
+    exitTransition = {
+        slideOutOfContainer(
+            towards = AnimatedContentTransitionScope.SlideDirection.Start,
+            animationSpec = slideOutEffect,
+            targetOffset = offsetFunc,
+        ) + fadeOut(animationSpec = fadeOutEffect)
+    },
+    popEnterTransition = {
+        slideIntoContainer(
+            towards = AnimatedContentTransitionScope.SlideDirection.End,
+            animationSpec = slideInEffect,
+            initialOffset = offsetFunc,
+        ) + fadeIn(animationSpec = fadeInEffect)
+    },
+    popExitTransition = {
+        slideOutOfContainer(
+            towards = AnimatedContentTransitionScope.SlideDirection.End,
+            animationSpec = slideOutEffect,
+            targetOffset = offsetFunc,
+        ) + fadeOut(animationSpec = fadeOutEffect)
+    },
+    content = content,
+)
+
+private const val FADE_OUT_MILLIS = 75
+private const val FADE_IN_MILLIS = 300
+
+private val slideInEffect = tween<IntOffset>(
+    durationMillis = FADE_IN_MILLIS,
+    delayMillis = FADE_OUT_MILLIS,
+    easing = LinearOutSlowInEasing,
+)
+private val slideOutEffect = tween<IntOffset>(durationMillis = FADE_IN_MILLIS)
+private val fadeOutEffect = tween<Float>(
+    durationMillis = FADE_OUT_MILLIS,
+    easing = FastOutLinearInEasing,
+)
+private val fadeInEffect = tween<Float>(
+    durationMillis = FADE_IN_MILLIS,
+    delayMillis = FADE_OUT_MILLIS,
+    easing = LinearOutSlowInEasing,
+)
+private val offsetFunc: (offsetForFullSlide: Int) -> Int = { it.div(5) }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavHost.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavHost.kt
deleted file mode 100644
index 81bbc24..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavHost.kt
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.framework.compose
-
-import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.AnimatedContentTransitionScope
-import androidx.compose.animation.ContentTransform
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.core.updateTransition
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.with
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveableStateHolder
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalLifecycleOwner
-import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
-import androidx.navigation.NavBackStackEntry
-import androidx.navigation.NavDestination
-import androidx.navigation.NavDestination.Companion.hierarchy
-import androidx.navigation.NavGraph
-import androidx.navigation.NavGraphBuilder
-import androidx.navigation.NavHostController
-import androidx.navigation.Navigator
-import androidx.navigation.compose.DialogHost
-import androidx.navigation.compose.DialogNavigator
-import androidx.navigation.compose.LocalOwnersProvider
-import androidx.navigation.createGraph
-import androidx.navigation.get
-import kotlinx.coroutines.flow.map
-
-/**
- * Provides in place in the Compose hierarchy for self contained navigation to occur.
- *
- * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
- * the provided [navController].
- *
- * The builder passed into this method is [remember]ed. This means that for this NavHost, the
- * contents of the builder cannot be changed.
- *
- * @param navController the navController for this host
- * @param startDestination the route for the start destination
- * @param modifier The modifier to be applied to the layout.
- * @param route the route for the graph
- * @param enterTransition callback to define enter transitions for destination in this host
- * @param exitTransition callback to define exit transitions for destination in this host
- * @param popEnterTransition callback to define popEnter transitions for destination in this host
- * @param popExitTransition callback to define popExit transitions for destination in this host
- * @param builder the builder used to construct the graph
- */
-@Composable
-@ExperimentalAnimationApi
-public fun AnimatedNavHost(
-    navController: NavHostController,
-    startDestination: String,
-    modifier: Modifier = Modifier,
-    contentAlignment: Alignment = Alignment.Center,
-    route: String? = null,
-    enterTransition: (AnimatedScope.() -> EnterTransition) = { fadeIn(animationSpec = tween(700)) },
-    exitTransition: (AnimatedScope.() -> ExitTransition) = { fadeOut(animationSpec = tween(700)) },
-    popEnterTransition: (AnimatedScope.() -> EnterTransition) = enterTransition,
-    popExitTransition: (AnimatedScope.() -> ExitTransition) = exitTransition,
-    builder: NavGraphBuilder.() -> Unit
-) {
-    AnimatedNavHost(
-        navController,
-        remember(route, startDestination, builder) {
-            navController.createGraph(startDestination, route, builder)
-        },
-        modifier,
-        contentAlignment,
-        enterTransition,
-        exitTransition,
-        popEnterTransition,
-        popExitTransition
-    )
-}
-
-/**
- * Provides in place in the Compose hierarchy for self contained navigation to occur.
- *
- * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
- * the provided [navController].
- *
- * @param navController the navController for this host
- * @param graph the graph for this host
- * @param modifier The modifier to be applied to the layout.
- * @param enterTransition callback to define enter transitions for destination in this host
- * @param exitTransition callback to define exit transitions for destination in this host
- * @param popEnterTransition callback to define popEnter transitions for destination in this host
- * @param popExitTransition callback to define popExit transitions for destination in this host
- */
-@ExperimentalAnimationApi
-@Composable
-public fun AnimatedNavHost(
-    navController: NavHostController,
-    graph: NavGraph,
-    modifier: Modifier = Modifier,
-    contentAlignment: Alignment = Alignment.Center,
-    enterTransition: (AnimatedScope.() -> EnterTransition) = { fadeIn(animationSpec = tween(700)) },
-    exitTransition: (AnimatedScope.() -> ExitTransition) = { fadeOut(animationSpec = tween(700)) },
-    popEnterTransition: (AnimatedScope.() -> EnterTransition) = enterTransition,
-    popExitTransition: (AnimatedScope.() -> ExitTransition) = exitTransition,
-) {
-
-    val lifecycleOwner = LocalLifecycleOwner.current
-    val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
-        "NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner"
-    }
-    val onBackPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
-    val onBackPressedDispatcher = onBackPressedDispatcherOwner?.onBackPressedDispatcher
-
-    // on successful recompose we setup the navController with proper inputs
-    // after the first time, this will only happen again if one of the inputs changes
-    navController.setLifecycleOwner(lifecycleOwner)
-    navController.setViewModelStore(viewModelStoreOwner.viewModelStore)
-    if (onBackPressedDispatcher != null) {
-        navController.setOnBackPressedDispatcher(onBackPressedDispatcher)
-    }
-
-    navController.graph = graph
-
-    val saveableStateHolder = rememberSaveableStateHolder()
-
-    // Find the ComposeNavigator, returning early if it isn't found
-    // (such as is the case when using TestNavHostController)
-    val composeNavigator = navController.navigatorProvider.get<Navigator<out NavDestination>>(
-        AnimatedComposeNavigator.NAME
-    ) as? AnimatedComposeNavigator ?: return
-    val visibleEntries by remember(navController.visibleEntries) {
-        navController.visibleEntries.map {
-            it.filter { entry ->
-                entry.destination.navigatorName == AnimatedComposeNavigator.NAME
-            }
-        }
-    }.collectAsState(emptyList())
-
-    val backStackEntry = visibleEntries.lastOrNull()
-
-    if (backStackEntry != null) {
-        val finalEnter: AnimatedScope.() -> EnterTransition = {
-            val targetDestination = targetState.destination as AnimatedComposeNavigator.Destination
-
-            if (composeNavigator.isPop.value) {
-                targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
-                    popEnterTransitions[destination.route]?.invoke(this)
-                } ?: popEnterTransition.invoke(this)
-            } else {
-                targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
-                    enterTransitions[destination.route]?.invoke(this)
-                } ?: enterTransition.invoke(this)
-            }
-        }
-
-        val finalExit: AnimatedScope.() -> ExitTransition = {
-            val initialDestination =
-                initialState.destination as AnimatedComposeNavigator.Destination
-
-            if (composeNavigator.isPop.value) {
-                initialDestination.hierarchy.firstNotNullOfOrNull { destination ->
-                    popExitTransitions[destination.route]?.invoke(this)
-                } ?: popExitTransition.invoke(this)
-            } else {
-                initialDestination.hierarchy.firstNotNullOfOrNull { destination ->
-                    exitTransitions[destination.route]?.invoke(this)
-                } ?: exitTransition.invoke(this)
-            }
-        }
-
-        val transition = updateTransition(backStackEntry, label = "entry")
-        transition.AnimatedContent(
-            modifier,
-            transitionSpec = {
-                val zIndex = composeNavigator.backStack.value.size.toFloat()
-                // If the initialState of the AnimatedContent is not in visibleEntries, we are in
-                // a case where visible has cleared the old state for some reason, so instead of
-                // attempting to animate away from the initialState, we skip the animation.
-                if (initialState in visibleEntries) {
-                    ContentTransform(finalEnter(this), finalExit(this), zIndex)
-                } else {
-                    EnterTransition.None with ExitTransition.None
-                }
-            },
-            contentAlignment,
-            contentKey = { it.id }
-        ) {
-            // In some specific cases, such as clearing your back stack by changing your
-            // start destination, AnimatedContent can contain an entry that is no longer
-            // part of visible entries since it was cleared from the back stack and is not
-            // animating. In these cases the currentEntry will be null, and in those cases,
-            // AnimatedContent will just skip attempting to transition the old entry.
-            // See https://issuetracker.google.com/238686802
-            val currentEntry = visibleEntries.lastOrNull { entry ->
-                it == entry
-            }
-            // while in the scope of the composable, we provide the navBackStackEntry as the
-            // ViewModelStoreOwner and LifecycleOwner
-            currentEntry?.LocalOwnersProvider(saveableStateHolder) {
-                (currentEntry.destination as AnimatedComposeNavigator.Destination)
-                    .content(this, currentEntry)
-            }
-        }
-        if (transition.currentState == transition.targetState) {
-            visibleEntries.forEach { entry ->
-                composeNavigator.markTransitionComplete(entry)
-            }
-        }
-    }
-
-    val dialogNavigator = navController.navigatorProvider.get<Navigator<out NavDestination>>(
-        "dialog"
-    ) as? DialogNavigator ?: return
-
-    // Show any dialog destinations
-    DialogHost(dialogNavigator)
-}
-
-@OptIn(ExperimentalAnimationApi::class)
-internal typealias AnimatedScope = AnimatedContentTransitionScope<NavBackStackEntry>
-
-@ExperimentalAnimationApi
-internal val enterTransitions = mutableMapOf<String?, (AnimatedScope.() -> EnterTransition?)?>()
-
-@ExperimentalAnimationApi
-internal val exitTransitions = mutableMapOf<String?, (AnimatedScope.() -> ExitTransition?)?>()
-@ExperimentalAnimationApi
-internal val popEnterTransitions = mutableMapOf<String?, (AnimatedScope.() -> EnterTransition?)?>()
-
-@ExperimentalAnimationApi
-internal val popExitTransitions = mutableMapOf<String?, (AnimatedScope.() -> ExitTransition?)?>()
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavGraphBuilder.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavGraphBuilder.kt
deleted file mode 100644
index bf92f5d..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavGraphBuilder.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.framework.compose
-
-import androidx.compose.animation.AnimatedVisibilityScope
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.runtime.Composable
-import androidx.navigation.NamedNavArgument
-import androidx.navigation.NavBackStackEntry
-import androidx.navigation.NavDeepLink
-import androidx.navigation.NavGraphBuilder
-import androidx.navigation.get
-
-/**
- * Add the [Composable] to the [NavGraphBuilder]
- *
- * @param route route for the destination
- * @param arguments list of arguments to associate with destination
- * @param deepLinks list of deep links to associate with the destinations
- * @param enterTransition callback to determine the destination's enter transition
- * @param exitTransition callback to determine the destination's exit transition
- * @param popEnterTransition callback to determine the destination's popEnter transition
- * @param popExitTransition callback to determine the destination's popExit transition
- * @param content composable for the destination
- */
-@ExperimentalAnimationApi
-public fun NavGraphBuilder.composable(
-    route: String,
-    arguments: List<NamedNavArgument> = emptyList(),
-    deepLinks: List<NavDeepLink> = emptyList(),
-    enterTransition: (AnimatedScope.() -> EnterTransition?)? = null,
-    exitTransition: (AnimatedScope.() -> ExitTransition?)? = null,
-    popEnterTransition: (AnimatedScope.() -> EnterTransition?)? = enterTransition,
-    popExitTransition: (AnimatedScope.() -> ExitTransition?)? = exitTransition,
-    content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit
-) {
-    addDestination(
-        AnimatedComposeNavigator.Destination(
-            provider[AnimatedComposeNavigator::class],
-            content
-        ).apply {
-            this.route = route
-            arguments.forEach { (argumentName, argument) ->
-                addArgument(argumentName, argument)
-            }
-            deepLinks.forEach { deepLink ->
-                addDeepLink(deepLink)
-            }
-            enterTransition?.let { enterTransitions[route] = enterTransition }
-            exitTransition?.let { exitTransitions[route] = exitTransition }
-            popEnterTransition?.let { popEnterTransitions[route] = popEnterTransition }
-            popExitTransition?.let { popExitTransitions[route] = popExitTransition }
-        }
-    )
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavHostController.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavHostController.kt
deleted file mode 100644
index a8ac86c..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavHostController.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.framework.compose
-
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.navigation.NavDestination
-import androidx.navigation.NavHostController
-import androidx.navigation.Navigator
-import androidx.navigation.compose.rememberNavController
-
-/**
- * Creates a NavHostController that handles the adding of the [ComposeNavigator], [DialogNavigator]
- * and [AnimatedComposeNavigator]. Additional [androidx.navigation.Navigator] instances should be
- * added in a [androidx.compose.runtime.SideEffect] block.
- *
- * @see AnimatedNavHost
- */
-@ExperimentalAnimationApi
-@Composable
-fun rememberAnimatedNavController(
-    vararg navigators: Navigator<out NavDestination>
-): NavHostController {
-    val animatedNavigator = remember { AnimatedComposeNavigator() }
-    return rememberNavController(animatedNavigator, *navigators)
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
index e07a629..90a723f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
@@ -50,7 +50,7 @@
 import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Alignment
@@ -286,21 +286,22 @@
         )
     }
     val pinnedHeightPx: Float
-    val density = LocalDensity.current
-    val maxHeightPx = density.run {
-        remember { mutableStateOf((MaxHeightWithoutTitle + DefaultTitleHeight).toPx()) }
-    }
     val titleBottomPaddingPx: Int
+    val defaultMaxHeightPx: Float
+    val density = LocalDensity.current
     density.run {
         pinnedHeightPx = pinnedHeight.toPx()
         titleBottomPaddingPx = titleBottomPadding.roundToPx()
+        defaultMaxHeightPx = (MaxHeightWithoutTitle + DefaultTitleHeight).toPx()
     }
 
+    val maxHeightPx = remember(density) { mutableFloatStateOf(defaultMaxHeightPx) }
+
     // Sets the app bar's height offset limit to hide just the bottom title area and keep top title
     // visible when collapsed.
     SideEffect {
-        if (scrollBehavior?.state?.heightOffsetLimit != pinnedHeightPx - maxHeightPx.value) {
-            scrollBehavior?.state?.heightOffsetLimit = pinnedHeightPx - maxHeightPx.value
+        if (scrollBehavior?.state?.heightOffsetLimit != pinnedHeightPx - maxHeightPx.floatValue) {
+            scrollBehavior?.state?.heightOffsetLimit = pinnedHeightPx - maxHeightPx.floatValue
         }
     }
 
@@ -370,16 +371,20 @@
             )
             TopAppBarLayout(
                 modifier = Modifier.clipToBounds(),
-                heightPx = maxHeightPx.value - pinnedHeightPx +
+                heightPx = maxHeightPx.floatValue - pinnedHeightPx +
                     (scrollBehavior?.state?.heightOffset ?: 0f),
                 navigationIconContentColor = colors.navigationIconContentColor,
                 titleContentColor = colors.titleContentColor,
                 actionIconContentColor = colors.actionIconContentColor,
                 title = {
                     Box(modifier = Modifier.onGloballyPositioned { coordinates ->
-                        density.run {
-                            maxHeightPx.value =
-                                MaxHeightWithoutTitle.toPx() + coordinates.size.height.toFloat()
+                        val measuredMaxHeightPx = density.run {
+                            MaxHeightWithoutTitle.toPx() + coordinates.size.height.toFloat()
+                        }
+                        // Allow larger max height for multi-line title, but do not reduce
+                        // max height to prevent flaky.
+                        if (measuredMaxHeightPx > defaultMaxHeightPx) {
+                            maxHeightPx.floatValue = measuredMaxHeightPx
                         }
                     }) { title() }
                 },
@@ -506,7 +511,7 @@
                 0
             }
 
-        val layoutHeight = heightPx.roundToInt()
+        val layoutHeight = if (heightPx.isNaN()) 0 else heightPx.roundToInt()
 
         layout(constraints.maxWidth, layoutHeight) {
             // Navigation icon
@@ -612,9 +617,9 @@
 // Medium or Large app bar.
 private val TopTitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f)
 
-private val MaxHeightWithoutTitle = 124.dp
-private val DefaultTitleHeight = 52.dp
-private val ContainerHeight = 56.dp
+internal val MaxHeightWithoutTitle = 124.dp
+internal val DefaultTitleHeight = 52.dp
+internal val ContainerHeight = 56.dp
 private val LargeTitleBottomPadding = 28.dp
 private val TopAppBarHorizontalPadding = 4.dp
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
index 67c4cdc..67f4418 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
@@ -163,7 +163,6 @@
     BackHandler { onClose() }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun SearchBox(query: TextFieldValue, onQueryChange: (TextFieldValue) -> Unit) {
     val focusRequester = remember { FocusRequester() }
@@ -186,8 +185,9 @@
         keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
         keyboardActions = KeyboardActions(onSearch = { hideKeyboardAction() }),
         singleLine = true,
-        colors = TextFieldDefaults.textFieldColors(
-            containerColor = Color.Transparent,
+        colors = TextFieldDefaults.colors(
+            focusedContainerColor = Color.Transparent,
+            unfocusedContainerColor = Color.Transparent,
             focusedIndicatorColor = Color.Transparent,
             unfocusedIndicatorColor = Color.Transparent,
         ),
diff --git a/packages/SettingsLib/Spa/tests/Android.bp b/packages/SettingsLib/Spa/tests/Android.bp
index f9e64ae..0d9ba54 100644
--- a/packages/SettingsLib/Spa/tests/Android.bp
+++ b/packages/SettingsLib/Spa/tests/Android.bp
@@ -36,5 +36,6 @@
         "mockito-target-minus-junit4",
     ],
     kotlincflags: ["-Xjvm-default=all"],
+    sdk_version: "current",
     min_sdk_version: "31",
 }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBarTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBarTest.kt
new file mode 100644
index 0000000..a6a5ed22
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBarTest.kt
@@ -0,0 +1,457 @@
+/*
+ * 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.
+ */
+
+package com.android.settingslib.spa.widget.scaffold
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.rootWidth
+import com.android.settingslib.spa.testutils.setContentForSizeAssertions
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalMaterial3Api::class)
+@RunWith(AndroidJUnit4::class)
+class CustomizedAppBarTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun smallTopAppBar_expandsToScreen() {
+        rule
+            .setContentForSizeAssertions {
+                CustomizedTopAppBar(title = { Text("Title") })
+            }
+            .assertHeightIsEqualTo(ContainerHeight)
+            .assertWidthIsEqualTo(rule.rootWidth())
+    }
+
+    @Test
+    fun smallTopAppBar_withTitle() {
+        val title = "Title"
+        rule.setContent {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                CustomizedTopAppBar(title = { Text(title) })
+            }
+        }
+        rule.onNodeWithText(title).assertIsDisplayed()
+    }
+
+    @Test
+    fun smallTopAppBar_default_positioning() {
+        rule.setContent {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                CustomizedTopAppBar(
+                    navigationIcon = {
+                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    },
+                    title = {
+                        Text("Title", Modifier.testTag(TitleTestTag))
+                    },
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    }
+                )
+            }
+        }
+        assertSmallDefaultPositioning()
+    }
+
+    @Test
+    fun smallTopAppBar_noNavigationIcon_positioning() {
+        rule.setContent {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                CustomizedTopAppBar(
+                    title = {
+                        Text("Title", Modifier.testTag(TitleTestTag))
+                    },
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    }
+                )
+            }
+        }
+        assertSmallPositioningWithoutNavigation()
+    }
+
+    @Test
+    fun smallTopAppBar_titleDefaultStyle() {
+        var textStyle: TextStyle? = null
+        var expectedTextStyle: TextStyle? = null
+        rule.setContent {
+            CustomizedTopAppBar(
+                title = {
+                    Text("Title")
+                    textStyle = LocalTextStyle.current
+                    expectedTextStyle = MaterialTheme.typography.titleMedium
+                },
+            )
+        }
+        assertThat(textStyle).isNotNull()
+        assertThat(textStyle).isEqualTo(expectedTextStyle)
+    }
+
+    @Test
+    fun smallTopAppBar_contentColor() {
+        var titleColor: Color = Color.Unspecified
+        var navigationIconColor: Color = Color.Unspecified
+        var actionsColor: Color = Color.Unspecified
+        var expectedTitleColor: Color = Color.Unspecified
+        var expectedNavigationIconColor: Color = Color.Unspecified
+        var expectedActionsColor: Color = Color.Unspecified
+
+        rule.setContent {
+            CustomizedTopAppBar(
+                navigationIcon = {
+                    FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    navigationIconColor = LocalContentColor.current
+                    expectedNavigationIconColor =
+                        TopAppBarDefaults.topAppBarColors().navigationIconContentColor
+                    // fraction = 0f to indicate no scroll.
+                },
+                title = {
+                    Text("Title", Modifier.testTag(TitleTestTag))
+                    titleColor = LocalContentColor.current
+                    expectedTitleColor = TopAppBarDefaults.topAppBarColors().titleContentColor
+                },
+                actions = {
+                    FakeIcon(Modifier.testTag(ActionsTestTag))
+                    actionsColor = LocalContentColor.current
+                    expectedActionsColor =
+                        TopAppBarDefaults.topAppBarColors().actionIconContentColor
+                }
+            )
+        }
+        assertThat(navigationIconColor).isNotNull()
+        assertThat(titleColor).isNotNull()
+        assertThat(actionsColor).isNotNull()
+        assertThat(navigationIconColor).isEqualTo(expectedNavigationIconColor)
+        assertThat(titleColor).isEqualTo(expectedTitleColor)
+        assertThat(actionsColor).isEqualTo(expectedActionsColor)
+    }
+
+    @Test
+    fun largeTopAppBar_scrolled_positioning() {
+        val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                CustomizedLargeTopAppBar(
+                    navigationIcon = {
+                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    },
+                    title = "Title",
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    },
+                    scrollBehavior = scrollBehavior,
+                )
+            }
+        }
+        assertLargeScrolledHeight(
+            MaxHeightWithoutTitle + DefaultTitleHeight,
+            MaxHeightWithoutTitle + DefaultTitleHeight,
+            content,
+        )
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun topAppBar_enterAlways_allowHorizontalScroll() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            MultiPageContent(TopAppBarDefaults.enterAlwaysScrollBehavior(), state)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun topAppBar_exitUntilCollapsed_allowHorizontalScroll() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            MultiPageContent(TopAppBarDefaults.exitUntilCollapsedScrollBehavior(), state)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun topAppBar_pinned_allowHorizontalScroll() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            state = rememberLazyListState()
+            MultiPageContent(
+                TopAppBarDefaults.pinnedScrollBehavior(),
+                state
+            )
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Composable
+    private fun MultiPageContent(scrollBehavior: TopAppBarScrollBehavior, state: LazyListState) {
+        Scaffold(
+            modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+            topBar = {
+                CustomizedTopAppBar(
+                    title = { Text(text = "Title") },
+                )
+            }
+        ) { contentPadding ->
+            LazyRow(
+                Modifier
+                    .fillMaxSize()
+                    .testTag(LazyListTag), state
+            ) {
+                items(2) { page ->
+                    LazyColumn(
+                        modifier = Modifier.fillParentMaxSize(),
+                        contentPadding = contentPadding
+                    ) {
+                        items(50) {
+                            Text(
+                                modifier = Modifier.fillParentMaxWidth(),
+                                text = "Item #$page x $it"
+                            )
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks the app bar's components positioning when it's a [CustomizedTopAppBar], a
+     * [CenterAlignedTopAppBar], or a larger app bar that is scrolled up and collapsed into a small
+     * configuration and there is no navigation icon.
+     */
+    private fun assertSmallPositioningWithoutNavigation(isCenteredTitle: Boolean = false) {
+        val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
+        val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
+
+        val titleNode = rule.onNodeWithTag(TitleTestTag)
+        // Title should be vertically centered
+        titleNode.assertTopPositionInRootIsEqualTo((appBarBounds.height - titleBounds.height) / 2)
+        if (isCenteredTitle) {
+            // Title should be horizontally centered
+            titleNode.assertLeftPositionInRootIsEqualTo(
+                (appBarBounds.width - titleBounds.width) / 2
+            )
+        } else {
+            // Title should now be placed 16.dp from the start, as there is no navigation icon
+            // 4.dp padding for the whole app bar + 12.dp inset
+            titleNode.assertLeftPositionInRootIsEqualTo(4.dp + 12.dp)
+        }
+
+        rule.onNodeWithTag(ActionsTestTag)
+            // Action should still be placed at the end
+            .assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
+    }
+
+    /**
+     * Checks the app bar's components positioning when it's a [CustomizedTopAppBar] or a
+     * [CenterAlignedTopAppBar].
+     */
+    private fun assertSmallDefaultPositioning(isCenteredTitle: Boolean = false) {
+        val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
+        val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
+        val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
+
+        rule.onNodeWithTag(NavigationIconTestTag)
+            // Navigation icon should be 4.dp from the start
+            .assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding)
+            // Navigation icon should be centered within the height of the app bar.
+            .assertTopPositionInRootIsEqualTo(
+                appBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
+            )
+
+        val titleNode = rule.onNodeWithTag(TitleTestTag)
+        // Title should be vertically centered
+        titleNode.assertTopPositionInRootIsEqualTo((appBarBounds.height - titleBounds.height) / 2)
+        if (isCenteredTitle) {
+            // Title should be horizontally centered
+            titleNode.assertLeftPositionInRootIsEqualTo(
+                (appBarBounds.width - titleBounds.width) / 2
+            )
+        } else {
+            // Title should be 56.dp from the start
+            // 4.dp padding for the whole app bar + 48.dp icon size + 4.dp title padding.
+            titleNode.assertLeftPositionInRootIsEqualTo(4.dp + FakeIconSize + 4.dp)
+        }
+
+        rule.onNodeWithTag(ActionsTestTag)
+            // Action should be placed at the end
+            .assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
+            // Action should be 8.dp from the top
+            .assertTopPositionInRootIsEqualTo(
+                appBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
+            )
+    }
+
+    /**
+     * Checks that changing values at a [CustomizedLargeTopAppBar] scroll behavior
+     * affects the height of the app bar.
+     *
+     * This check partially and fully collapses the app bar to test its height.
+     *
+     * @param appBarMaxHeight the max height of the app bar [content]
+     * @param appBarMinHeight the min height of the app bar [content]
+     * @param content a Composable that adds a CustomizedLargeTopAppBar
+     */
+    @OptIn(ExperimentalMaterial3Api::class)
+    private fun assertLargeScrolledHeight(
+        appBarMaxHeight: Dp,
+        appBarMinHeight: Dp,
+        content: @Composable (TopAppBarScrollBehavior?) -> Unit
+    ) {
+        val fullyCollapsedOffsetDp = appBarMaxHeight - appBarMinHeight
+        val partiallyCollapsedOffsetDp = fullyCollapsedOffsetDp / 3
+        var partiallyCollapsedHeightOffsetPx = 0f
+        var fullyCollapsedHeightOffsetPx = 0f
+        lateinit var scrollBehavior: TopAppBarScrollBehavior
+        rule.setContent {
+            scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+            with(LocalDensity.current) {
+                partiallyCollapsedHeightOffsetPx = partiallyCollapsedOffsetDp.toPx()
+                fullyCollapsedHeightOffsetPx = fullyCollapsedOffsetDp.toPx()
+            }
+
+            content(scrollBehavior)
+        }
+
+        // Simulate a partially collapsed app bar.
+        rule.runOnIdle {
+            scrollBehavior.state.heightOffset = -partiallyCollapsedHeightOffsetPx
+            scrollBehavior.state.contentOffset = -partiallyCollapsedHeightOffsetPx
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag(TopAppBarTestTag)
+            .assertHeightIsEqualTo(
+                appBarMaxHeight - partiallyCollapsedOffsetDp
+            )
+
+        // Simulate a fully collapsed app bar.
+        rule.runOnIdle {
+            scrollBehavior.state.heightOffset = -fullyCollapsedHeightOffsetPx
+            // Simulate additional content scroll beyond the max offset scroll.
+            scrollBehavior.state.contentOffset =
+                -fullyCollapsedHeightOffsetPx - partiallyCollapsedHeightOffsetPx
+        }
+        rule.waitForIdle()
+        // Check that the app bar collapsed to its min height.
+        rule.onNodeWithTag(TopAppBarTestTag).assertHeightIsEqualTo(appBarMinHeight)
+    }
+
+    /**
+     * An [IconButton] with an [Icon] inside for testing positions.
+     *
+     * An [IconButton] is defaulted to be 48X48dp, while its child [Icon] is defaulted to 24x24dp.
+     */
+    private val FakeIcon = @Composable { modifier: Modifier ->
+        IconButton(
+            onClick = { /* doSomething() */ },
+            modifier = modifier.semantics(mergeDescendants = true) {}
+        ) {
+            Icon(ColorPainter(Color.Red), null)
+        }
+    }
+
+    private fun expectedActionPosition(appBarWidth: Dp): Dp =
+        appBarWidth - AppBarStartAndEndPadding - FakeIconSize
+
+    private val FakeIconSize = 48.dp
+    private val AppBarStartAndEndPadding = 4.dp
+    private val AppBarTopAndBottomPadding = (ContainerHeight - FakeIconSize) / 2
+
+    private val LazyListTag = "lazyList"
+    private val TopAppBarTestTag = "bar"
+    private val NavigationIconTestTag = "navigationIcon"
+    private val TitleTestTag = "title"
+    private val ActionsTestTag = "actions"
+}
diff --git a/packages/SettingsLib/Spa/testutils/build.gradle b/packages/SettingsLib/Spa/testutils/build.gradle
deleted file mode 100644
index 23a9add..0000000
--- a/packages/SettingsLib/Spa/testutils/build.gradle
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-plugins {
-    id 'com.android.library'
-    id 'kotlin-android'
-}
-
-android {
-    namespace 'com.android.settingslib.spa.testutils'
-    compileSdk TARGET_SDK
-    buildToolsVersion = BUILD_TOOLS_VERSION
-
-    defaultConfig {
-        minSdk MIN_SDK
-        targetSdk TARGET_SDK
-    }
-
-    sourceSets {
-        main {
-            kotlin {
-                srcDir "src"
-            }
-            manifest.srcFile "AndroidManifest.xml"
-        }
-    }
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_17
-        targetCompatibility JavaVersion.VERSION_17
-    }
-    buildFeatures {
-        compose true
-    }
-    composeOptions {
-        kotlinCompilerExtensionVersion jetpack_compose_compiler_version
-    }
-}
-
-dependencies {
-    api project(":spa")
-
-    api "androidx.arch.core:core-testing:2.2.0-alpha01"
-    api "androidx.compose.ui:ui-test-junit4:$jetpack_compose_version"
-    api "com.google.truth:truth:1.1.3"
-    api "org.mockito:mockito-core:2.21.0"
-    debugApi "androidx.compose.ui:ui-test-manifest:$jetpack_compose_version"
-}
diff --git a/packages/SettingsLib/Spa/testutils/build.gradle.kts b/packages/SettingsLib/Spa/testutils/build.gradle.kts
new file mode 100644
index 0000000..f5a22c9
--- /dev/null
+++ b/packages/SettingsLib/Spa/testutils/build.gradle.kts
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+    alias(libs.plugins.android.library)
+    alias(libs.plugins.kotlin.android)
+}
+
+val jetpackComposeVersion: String? by extra
+
+android {
+    namespace = "com.android.settingslib.spa.testutils"
+
+    sourceSets {
+        sourceSets.getByName("main") {
+            kotlin.setSrcDirs(listOf("src"))
+            manifest.srcFile("AndroidManifest.xml")
+        }
+    }
+    buildFeatures {
+        compose = true
+    }
+}
+
+dependencies {
+    api(project(":spa"))
+
+    api("androidx.arch.core:core-testing:2.2.0-alpha01")
+    api("androidx.compose.ui:ui-test-junit4:$jetpackComposeVersion")
+    api("androidx.lifecycle:lifecycle-runtime-testing")
+    api(libs.truth)
+    api("org.mockito:mockito-core:2.21.0")
+    debugApi("androidx.compose.ui:ui-test-manifest:$jetpackComposeVersion")
+}
diff --git a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/ComposeContentTestRuleExt.kt b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/ComposeContentTestRuleExt.kt
index a5d1f40..0436fc2 100644
--- a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/ComposeContentTestRuleExt.kt
+++ b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/ComposeContentTestRuleExt.kt
@@ -16,13 +16,28 @@
 
 package com.android.settingslib.spa.testutils
 
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.ComposeTimeoutException
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.hasAnyAncestor
 import androidx.compose.ui.test.hasText
 import androidx.compose.ui.test.isDialog
 import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
+import com.android.settingslib.spa.framework.theme.SettingsTheme
 
 /** Blocks until the found a semantics node that match the given condition. */
 fun ComposeContentTestRule.waitUntilExists(matcher: SemanticsMatcher) = waitUntil {
@@ -39,3 +54,39 @@
 /** Finds a text node that within dialog. */
 fun ComposeContentTestRule.onDialogText(text: String): SemanticsNodeInteraction =
     onNode(hasAnyAncestor(isDialog()) and hasText(text))
+
+fun ComposeTestRule.rootWidth(): Dp = onRoot().getUnclippedBoundsInRoot().width
+
+fun ComposeTestRule.rootHeight(): Dp = onRoot().getUnclippedBoundsInRoot().height
+
+/**
+ * Constant to emulate very big but finite constraints
+ */
+private val sizeAssertionMaxSize = 5000.dp
+
+private const val SIZE_ASSERTION_TAG = "containerForSizeAssertion"
+
+fun ComposeContentTestRule.setContentForSizeAssertions(
+    parentMaxWidth: Dp = sizeAssertionMaxSize,
+    parentMaxHeight: Dp = sizeAssertionMaxSize,
+    // TODO : figure out better way to make it flexible
+    content: @Composable () -> Unit
+): SemanticsNodeInteraction {
+    setContent {
+        SettingsTheme {
+            Surface {
+                Box {
+                    Box(
+                        Modifier
+                            .sizeIn(maxWidth = parentMaxWidth, maxHeight = parentMaxHeight)
+                            .testTag(SIZE_ASSERTION_TAG)
+                    ) {
+                        content()
+                    }
+                }
+            }
+        }
+    }
+
+    return onNodeWithTag(SIZE_ASSERTION_TAG)
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
index 2b38b4c..8e0cf89 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
@@ -34,11 +34,11 @@
 /**
  * The repository to load the App List data.
  */
-internal interface AppListRepository {
+interface AppListRepository {
     /** Loads the list of [ApplicationInfo]. */
     suspend fun loadApps(
         userId: Int,
-        showInstantApps: Boolean = false,
+        loadInstantApps: Boolean = false,
         matchAnyUserForAdmin: Boolean = false,
     ): List<ApplicationInfo>
 
@@ -50,6 +50,9 @@
 
     /** Gets the system app package names. */
     fun getSystemPackageNamesBlocking(userId: Int): Set<String>
+
+    /** Loads the list of [ApplicationInfo], and filter base on `isSystemApp`. */
+    suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean): List<ApplicationInfo>
 }
 
 /**
@@ -62,13 +65,13 @@
         AppListRepositoryImpl(context).getSystemPackageNamesBlocking(userId)
 }
 
-internal class AppListRepositoryImpl(private val context: Context) : AppListRepository {
+class AppListRepositoryImpl(private val context: Context) : AppListRepository {
     private val packageManager = context.packageManager
     private val userManager = context.userManager
 
     override suspend fun loadApps(
         userId: Int,
-        showInstantApps: Boolean,
+        loadInstantApps: Boolean,
         matchAnyUserForAdmin: Boolean,
     ): List<ApplicationInfo> = coroutineScope {
         val hiddenSystemModulesDeferred = async {
@@ -86,7 +89,7 @@
         val hiddenSystemModules = hiddenSystemModulesDeferred.await()
         val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await()
         installedApplicationsAsUser.filter { app ->
-            app.isInAppList(showInstantApps, hiddenSystemModules, hideWhenDisabledPackages)
+            app.isInAppList(loadInstantApps, hiddenSystemModules, hideWhenDisabledPackages)
         }
     }
 
@@ -136,17 +139,17 @@
     ): Flow<(app: ApplicationInfo) -> Boolean> =
         userIdFlow.combine(showSystemFlow, ::showSystemPredicate)
 
-    override fun getSystemPackageNamesBlocking(userId: Int) =
-        runBlocking { getSystemPackageNames(userId) }
+    override fun getSystemPackageNamesBlocking(userId: Int) = runBlocking {
+        loadAndFilterApps(userId = userId, isSystemApp = true).map { it.packageName }.toSet()
+    }
 
-    private suspend fun getSystemPackageNames(userId: Int): Set<String> =
-        coroutineScope {
-            val loadAppsDeferred = async { loadApps(userId) }
-            val homeOrLauncherPackages = loadHomeOrLauncherPackages(userId)
-            val showSystemPredicate =
-                { app: ApplicationInfo -> isSystemApp(app, homeOrLauncherPackages) }
-            loadAppsDeferred.await().filter(showSystemPredicate).map { it.packageName }.toSet()
+    override suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean) = coroutineScope {
+        val loadAppsDeferred = async { loadApps(userId) }
+        val homeOrLauncherPackages = loadHomeOrLauncherPackages(userId)
+        loadAppsDeferred.await().filter { app ->
+            isSystemApp(app, homeOrLauncherPackages) == isSystemApp
         }
+    }
 
     private suspend fun showSystemPredicate(
         userId: Int,
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
index 030b70a7..07e4235 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
@@ -42,7 +42,6 @@
     showInstantApps: Boolean = false,
     noMoreOptions: Boolean = false,
     matchAnyUserForAdmin: Boolean = false,
-    primaryUserOnly: Boolean = false,
     noItemMessage: String? = null,
     moreOptions: @Composable MoreOptionsScope.() -> Unit = {},
     header: @Composable () -> Unit = {},
@@ -60,7 +59,7 @@
             }
         },
     ) { bottomPadding, searchQuery ->
-        UserProfilePager(primaryUserOnly) { userGroup ->
+        UserProfilePager { userGroup ->
             val appListInput = AppListInput(
                 config = AppListConfig(
                     userIds = userGroup.userInfos.map { it.id },
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
index b5a4929..6a76c93 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
@@ -38,14 +38,9 @@
 )
 
 @Composable
-fun UserProfilePager(
-    primaryUserOnly: Boolean = false,
-    content: @Composable (userGroup: UserGroup) -> Unit,
-) {
+fun UserProfilePager(content: @Composable (userGroup: UserGroup) -> Unit) {
     val context = LocalContext.current
-    val userGroups = remember {
-        context.userManager.getUserGroups(primaryUserOnly)
-    }
+    val userGroups = remember { context.userManager.getUserGroups() }
     val titles = remember {
         val enterpriseRepository = EnterpriseRepository(context)
         userGroups.map { userGroup ->
@@ -60,10 +55,9 @@
     }
 }
 
-private fun UserManager.getUserGroups(primaryUserOnly: Boolean): List<UserGroup> {
+private fun UserManager.getUserGroups(): List<UserGroup> {
     val userGroupList = mutableListOf<UserGroup>()
     val profileToShowInSettingsList = getProfiles(UserHandle.myUserId())
-        .filter { userInfo -> !primaryUserOnly || userInfo.isPrimary }
         .map { userInfo -> userInfo to getUserProperties(userInfo.userHandle).showInSettings }
 
     profileToShowInSettingsList.filter { it.second == UserProperties.SHOW_IN_SETTINGS_WITH_PARENT }
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
index 302f780..375ed60 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
@@ -108,7 +108,7 @@
 
         val appList = repository.loadApps(
             userId = ADMIN_USER_ID,
-            showInstantApps = false,
+            loadInstantApps = false,
         )
 
         assertThat(appList).containsExactly(NORMAL_APP)
@@ -120,7 +120,7 @@
 
         val appList = repository.loadApps(
             userId = ADMIN_USER_ID,
-            showInstantApps = true,
+            loadInstantApps = true,
         )
 
         assertThat(appList).containsExactly(NORMAL_APP, INSTANT_APP)
@@ -325,6 +325,21 @@
         assertThat(systemPackageNames).containsExactly(SYSTEM_APP.packageName)
     }
 
+    @Test
+    fun loadAndFilterApps_loadNonSystemApp_returnExpectedValues() = runTest {
+        mockInstalledApplications(
+            apps = listOf(
+                NORMAL_APP, INSTANT_APP, SYSTEM_APP, UPDATED_SYSTEM_APP, HOME_APP, IN_LAUNCHER_APP
+            ),
+            userId = ADMIN_USER_ID,
+        )
+
+        val appList = repository.loadAndFilterApps(userId = ADMIN_USER_ID, isSystemApp = false)
+
+        assertThat(appList)
+            .containsExactly(NORMAL_APP, UPDATED_SYSTEM_APP, HOME_APP, IN_LAUNCHER_APP)
+    }
+
     private suspend fun getShowSystemPredicate(showSystem: Boolean) =
         repository.showSystemPredicate(
             userIdFlow = flowOf(ADMIN_USER_ID),
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt
index 6889e5d..9b22497 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt
@@ -87,7 +87,7 @@
     private object FakeAppListRepository : AppListRepository {
         override suspend fun loadApps(
             userId: Int,
-            showInstantApps: Boolean,
+            loadInstantApps: Boolean,
             matchAnyUserForAdmin: Boolean,
         ) = listOf(APP)
 
@@ -97,6 +97,9 @@
         ): Flow<(app: ApplicationInfo) -> Boolean> = flowOf { true }
 
         override fun getSystemPackageNamesBlocking(userId: Int): Set<String> = emptySet()
+
+        override suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean) =
+            emptyList<ApplicationInfo>()
     }
 
     private object FakeAppRepository : AppRepository {
diff --git a/packages/SettingsLib/res/drawable/dialog_btn_filled.xml b/packages/SettingsLib/res/drawable/dialog_btn_filled.xml
index 14cb1de9..0614f9d 100644
--- a/packages/SettingsLib/res/drawable/dialog_btn_filled.xml
+++ b/packages/SettingsLib/res/drawable/dialog_btn_filled.xml
@@ -22,12 +22,12 @@
         <item android:id="@android:id/mask">
             <shape android:shape="rectangle">
                 <solid android:color="@android:color/white"/>
-                <corners android:radius="?android:attr/buttonCornerRadius"/>
+                <corners android:radius="@dimen/button_corner_radius"/>
             </shape>
         </item>
         <item>
             <shape android:shape="rectangle">
-                <corners android:radius="?android:attr/buttonCornerRadius"/>
+                <corners android:radius="@dimen/button_corner_radius"/>
                 <solid android:color="?androidprv:attr/colorAccentPrimary"/>
                 <padding android:left="@dimen/dialog_button_horizontal_padding"
                          android:top="@dimen/dialog_button_vertical_padding"
diff --git a/packages/SettingsLib/res/drawable/dialog_btn_outline.xml b/packages/SettingsLib/res/drawable/dialog_btn_outline.xml
index 1e77759..a920b50 100644
--- a/packages/SettingsLib/res/drawable/dialog_btn_outline.xml
+++ b/packages/SettingsLib/res/drawable/dialog_btn_outline.xml
@@ -22,16 +22,15 @@
         <item android:id="@android:id/mask">
             <shape android:shape="rectangle">
                 <solid android:color="@android:color/white"/>
-                <corners android:radius="?android:attr/buttonCornerRadius"/>
+                <corners android:radius="@dimen/button_corner_radius"/>
             </shape>
         </item>
         <item>
             <shape android:shape="rectangle">
-                <corners android:radius="?android:attr/buttonCornerRadius"/>
+                <corners android:radius="@dimen/button_corner_radius"/>
                 <solid android:color="@android:color/transparent"/>
                 <stroke android:color="?androidprv:attr/colorAccentPrimaryVariant"
-                        android:width="1dp"
-                />
+                        android:width="1dp"/>
                 <padding android:left="@dimen/dialog_button_horizontal_padding"
                          android:top="@dimen/dialog_button_vertical_padding"
                          android:right="@dimen/dialog_button_horizontal_padding"
diff --git a/packages/SettingsLib/res/layout/dialog_with_icon.xml b/packages/SettingsLib/res/layout/dialog_with_icon.xml
index 54f8096..3586dcb 100644
--- a/packages/SettingsLib/res/layout/dialog_with_icon.xml
+++ b/packages/SettingsLib/res/layout/dialog_with_icon.xml
@@ -33,15 +33,14 @@
             android:importantForAccessibility="no"/>
         <TextView
             android:id="@+id/dialog_with_icon_title"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:gravity="center"
             style="@style/DialogWithIconTitle"/>
         <TextView
             android:id="@+id/dialog_with_icon_message"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:padding="10dp"
             android:gravity="center"
             style="@style/TextAppearanceSmall"/>
 
@@ -64,7 +63,6 @@
                 android:id="@+id/button_cancel"
                 style="@style/DialogButtonNegative"
                 android:layout_width="wrap_content"
-                android:buttonCornerRadius="28dp"
                 android:layout_height="wrap_content"
                 android:visibility="gone"/>
 
@@ -79,7 +77,6 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 style="@style/DialogButtonNegative"
-                android:buttonCornerRadius="40dp"
                 android:visibility="gone"/>
 
             <Space
@@ -93,8 +90,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 style="@style/DialogButtonPositive"
-                android:visibility="gone"
-            />
+                android:visibility="gone"/>
         </LinearLayout>
     </LinearLayout>
 </ScrollView>
diff --git a/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml b/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml
index 4ffaf1b..2ded3c6 100644
--- a/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml
+++ b/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml
@@ -14,62 +14,48 @@
      limitations under the License.
 -->
 
-<ScrollView
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/user_info_editor"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:id="@+id/user_info_scroll"
-    android:padding="16dp">
-
-    <LinearLayout
-        android:layout_width="match_parent"
+    android:baselineAligned="false"
+    android:orientation="vertical">
+    <FrameLayout
+        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:baselineAligned="false"
-        android:orientation="vertical">
-        <TextView
-            android:id="@+id/user_info_title"
-            android:gravity="center"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            style="@style/EditUserDialogTitle"
-            android:text="@string/user_info_settings_title"
-            android:textDirection="locale"/>
-        <FrameLayout
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center">
-            <ImageView
-                android:id="@+id/user_photo"
-                android:layout_width="@dimen/user_photo_size_in_user_info_dialog"
-                android:layout_height="@dimen/user_photo_size_in_user_info_dialog"
-                android:contentDescription="@string/user_image_photo_selector"
-                android:scaleType="fitCenter"/>
-            <ImageView
-                android:id="@+id/add_a_photo_icon"
-                android:layout_width="@dimen/add_a_photo_icon_size_in_user_info_dialog"
-                android:layout_height="@dimen/add_a_photo_icon_size_in_user_info_dialog"
-                android:src="@drawable/add_a_photo_circled"
-                android:layout_gravity="bottom|right"/>
-        </FrameLayout>
+        android:layout_gravity="center">
+        <ImageView
+            android:id="@+id/user_photo"
+            android:layout_width="@dimen/user_photo_size_in_user_info_dialog"
+            android:layout_height="@dimen/user_photo_size_in_user_info_dialog"
+            android:contentDescription="@string/user_image_photo_selector"
+            android:scaleType="fitCenter"/>
+        <ImageView
+            android:id="@+id/add_a_photo_icon"
+            android:layout_width="@dimen/add_a_photo_icon_size_in_user_info_dialog"
+            android:layout_height="@dimen/add_a_photo_icon_size_in_user_info_dialog"
+            android:src="@drawable/add_a_photo_circled"
+            android:layout_gravity="bottom|right"/>
+    </FrameLayout>
 
-        <EditText
-            android:id="@+id/user_name"
-            android:layout_width="match_parent"
-            android:layout_height="@dimen/user_name_height_in_user_info_dialog"
-            android:layout_gravity="center"
-            android:minWidth="200dp"
-            android:layout_marginStart="6dp"
-            android:minHeight="@dimen/min_tap_target_size"
-            android:ellipsize="end"
-            android:singleLine="true"
-            android:textAppearance="?android:attr/textAppearanceMedium"
-            android:textAlignment="viewStart"
-            android:inputType="text|textCapWords"
-            android:selectAllOnFocus="true"
-            android:hint="@string/user_nickname"
-            android:maxLength="100"/>
+    <EditText
+        android:id="@+id/user_name"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/user_name_height_in_user_info_dialog"
+        android:layout_gravity="center"
+        android:minWidth="200dp"
+        android:layout_marginStart="6dp"
+        android:minHeight="@dimen/min_tap_target_size"
+        android:ellipsize="end"
+        android:singleLine="true"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:textAlignment="viewStart"
+        android:inputType="text|textCapWords"
+        android:selectAllOnFocus="true"
+        android:hint="@string/user_nickname"
+        android:maxLength="100"/>
 
-    </LinearLayout>
+</LinearLayout>
 
-</ScrollView>
 
diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml
index 91549d7..07854bd 100644
--- a/packages/SettingsLib/res/values/dimens.xml
+++ b/packages/SettingsLib/res/values/dimens.xml
@@ -124,4 +124,5 @@
     <dimen name="dialog_bottom_padding">18dp</dimen>
     <dimen name="dialog_side_padding">24dp</dimen>
     <dimen name="dialog_button_bar_top_padding">32dp</dimen>
+    <dimen name="button_corner_radius">28dp</dimen>
 </resources>
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index b44c0e0..60bc226 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -241,12 +241,12 @@
     <!-- Bluetooth settings. Similar to bluetooth_profile_a2dp_high_quality, but used when the device supports high quality audio but we don't know which codec that will be used. -->
     <string name="bluetooth_profile_a2dp_high_quality_unknown_codec">HD audio</string>
 
-    <!-- Bluetooth settings.  The user-visible string that is used whenever referring to the Hearing Aid profile. -->
-    <string name="bluetooth_profile_hearing_aid">Hearing Aids</string>
+    <!-- Bluetooth settings.  The user-visible string that is used whenever referring to the Hearing aid profile. -->
+    <string name="bluetooth_profile_hearing_aid">Hearing aids</string>
     <!-- Bluetooth settings.  The user-visible string that is used whenever referring to the LE audio profile. -->
     <string name="bluetooth_profile_le_audio">LE Audio</string>
-    <!-- Bluetooth settings.  Connection options screen.  The summary for the Hearing Aid checkbox preference when Hearing Aid is connected. -->
-    <string name="bluetooth_hearing_aid_profile_summary_connected">Connected to Hearing Aids</string>
+    <!-- Bluetooth settings.  Connection options screen.  The summary for the Hearing aid checkbox preference when hearing aid is connected. -->
+    <string name="bluetooth_hearing_aid_profile_summary_connected">Connected to hearing aids</string>
     <!-- Bluetooth settings.  Connection options screen.  The summary for the LE audio checkbox preference when LE audio is connected. -->
     <string name="bluetooth_le_audio_profile_summary_connected">Connected to LE audio</string>
 
@@ -287,8 +287,8 @@
          for the HID checkbox preference that describes how checking it
          will set the HID profile as preferred. -->
     <string name="bluetooth_hid_profile_summary_use_for">Use for input</string>
-    <!-- Bluetooth settings.  Connection options screen.  The summary for the Hearing Aid checkbox preference that describes how checking it will set the Hearing Aid profile as preferred. -->
-    <string name="bluetooth_hearing_aid_profile_summary_use_for">Use for Hearing Aids</string>
+    <!-- Bluetooth settings.  Connection options screen.  The summary for the Hearing aid checkbox preference that describes how checking it will set the hearing aid related profile as preferred. -->
+    <string name="bluetooth_hearing_aid_profile_summary_use_for">Use for hearing aids</string>
     <!-- Bluetooth settings.  Connection options screen.  The summary for the LE_AUDIO checkbox preference that describes how checking it will set the LE_AUDIO profile as preferred. -->
     <string name="bluetooth_le_audio_profile_summary_use_for">Use for LE_AUDIO</string>
 
@@ -827,6 +827,11 @@
     <!-- UI debug setting: show touches location summary [CHAR LIMIT=50] -->
     <string name="show_touches_summary">Show visual feedback for taps</string>
 
+    <!-- UI debug setting: show key presses? [CHAR LIMIT=50] -->
+    <string name="show_key_presses">Show key presses</string>
+    <!-- UI debug setting: show physical key presses summary [CHAR LIMIT=150] -->
+    <string name="show_key_presses_summary">Show visual feedback for physical key presses</string>
+
     <!-- UI debug setting: show where surface updates happen? [CHAR LIMIT=25] -->
     <string name="show_screen_updates">Show surface updates</string>
     <!-- UI debug setting: show surface updates summary [CHAR LIMIT=50] -->
diff --git a/packages/SettingsLib/res/values/styles.xml b/packages/SettingsLib/res/values/styles.xml
index 4ac4aae..1249c6b 100644
--- a/packages/SettingsLib/res/values/styles.xml
+++ b/packages/SettingsLib/res/values/styles.xml
@@ -93,7 +93,6 @@
     </style>
 
     <style name="DialogButtonPositive">
-        <item name="android:buttonCornerRadius">0dp</item>
         <item name="android:background">@drawable/dialog_btn_filled</item>
         <item name="android:textColor">?androidprv:attr/textColorOnAccent</item>
         <item name="android:textSize">14sp</item>
@@ -104,7 +103,6 @@
     </style>
 
     <style name="DialogButtonNegative">
-        <item name="android:buttonCornerRadius">28dp</item>
         <item name="android:background">@drawable/dialog_btn_outline</item>
         <item name="android:textColor">?android:attr/textColorPrimary</item>
         <item name="android:textSize">14sp</item>
diff --git a/packages/SettingsLib/search/Android.bp b/packages/SettingsLib/search/Android.bp
index cfff519..918d696 100644
--- a/packages/SettingsLib/search/Android.bp
+++ b/packages/SettingsLib/search/Android.bp
@@ -7,8 +7,18 @@
     default_applicable_licenses: ["frameworks_base_license"],
 }
 
+java_library {
+    name: "SettingsLib-search-interface",
+    visibility: ["//visibility:private"],
+    srcs: ["interface-src/**/*.java"],
+    host_supported: true,
+}
+
 android_library {
     name: "SettingsLib-search",
+    static_libs: [
+        "SettingsLib-search-interface",
+    ],
     srcs: ["src/**/*.java"],
 
     sdk_version: "system_current",
@@ -19,12 +29,10 @@
     name: "SettingsLib-annotation-processor",
     processor_class: "com.android.settingslib.search.IndexableProcessor",
     static_libs: [
+        "SettingsLib-search-interface",
         "javapoet",
     ],
-    srcs: [
-        "processor-src/**/*.java",
-        "src/com/android/settingslib/search/SearchIndexable.java",
-    ],
+    srcs: ["processor-src/**/*.java"],
     java_resource_dirs: ["resources"],
 }
 
diff --git a/packages/SettingsLib/search/common.mk b/packages/SettingsLib/search/common.mk
deleted file mode 100644
index 05226db..0000000
--- a/packages/SettingsLib/search/common.mk
+++ /dev/null
@@ -1,10 +0,0 @@
-# Include this file to generate SearchIndexableResourcesImpl
-
-LOCAL_ANNOTATION_PROCESSORS += \
-    SettingsLib-annotation-processor
-
-LOCAL_ANNOTATION_PROCESSOR_CLASSES += \
-    com.android.settingslib.search.IndexableProcessor
-
-LOCAL_STATIC_JAVA_LIBRARIES += \
-    SettingsLib-search
diff --git a/packages/SettingsLib/search/interface-src/com/android/settingslib/search/SearchIndexable.java b/packages/SettingsLib/search/interface-src/com/android/settingslib/search/SearchIndexable.java
new file mode 100644
index 0000000..174f337
--- /dev/null
+++ b/packages/SettingsLib/search/interface-src/com/android/settingslib/search/SearchIndexable.java
@@ -0,0 +1,65 @@
+/*
+ * 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.settingslib.search;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Denotes that the class should participate in search indexing.
+ */
+@Retention(RetentionPolicy.SOURCE)
+public @interface SearchIndexable {
+    /**
+     * Bitfield for the form factors this class should be considered indexable for.
+     * Default is {@link #ALL}.
+     * <p>
+     * TODO: actually use this value somehow
+     */
+    int forTarget() default ALL;
+
+    /**
+     * Indicates that the class should be considered indexable for Mobile.
+     */
+    int MOBILE = 1 << 0;
+
+    /**
+     * Indicates that the class should be considered indexable for TV.
+     */
+    int TV = 1 << 1;
+
+    /**
+     * Indicates that the class should be considered indexable for Wear.
+     */
+    int WEAR = 1 << 2;
+
+    /**
+     * Indicates that the class should be considered indexable for Auto.
+     */
+    int AUTO = 1 << 3;
+
+    /**
+     * Indicates that the class should be considered indexable for ARC++.
+     */
+    int ARC = 1 << 4;
+
+    /**
+     * Indicates that the class should be considered indexable for all targets.
+     */
+    int ALL = MOBILE | TV | WEAR | AUTO | ARC;
+
+}
diff --git a/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java b/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java
index e92157e..fa43915 100644
--- a/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java
+++ b/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java
@@ -34,6 +34,7 @@
 import javax.annotation.processing.ProcessingEnvironment;
 import javax.annotation.processing.RoundEnvironment;
 import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedOptions;
 import javax.annotation.processing.SupportedSourceVersion;
 import javax.lang.model.SourceVersion;
 import javax.lang.model.element.Element;
@@ -48,10 +49,11 @@
  * subclasses.
  */
 @SupportedSourceVersion(SourceVersion.RELEASE_17)
+@SupportedOptions(IndexableProcessor.PACKAGE_KEY)
 @SupportedAnnotationTypes({"com.android.settingslib.search.SearchIndexable"})
 public class IndexableProcessor extends AbstractProcessor {
 
-    private static final String PACKAGE = "com.android.settingslib.search";
+    private static final String SETTINGSLIB_SEARCH_PACKAGE = "com.android.settingslib.search";
     private static final String CLASS_BASE = "SearchIndexableResourcesBase";
     private static final String CLASS_MOBILE = "SearchIndexableResourcesMobile";
     private static final String CLASS_TV = "SearchIndexableResourcesTv";
@@ -59,6 +61,9 @@
     private static final String CLASS_AUTO = "SearchIndexableResourcesAuto";
     private static final String CLASS_ARC = "SearchIndexableResourcesArc";
 
+    static final String PACKAGE_KEY = "com.android.settingslib.search.processor.package";
+
+    private String mPackage;
     private Filer mFiler;
     private Messager mMessager;
     private boolean mRanOnce;
@@ -72,7 +77,8 @@
         }
         mRanOnce = true;
 
-        final ClassName searchIndexableData = ClassName.get(PACKAGE, "SearchIndexableData");
+        final ClassName searchIndexableData =
+                ClassName.get(SETTINGSLIB_SEARCH_PACKAGE, "SearchIndexableData");
 
         final FieldSpec providers = FieldSpec.builder(
                 ParameterizedTypeName.get(
@@ -130,7 +136,7 @@
                         builder = arcConstructorBuilder;
                     }
                     builder.addCode(
-                            "$N(new SearchIndexableData($L.class, $L"
+                            "$N(new com.android.settingslib.search.SearchIndexableData($L.class, $L"
                                     + ".SEARCH_INDEX_DATA_PROVIDER));\n",
                             addIndex, className, className);
                 } else {
@@ -150,50 +156,51 @@
 
         final TypeSpec baseClass = TypeSpec.classBuilder(CLASS_BASE)
                 .addModifiers(Modifier.PUBLIC)
-                .addSuperinterface(ClassName.get(PACKAGE, "SearchIndexableResources"))
+                .addSuperinterface(
+                        ClassName.get(SETTINGSLIB_SEARCH_PACKAGE, "SearchIndexableResources"))
                 .addField(providers)
                 .addMethod(baseConstructorBuilder.build())
                 .addMethod(addIndex)
                 .addMethod(getProviderValues)
                 .build();
-        final JavaFile searchIndexableResourcesBase = JavaFile.builder(PACKAGE, baseClass).build();
+        final JavaFile searchIndexableResourcesBase = JavaFile.builder(mPackage, baseClass).build();
 
-        final JavaFile searchIndexableResourcesMobile = JavaFile.builder(PACKAGE,
+        final JavaFile searchIndexableResourcesMobile = JavaFile.builder(mPackage,
                 TypeSpec.classBuilder(CLASS_MOBILE)
                         .addModifiers(Modifier.PUBLIC)
-                        .superclass(ClassName.get(PACKAGE, baseClass.name))
+                        .superclass(ClassName.get(mPackage, baseClass.name))
                         .addMethod(mobileConstructorBuilder.build())
                         .build())
                 .build();
 
-        final JavaFile searchIndexableResourcesTv = JavaFile.builder(PACKAGE,
+        final JavaFile searchIndexableResourcesTv = JavaFile.builder(mPackage,
                 TypeSpec.classBuilder(CLASS_TV)
                         .addModifiers(Modifier.PUBLIC)
-                        .superclass(ClassName.get(PACKAGE, baseClass.name))
+                        .superclass(ClassName.get(mPackage, baseClass.name))
                         .addMethod(tvConstructorBuilder.build())
                         .build())
                 .build();
 
-        final JavaFile searchIndexableResourcesWear = JavaFile.builder(PACKAGE,
+        final JavaFile searchIndexableResourcesWear = JavaFile.builder(mPackage,
                 TypeSpec.classBuilder(CLASS_WEAR)
                         .addModifiers(Modifier.PUBLIC)
-                        .superclass(ClassName.get(PACKAGE, baseClass.name))
+                        .superclass(ClassName.get(mPackage, baseClass.name))
                         .addMethod(wearConstructorBuilder.build())
                         .build())
                 .build();
 
-        final JavaFile searchIndexableResourcesAuto = JavaFile.builder(PACKAGE,
+        final JavaFile searchIndexableResourcesAuto = JavaFile.builder(mPackage,
                 TypeSpec.classBuilder(CLASS_AUTO)
                         .addModifiers(Modifier.PUBLIC)
-                        .superclass(ClassName.get(PACKAGE, baseClass.name))
+                        .superclass(ClassName.get(mPackage, baseClass.name))
                         .addMethod(autoConstructorBuilder.build())
                         .build())
                 .build();
 
-        final JavaFile searchIndexableResourcesArc = JavaFile.builder(PACKAGE,
+        final JavaFile searchIndexableResourcesArc = JavaFile.builder(mPackage,
                 TypeSpec.classBuilder(CLASS_ARC)
                         .addModifiers(Modifier.PUBLIC)
-                        .superclass(ClassName.get(PACKAGE, baseClass.name))
+                        .superclass(ClassName.get(mPackage, baseClass.name))
                         .addMethod(arcConstructorBuilder.build())
                         .build())
                 .build();
@@ -214,6 +221,8 @@
     @Override
     public synchronized void init(ProcessingEnvironment processingEnvironment) {
         super.init(processingEnvironment);
+        mPackage = processingEnvironment.getOptions()
+                .getOrDefault(PACKAGE_KEY, SETTINGSLIB_SEARCH_PACKAGE);
         mFiler = processingEnvironment.getFiler();
         mMessager = processingEnvironment.getMessager();
     }
diff --git a/packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexable.java b/packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexable.java
deleted file mode 100644
index 638fa3e9..0000000
--- a/packages/SettingsLib/search/src/com/android/settingslib/search/SearchIndexable.java
+++ /dev/null
@@ -1,65 +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.settingslib.search;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Denotes that the class should participate in search indexing.
- */
-@Retention(RetentionPolicy.SOURCE)
-public @interface SearchIndexable {
-    /**
-     * Bitfield for the form factors this class should be considered indexable for.
-     * Default is {@link #ALL}.
-     *
-     * TODO: actually use this value somehow
-     */
-    int forTarget() default ALL;
-
-    /**
-     * Indicates that the class should be considered indexable for Mobile.
-     */
-    int MOBILE = 1<<0;
-
-    /**
-     * Indicates that the class should be considered indexable for TV.
-     */
-    int TV = 1<<1;
-
-    /**
-     * Indicates that the class should be considered indexable for Wear.
-     */
-    int WEAR = 1<<2;
-
-    /**
-     * Indicates that the class should be considered indexable for Auto.
-     */
-    int AUTO = 1<<3;
-
-    /**
-     * Indicates that the class should be considered indexable for ARC++.
-     */
-    int ARC = 1<<4;
-
-    /**
-     * Indicates that the class should be considered indexable for all targets.
-     */
-    int ALL = MOBILE | TV | WEAR | AUTO | ARC;
-
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
index 64a0781..5d520ce 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
@@ -28,11 +28,11 @@
 public class InterestingConfigChanges {
     private final Configuration mLastConfiguration = new Configuration();
     private final int mFlags;
-    private int mLastDensity;
 
     public InterestingConfigChanges() {
         this(ActivityInfo.CONFIG_LOCALE | ActivityInfo.CONFIG_LAYOUT_DIRECTION
-                | ActivityInfo.CONFIG_UI_MODE | ActivityInfo.CONFIG_ASSETS_PATHS);
+                | ActivityInfo.CONFIG_UI_MODE | ActivityInfo.CONFIG_ASSETS_PATHS
+                | ActivityInfo.CONFIG_DENSITY);
     }
 
     public InterestingConfigChanges(int flags) {
@@ -50,11 +50,6 @@
     public boolean applyNewConfig(Resources res) {
         int configChanges = mLastConfiguration.updateFrom(
                 Configuration.generateDelta(mLastConfiguration, res.getConfiguration()));
-        boolean densityChanged = mLastDensity != res.getDisplayMetrics().densityDpi;
-        if (densityChanged || (configChanges & (mFlags)) != 0) {
-            mLastDensity = res.getDisplayMetrics().densityDpi;
-            return true;
-        }
-        return false;
+        return (configChanges & (mFlags)) != 0;
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index 79fb566..a05a6e9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -237,7 +237,8 @@
                     "startBroadcast: language = " + language + " ,programInfo = " + programInfo);
         }
         buildContentMetadata(language, programInfo);
-        mService.startBroadcast(mBluetoothLeAudioContentMetadata, mBroadcastCode);
+        mService.startBroadcast(mBluetoothLeAudioContentMetadata,
+                (mBroadcastCode != null && mBroadcastCode.length > 0) ? mBroadcastCode : null);
     }
 
     public String getProgramInfo() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
index b0392be..bb103b8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
@@ -221,6 +221,27 @@
     }
 
     /**
+     * Stops an ongoing search for nearby Broadcast Sources.
+     *
+     * On success, {@link BluetoothLeBroadcastAssistant.Callback#onSearchStopped(int)} will be
+     * called with reason code {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}.
+     * On failure, {@link BluetoothLeBroadcastAssistant.Callback#onSearchStopFailed(int)} will be
+     * called with reason code
+     *
+     * @throws IllegalStateException if callback was not registered
+     */
+    public void stopSearchingForSources() {
+        if (DEBUG) {
+            Log.d(TAG, "stopSearchingForSources()");
+        }
+        if (mService == null) {
+            Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
+            return;
+        }
+        mService.stopSearchingForSources();
+    }
+
+    /**
      * Get information about all Broadcast Sources that a Broadcast Sink knows about.
      *
      * @param sink Broadcast Sink from which to get all Broadcast Sources
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java
index 9567a3b..ea65ade 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java
@@ -1,3 +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.settingslib.core;
 
 import android.app.admin.DevicePolicyManager;
@@ -6,8 +22,11 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.EmptySuper;
+import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.core.os.BuildCompat;
+import androidx.lifecycle.LifecycleOwner;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceGroup;
 import androidx.preference.PreferenceScreen;
@@ -52,6 +71,13 @@
     }
 
     /**
+     * Called on view created.
+     */
+    @EmptySuper
+    public void onViewCreated(@NonNull LifecycleOwner viewLifecycleOwner) {
+    }
+
+    /**
      * Updates the current status of preference (summary, switch state, etc)
      */
     public void updateState(Preference preference) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
index e61c8f5..997d1f4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
@@ -59,6 +59,7 @@
     private static final String KEY_IS_ADMIN = "admin_status";
     private static final String KEY_ADD_USER_LONG_MESSAGE_DISPLAYED =
             "key_add_user_long_message_displayed";
+    public static final int MESSAGE_PADDING = 10;
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({EXIT_DIALOG, INITIAL_DIALOG, GRANT_ADMIN_DIALOG,
@@ -191,6 +192,7 @@
             cancelCallback.run();
             clear();
         });
+        mCustomDialogHelper.setMessagePadding(MESSAGE_PADDING);
         mUserCreationDialog.setCanceledOnTouchOutside(true);
         return mUserCreationDialog;
     }
@@ -212,7 +214,6 @@
             }
             updateLayout();
         });
-        return;
     }
 
     private void updateLayout() {
@@ -234,7 +235,6 @@
                 }
                 Drawable icon = mActivity.getDrawable(R.drawable.ic_person_add);
                 mCustomDialogHelper.setVisibility(mCustomDialogHelper.ICON, true)
-                        .setVisibility(mCustomDialogHelper.TITLE, true)
                         .setVisibility(mCustomDialogHelper.MESSAGE, true)
                         .setIcon(icon)
                         .setButtonEnabled(true)
@@ -248,7 +248,6 @@
                 mGrantAdminView.setVisibility(View.VISIBLE);
                 mCustomDialogHelper
                         .setVisibility(mCustomDialogHelper.ICON, true)
-                        .setVisibility(mCustomDialogHelper.TITLE, true)
                         .setVisibility(mCustomDialogHelper.MESSAGE, true)
                         .setIcon(mActivity.getDrawable(R.drawable.ic_admin_panel_settings))
                         .setTitle(R.string.user_grant_admin_title)
@@ -262,8 +261,8 @@
             case EDIT_NAME_DIALOG:
                 mCustomDialogHelper
                         .setVisibility(mCustomDialogHelper.ICON, false)
-                        .setVisibility(mCustomDialogHelper.TITLE, false)
                         .setVisibility(mCustomDialogHelper.MESSAGE, false)
+                        .setTitle(R.string.user_info_settings_title)
                         .setNegativeButtonText(R.string.back)
                         .setPositiveButtonText(R.string.done);
                 mEditUserInfoView.setVisibility(View.VISIBLE);
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java
index e55d7ea..cd5f597 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java
@@ -17,7 +17,6 @@
 package com.android.settingslib.users;
 
 import android.app.Activity;
-import android.app.AlertDialog;
 import android.app.Dialog;
 import android.content.Context;
 import android.content.Intent;
@@ -31,7 +30,6 @@
 import android.view.WindowManager;
 import android.widget.EditText;
 import android.widget.ImageView;
-import android.widget.ScrollView;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -41,6 +39,7 @@
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.settingslib.RestrictedLockUtilsInternal;
 import com.android.settingslib.drawable.CircleFramedDrawable;
+import com.android.settingslib.utils.CustomDialogHelper;
 
 import java.io.File;
 import java.util.function.BiConsumer;
@@ -128,7 +127,7 @@
      *                        codes to take photo/choose photo/crop photo.
      */
     public Dialog createDialog(Activity activity, ActivityStarter activityStarter,
-            @Nullable Drawable oldUserIcon, String defaultUserName, String title,
+            @Nullable Drawable oldUserIcon, String defaultUserName,
             BiConsumer<String, Drawable> successCallback, Runnable cancelCallback) {
         LayoutInflater inflater = LayoutInflater.from(activity);
         View content = inflater.inflate(R.layout.edit_user_info_dialog_content, null);
@@ -160,10 +159,8 @@
                         userPhotoView);
             }
         }
-        ScrollView scrollView = content.findViewById(R.id.user_info_scroll);
-        scrollView.setClipToOutline(true);
         mEditUserInfoDialog = buildDialog(activity, content, userNameView, oldUserIcon,
-                defaultUserName, title, successCallback, cancelCallback);
+                defaultUserName, successCallback, cancelCallback);
 
         // Make sure the IME is up.
         mEditUserInfoDialog.getWindow()
@@ -181,12 +178,13 @@
     }
 
     private Dialog buildDialog(Activity activity, View content, EditText userNameView,
-            @Nullable Drawable oldUserIcon, String defaultUserName, String title,
+            @Nullable Drawable oldUserIcon, String defaultUserName,
             BiConsumer<String, Drawable> successCallback, Runnable cancelCallback) {
-        return new AlertDialog.Builder(activity)
-                .setView(content)
-                .setCancelable(true)
-                .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+        CustomDialogHelper dialogHelper = new CustomDialogHelper(activity);
+        dialogHelper
+                .setTitle(R.string.user_info_settings_title)
+                .addCustomView(content)
+                .setPositiveButton(android.R.string.ok, view -> {
                     Drawable newUserIcon = mEditUserPhotoController != null
                             ? mEditUserPhotoController.getNewUserPhotoDrawable()
                             : null;
@@ -201,20 +199,23 @@
                     if (successCallback != null) {
                         successCallback.accept(userName, userIcon);
                     }
+                    dialogHelper.getDialog().dismiss();
                 })
-                .setNegativeButton(android.R.string.cancel, (dialog, which) -> {
+                .setBackButton(android.R.string.cancel, view -> {
                     clear();
                     if (cancelCallback != null) {
                         cancelCallback.run();
                     }
-                })
-                .setOnCancelListener(dialog -> {
-                    clear();
-                    if (cancelCallback != null) {
-                        cancelCallback.run();
-                    }
-                })
-                .create();
+                    dialogHelper.getDialog().dismiss();
+                });
+        dialogHelper.getDialog().setOnCancelListener(dialog -> {
+            clear();
+            if (cancelCallback != null) {
+                cancelCallback.run();
+            }
+            dialogHelper.getDialog().dismiss();
+        });
+        return dialogHelper.getDialog();
     }
 
     @VisibleForTesting
diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java b/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java
index de48814..5201b3d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java
+++ b/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java
@@ -193,6 +193,14 @@
     }
 
     /**
+     * Sets message padding of the dialog.
+     */
+    public CustomDialogHelper setMessagePadding(int dp) {
+        mDialogMessage.setPadding(dp, dp, dp, dp);
+        return this;
+    }
+
+    /**
      * Sets icon of the dialog.
      */
     public CustomDialogHelper setIcon(Drawable icon) {
diff --git a/packages/SettingsLib/tests/robotests/Android.bp b/packages/SettingsLib/tests/robotests/Android.bp
index 5c55a43..c037c40 100644
--- a/packages/SettingsLib/tests/robotests/Android.bp
+++ b/packages/SettingsLib/tests/robotests/Android.bp
@@ -42,7 +42,10 @@
     name: "SettingsLibRoboTests",
     srcs: ["src/**/*.java"],
     static_libs: [
+        "Settings_robolectric_meta_service_file",
+        "Robolectric_shadows_androidx_fragment_upstream",
         "SettingsLib-robo-testutils",
+        "androidx.fragment_fragment",
         "androidx.test.core",
         "androidx.core_core",
         "testng", // TODO: remove once JUnit on Android provides assertThrows
@@ -53,6 +56,20 @@
     test_options: {
         timeout: 36000,
     },
+    upstream: true,
+}
+
+java_genrule {
+    name: "Settings_robolectric_meta_service_file",
+    out: ["robolectric_meta_service_file.jar"],
+    tools: ["soong_zip"],
+    cmd: "mkdir -p $(genDir)/META-INF/services/ && touch $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider &&" +
+        "echo -e 'org.robolectric.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " +
+        "echo -e 'org.robolectric.shadows.multidex.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " +
+        "echo -e 'org.robolectric.shadows.httpclient.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " +
+        //"echo -e 'com.android.settings.testutils.shadow.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " +
+        "echo -e 'com.android.settingslib.testutils.shadow.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " +
+        "$(location soong_zip) -o $(out) -C $(genDir) -D $(genDir)/META-INF/services/",
 }
 
 java_library {
@@ -60,9 +77,23 @@
     srcs: [
         "testutils/com/android/settingslib/testutils/**/*.java",
     ],
-
+    javacflags: [
+        "-Aorg.robolectric.annotation.processing.shadowPackage=com.android.settingslib.testutils.shadow",
+        "-Aorg.robolectric.annotation.processing.sdkCheckMode=ERROR",
+        // Uncomment the below to debug annotation processors not firing.
+        //"-verbose",
+        //"-XprintRounds",
+        //"-XprintProcessorInfo",
+        //"-Xlint",
+        //"-J-verbose",
+    ],
+    plugins: [
+        "auto_value_plugin_1.9",
+        "auto_value_builder_plugin_1.9",
+        "Robolectric_processor_upstream",
+    ],
     libs: [
-        "Robolectric_all-target",
+        "Robolectric_all-target_upstream",
         "mockito-robolectric-prebuilt",
         "truth-prebuilt",
     ],
diff --git a/packages/SettingsLib/tests/robotests/config/robolectric.properties b/packages/SettingsLib/tests/robotests/config/robolectric.properties
index fab7251..2a9e50d 100644
--- a/packages/SettingsLib/tests/robotests/config/robolectric.properties
+++ b/packages/SettingsLib/tests/robotests/config/robolectric.properties
@@ -1 +1,2 @@
 sdk=NEWEST_SDK
+instrumentedPackages=androidx.preference
diff --git a/packages/SettingsLib/tests/robotests/fragment/Android.bp b/packages/SettingsLib/tests/robotests/fragment/Android.bp
new file mode 100644
index 0000000..3e67156
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/fragment/Android.bp
@@ -0,0 +1,40 @@
+//#############################################
+// Compile Robolectric shadows framework misapplied to androidx
+//#############################################
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library {
+    name: "Robolectric_shadows_androidx_fragment_upstream",
+    srcs: [
+        "src/main/java/**/*.java",
+        "src/main/java/**/*.kt",
+    ],
+    javacflags: [
+        "-Aorg.robolectric.annotation.processing.shadowPackage=org.robolectric.shadows.androidx.fragment",
+        "-Aorg.robolectric.annotation.processing.sdkCheckMode=ERROR",
+        // Uncomment the below to debug annotation processors not firing.
+        //"-verbose",
+        //"-XprintRounds",
+        //"-XprintProcessorInfo",
+        //"-Xlint",
+        //"-J-verbose",
+    ],
+    libs: [
+        "Robolectric_all-target_upstream",
+        "androidx.fragment_fragment",
+    ],
+    plugins: [
+        "auto_value_plugin_1.9",
+        "auto_value_builder_plugin_1.9",
+        "Robolectric_processor_upstream",
+    ],
+
+}
diff --git a/packages/SettingsLib/tests/robotests/fragment/BUILD b/packages/SettingsLib/tests/robotests/fragment/BUILD
new file mode 100644
index 0000000..393a02e
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/fragment/BUILD
@@ -0,0 +1,69 @@
+load("//third_party/java/android/android_sdk_linux/extras/android/compatibility/jetify:jetify.bzl", "jetify_android_library", "jetify_android_local_test")
+
+package(
+    default_applicable_licenses = ["//third_party/java_src/robolectric:license"],
+    default_visibility = ["//third_party/java_src/robolectric:__subpackages__"],
+)
+
+licenses(["notice"])
+
+#==============================================================================
+# Test resources library
+#==============================================================================
+jetify_android_library(
+    name = "test_resources",
+    custom_package = "org.robolectric.shadows.androidx.fragment",
+    manifest = "src/test/AndroidManifest.xml",
+    resource_files = glob(
+        ["src/test/resources/**/*"],
+    ),
+)
+
+#==============================================================================
+# AndroidX fragment module library
+#==============================================================================
+jetify_android_library(
+    name = "androidx_fragment",
+    testonly = 1,
+    srcs = glob(
+        ["src/main/java/**"],
+    ),
+    custom_package = "org.robolectric.shadows.androidx.fragment",
+    javacopts = [
+        "-Aorg.robolectric.annotation.processing.shadowPackage=org.robolectric.shadows.androidx.fragment",
+    ],
+    jetify_sources = True,
+    plugins = [
+        "//java/com/google/thirdparty/robolectric/processor",
+    ],
+    deps = [
+        "//third_party/java/androidx/core",
+        "//third_party/java/androidx/fragment",
+        "//third_party/java/androidx/lifecycle",
+        "//third_party/java_src/robolectric/shadowapi",
+        "//third_party/java_src/robolectric/shadows/framework",
+    ],
+)
+
+[
+    jetify_android_local_test(
+        name = "test_" + src.rstrip(".java"),
+        size = "small",
+        srcs = glob(
+            ["src/test/java/**/*.java"],
+        ),
+        jetify_sources = True,
+        deps = [
+            ":androidx_fragment",
+            ":test_resources",
+            "//third_party/java/androidx/fragment",
+            "//third_party/java/androidx/loader",
+            "//third_party/java/mockito",
+            "//third_party/java/robolectric",
+            "//third_party/java/truth",
+        ],
+    )
+    for src in glob(
+        ["src/test/java/**/*Test.java"],
+    )
+]
diff --git a/packages/SettingsLib/tests/robotests/fragment/build.gradle b/packages/SettingsLib/tests/robotests/fragment/build.gradle
new file mode 100644
index 0000000..d9dcd84
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/fragment/build.gradle
@@ -0,0 +1,48 @@
+plugins {
+    id "net.ltgt.errorprone" version "0.0.13"
+}
+
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 28
+
+    android {
+        sourceSets {
+            main {
+                res.srcDirs = ['src/test/resources/res']
+            }
+        }
+        testOptions {
+            unitTests {
+                includeAndroidResources = true
+            }
+        }
+    }
+}
+
+dependencies {
+    // Project dependencies
+    compileOnly project(":robolectric")
+
+    // Compile dependencies
+    compileOnly AndroidSdk.MAX_SDK.coordinates
+    compileOnly "androidx.core:core:1.0.0-rc02"
+    compileOnly 'androidx.fragment:fragment:1.0.0-rc02'
+    compileOnly "androidx.lifecycle:lifecycle-viewmodel:2.0.0-rc01"
+    compileOnly "androidx.lifecycle:lifecycle-common:2.0.0-beta01"
+
+    // Testing dependencies
+    testImplementation "com.google.truth:truth:0.44"
+    testImplementation "org.mockito:mockito-core:2.5.4"
+    testImplementation "androidx.arch.core:core-common:2.0.0-beta01"
+    testImplementation "androidx.arch.core:core-runtime:2.0.0-rc01"
+    testImplementation "androidx.collection:collection:1.0.0-rc01"
+    testImplementation "androidx.core:core:1.0.0-rc02"
+    testImplementation 'androidx.fragment:fragment:1.0.0-rc02'
+    testImplementation "androidx.lifecycle:lifecycle-viewmodel:2.0.0-rc01"
+    testImplementation "androidx.lifecycle:lifecycle-common:2.0.0-beta01"
+    testImplementation "androidx.lifecycle:lifecycle-runtime:2.0.0-rc01"
+    testImplementation "androidx.lifecycle:lifecycle-livedata-core:2.0.0-rc01"
+    testImplementation "androidx.loader:loader:1.0.0-rc02"
+}
diff --git a/packages/SettingsLib/tests/robotests/fragment/src/main/java/org/robolectric/shadows/androidx/fragment/FragmentController.java b/packages/SettingsLib/tests/robotests/fragment/src/main/java/org/robolectric/shadows/androidx/fragment/FragmentController.java
new file mode 100644
index 0000000..c688683
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/fragment/src/main/java/org/robolectric/shadows/androidx/fragment/FragmentController.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.shadows.androidx.fragment;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.LinearLayout;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.android.controller.ComponentController;
+import org.robolectric.util.ReflectionHelpers;
+
+/** A Controller that can be used to drive the lifecycle of a {@link Fragment} */
+public class FragmentController<F extends Fragment>
+        extends ComponentController<FragmentController<F>, F> {
+
+    private final F mFragment;
+    private final ActivityController<? extends FragmentActivity> mActivityController;
+
+    private FragmentController(F fragment, Class<? extends FragmentActivity> activityClass) {
+        this(fragment, activityClass, null /*intent*/, null /*arguments*/);
+    }
+
+    private FragmentController(
+            F fragment, Class<? extends FragmentActivity> activityClass, Intent intent) {
+        this(fragment, activityClass, intent, null /*arguments*/);
+    }
+
+    private FragmentController(
+            F fragment, Class<? extends FragmentActivity> activityClass, Bundle arguments) {
+        this(fragment, activityClass, null /*intent*/, arguments);
+    }
+
+    private FragmentController(
+            F fragment,
+            Class<? extends FragmentActivity> activityClass,
+            Intent intent,
+            Bundle arguments) {
+        super(fragment, intent);
+        this.mFragment = fragment;
+        if (arguments != null) {
+            this.mFragment.setArguments(arguments);
+        }
+        this.mActivityController =
+                ActivityController.of(ReflectionHelpers.callConstructor(activityClass), intent);
+    }
+
+    /**
+     * Generate the {@link FragmentController} for specific fragment.
+     *
+     * @param fragment the fragment which you'd like to drive lifecycle
+     * @return {@link FragmentController}
+     */
+    public static <F extends Fragment> FragmentController<F> of(F fragment) {
+        return new FragmentController<>(fragment, FragmentControllerActivity.class);
+    }
+
+    /**
+     * Generate the {@link FragmentController} for specific fragment and intent.
+     *
+     * @param fragment the fragment which you'd like to drive lifecycle
+     * @param intent   the intent which will be retained by activity
+     * @return {@link FragmentController}
+     */
+    public static <F extends Fragment> FragmentController<F> of(F fragment, Intent intent) {
+        return new FragmentController<>(fragment, FragmentControllerActivity.class, intent);
+    }
+
+    /**
+     * Generate the {@link FragmentController} for specific fragment and arguments.
+     *
+     * @param fragment  the fragment which you'd like to drive lifecycle
+     * @param arguments the arguments which will be retained by fragment
+     * @return {@link FragmentController}
+     */
+    public static <F extends Fragment> FragmentController<F> of(F fragment, Bundle arguments) {
+        return new FragmentController<>(fragment, FragmentControllerActivity.class, arguments);
+    }
+
+    /**
+     * Generate the {@link FragmentController} for specific fragment and activity class.
+     *
+     * @param fragment      the fragment which you'd like to drive lifecycle
+     * @param activityClass the activity which will be attached by fragment
+     * @return {@link FragmentController}
+     */
+    public static <F extends Fragment> FragmentController<F> of(
+            F fragment, Class<? extends FragmentActivity> activityClass) {
+        return new FragmentController<>(fragment, activityClass);
+    }
+
+    /**
+     * Generate the {@link FragmentController} for specific fragment, intent and arguments.
+     *
+     * @param fragment  the fragment which you'd like to drive lifecycle
+     * @param intent    the intent which will be retained by activity
+     * @param arguments the arguments which will be retained by fragment
+     * @return {@link FragmentController}
+     */
+    public static <F extends Fragment> FragmentController<F> of(
+            F fragment, Intent intent, Bundle arguments) {
+        return new FragmentController<>(fragment, FragmentControllerActivity.class, intent,
+                arguments);
+    }
+
+    /**
+     * Generate the {@link FragmentController} for specific fragment, activity class and intent.
+     *
+     * @param fragment      the fragment which you'd like to drive lifecycle
+     * @param activityClass the activity which will be attached by fragment
+     * @param intent        the intent which will be retained by activity
+     * @return {@link FragmentController}
+     */
+    public static <F extends Fragment> FragmentController<F> of(
+            F fragment, Class<? extends FragmentActivity> activityClass, Intent intent) {
+        return new FragmentController<>(fragment, activityClass, intent);
+    }
+
+    /**
+     * Generate the {@link FragmentController} for specific fragment, activity class and arguments.
+     *
+     * @param fragment      the fragment which you'd like to drive lifecycle
+     * @param activityClass the activity which will be attached by fragment
+     * @param arguments     the arguments which will be retained by fragment
+     * @return {@link FragmentController}
+     */
+    public static <F extends Fragment> FragmentController<F> of(
+            F fragment, Class<? extends FragmentActivity> activityClass, Bundle arguments) {
+        return new FragmentController<>(fragment, activityClass, arguments);
+    }
+
+    /**
+     * Generate the {@link FragmentController} for specific fragment, activity class, intent and
+     * arguments.
+     *
+     * @param fragment      the fragment which you'd like to drive lifecycle
+     * @param activityClass the activity which will be attached by fragment
+     * @param intent        the intent which will be retained by activity
+     * @param arguments     the arguments which will be retained by fragment
+     * @return {@link FragmentController}
+     */
+    public static <F extends Fragment> FragmentController<F> of(
+            F fragment,
+            Class<? extends FragmentActivity> activityClass,
+            Intent intent,
+            Bundle arguments) {
+        return new FragmentController<>(fragment, activityClass, intent, arguments);
+    }
+
+    /**
+     * Sets up the given fragment by attaching it to an activity, calling its onCreate() through
+     * onResume() lifecycle methods, and then making it visible. Note that the fragment will be
+     * added
+     * to the view with ID 1.
+     */
+    public static <F extends Fragment> F setupFragment(F fragment) {
+        return FragmentController.of(fragment).create().start().resume().visible().get();
+    }
+
+    /**
+     * Sets up the given fragment by attaching it to an activity, calling its onCreate() through
+     * onResume() lifecycle methods, and then making it visible. Note that the fragment will be
+     * added
+     * to the view with ID 1.
+     */
+    public static <F extends Fragment> F setupFragment(
+            F fragment, Class<? extends FragmentActivity> fragmentActivityClass) {
+        return FragmentController.of(fragment, fragmentActivityClass)
+                .create()
+                .start()
+                .resume()
+                .visible()
+                .get();
+    }
+
+    /**
+     * Sets up the given fragment by attaching it to an activity created with the given bundle,
+     * calling its onCreate() through onResume() lifecycle methods, and then making it visible. Note
+     * that the fragment will be added to the view with ID 1.
+     */
+    public static <F extends Fragment> F setupFragment(
+            F fragment, Class<? extends FragmentActivity> fragmentActivityClass, Bundle bundle) {
+        return FragmentController.of(fragment, fragmentActivityClass)
+                .create(bundle)
+                .start()
+                .resume()
+                .visible()
+                .get();
+    }
+
+    /**
+     * Sets up the given fragment by attaching it to an activity created with the given bundle and
+     * container id, calling its onCreate() through onResume() lifecycle methods, and then making it
+     * visible.
+     */
+    public static <F extends Fragment> F setupFragment(
+            F fragment,
+            Class<? extends FragmentActivity> fragmentActivityClass,
+            int containerViewId,
+            Bundle bundle) {
+        return FragmentController.of(fragment, fragmentActivityClass)
+                .create(containerViewId, bundle)
+                .start()
+                .resume()
+                .visible()
+                .get();
+    }
+
+    /**
+     * Creates the activity with {@link Bundle} and adds the fragment to the view with ID {@code
+     * contentViewId}.
+     */
+    public FragmentController<F> create(final int contentViewId, final Bundle bundle) {
+        shadowMainLooper.runPaused(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mActivityController
+                                .create(bundle)
+                                .get()
+                                .getSupportFragmentManager()
+                                .beginTransaction()
+                                .add(contentViewId, mFragment)
+                                .commit();
+                    }
+                });
+        return this;
+    }
+
+    /**
+     * Creates the activity with {@link Bundle} and adds the fragment to it. Note that the fragment
+     * will be added to the view with ID 1.
+     */
+    public FragmentController<F> create(final Bundle bundle) {
+        return create(1, bundle);
+    }
+
+    /**
+     * Creates the {@link Fragment} in a newly initialized state and hence will receive a null
+     * savedInstanceState {@link Bundle parameter}
+     */
+    @Override
+    public FragmentController<F> create() {
+        return create(null);
+    }
+
+    /** Drive lifecycle of activity to Start lifetime */
+    public FragmentController<F> start() {
+        shadowMainLooper.runPaused(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mActivityController.start();
+                    }
+                });
+        return this;
+    }
+
+    /** Drive lifecycle of activity to Resume lifetime */
+    public FragmentController<F> resume() {
+        shadowMainLooper.runPaused(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mActivityController.resume();
+                    }
+                });
+        return this;
+    }
+
+    /** Drive lifecycle of activity to Pause lifetime */
+    public FragmentController<F> pause() {
+        shadowMainLooper.runPaused(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mActivityController.pause();
+                    }
+                });
+        return this;
+    }
+
+    /** Drive lifecycle of activity to Stop lifetime */
+    public FragmentController<F> stop() {
+        shadowMainLooper.runPaused(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mActivityController.stop();
+                    }
+                });
+        return this;
+    }
+
+    /** Drive lifecycle of activity to Destroy lifetime */
+    @Override
+    public FragmentController<F> destroy() {
+        shadowMainLooper.runPaused(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mActivityController.destroy();
+                    }
+                });
+        return this;
+    }
+
+    /** Let activity can be visible lifetime */
+    public FragmentController<F> visible() {
+        shadowMainLooper.runPaused(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mActivityController.visible();
+                    }
+                });
+        return this;
+    }
+
+    private static class FragmentControllerActivity extends FragmentActivity {
+
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            LinearLayout view = new LinearLayout(this);
+            view.setId(1);
+
+            setContentView(view);
+        }
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/fragment/src/main/java/org/robolectric/shadows/androidx/fragment/package-info.java b/packages/SettingsLib/tests/robotests/fragment/src/main/java/org/robolectric/shadows/androidx/fragment/package-info.java
new file mode 100644
index 0000000..dd89441
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/fragment/src/main/java/org/robolectric/shadows/androidx/fragment/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Testing infrastructure for androidx.fragment library.
+ *
+ * <p>To use this in your project, add the artifact {@code
+ * org.robolectric:shadows-androidx-fragment} to your project.
+ */
+package org.robolectric.shadows.androidx.fragment;
diff --git a/packages/SettingsLib/tests/robotests/fragment/src/test/AndroidManifest.xml b/packages/SettingsLib/tests/robotests/fragment/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..8493c02
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/fragment/src/test/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="org.robolectric.shadows.androidx.fragment">
+
+    <uses-sdk android:targetSdkVersion="28"/>
+</manifest>
diff --git a/packages/SettingsLib/tests/robotests/fragment/src/test/java/org/robolectric/shadows/androidx/fragment/FragmentControllerTest.java b/packages/SettingsLib/tests/robotests/fragment/src/test/java/org/robolectric/shadows/androidx/fragment/FragmentControllerTest.java
new file mode 100644
index 0000000..ef63058
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/fragment/src/test/java/org/robolectric/shadows/androidx/fragment/FragmentControllerTest.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.shadows.androidx.fragment;
+
+import static android.os.Looper.getMainLooper;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Tests for {@link FragmentController} */
+@RunWith(RobolectricTestRunner.class)
+public class FragmentControllerTest {
+
+    @After
+    public void tearDown() {
+        TranscriptFragment.clearLifecycleEvents();
+    }
+
+    @Test
+    public void initialNotAttached() {
+        final FragmentController<TranscriptFragment> controller =
+                FragmentController.of(new TranscriptFragment());
+
+        assertThat(controller.get().getView()).isNull();
+        assertThat(controller.get().getActivity()).isNull();
+        assertThat(controller.get().isAdded()).isFalse();
+    }
+
+    @Test
+    public void initialNotAttached_customActivity() {
+        final FragmentController<TranscriptFragment> controller =
+                FragmentController.of(new TranscriptFragment(), TestActivity.class);
+
+        assertThat(controller.get().getView()).isNull();
+        assertThat(controller.get().getActivity()).isNull();
+        assertThat(controller.get().isAdded()).isFalse();
+    }
+
+    @Test
+    public void attachedAfterCreate() {
+        final FragmentController<TranscriptFragment> controller =
+                FragmentController.of(new TranscriptFragment());
+
+        controller.create();
+        shadowOf(getMainLooper()).idle();
+
+        assertThat(controller.get().getActivity()).isNotNull();
+        assertThat(controller.get().isAdded()).isTrue();
+        assertThat(controller.get().isResumed()).isFalse();
+    }
+
+    @Test
+    public void attachedAfterCreate_customActivity() {
+        final FragmentController<TranscriptFragment> controller =
+                FragmentController.of(new TranscriptFragment(), TestActivity.class);
+
+        controller.create();
+        shadowOf(getMainLooper()).idle();
+
+        assertThat(controller.get().getActivity()).isNotNull();
+        assertThat(controller.get().getActivity()).isInstanceOf(TestActivity.class);
+        assertThat(controller.get().isAdded()).isTrue();
+        assertThat(controller.get().isResumed()).isFalse();
+    }
+
+    @Test
+    public void attachedAfterCreate_customizedViewId() {
+        final FragmentController<TranscriptFragment> controller =
+                FragmentController.of(new TranscriptFragment(), CustomizedViewIdTestActivity.class);
+
+        controller.create(R.id.custom_activity_view, null).start();
+
+        assertThat(controller.get().getView()).isNotNull();
+        assertThat(controller.get().getActivity()).isNotNull();
+        assertThat(controller.get().isAdded()).isTrue();
+        assertThat(controller.get().isResumed()).isFalse();
+        assertThat((TextView) controller.get().getView().findViewById(R.id.tacos)).isNotNull();
+    }
+
+    @Test
+    public void hasViewAfterStart() {
+        final FragmentController<TranscriptFragment> controller =
+                FragmentController.of(new TranscriptFragment());
+
+        controller.create().start();
+
+        assertThat(controller.get().getView()).isNotNull();
+    }
+
+    @Test
+    public void isResumed() {
+        final FragmentController<TranscriptFragment> controller =
+                FragmentController.of(new TranscriptFragment(), TestActivity.class);
+
+        controller.create().start().resume();
+
+        assertThat(controller.get().getView()).isNotNull();
+        assertThat(controller.get().getActivity()).isNotNull();
+        assertThat(controller.get().isAdded()).isTrue();
+        assertThat(controller.get().isResumed()).isTrue();
+        assertThat((TextView) controller.get().getView().findViewById(R.id.tacos)).isNotNull();
+    }
+
+    @Test
+    public void isPaused() {
+        final FragmentController<TranscriptFragment> controller =
+                FragmentController.of(new TranscriptFragment(), TestActivity.class);
+
+        controller.create().start().resume().pause();
+
+        assertThat(controller.get().getView()).isNotNull();
+        assertThat(controller.get().getActivity()).isNotNull();
+        assertThat(controller.get().isAdded()).isTrue();
+        assertThat(controller.get().isResumed()).isFalse();
+        assertThat(controller.get().getLifecycleEvents())
+                .containsExactly("onCreate", "onStart", "onResume", "onPause")
+                .inOrder();
+    }
+
+    @Test
+    public void isStopped() {
+        final FragmentController<TranscriptFragment> controller =
+                FragmentController.of(new TranscriptFragment(), TestActivity.class);
+
+        controller.create().start().resume().pause().stop();
+
+        assertThat(controller.get().getView()).isNotNull();
+        assertThat(controller.get().getActivity()).isNotNull();
+        assertThat(controller.get().isAdded()).isTrue();
+        assertThat(controller.get().isResumed()).isFalse();
+        assertThat(controller.get().getLifecycleEvents())
+                .containsExactly("onCreate", "onStart", "onResume", "onPause", "onStop")
+                .inOrder();
+    }
+
+    @Test
+    public void withIntent() {
+        final Intent intent = generateTestIntent();
+        final FragmentController<TranscriptFragment> controller =
+                FragmentController.of(new TranscriptFragment(), TestActivity.class, intent);
+
+        controller.create();
+        shadowOf(getMainLooper()).idle();
+        final Intent intentInFragment = controller.get().getActivity().getIntent();
+
+        assertThat(intentInFragment.getAction()).isEqualTo("test_action");
+        assertThat(intentInFragment.getExtras().getString("test_key")).isEqualTo("test_value");
+    }
+
+    @Test
+    public void withArguments() {
+        final Bundle bundle = generateTestBundle();
+        final FragmentController<TranscriptFragment> controller =
+                FragmentController.of(new TranscriptFragment(), TestActivity.class, bundle);
+
+        controller.create();
+        final Bundle args = controller.get().getArguments();
+
+        assertThat(args.getString("test_key")).isEqualTo("test_value");
+    }
+
+    @Test
+    public void withIntentAndArguments() {
+        final Bundle bundle = generateTestBundle();
+        final Intent intent = generateTestIntent();
+        final FragmentController<TranscriptFragment> controller =
+                FragmentController.of(new TranscriptFragment(), TestActivity.class, intent, bundle);
+
+        controller.create();
+        shadowOf(getMainLooper()).idle();
+        final Intent intentInFragment = controller.get().getActivity().getIntent();
+        final Bundle args = controller.get().getArguments();
+
+        assertThat(intentInFragment.getAction()).isEqualTo("test_action");
+        assertThat(intentInFragment.getExtras().getString("test_key")).isEqualTo("test_value");
+        assertThat(args.getString("test_key")).isEqualTo("test_value");
+    }
+
+    @Test
+    public void visible() {
+        final FragmentController<TranscriptFragment> controller =
+                FragmentController.of(new TranscriptFragment(), TestActivity.class);
+
+        controller.create().start().resume();
+
+        assertThat(controller.get().isVisible()).isFalse();
+
+        controller.visible();
+
+        assertThat(controller.get().isVisible()).isTrue();
+    }
+
+    @Test
+    public void setupFragmentWithFragment_fragmentHasCorrectLifecycle() {
+        TranscriptFragment fragment = FragmentController.setupFragment(new TranscriptFragment());
+
+        assertThat(fragment.getLifecycleEvents())
+                .containsExactly("onCreate", "onStart", "onResume")
+                .inOrder();
+        assertThat(fragment.isVisible()).isTrue();
+    }
+
+    @Test
+    public void setupFragmentWithFragmentAndActivity_fragmentHasCorrectLifecycle() {
+        TranscriptFragment fragment =
+                FragmentController.setupFragment(new TranscriptFragment(), TestActivity.class);
+
+        assertThat(fragment.getLifecycleEvents())
+                .containsExactly("onCreate", "onStart", "onResume")
+                .inOrder();
+        assertThat(fragment.isVisible()).isTrue();
+    }
+
+    @Test
+    public void setupFragmentWithFragmentAndActivityAndBundle_HasCorrectLifecycle() {
+        Bundle testBundle = generateTestBundle();
+        TranscriptFragment fragment =
+                FragmentController.setupFragment(new TranscriptFragment(), TestActivity.class,
+                        testBundle);
+
+        assertThat(fragment.getLifecycleEvents())
+                .containsExactly("onCreate", "onStart", "onResume")
+                .inOrder();
+        assertThat(fragment.isVisible()).isTrue();
+    }
+
+    @Test
+    public void
+            setupFragmentWithFragment_Activity_ContainViewIdAndBundle_HasCorrectLifecycle() {
+        Bundle testBundle = generateTestBundle();
+        TranscriptFragment fragment =
+                FragmentController.setupFragment(
+                        new TranscriptFragment(),
+                        CustomizedViewIdTestActivity.class,
+                        R.id.custom_activity_view,
+                        testBundle);
+
+        assertThat(fragment.getLifecycleEvents())
+                .containsExactly("onCreate", "onStart", "onResume")
+                .inOrder();
+        assertThat(fragment.isVisible()).isTrue();
+    }
+
+    private Intent generateTestIntent() {
+        final Intent testIntent = new Intent("test_action").putExtra("test_key", "test_value");
+        return testIntent;
+    }
+
+    private Bundle generateTestBundle() {
+        final Bundle testBundle = new Bundle();
+        testBundle.putString("test_key", "test_value");
+
+        return testBundle;
+    }
+
+    /** A Fragment which can record lifecycle status for test. */
+    public static class TranscriptFragment extends Fragment {
+
+        public static final List<String> sLifecycleEvents = new ArrayList<>();
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            sLifecycleEvents.add("onCreate");
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            sLifecycleEvents.add("onStart");
+        }
+
+        @Override
+        public void onResume() {
+            super.onResume();
+            sLifecycleEvents.add("onResume");
+        }
+
+        @Override
+        public void onPause() {
+            super.onPause();
+            sLifecycleEvents.add("onPause");
+        }
+
+        @Override
+        public void onStop() {
+            super.onStop();
+            sLifecycleEvents.add("onStop");
+        }
+
+        @Override
+        public View onCreateView(
+                LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+            return inflater.inflate(R.layout.fragment_contents, container, false);
+        }
+
+        public List<String> getLifecycleEvents() {
+            return sLifecycleEvents;
+        }
+
+        public static void clearLifecycleEvents() {
+            sLifecycleEvents.clear();
+        }
+    }
+
+    /** A Activity which set a default view for test. */
+    public static class TestActivity extends FragmentActivity {
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            LinearLayout view = new LinearLayout(this);
+            view.setId(1);
+
+            setContentView(view);
+        }
+    }
+
+    /** A Activity which has a custom view for test. */
+    public static class CustomizedViewIdTestActivity extends FragmentActivity {
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setContentView(R.layout.custom_activity_view);
+        }
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/fragment/src/test/resources/res/layout/custom_activity_view.xml b/packages/SettingsLib/tests/robotests/fragment/src/test/resources/res/layout/custom_activity_view.xml
new file mode 100644
index 0000000..c074f30
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/fragment/src/test/resources/res/layout/custom_activity_view.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/custom_activity_view"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical">
+
+</LinearLayout>
diff --git a/packages/SettingsLib/tests/robotests/fragment/src/test/resources/res/layout/fragment_contents.xml b/packages/SettingsLib/tests/robotests/fragment/src/test/resources/res/layout/fragment_contents.xml
new file mode 100644
index 0000000..425b2bb
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/fragment/src/test/resources/res/layout/fragment_contents.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical">
+
+  <TextView
+      android:id="@+id/tacos"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:text="TACOS"/>
+
+  <TextView
+      android:id="@+id/burritos"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:text="BURRITOS"/>
+
+</LinearLayout>
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
index 4a913c8..bb72375 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
@@ -25,7 +25,6 @@
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
@@ -58,12 +57,10 @@
 import org.robolectric.shadows.ShadowSettings;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 @RunWith(RobolectricTestRunner.class)
-@Config(shadows = {UtilsTest.ShadowSecure.class, UtilsTest.ShadowLocationManager.class})
+@Config(shadows = {UtilsTest.ShadowLocationManager.class})
 public class UtilsTest {
     private static final double[] TEST_PERCENTAGES = {0, 0.4, 0.5, 0.6, 49, 49.3, 49.8, 50, 100};
     private static final String TAG = "UtilsTest";
@@ -94,7 +91,7 @@
         mContext = spy(RuntimeEnvironment.application);
         when(mContext.getSystemService(Context.LOCATION_SERVICE)).thenReturn(mLocationManager);
         when(mContext.getSystemService(UsbManager.class)).thenReturn(mUsbManager);
-        ShadowSecure.reset();
+        ShadowSettings.ShadowSecure.reset();
         mAudioManager = mContext.getSystemService(AudioManager.class);
     }
 
@@ -111,15 +108,16 @@
                 Settings.Secure.LOCATION_CHANGER_QUICK_SETTINGS);
 
         assertThat(Settings.Secure.getInt(mContext.getContentResolver(),
-                Settings.Secure.LOCATION_CHANGER, Settings.Secure.LOCATION_CHANGER_UNKNOWN))
-                .isEqualTo(Settings.Secure.LOCATION_CHANGER_QUICK_SETTINGS);
+                Settings.Secure.LOCATION_CHANGER,
+                Settings.Secure.LOCATION_CHANGER_UNKNOWN)).isEqualTo(
+                Settings.Secure.LOCATION_CHANGER_QUICK_SETTINGS);
     }
 
     @Test
     public void testFormatPercentage_RoundTrue_RoundUpIfPossible() {
-        final String[] expectedPercentages = {PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_1,
-                PERCENTAGE_1, PERCENTAGE_49, PERCENTAGE_49, PERCENTAGE_50, PERCENTAGE_50,
-                PERCENTAGE_100};
+        final String[] expectedPercentages =
+                {PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_1, PERCENTAGE_1, PERCENTAGE_49,
+                        PERCENTAGE_49, PERCENTAGE_50, PERCENTAGE_50, PERCENTAGE_100};
 
         for (int i = 0, size = TEST_PERCENTAGES.length; i < size; i++) {
             final String percentage = Utils.formatPercentage(TEST_PERCENTAGES[i], true);
@@ -129,9 +127,9 @@
 
     @Test
     public void testFormatPercentage_RoundFalse_NoRound() {
-        final String[] expectedPercentages = {PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_0,
-                PERCENTAGE_0, PERCENTAGE_49, PERCENTAGE_49, PERCENTAGE_49, PERCENTAGE_50,
-                PERCENTAGE_100};
+        final String[] expectedPercentages =
+                {PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_49,
+                        PERCENTAGE_49, PERCENTAGE_49, PERCENTAGE_50, PERCENTAGE_100};
 
         for (int i = 0, size = TEST_PERCENTAGES.length; i < size; i++) {
             final String percentage = Utils.formatPercentage(TEST_PERCENTAGES[i], false);
@@ -143,12 +141,7 @@
     public void testGetDefaultStorageManagerDaysToRetain_storageManagerDaysToRetainUsesResources() {
         Resources resources = mock(Resources.class);
         when(resources.getInteger(
-                eq(
-                        com.android
-                                .internal
-                                .R
-                                .integer
-                                .config_storageManagerDaystoRetainDefault)))
+                eq(com.android.internal.R.integer.config_storageManagerDaystoRetainDefault)))
                 .thenReturn(60);
         assertThat(Utils.getDefaultStorageManagerDaysToRetain(resources)).isEqualTo(60);
     }
@@ -163,31 +156,6 @@
         return intent -> TextUtils.equals(expected, intent.getAction());
     }
 
-    @Implements(value = Settings.Secure.class)
-    public static class ShadowSecure extends ShadowSettings.ShadowSecure {
-        private static Map<String, Integer> map = new HashMap<>();
-
-        @Implementation
-        public static boolean putIntForUser(ContentResolver cr, String name, int value,
-                int userHandle) {
-            map.put(name, value);
-            return true;
-        }
-
-        @Implementation
-        public static int getIntForUser(ContentResolver cr, String name, int def, int userHandle) {
-            if (map.containsKey(name)) {
-                return map.get(name);
-            } else {
-                return def;
-            }
-        }
-
-        public static void reset() {
-            map.clear();
-        }
-    }
-
     @Implements(value = LocationManager.class)
     public static class ShadowLocationManager {
 
@@ -337,9 +305,8 @@
 
     @Test
     public void getBatteryStatus_statusIsFull_returnFullString() {
-        final Intent intent = new Intent()
-                .putExtra(BatteryManager.EXTRA_LEVEL, 100)
-                .putExtra(BatteryManager.EXTRA_SCALE, 100);
+        final Intent intent = new Intent().putExtra(BatteryManager.EXTRA_LEVEL, 100).putExtra(
+                BatteryManager.EXTRA_SCALE, 100);
         final Resources resources = mContext.getResources();
 
         assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false)).isEqualTo(
@@ -348,9 +315,8 @@
 
     @Test
     public void getBatteryStatus_statusIsFullAndUseCompactStatus_returnFullyChargedString() {
-        final Intent intent = new Intent()
-                .putExtra(BatteryManager.EXTRA_LEVEL, 100)
-                .putExtra(BatteryManager.EXTRA_SCALE, 100);
+        final Intent intent = new Intent().putExtra(BatteryManager.EXTRA_LEVEL, 100).putExtra(
+                BatteryManager.EXTRA_SCALE, 100);
         final Resources resources = mContext.getResources();
 
         assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true)).isEqualTo(
@@ -516,7 +482,6 @@
         when(mUsbPort.getStatus()).thenReturn(mUsbPortStatus);
         when(mUsbPort.supportsComplianceWarnings()).thenReturn(true);
         when(mUsbPortStatus.isConnected()).thenReturn(true);
-        when(mUsbPortStatus.getComplianceWarnings())
-                .thenReturn(new int[]{complianceWarningType});
+        when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[]{complianceWarningType});
     }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/accessibility/AccessibilityUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/accessibility/AccessibilityUtilsTest.java
index 44fdaec..3de8446 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/accessibility/AccessibilityUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/accessibility/AccessibilityUtilsTest.java
@@ -23,13 +23,17 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 
+import com.android.settingslib.testutils.shadow.ShadowSecure;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
 
 @RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowSecure.class})
 public class AccessibilityUtilsTest {
 
     private Context mContext;
@@ -46,7 +50,7 @@
 
     @Test
     public void getEnabledServicesFromSettings_badFormat_emptyResult() {
-        Settings.Secure.putStringForUser(
+        ShadowSecure.putStringForUser(
                 mContext.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
                 ":",
                 UserHandle.myUserId());
@@ -57,7 +61,7 @@
     @Test
     public void getEnabledServicesFromSettings_1Service_1result() {
         final ComponentName cn = new ComponentName("pkg", "serv");
-        Settings.Secure.putStringForUser(
+        ShadowSecure.putStringForUser(
                 mContext.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
                 cn.flattenToString() + ":",
                 UserHandle.myUserId());
@@ -70,7 +74,7 @@
     public void getEnabledServicesFromSettings_2Services_2results() {
         final ComponentName cn1 = new ComponentName("pkg", "serv");
         final ComponentName cn2 = new ComponentName("pkg", "serv2");
-        Settings.Secure.putStringForUser(
+        ShadowSecure.putStringForUser(
                 mContext.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
                 cn1.flattenToString() + ":" + cn2.flattenToString(),
                 UserHandle.myUserId());
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java
index cb62a73..f9505dd 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java
@@ -37,6 +37,8 @@
 import android.os.UserManager;
 import android.util.LongSparseArray;
 
+import com.android.settingslib.testutils.shadow.ShadowPermissionChecker;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -45,7 +47,6 @@
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
-import org.robolectric.shadows.ShadowPermissionChecker;
 
 import java.time.Clock;
 import java.util.ArrayList;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java
index dd8d54a..a2e8c59 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java
@@ -38,6 +38,7 @@
 import org.robolectric.Robolectric;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
 import org.robolectric.util.ReflectionHelpers;
 
 import java.util.ArrayList;
@@ -167,6 +168,7 @@
     }
 
     @Test
+    @LooperMode(LooperMode.Mode.PAUSED)
     public void getAttribution_notSet_shouldReturnUnknown() {
         final Activity activity = Robolectric.setupActivity(Activity.class);
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java
index d67d44b..25833b3 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java
@@ -36,6 +36,7 @@
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.jank.InteractionJankMonitor.CujType;
+import com.android.settingslib.testutils.OverpoweredReflectionHelper;
 import com.android.settingslib.testutils.shadow.ShadowInteractionJankMonitor;
 
 import org.junit.Before;
@@ -51,7 +52,6 @@
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
 import org.robolectric.annotation.Resetter;
-import org.robolectric.util.ReflectionHelpers;
 
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
@@ -83,8 +83,10 @@
     public void setUp() {
         ShadowInteractionJankMonitor.reset();
         when(ShadowInteractionJankMonitor.MOCK_INSTANCE.begin(any())).thenReturn(true);
-        ReflectionHelpers.setStaticField(SettingsJankMonitor.class, "scheduledExecutorService",
-                mScheduledExecutorService);
+        OverpoweredReflectionHelper
+                .setStaticField(SettingsJankMonitor.class,
+                        "scheduledExecutorService",
+                        mScheduledExecutorService);
     }
 
     @Test
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/lifecycle/HideNonSystemOverlayMixinTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/lifecycle/HideNonSystemOverlayMixinTest.java
index cf702b53..471dac0 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/lifecycle/HideNonSystemOverlayMixinTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/lifecycle/HideNonSystemOverlayMixinTest.java
@@ -37,8 +37,10 @@
 import org.robolectric.Robolectric;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.android.controller.ActivityController;
+import org.robolectric.annotation.LooperMode;
 
 @RunWith(RobolectricTestRunner.class)
+@LooperMode(LooperMode.Mode.PAUSED)
 public class HideNonSystemOverlayMixinTest {
 
     private ActivityController<TestActivity> mActivityController;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/development/DevelopmentSettingsEnablerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/development/DevelopmentSettingsEnablerTest.java
index 3475ff7..b009abd 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/development/DevelopmentSettingsEnablerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/development/DevelopmentSettingsEnablerTest.java
@@ -22,13 +22,14 @@
 import android.os.UserManager;
 import android.provider.Settings;
 
+import com.android.settingslib.testutils.shadow.ShadowUserManager;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.shadow.api.Shadow;
-import org.robolectric.shadows.ShadowUserManager;
 
 @RunWith(RobolectricTestRunner.class)
 public class DevelopmentSettingsEnablerTest {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java
index 8e33ca3..0cabab2 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java
@@ -21,6 +21,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.LooperMode;
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.ByteArrayInputStream;
@@ -269,6 +270,7 @@
     }
 
     @Test
+    @LooperMode(LooperMode.Mode.PAUSED)
     public void testGenerateHtmlWithCustomHeading() throws Exception {
         List<File> xmlFiles = new ArrayList<>();
         Map<String, Map<String, Set<String>>> fileNameToLibraryToContentIdMap = new HashMap<>();
@@ -292,6 +294,7 @@
     }
 
     @Test
+    @LooperMode(LooperMode.Mode.PAUSED)
     public void testGenerateNewHtmlWithCustomHeading() throws Exception {
         List<File> xmlFiles = new ArrayList<>();
         Map<String, Map<String, Set<String>>> fileNameToLibraryToContentIdMap = new HashMap<>();
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/package-info.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/package-info.java
new file mode 100644
index 0000000..9e9725f
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@LooperMode(LooperMode.Mode.LEGACY)
+package com.android.settingslib;
+
+import org.robolectric.annotation.LooperMode;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java
index e989ed2..b538077 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java
@@ -111,13 +111,13 @@
                 mActivityStarter, true, null,
                 cancelCallback);
         dialog.show();
-        assertThat(dialog.findViewById(R.id.user_info_scroll).getVisibility()).isEqualTo(View.GONE);
+        assertThat(dialog.findViewById(R.id.user_info_editor).getVisibility()).isEqualTo(View.GONE);
         Button next = dialog.findViewById(R.id.button_ok);
         next.performClick();
         ((RadioButton) dialog.findViewById(R.id.grant_admin_yes)).setChecked(true);
-        assertThat(dialog.findViewById(R.id.user_info_scroll).getVisibility()).isEqualTo(View.GONE);
+        assertThat(dialog.findViewById(R.id.user_info_editor).getVisibility()).isEqualTo(View.GONE);
         next.performClick();
-        assertThat(dialog.findViewById(R.id.user_info_scroll).getVisibility())
+        assertThat(dialog.findViewById(R.id.user_info_editor).getVisibility())
                 .isEqualTo(View.VISIBLE);
         dialog.dismiss();
     }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java
index f760032..f595cd3 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java
@@ -27,7 +27,6 @@
 
 import android.app.Activity;
 import android.app.AlertDialog;
-import android.app.Dialog;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.drawable.Drawable;
@@ -108,7 +107,7 @@
     @Test
     public void photoControllerOnActivityResult_whenWaiting_isCalled() {
         mController.createDialog(mActivity, mActivityStarter, mCurrentIcon, "test user",
-                "title", null, null);
+                null, null);
         mController.startingActivityForResult();
         Intent resultData = new Intent();
         mController.onActivityResult(0, 0, resultData);
@@ -126,9 +125,7 @@
                 () -> String.valueOf('A')).limit(200).collect(Collectors.joining());
 
         final AlertDialog dialog = (AlertDialog) mController.createDialog(mActivity,
-                mActivityStarter, mCurrentIcon,
-                "test user", "title", null,
-                null);
+                mActivityStarter, mCurrentIcon, "test user", null, null);
         dialog.show();
         final EditText userNameEditText = dialog.findViewById(R.id.user_name);
         userNameEditText.setText(longName);
@@ -143,7 +140,7 @@
 
         AlertDialog dialog = (AlertDialog) mController.createDialog(
                 mActivity, mActivityStarter, mCurrentIcon, "test",
-                "title", successCallback, cancelCallback);
+                successCallback, cancelCallback);
         dialog.show();
         dialog.cancel();
 
@@ -159,9 +156,9 @@
 
         AlertDialog dialog = (AlertDialog) mController.createDialog(
                 mActivity, mActivityStarter, mCurrentIcon, "test",
-                "title", successCallback, cancelCallback);
+                successCallback, cancelCallback);
         dialog.show();
-        dialog.getButton(Dialog.BUTTON_NEGATIVE).performClick();
+        dialog.findViewById(R.id.button_back).performClick();
 
         verifyNoInteractions(successCallback);
         verify(cancelCallback, times(1))
@@ -176,11 +173,11 @@
         Drawable oldUserIcon = mCurrentIcon;
         AlertDialog dialog = (AlertDialog) mController.createDialog(
                 mActivity, mActivityStarter, oldUserIcon, "test",
-                "title", successCallback, cancelCallback);
+                successCallback, cancelCallback);
         // No change to the photo.
         when(mController.getPhotoController().getNewUserPhotoDrawable()).thenReturn(null);
         dialog.show();
-        dialog.getButton(Dialog.BUTTON_POSITIVE).performClick();
+        dialog.findViewById(R.id.button_ok).performClick();
 
         verify(successCallback, times(1))
                 .accept("test", oldUserIcon);
@@ -194,11 +191,11 @@
 
         AlertDialog dialog = (AlertDialog) mController.createDialog(
                 mActivity, mActivityStarter, null, "test",
-                "title", successCallback, cancelCallback);
+                successCallback, cancelCallback);
         // No change to the photo.
         when(mController.getPhotoController().getNewUserPhotoDrawable()).thenReturn(null);
         dialog.show();
-        dialog.getButton(Dialog.BUTTON_POSITIVE).performClick();
+        dialog.findViewById(R.id.button_ok).performClick();
 
         verify(successCallback, times(1))
                 .accept("test", null);
@@ -212,14 +209,14 @@
 
         AlertDialog dialog = (AlertDialog) mController.createDialog(
                 mActivity, mActivityStarter, mCurrentIcon, "test",
-                "title", successCallback, cancelCallback);
+                successCallback, cancelCallback);
         // No change to the photo.
         when(mController.getPhotoController().getNewUserPhotoDrawable()).thenReturn(null);
         dialog.show();
         String expectedNewName = "new test user";
         EditText editText = (EditText) dialog.findViewById(R.id.user_name);
         editText.setText(expectedNewName);
-        dialog.getButton(Dialog.BUTTON_POSITIVE).performClick();
+        dialog.findViewById(R.id.button_ok).performClick();
 
         verify(successCallback, times(1))
                 .accept(expectedNewName, mCurrentIcon);
@@ -233,12 +230,12 @@
 
         AlertDialog dialog = (AlertDialog) mController.createDialog(
                 mActivity, mActivityStarter, mCurrentIcon, "test",
-                "title", successCallback, cancelCallback);
+                successCallback, cancelCallback);
         // A different drawable.
         Drawable newPhoto = mock(Drawable.class);
         when(mController.getPhotoController().getNewUserPhotoDrawable()).thenReturn(newPhoto);
         dialog.show();
-        dialog.getButton(Dialog.BUTTON_POSITIVE).performClick();
+        dialog.findViewById(R.id.button_ok).performClick();
 
         verify(successCallback, times(1))
                 .accept("test", newPhoto);
@@ -252,12 +249,12 @@
 
         AlertDialog dialog = (AlertDialog) mController.createDialog(
                 mActivity, mActivityStarter, null, "test",
-                "title", successCallback, cancelCallback);
+                 successCallback, cancelCallback);
         // A different drawable.
         Drawable newPhoto = mock(Drawable.class);
         when(mController.getPhotoController().getNewUserPhotoDrawable()).thenReturn(newPhoto);
         dialog.show();
-        dialog.getButton(Dialog.BUTTON_POSITIVE).performClick();
+        dialog.findViewById(R.id.button_ok).performClick();
 
         verify(successCallback, times(1))
                 .accept("test", newPhoto);
@@ -269,7 +266,7 @@
         mPhotoRestrictedByBase = true;
 
         mController.createDialog(mActivity, mActivityStarter, mCurrentIcon,
-                "test", "title", null, null);
+                "test", null, null);
 
         assertThat(mController.mPhotoController).isNull();
     }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AnimatedImageViewTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AnimatedImageViewTest.java
index d41d511..faec02f7 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AnimatedImageViewTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AnimatedImageViewTest.java
@@ -27,6 +27,7 @@
 import org.junit.runner.RunWith;
 import org.robolectric.Robolectric;
 import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.LooperMode;
 
 @RunWith(RobolectricTestRunner.class)
 public class AnimatedImageViewTest {
@@ -40,6 +41,7 @@
     }
 
     @Test
+    @LooperMode(LooperMode.Mode.PAUSED)
     public void testAnimation_ViewVisible_AnimationRunning() {
         mAnimatedImageView.setVisibility(View.VISIBLE);
         mAnimatedImageView.setAnimating(true);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java
index 0a48f19..0d88913 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java
@@ -41,6 +41,8 @@
 import androidx.preference.PreferenceViewHolder;
 import androidx.preference.R;
 
+import com.android.settingslib.testutils.OverpoweredReflectionHelper;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -502,14 +504,18 @@
     private void assumeAndroidR() {
         ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 30);
         ReflectionHelpers.setStaticField(Build.VERSION.class, "CODENAME", "R");
-        ReflectionHelpers.setStaticField(BannerMessagePreference.class, "IS_AT_LEAST_S", false);
+        OverpoweredReflectionHelper
+                .setStaticField(BannerMessagePreference.class, "IS_AT_LEAST_S", false);
         // Reset view holder to use correct layout.
     }
 
+
+
     private void assumeAndroidS() {
         ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 31);
         ReflectionHelpers.setStaticField(Build.VERSION.class, "CODENAME", "S");
-        ReflectionHelpers.setStaticField(BannerMessagePreference.class, "IS_AT_LEAST_S", true);
+        OverpoweredReflectionHelper
+                .setStaticField(BannerMessagePreference.class, "IS_AT_LEAST_S", true);
         // Re-inflate view to update layout.
         setUpViewHolder();
     }
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/OverpoweredReflectionHelper.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/OverpoweredReflectionHelper.java
new file mode 100644
index 0000000..4fcc5a1
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/OverpoweredReflectionHelper.java
@@ -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.settingslib.testutils;
+
+import org.robolectric.util.ReflectionHelpers;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+public class OverpoweredReflectionHelper extends ReflectionHelpers {
+
+    /**
+     * Robolectric upstream does not rely on or encourage this behaviour.
+     *
+     * @param field
+     */
+    private static void makeFieldVeryAccessible(Field field) {
+        field.setAccessible(true);
+        // remove 'final' modifier if present
+        if ((field.getModifiers() & Modifier.FINAL) == Modifier.FINAL) {
+            Field modifiersField = getModifiersField();
+            modifiersField.setAccessible(true);
+            try {
+                modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
+            } catch (IllegalAccessException e) {
+
+                throw new AssertionError(e);
+            }
+        }
+    }
+
+    private static Field getModifiersField() {
+        try {
+            return Field.class.getDeclaredField("modifiers");
+        } catch (NoSuchFieldException e) {
+            try {
+                Method getFieldsMethod =
+                        Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
+                getFieldsMethod.setAccessible(true);
+                Field[] fields = (Field[]) getFieldsMethod.invoke(Field.class, false);
+                for (Field modifiersField : fields) {
+                    if ("modifiers".equals(modifiersField.getName())) {
+                        return modifiersField;
+                    }
+                }
+            } catch (ReflectiveOperationException innerE) {
+                throw new AssertionError(innerE);
+            }
+        }
+        throw new AssertionError();
+    }
+
+    /**
+     * Reflectively set the value of a static field.
+     *
+     * @param field Field object.
+     * @param fieldNewValue The new value.
+     */
+    public static void setStaticField(Field field, Object fieldNewValue) {
+        try {
+            makeFieldVeryAccessible(field);
+            field.setAccessible(true);
+            field.set(null, fieldNewValue);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Reflectively set the value of a static field.
+     *
+     * @param clazz Target class.
+     * @param fieldName The field name.
+     * @param fieldNewValue The new value.
+     */
+    public static void setStaticField(Class<?> clazz, String fieldName, Object fieldNewValue) {
+        try {
+            setStaticField(clazz.getDeclaredField(fieldName), fieldNewValue);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowActivityManager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowActivityManager.java
index 924eb04..0b9ba8d 100644
--- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowActivityManager.java
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowActivityManager.java
@@ -16,23 +16,27 @@
 
 package com.android.settingslib.testutils.shadow;
 
+import static android.os.Build.VERSION_CODES.O;
+
 import android.app.ActivityManager;
+import android.app.IActivityManager;
 
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
 import org.robolectric.annotation.Resetter;
 import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers;
 
 @Implements(ActivityManager.class)
 public class ShadowActivityManager {
     private static int sCurrentUserId = 0;
-    private int mUserSwitchedTo = -1;
+    private static int sUserSwitchedTo = -1;
 
     @Resetter
-    public void reset() {
+    public static void reset() {
         sCurrentUserId = 0;
-        mUserSwitchedTo = 0;
+        sUserSwitchedTo = 0;
     }
 
     @Implementation
@@ -42,16 +46,21 @@
 
     @Implementation
     protected boolean switchUser(int userId) {
-        mUserSwitchedTo = userId;
+        sUserSwitchedTo = userId;
         return true;
     }
 
+    @Implementation(minSdk = O)
+    protected static IActivityManager getService() {
+        return ReflectionHelpers.createNullProxy(IActivityManager.class);
+    }
+
     public boolean getSwitchUserCalled() {
-        return mUserSwitchedTo != -1;
+        return sUserSwitchedTo != -1;
     }
 
     public int getUserSwitchedTo() {
-        return mUserSwitchedTo;
+        return sUserSwitchedTo;
     }
 
     public static void setCurrentUser(int userId) {
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowDefaultDialerManager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowDefaultDialerManager.java
index 2c0792f..bbfdb7f 100644
--- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowDefaultDialerManager.java
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowDefaultDialerManager.java
@@ -29,7 +29,7 @@
     private static String sDefaultDialer;
 
     @Resetter
-    public void reset() {
+    public static void reset() {
         sDefaultDialer = null;
     }
 
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowPermissionChecker.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowPermissionChecker.java
new file mode 100644
index 0000000..fae3aea
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowPermissionChecker.java
@@ -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.settingslib.testutils.shadow;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.content.PermissionChecker;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.util.HashMap;
+import java.util.Map;
+/** Shadow class of {@link PermissionChecker}. */
+@Implements(PermissionChecker.class)
+public class ShadowPermissionChecker {
+    private static final Map<String, Map<String, Integer>> RESULTS = new HashMap<>();
+    /** Set the result of permission check for a specific permission. */
+    public static void setResult(String packageName, String permission, int result) {
+        if (!RESULTS.containsKey(packageName)) {
+            RESULTS.put(packageName, new HashMap<>());
+        }
+        RESULTS.get(packageName).put(permission, result);
+    }
+    /** Check the permission of calling package. */
+    @Implementation
+    public static int checkCallingPermissionForDataDelivery(
+            Context context,
+            String permission,
+            String packageName,
+            String attributionTag,
+            String message) {
+        return RESULTS.containsKey(packageName) && RESULTS.get(packageName).containsKey(permission)
+                ? RESULTS.get(packageName).get(permission)
+                : PermissionChecker.checkCallingPermissionForDataDelivery(
+                        context, permission, packageName, attributionTag, message);
+    }
+    /** Check general permission. */
+    @Implementation
+    public static int checkPermissionForDataDelivery(
+            Context context,
+            String permission,
+            int pid,
+            int uid,
+            String packageName,
+            String attributionTag,
+            String message) {
+        return RESULTS.containsKey(packageName) && RESULTS.get(packageName).containsKey(permission)
+                ? RESULTS.get(packageName).get(permission)
+                : PermissionChecker.checkPermissionForDataDelivery(
+                        context, permission, pid, uid, packageName, attributionTag, message);
+    }
+    /** Check general permission. */
+    @Implementation
+    public static int checkPermissionForPreflight(@NonNull Context context,
+            @NonNull String permission, int pid, int uid, @Nullable String packageName) {
+        return checkPermissionForPreflight(context, permission, new AttributionSource(
+                uid, packageName, null /*attributionTag*/));
+    }
+    /** Check general permission. */
+    @Implementation
+    public static int checkPermissionForPreflight(@NonNull Context context,
+            @NonNull String permission, @NonNull AttributionSource attributionSource) {
+        final String packageName = attributionSource.getPackageName();
+        return RESULTS.containsKey(packageName) && RESULTS.get(packageName).containsKey(permission)
+                ? RESULTS.get(packageName).get(permission)
+                : PermissionChecker.checkPermissionForPreflight(
+                        context, permission, attributionSource);
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowSecure.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowSecure.java
new file mode 100644
index 0000000..70ebc67
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowSecure.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.settingslib.testutils.shadow;
+
+import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
+
+import android.content.ContentResolver;
+import android.provider.Settings;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowSettings;
+
+@Implements(value = Settings.Secure.class)
+public class ShadowSecure extends ShadowSettings.ShadowSecure {
+    @Implementation(minSdk = JELLY_BEAN_MR1)
+    public static boolean putStringForUser(ContentResolver cr, String name, String value,
+            int userHandle) {
+        return putString(cr, name, value);
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowSmsApplication.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowSmsApplication.java
index 381d072..5ac0a87 100644
--- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowSmsApplication.java
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowSmsApplication.java
@@ -31,7 +31,7 @@
     private static ComponentName sDefaultSmsApplication;
 
     @Resetter
-    public void reset() {
+    public static void reset() {
         sDefaultSmsApplication = null;
     }
 
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowUserManager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowUserManager.java
index ca1eefc..60d7721 100644
--- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowUserManager.java
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowUserManager.java
@@ -16,20 +16,28 @@
 
 package com.android.settingslib.testutils.shadow;
 
+import static android.os.Build.VERSION_CODES.N_MR1;
+
 import android.annotation.UserIdInt;
 import android.content.Context;
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
+import android.os.UserHandle;
 import android.os.UserManager;
 
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowBuild;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 @Implements(value = UserManager.class)
 public class ShadowUserManager extends org.robolectric.shadows.ShadowUserManager {
     private List<UserInfo> mUserInfos = addProfile(0, "Owner");
+    private final Map<Integer, UserProperties> mUserPropertiesMap = new HashMap<>();
 
     @Implementation
     protected static UserManager get(Context context) {
@@ -62,4 +70,37 @@
     protected List<UserInfo> getProfiles(@UserIdInt int userHandle) {
         return getProfiles();
     }
+
+    /**
+     * @return {@code false} by default, or the value specified via {@link #setIsAdminUser(boolean)}
+     */
+    @Implementation(minSdk = N_MR1)
+    public boolean isAdminUser() {
+        return getUserInfo(UserHandle.myUserId()).isAdmin();
+    }
+
+    /**
+     * Sets that the current user is an admin user; controls the return value of
+     * {@link UserManager#isAdminUser}.
+     */
+    public void setIsAdminUser(boolean isAdminUser) {
+        UserInfo userInfo = getUserInfo(UserHandle.myUserId());
+        if (isAdminUser) {
+            userInfo.flags |= UserInfo.FLAG_ADMIN;
+        } else {
+            userInfo.flags &= ~UserInfo.FLAG_ADMIN;
+        }
+    }
+
+    public void setupUserProperty(int userId, int showInSettings) {
+        UserProperties userProperties = new UserProperties(new UserProperties.Builder()
+                .setShowInSettings(showInSettings).build());
+        mUserPropertiesMap.putIfAbsent(userId, userProperties);
+    }
+
+    @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+    protected UserProperties getUserProperties(UserHandle user) {
+        return mUserPropertiesMap.getOrDefault(user.getIdentifier(),
+            new UserProperties(new UserProperties.Builder().build()));
+    }
 }
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 873b434..c740423 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -345,6 +345,8 @@
                     Settings.Global.MIN_DURATION_BETWEEN_RECOVERY_STEPS_IN_MS,
                     Settings.Global.MOBILE_DATA, // Candidate for backup?
                     Settings.Global.MOBILE_DATA_ALWAYS_ON,
+                    Settings.Global.DSRM_DURATION_MILLIS,
+                    Settings.Global.DSRM_ENABLED_ACTIONS,
                     Settings.Global.MODE_RINGER,
                     Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION,
                     Settings.Global.MULTI_SIM_SMS_PROMPT,
@@ -590,6 +592,7 @@
                     Settings.Global.APPOP_HISTORY_BASE_INTERVAL_MILLIS,
                     Settings.Global.AUTO_REVOKE_PARAMETERS,
                     Settings.Global.ENABLE_RADIO_BUG_DETECTION,
+                    Settings.Global.REPAIR_MODE_ACTIVE,
                     Settings.Global.RADIO_BUG_WAKELOCK_TIMEOUT_COUNT_THRESHOLD,
                     Settings.Global.RADIO_BUG_SYSTEM_ERROR_COUNT_THRESHOLD,
                     Settings.Global.ENABLED_SUBSCRIPTION_FOR_SLOT,
@@ -724,6 +727,7 @@
                  Settings.Secure.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS,
                  Settings.Secure.CONTENT_CAPTURE_ENABLED,
                  Settings.Secure.DEFAULT_INPUT_METHOD,
+                 Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
                  Settings.Secure.DEVICE_PAIRED,
                  Settings.Secure.DIALER_DEFAULT_APPLICATION,
                  Settings.Secure.DISABLED_PRINT_SERVICES,
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 7df3dfb..73fb0f0 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -65,6 +65,7 @@
                 "androidx.compose.runtime_runtime",
                 "androidx.compose.material3_material3",
                 "androidx.activity_activity-compose",
+                "androidx.compose.animation_animation-graphics",
             ],
 
             // By default, Compose is disabled and we compile the ComposeFacade
@@ -250,17 +251,17 @@
         "tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt",
         "tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt",
         // domain
-        "tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt",
+        "tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt",
+        "tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt",
+        "tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt",
         "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt",
         "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.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/interactor/PrimaryBouncerCallbackInteractorTest.kt",
-        "tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt",
         // ui
+        "tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt",
         "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",
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 5c03698..4fd4723 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -176,6 +176,7 @@
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
     <uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT"/>
+    <uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
 
     <!-- Assist -->
     <uses-permission android:name="android.permission.ACCESS_VOICE_INTERACTION_SERVICE" />
@@ -369,7 +370,7 @@
         android:defaultToDeviceProtectedStorage="true"
         android:directBootAware="true"
         tools:replace="android:appComponentFactory"
-        android:appComponentFactory=".SystemUIAppComponentFactory">
+        android:appComponentFactory=".PhoneSystemUIAppComponentFactory">
         <!-- Keep theme in sync with SystemUIApplication.onCreate().
              Setting the theme on the application does not affect views inflated by services.
              The application theme is set again from onCreate to take effect for those views. -->
@@ -451,12 +452,14 @@
             android:noHistory="true" />
 
         <service android:name=".screenshot.appclips.AppClipsScreenshotHelperService"
-            android:permission="com.android.systemui.permission.SELF"
-            android:exported="false" />
+            android:exported="false"
+            android:singleUser="true"
+            android:permission="com.android.systemui.permission.SELF" />
 
         <service android:name=".screenshot.appclips.AppClipsService"
-            android:permission="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE"
-            android:exported="true" />
+            android:exported="true"
+            android:singleUser="true"
+            android:permission="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE" />
 
         <service android:name=".screenrecord.RecordingService"
                  android:foregroundServiceType="systemExempted"/>
@@ -814,7 +817,8 @@
         <service
             android:name=".dreams.DreamOverlayService"
             android:enabled="false"
-            android:exported="true" />
+            android:exported="true"
+            android:singleUser="true" />
 
         <activity android:name=".keyguard.WorkLockActivity"
                   android:label="@string/accessibility_desc_work_lock"
@@ -861,7 +865,7 @@
         <activity
             android:name=".settings.brightness.BrightnessDialog"
             android:label="@string/quick_settings_brightness_dialog_title"
-            android:theme="@style/Theme.SystemUI.QuickSettings.BrightnessDialog"
+            android:theme="@style/BrightnessDialog"
             android:finishOnCloseSystemDialogs="true"
             android:launchMode="singleInstance"
             android:excludeFromRecents="true"
@@ -992,6 +996,11 @@
 
         <service android:name=".notetask.NoteTaskControllerUpdateService" />
 
+        <service android:name=".notetask.NoteTaskBubblesController$NoteTaskBubblesService"
+            android:exported="false"
+            android:singleUser="true"
+            android:permission="com.android.systemui.permission.SELF" />
+
         <activity
             android:name=".notetask.shortcut.LaunchNoteTaskActivity"
             android:exported="true"
@@ -1004,20 +1013,10 @@
             </intent-filter>
         </activity>
 
-        <!-- LaunchNoteTaskManagedProfileProxyActivity MUST NOT be exported because it allows caller
-             to specify an Android user when launching the default notes app. -->
-        <activity
-            android:name=".notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity"
-            android:exported="false"
-            android:enabled="true"
-            android:excludeFromRecents="true"
-            android:theme="@android:style/Theme.NoDisplay" />
-
         <activity
             android:name=".notetask.LaunchNotesRoleSettingsTrampolineActivity"
             android:exported="true"
             android:excludeFromRecents="true"
-            android:resizeableActivity="false"
             android:theme="@android:style/Theme.NoDisplay" >
             <intent-filter>
                 <action android:name="com.android.systemui.action.MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE" />
@@ -1069,15 +1068,6 @@
                   android:exported="true">
         </provider>
 
-        <!-- Provides list and realistic previews of clock faces for the picker app. -->
-        <provider
-            android:name="com.android.keyguard.clock.ClockOptionsProvider"
-            android:authorities="com.android.keyguard.clock"
-            android:enabled="false"
-            android:exported="false"
-            android:grantUriPermissions="true">
-        </provider>
-
         <receiver
             android:name=".statusbar.KeyboardShortcutsReceiver"
             android:exported="true">
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 48dd08f..dbfa192 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -36,6 +36,7 @@
 import com.android.app.animation.Interpolators
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.jank.InteractionJankMonitor.CujType
+import com.android.systemui.animation.view.LaunchableFrameLayout
 import com.android.systemui.util.registerAnimationOnBackInvoked
 import kotlin.math.roundToInt
 
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableFrameLayout.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableFrameLayout.kt
deleted file mode 100644
index 2eb503b..0000000
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableFrameLayout.kt
+++ /dev/null
@@ -1,53 +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.animation
-
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.FrameLayout
-
-/** 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,
-        attrs: AttributeSet?,
-        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)
-    }
-
-    override fun setVisibility(visibility: Int) {
-        delegate.setVisibility(visibility)
-    }
-}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableFrameLayout.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableFrameLayout.kt
new file mode 100644
index 0000000..7538f18
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableFrameLayout.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.FrameLayout
+import com.android.systemui.animation.LaunchableView
+import com.android.systemui.animation.LaunchableViewDelegate
+
+/** 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,
+        attrs: AttributeSet?,
+        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)
+    }
+
+    override fun setVisibility(visibility: Int) {
+        delegate.setVisibility(visibility)
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt
index 8fe1f48..4fe9f89 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt
@@ -27,37 +27,52 @@
 object Easings {
 
     /** The standard interpolator that should be used on every normal animation */
-    val StandardEasing = fromInterpolator(InterpolatorsAndroidX.STANDARD)
+    val Standard = fromInterpolator(InterpolatorsAndroidX.STANDARD)
 
     /**
      * The standard accelerating interpolator that should be used on every regular movement of
      * content that is disappearing e.g. when moving off screen.
      */
-    val StandardAccelerateEasing = fromInterpolator(InterpolatorsAndroidX.STANDARD_ACCELERATE)
+    val StandardAccelerate = fromInterpolator(InterpolatorsAndroidX.STANDARD_ACCELERATE)
 
     /**
      * The standard decelerating interpolator that should be used on every regular movement of
      * content that is appearing e.g. when coming from off screen.
      */
-    val StandardDecelerateEasing = fromInterpolator(InterpolatorsAndroidX.STANDARD_DECELERATE)
+    val StandardDecelerate = fromInterpolator(InterpolatorsAndroidX.STANDARD_DECELERATE)
 
     /** The default emphasized interpolator. Used for hero / emphasized movement of content. */
-    val EmphasizedEasing = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED)
+    val Emphasized = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED)
 
     /**
      * The accelerated emphasized interpolator. Used for hero / emphasized movement of content that
      * is disappearing e.g. when moving off screen.
      */
-    val EmphasizedAccelerateEasing = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED_ACCELERATE)
+    val EmphasizedAccelerate = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED_ACCELERATE)
 
     /**
      * The decelerating emphasized interpolator. Used for hero / emphasized movement of content that
      * is appearing e.g. when coming from off screen
      */
-    val EmphasizedDecelerateEasing = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED_DECELERATE)
+    val EmphasizedDecelerate = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED_DECELERATE)
 
     /** The linear interpolator. */
-    val LinearEasing = fromInterpolator(InterpolatorsAndroidX.LINEAR)
+    val Linear = fromInterpolator(InterpolatorsAndroidX.LINEAR)
+
+    /** The default legacy interpolator as defined in Material 1. Also known as FAST_OUT_SLOW_IN. */
+    val Legacy = fromInterpolator(InterpolatorsAndroidX.LEGACY)
+
+    /**
+     * The default legacy accelerating interpolator as defined in Material 1. Also known as
+     * FAST_OUT_LINEAR_IN.
+     */
+    val LegacyAccelerate = fromInterpolator(InterpolatorsAndroidX.LEGACY_ACCELERATE)
+
+    /**
+     * T The default legacy decelerating interpolator as defined in Material 1. Also known as
+     * LINEAR_OUT_SLOW_IN.
+     */
+    val LegacyDecelerate = fromInterpolator(InterpolatorsAndroidX.LEGACY_DECELERATE)
 
     private fun fromInterpolator(source: Interpolator) = Easing { x -> source.getInterpolation(x) }
 }
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 d4a81f9..ac1ef15 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
@@ -70,8 +70,10 @@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.dp
-import androidx.lifecycle.ViewTreeLifecycleOwner
-import androidx.lifecycle.ViewTreeViewModelStoreOwner
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeViewModelStoreOwner
 import com.android.systemui.animation.Expandable
 import com.android.systemui.animation.LaunchAnimator
 import kotlin.math.max
@@ -368,13 +370,10 @@
                     context,
                     overlay,
                 )
-            ViewTreeLifecycleOwner.set(
-                overlayViewGroup,
-                ViewTreeLifecycleOwner.get(composeViewRoot),
-            )
-            ViewTreeViewModelStoreOwner.set(
-                overlayViewGroup,
-                ViewTreeViewModelStoreOwner.get(composeViewRoot),
+
+            overlayViewGroup.setViewTreeLifecycleOwner(composeViewRoot.findViewTreeLifecycleOwner())
+            overlayViewGroup.setViewTreeViewModelStoreOwner(
+                composeViewRoot.findViewTreeViewModelStoreOwner()
             )
             ViewTreeSavedStateRegistryOwner.set(
                 overlayViewGroup,
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 cc33745..82fe3f2 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
@@ -24,6 +24,9 @@
 import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
+import com.android.systemui.scene.shared.model.Scene
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
 import com.android.systemui.util.time.SystemClock
 
 /** The Compose facade, when Compose is *not* available. */
@@ -58,6 +61,14 @@
         throwComposeUnavailableError()
     }
 
+    override fun createSceneContainerView(
+        context: Context,
+        viewModel: SceneContainerViewModel,
+        sceneByKey: Map<SceneKey, Scene>,
+    ): View {
+        throwComposeUnavailableError()
+    }
+
     private fun throwComposeUnavailableError(): Nothing {
         error(
             "Compose is not available. Make sure to check isComposeAvailable() before calling any" +
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 0e79b18..7926f92 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
@@ -29,6 +29,11 @@
 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
+import com.android.systemui.scene.shared.model.Scene
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.ui.composable.ComposableScene
+import com.android.systemui.scene.ui.composable.SceneContainer
+import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
 import com.android.systemui.util.time.SystemClock
 
 /** The Compose facade, when Compose is available. */
@@ -71,4 +76,22 @@
             }
         }
     }
+
+    override fun createSceneContainerView(
+        context: Context,
+        viewModel: SceneContainerViewModel,
+        sceneByKey: Map<SceneKey, Scene>,
+    ): View {
+        return ComposeView(context).apply {
+            setContent {
+                PlatformTheme {
+                    SceneContainer(
+                        viewModel = viewModel,
+                        sceneByKey =
+                            sceneByKey.mapValues { (_, scene) -> scene as ComposableScene },
+                    )
+                }
+            }
+        }
+    }
 }
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
index fbd7f83..1674591 100644
--- 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
@@ -17,9 +17,9 @@
 package com.android.systemui.compose
 
 import android.view.View
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeLifecycleOwner
 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
@@ -27,7 +27,7 @@
 
 internal object ComposeInitializerImpl : ComposeInitializer {
     override fun onAttachedToWindow(root: View) {
-        if (ViewTreeLifecycleOwner.get(root) != null) {
+        if (root.findViewTreeLifecycleOwner() != null) {
             error("root $root already has a LifecycleOwner")
         }
 
@@ -54,7 +54,8 @@
 
                 override val savedStateRegistry = savedStateRegistryController.savedStateRegistry
 
-                override fun getLifecycle(): Lifecycle = lifecycleOwner.lifecycle
+                override val lifecycle: Lifecycle
+                    get() = lifecycleOwner.lifecycle
             }
 
         // We must call [ViewLifecycleOwner.onCreate] after creating the [SavedStateRegistryOwner]
@@ -64,13 +65,13 @@
 
         // Set the owners on the root. They will be reused by any ComposeView inside the root
         // hierarchy.
-        ViewTreeLifecycleOwner.set(root, lifecycleOwner)
+        root.setViewTreeLifecycleOwner(lifecycleOwner)
         ViewTreeSavedStateRegistryOwner.set(root, savedStateRegistryOwner)
     }
 
     override fun onDetachedFromWindow(root: View) {
-        (ViewTreeLifecycleOwner.get(root) as ViewLifecycleOwner).onDestroy()
-        ViewTreeLifecycleOwner.set(root, null)
+        (root.findViewTreeLifecycleOwner() as ViewLifecycleOwner).onDestroy()
+        root.setViewTreeLifecycleOwner(null)
         ViewTreeSavedStateRegistryOwner.set(root, null)
     }
 }
diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp
index 8f8bb1b..c6438c9 100644
--- a/packages/SystemUI/compose/features/Android.bp
+++ b/packages/SystemUI/compose/features/Android.bp
@@ -34,6 +34,7 @@
         "PlatformComposeCore",
 
         "androidx.compose.runtime_runtime",
+        "androidx.compose.animation_animation-graphics",
         "androidx.compose.material3_material3",
         "androidx.activity_activity-compose",
     ],
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index 240bace..d83596e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -99,7 +99,10 @@
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.spacedBy(60.dp),
         modifier =
-            modifier.background(MaterialTheme.colorScheme.surface).fillMaxSize().padding(32.dp)
+            modifier
+                .fillMaxSize()
+                .background(MaterialTheme.colorScheme.surface)
+                .padding(start = 32.dp, top = 92.dp, end = 32.dp, bottom = 32.dp)
     ) {
         Crossfade(
             targetState = message,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 8844114..b3d2e35 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -131,7 +131,7 @@
                         animationSpec =
                             tween(
                                 durationMillis = SELECTED_DOT_REACTION_ANIMATION_DURATION_MS,
-                                easing = Easings.StandardAccelerateEasing,
+                                easing = Easings.StandardAccelerate,
                             ),
                     )
                 } else {
@@ -140,7 +140,7 @@
                         animationSpec =
                             tween(
                                 durationMillis = SELECTED_DOT_RETRACT_ANIMATION_DURATION_MS,
-                                easing = Easings.StandardDecelerateEasing,
+                                easing = Easings.StandardDecelerate,
                             ),
                     )
                 }
@@ -333,7 +333,7 @@
                                         FAILURE_ANIMATION_DOT_SHRINK_ANIMATION_DURATION_MS,
                                     delayMillis =
                                         rowIndex * FAILURE_ANIMATION_DOT_SHRINK_STAGGER_DELAY_MS,
-                                    easing = Easings.LinearEasing,
+                                    easing = Easings.Linear,
                                 ),
                         )
 
@@ -343,7 +343,7 @@
                                 tween(
                                     durationMillis =
                                         FAILURE_ANIMATION_DOT_REVERT_ANIMATION_DURATION,
-                                    easing = Easings.StandardEasing,
+                                    easing = Easings.Standard,
                                 ),
                         )
                     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index 968e5ab..85178bc 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -14,28 +14,30 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalAnimationApi::class)
+@file:OptIn(ExperimentalAnimationApi::class, ExperimentalAnimationGraphicsApi::class)
 
 package com.android.systemui.bouncer.ui.composable
 
 import android.view.HapticFeedbackConstants
-import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.ExperimentalAnimationApi
 import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.animateDp
 import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.snap
 import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.scaleIn
-import androidx.compose.animation.scaleOut
-import androidx.compose.animation.slideInHorizontally
-import androidx.compose.animation.slideOutHorizontally
-import androidx.compose.foundation.background
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
+import androidx.compose.foundation.Image
 import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
@@ -43,38 +45,49 @@
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.snapshots.SnapshotStateList
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.geometry.CornerRadius
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.Easings
 import com.android.compose.grid.VerticalGrid
+import com.android.internal.R.id.image
 import com.android.systemui.R
+import com.android.systemui.bouncer.ui.viewmodel.ActionButtonAppearance
+import com.android.systemui.bouncer.ui.viewmodel.EnteredKey
 import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.ui.compose.Icon
 import com.android.systemui.compose.modifiers.thenIf
-import kotlin.math.max
 import kotlin.time.Duration.Companion.milliseconds
 import kotlin.time.DurationUnit
 import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 
@@ -86,139 +99,305 @@
     // Report that the UI is shown to let the view-model run some logic.
     LaunchedEffect(Unit) { viewModel.onShown() }
 
-    // The length of the PIN input received so far, so we know how many dots to render.
-    val pinLength: Pair<Int, Int> by viewModel.pinLengths.collectAsState()
-    val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
-    val animateFailure: Boolean by viewModel.animateFailure.collectAsState()
-
-    // Show the failure animation if the user entered the wrong input.
-    LaunchedEffect(animateFailure) {
-        if (animateFailure) {
-            showFailureAnimation()
-            viewModel.onFailureAnimationShown()
-        }
-    }
-
     Column(
         horizontalAlignment = Alignment.CenterHorizontally,
         modifier = modifier,
     ) {
-        Row(
-            horizontalArrangement = Arrangement.spacedBy(12.dp),
-            modifier = Modifier.heightIn(min = 16.dp).animateContentSize(),
-        ) {
-            // TODO(b/281871687): add support for dot shapes.
-            val (previousPinLength, currentPinLength) = pinLength
-            val dotCount = max(previousPinLength, currentPinLength) + 1
-            repeat(dotCount) { index ->
-                AnimatedVisibility(
-                    visible = index < currentPinLength,
-                    enter = fadeIn() + scaleIn() + slideInHorizontally(),
-                    exit = fadeOut() + scaleOut() + slideOutHorizontally(),
-                ) {
-                    Box(
-                        modifier =
-                            Modifier.size(16.dp)
-                                .background(
-                                    MaterialTheme.colorScheme.onSurfaceVariant,
-                                    CircleShape,
-                                )
-                    )
-                }
-            }
-        }
-
+        PinInputDisplay(viewModel)
         Spacer(Modifier.height(100.dp))
+        PinPad(viewModel)
+    }
+}
 
-        VerticalGrid(
-            columns = 3,
-            verticalSpacing = 12.dp,
-            horizontalSpacing = 20.dp,
-        ) {
-            repeat(9) { index ->
-                val digit = index + 1
-                PinButton(
-                    onClicked = { viewModel.onPinButtonClicked(digit) },
-                    isEnabled = isInputEnabled,
-                ) { contentColor ->
-                    PinDigit(digit, contentColor)
+@Composable
+private fun PinInputDisplay(viewModel: PinBouncerViewModel) {
+    val currentPinEntries: List<EnteredKey> by viewModel.pinEntries.collectAsState()
+
+    // visiblePinEntries keeps pins removed from currentPinEntries in the composition until their
+    // disappear-animation completed. The list is sorted by the natural ordering of EnteredKey,
+    // which is guaranteed to produce the original edit order, since the model only modifies entries
+    // at the end.
+    val visiblePinEntries = remember { SnapshotStateList<EnteredKey>() }
+    currentPinEntries.forEach {
+        val index = visiblePinEntries.binarySearch(it)
+        if (index < 0) {
+            val insertionPoint = -(index + 1)
+            visiblePinEntries.add(insertionPoint, it)
+        }
+    }
+
+    Row(
+        modifier =
+            Modifier.heightIn(min = entryShapeSize)
+                // Pins overflowing horizontally should still be shown as scrolling.
+                .wrapContentSize(unbounded = true),
+    ) {
+        visiblePinEntries.forEachIndexed { index, entry ->
+            key(entry) {
+                val visibility = remember {
+                    MutableTransitionState<EntryVisibility>(EntryVisibility.Hidden)
                 }
-            }
+                visibility.targetState =
+                    when {
+                        currentPinEntries.isEmpty() && visiblePinEntries.size > 1 ->
+                            EntryVisibility.BulkHidden(index, visiblePinEntries.size)
+                        currentPinEntries.contains(entry) -> EntryVisibility.Shown
+                        else -> EntryVisibility.Hidden
+                    }
 
-            PinButton(
-                onClicked = { viewModel.onBackspaceButtonClicked() },
-                onLongPressed = { viewModel.onBackspaceButtonLongPressed() },
-                isEnabled = isInputEnabled,
-                isIconButton = true,
-            ) { contentColor ->
-                PinIcon(
-                    Icon.Resource(
-                        res = R.drawable.ic_backspace_24dp,
-                        contentDescription =
-                            ContentDescription.Resource(R.string.keyboardview_keycode_delete),
-                    ),
-                    contentColor,
-                )
-            }
+                val shape = viewModel.pinShapes.getShape(entry.sequenceNumber)
+                PinInputEntry(shape, updateTransition(visibility, label = "Pin Entry $entry"))
 
-            PinButton(
-                onClicked = { viewModel.onPinButtonClicked(0) },
-                isEnabled = isInputEnabled,
-            ) { contentColor ->
-                PinDigit(0, contentColor)
-            }
-
-            PinButton(
-                onClicked = { viewModel.onAuthenticateButtonClicked() },
-                isEnabled = isInputEnabled,
-                isIconButton = true,
-            ) { contentColor ->
-                PinIcon(
-                    Icon.Resource(
-                        res = R.drawable.ic_keyboard_tab_36dp,
-                        contentDescription =
-                            ContentDescription.Resource(R.string.keyboardview_keycode_enter),
-                    ),
-                    contentColor,
-                )
+                LaunchedEffect(entry) {
+                    // Remove entry from visiblePinEntries once the hide transition completed.
+                    snapshotFlow {
+                            visibility.currentState == visibility.targetState &&
+                                visibility.targetState != EntryVisibility.Shown
+                        }
+                        .collect { isRemoved ->
+                            if (isRemoved) {
+                                visiblePinEntries.remove(entry)
+                            }
+                        }
+                }
             }
         }
     }
 }
 
+private sealed class EntryVisibility {
+    object Shown : EntryVisibility()
+
+    object Hidden : EntryVisibility()
+
+    /**
+     * Same as [Hidden], but applies when multiple entries are hidden simultaneously, without
+     * collapsing during the hide.
+     */
+    data class BulkHidden(val staggerIndex: Int, val totalEntryCount: Int) : EntryVisibility()
+}
+
 @Composable
-private fun PinDigit(
+private fun PinInputEntry(shapeResourceId: Int, transition: Transition<EntryVisibility>) {
+    // spec: http://shortn/_DEhE3Xl2bi
+    val dismissStaggerDelayMs = 33
+    val dismissDurationMs = 450
+    val expansionDurationMs = 250
+    val shapeCollapseDurationMs = 200
+
+    val animatedEntryWidth by
+        transition.animateDp(
+            transitionSpec = {
+                when (val target = targetState) {
+                    is EntryVisibility.BulkHidden ->
+                        // only collapse horizontal space once all entries are removed
+                        snap(dismissDurationMs + dismissStaggerDelayMs * target.totalEntryCount)
+                    else -> tween(expansionDurationMs, easing = Easings.Standard)
+                }
+            },
+            label = "entry space"
+        ) { state ->
+            if (state == EntryVisibility.Shown) entryShapeSize else 0.dp
+        }
+
+    val animatedShapeSize by
+        transition.animateDp(
+            transitionSpec = {
+                when {
+                    EntryVisibility.Hidden isTransitioningTo EntryVisibility.Shown -> {
+                        // The AVD contains the entry transition.
+                        snap()
+                    }
+                    targetState is EntryVisibility.BulkHidden -> {
+                        val target = targetState as EntryVisibility.BulkHidden
+                        tween(
+                            dismissDurationMs,
+                            delayMillis = target.staggerIndex * dismissStaggerDelayMs,
+                            easing = Easings.Legacy,
+                        )
+                    }
+                    else -> tween(shapeCollapseDurationMs, easing = Easings.StandardDecelerate)
+                }
+            },
+            label = "shape size"
+        ) { state ->
+            if (state == EntryVisibility.Shown) entryShapeSize else 0.dp
+        }
+
+    val dotColor = MaterialTheme.colorScheme.onSurfaceVariant
+    Layout(
+        content = {
+            val image = AnimatedImageVector.animatedVectorResource(shapeResourceId)
+            var atEnd by remember { mutableStateOf(false) }
+            Image(
+                painter = rememberAnimatedVectorPainter(image, atEnd),
+                contentDescription = null,
+                contentScale = ContentScale.Crop,
+                colorFilter = ColorFilter.tint(dotColor),
+            )
+            LaunchedEffect(Unit) { atEnd = true }
+        }
+    ) { measurables, _ ->
+        val shapeSizePx = animatedShapeSize.roundToPx()
+        val placeable = measurables.single().measure(Constraints.fixed(shapeSizePx, shapeSizePx))
+
+        layout(animatedEntryWidth.roundToPx(), entryShapeSize.roundToPx()) {
+            placeable.place(
+                ((animatedEntryWidth - animatedShapeSize) / 2f).roundToPx(),
+                ((entryShapeSize - animatedShapeSize) / 2f).roundToPx()
+            )
+        }
+    }
+}
+
+@Composable
+private fun PinPad(viewModel: PinBouncerViewModel) {
+    val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
+    val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState()
+    val confirmButtonAppearance by viewModel.confirmButtonAppearance.collectAsState()
+    val animateFailure: Boolean by viewModel.animateFailure.collectAsState()
+
+    val buttonScaleAnimatables = remember { List(12) { Animatable(1f) } }
+    LaunchedEffect(animateFailure) {
+        // Show the failure animation if the user entered the wrong input.
+        if (animateFailure) {
+            showFailureAnimation(buttonScaleAnimatables)
+            viewModel.onFailureAnimationShown()
+        }
+    }
+
+    VerticalGrid(
+        columns = 3,
+        verticalSpacing = 12.dp,
+        horizontalSpacing = 20.dp,
+    ) {
+        repeat(9) { index ->
+            DigitButton(
+                index + 1,
+                isInputEnabled,
+                viewModel::onPinButtonClicked,
+                buttonScaleAnimatables[index]::value,
+            )
+        }
+
+        ActionButton(
+            icon =
+                Icon.Resource(
+                    res = R.drawable.ic_backspace_24dp,
+                    contentDescription =
+                        ContentDescription.Resource(R.string.keyboardview_keycode_delete),
+                ),
+            isInputEnabled = isInputEnabled,
+            onClicked = viewModel::onBackspaceButtonClicked,
+            onLongPressed = viewModel::onBackspaceButtonLongPressed,
+            appearance = backspaceButtonAppearance,
+            scaling = buttonScaleAnimatables[9]::value,
+        )
+
+        DigitButton(
+            0,
+            isInputEnabled,
+            viewModel::onPinButtonClicked,
+            buttonScaleAnimatables[10]::value,
+        )
+
+        ActionButton(
+            icon =
+                Icon.Resource(
+                    res = R.drawable.ic_keyboard_tab_36dp,
+                    contentDescription =
+                        ContentDescription.Resource(R.string.keyboardview_keycode_enter),
+                ),
+            isInputEnabled = isInputEnabled,
+            onClicked = viewModel::onAuthenticateButtonClicked,
+            appearance = confirmButtonAppearance,
+            scaling = buttonScaleAnimatables[11]::value,
+        )
+    }
+}
+
+@Composable
+private fun DigitButton(
     digit: Int,
-    contentColor: Color,
+    isInputEnabled: Boolean,
+    onClicked: (Int) -> Unit,
+    scaling: () -> Float,
 ) {
-    // TODO(b/281878426): once "color: () -> Color" (added to BasicText in aosp/2568972) makes it
-    //  into Text, use that here, to animate more efficiently.
-    Text(
-        text = digit.toString(),
-        style = MaterialTheme.typography.headlineLarge,
-        color = contentColor,
-    )
+    PinPadButton(
+        onClicked = { onClicked(digit) },
+        isEnabled = isInputEnabled,
+        backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
+        foregroundColor = MaterialTheme.colorScheme.onSurfaceVariant,
+        modifier =
+            Modifier.graphicsLayer {
+                val scale = scaling()
+                scaleX = scale
+                scaleY = scale
+            }
+    ) { contentColor ->
+        // TODO(b/281878426): once "color: () -> Color" (added to BasicText in aosp/2568972) makes
+        // it into Text, use that here, to animate more efficiently.
+        Text(
+            text = digit.toString(),
+            style = MaterialTheme.typography.headlineLarge,
+            color = contentColor(),
+        )
+    }
 }
 
 @Composable
-private fun PinIcon(
+private fun ActionButton(
     icon: Icon,
-    contentColor: Color,
+    isInputEnabled: Boolean,
+    onClicked: () -> Unit,
+    onLongPressed: (() -> Unit)? = null,
+    appearance: ActionButtonAppearance,
+    scaling: () -> Float,
 ) {
-    Icon(
-        icon = icon,
-        tint = contentColor,
-    )
+    val isHidden = appearance == ActionButtonAppearance.Hidden
+    val hiddenAlpha by animateFloatAsState(if (isHidden) 0f else 1f, label = "Action button alpha")
+
+    val foregroundColor =
+        when (appearance) {
+            ActionButtonAppearance.Shown -> MaterialTheme.colorScheme.onSecondaryContainer
+            else -> MaterialTheme.colorScheme.onSurface
+        }
+    val backgroundColor =
+        when (appearance) {
+            ActionButtonAppearance.Shown -> MaterialTheme.colorScheme.secondaryContainer
+            else -> MaterialTheme.colorScheme.surface
+        }
+
+    PinPadButton(
+        onClicked = onClicked,
+        onLongPressed = onLongPressed,
+        isEnabled = isInputEnabled && !isHidden,
+        backgroundColor = backgroundColor,
+        foregroundColor = foregroundColor,
+        modifier =
+            Modifier.graphicsLayer {
+                alpha = hiddenAlpha
+                val scale = scaling()
+                scaleX = scale
+                scaleY = scale
+            }
+    ) { contentColor ->
+        Icon(
+            icon = icon,
+            tint = contentColor(),
+        )
+    }
 }
 
 @Composable
-private fun PinButton(
+private fun PinPadButton(
     onClicked: () -> Unit,
     isEnabled: Boolean,
+    backgroundColor: Color,
+    foregroundColor: Color,
     modifier: Modifier = Modifier,
     onLongPressed: (() -> Unit)? = null,
-    isIconButton: Boolean = false,
-    content: @Composable (contentColor: Color) -> Unit,
+    content: @Composable (contentColor: () -> Color) -> Unit,
 ) {
     var isPressed: Boolean by remember { mutableStateOf(false) }
 
@@ -252,18 +431,16 @@
         animateColorAsState(
             when {
                 isPressed -> MaterialTheme.colorScheme.primary
-                isIconButton -> MaterialTheme.colorScheme.secondaryContainer
-                else -> MaterialTheme.colorScheme.surfaceVariant
+                else -> backgroundColor
             },
             label = "Pin button container color",
             animationSpec = colorAnimationSpec
         )
-    val contentColor: Color by
+    val contentColor =
         animateColorAsState(
             when {
                 isPressed -> MaterialTheme.colorScheme.onPrimary
-                isIconButton -> MaterialTheme.colorScheme.onSecondaryContainer
-                else -> MaterialTheme.colorScheme.onSurfaceVariant
+                else -> foregroundColor
             },
             label = "Pin button container color",
             animationSpec = colorAnimationSpec
@@ -302,19 +479,50 @@
                     }
                 },
     ) {
-        content(contentColor)
+        content(contentColor::value)
     }
 }
 
-private fun showFailureAnimation() {
-    // TODO(b/282730134): implement.
+private suspend fun showFailureAnimation(
+    buttonScaleAnimatables: List<Animatable<Float, AnimationVector1D>>
+) {
+    coroutineScope {
+        buttonScaleAnimatables.forEachIndexed { index, animatable ->
+            launch {
+                animatable.animateTo(
+                    targetValue = pinButtonErrorShrinkFactor,
+                    animationSpec =
+                        tween(
+                            durationMillis = pinButtonErrorShrinkMs,
+                            delayMillis = index * pinButtonErrorStaggerDelayMs,
+                            easing = Easings.Linear,
+                        ),
+                )
+
+                animatable.animateTo(
+                    targetValue = 1f,
+                    animationSpec =
+                        tween(
+                            durationMillis = pinButtonErrorRevertMs,
+                            easing = Easings.Legacy,
+                        ),
+                )
+            }
+        }
+    }
 }
 
+private val entryShapeSize = 30.dp
+
 private val pinButtonSize = 84.dp
+private val pinButtonErrorShrinkFactor = 67.dp / pinButtonSize
+private const val pinButtonErrorShrinkMs = 50
+private const val pinButtonErrorStaggerDelayMs = 33
+private const val pinButtonErrorRevertMs = 617
 
 // Pin button motion spec: http://shortn/_9TTIG6SoEa
 private val pinButtonPressedDuration = 100.milliseconds
-private val pinButtonPressedEasing = LinearEasing
+private val pinButtonPressedEasing = Easings.Linear
 private val pinButtonHoldTime = 33.milliseconds
 private val pinButtonReleasedDuration = 420.milliseconds
-private val pinButtonReleasedEasing = Easings.StandardEasing
+private val pinButtonReleasedEasing = Easings.Standard
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 75bf281..13acde2 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
@@ -121,8 +121,8 @@
         }
     }
 
-    val backgroundColor = colorAttr(R.attr.underSurfaceColor)
-    val contentColor = LocalAndroidColorScheme.current.deprecated.textColorPrimary
+    val backgroundColor = colorAttr(R.attr.underSurface)
+    val contentColor = LocalAndroidColorScheme.current.onSurface
     val backgroundTopRadius = dimensionResource(R.dimen.qs_corner_radius)
     val backgroundModifier =
         remember(
@@ -268,7 +268,7 @@
     val interactionSource = remember { MutableInteractionSource() }
 
     Expandable(
-        color = colorAttr(R.attr.offStateColor),
+        color = colorAttr(R.attr.shadeInactive),
         shape = CircleShape,
         onClick = onClick,
         interactionSource = interactionSource,
@@ -287,7 +287,7 @@
                     number.toString(),
                     modifier = Modifier.align(Alignment.Center),
                     style = MaterialTheme.typography.bodyLarge,
-                    color = LocalAndroidColorScheme.current.deprecated.textColorPrimary,
+                    color = colorAttr(R.attr.onShadeInactiveVariant),
                     // TODO(b/242040009): This should only use a standard text style instead and
                     // should not override the text size.
                     fontSize = 18.sp,
@@ -305,7 +305,7 @@
 @Composable
 private fun NewChangesDot(modifier: Modifier = Modifier) {
     val contentDescription = stringResource(R.string.fgs_dot_content_description)
-    val color = LocalAndroidColorScheme.current.deprecated.colorAccentTertiary
+    val color = LocalAndroidColorScheme.current.tertiary
 
     Canvas(modifier.size(12.dp).semantics { this.contentDescription = contentDescription }) {
         drawCircle(color)
@@ -323,10 +323,9 @@
 ) {
     Expandable(
         shape = CircleShape,
-        color = colorAttr(R.attr.underSurfaceColor),
-        contentColor = LocalAndroidColorScheme.current.deprecated.textColorSecondary,
-        borderStroke =
-            BorderStroke(1.dp, LocalAndroidColorScheme.current.deprecated.colorBackground),
+        color = colorAttr(R.attr.underSurface),
+        contentColor = LocalAndroidColorScheme.current.onSurfaceVariant,
+        borderStroke = BorderStroke(1.dp, colorAttr(R.attr.onShadeActive)),
         modifier = modifier.padding(horizontal = 4.dp),
         onClick = onClick,
     ) {
diff --git a/packages/SystemUI/compose/features/tests/AndroidManifest.xml b/packages/SystemUI/compose/features/tests/AndroidManifest.xml
index 2f41ea9..8fe9656 100644
--- a/packages/SystemUI/compose/features/tests/AndroidManifest.xml
+++ b/packages/SystemUI/compose/features/tests/AndroidManifest.xml
@@ -40,11 +40,6 @@
             android:enabled="false"
             tools:replace="android:authorities"
             tools:node="remove" />
-        <provider android:name="com.android.keyguard.clock.ClockOptionsProvider"
-            android:authorities="com.android.systemui.test.keyguard.clock.disabled"
-            android:enabled="false"
-            tools:replace="android:authorities"
-            tools:node="remove" />
         <provider android:name="com.android.systemui.people.PeopleProvider"
             android:authorities="com.android.systemui.test.people.disabled"
             android:enabled="false"
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 648ef03..d208404 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
@@ -34,7 +34,7 @@
 import com.android.systemui.animation.TextAnimator
 import com.android.systemui.customization.R
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.DEBUG
 import java.io.PrintWriter
 import java.util.Calendar
 import java.util.Locale
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 702cc05..12f7452 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
@@ -24,11 +24,11 @@
 import android.util.Log
 import androidx.annotation.OpenForTesting
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogMessage
 import com.android.systemui.log.LogMessageImpl
-import com.android.systemui.log.MessageInitializer
-import com.android.systemui.log.MessagePrinter
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogMessage
+import com.android.systemui.log.core.MessageInitializer
+import com.android.systemui.log.core.MessagePrinter
 import com.android.systemui.plugins.ClockController
 import com.android.systemui.plugins.ClockId
 import com.android.systemui.plugins.ClockMetadata
@@ -56,6 +56,7 @@
         "com.android.systemui.falcon.four" to listOf(ClockMetadata("DIGITAL_CLOCK_GROWTH")),
         "com.android.systemui.falcon.five" to listOf(ClockMetadata("DIGITAL_CLOCK_HANDWRITTEN")),
         "com.android.systemui.falcon.six" to listOf(ClockMetadata("DIGITAL_CLOCK_INFLATE")),
+        "com.android.systemui.falcon.seven" to listOf(ClockMetadata("DIGITAL_CLOCK_METRO")),
         "com.android.systemui.falcon.eight" to listOf(ClockMetadata("DIGITAL_CLOCK_NUMBEROVERLAP")),
         "com.android.systemui.falcon.nine" to listOf(ClockMetadata("DIGITAL_CLOCK_WEATHER")),
     )
@@ -112,6 +113,7 @@
     val logBuffer: LogBuffer? = null,
     val keepAllLoaded: Boolean,
     subTag: String,
+    var isTransitClockEnabled: Boolean = false,
 ) {
     private val TAG = "${ClockRegistry::class.simpleName} ($subTag)"
     interface ClockChangeListener {
@@ -203,6 +205,10 @@
                 var isClockListChanged = false
                 for (clock in plugin.getClocks()) {
                     val id = clock.clockId
+                    if (!isTransitClockEnabled && id == "DIGITAL_CLOCK_METRO") {
+                        continue
+                    }
+
                     val info =
                         availableClocks.concurrentGetOrPut(id, ClockInfo(clock, plugin, manager)) {
                             isClockListChanged = true
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 b6d5ef3..95a9ce9 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
@@ -184,6 +184,9 @@
         /** Flag denoting AI Wallpapers are enabled in wallpaper picker. */
         const val FLAG_NAME_WALLPAPER_PICKER_UI_FOR_AIWP = "wallpaper_picker_ui_for_aiwp"
 
+        /** Flag denoting transit clock are enabled in wallpaper picker. */
+        const val FLAG_NAME_TRANSIT_CLOCK = "lockscreen_custom_transit_clock"
+
         object Columns {
             /** String. Unique ID for the flag. */
             const val NAME = "name"
diff --git a/packages/SystemUI/docs/clock-plugins.md b/packages/SystemUI/docs/clock-plugins.md
index 2226d79..9cb115a 100644
--- a/packages/SystemUI/docs/clock-plugins.md
+++ b/packages/SystemUI/docs/clock-plugins.md
@@ -1,7 +1,7 @@
 # Clock Plugins
 
-The clock appearing on the lock screen and always on display (AOD) can be
-customized via the ClockProviderPlugin plugin interface.
+The clock appearing on the lock screen and always on display (AOD) can be customized via the
+ClockProviderPlugin plugin interface. The ClockPlugin interface has been removed.
 
 ## Lock screen integration
 The lockscreen code has two main components, a [clock customization library](../customization), and
diff --git a/packages/SystemUI/log/src/com/android/systemui/log/ConstantStringsLoggerImpl.kt b/packages/SystemUI/log/src/com/android/systemui/log/ConstantStringsLoggerImpl.kt
index 6fc52536..a4f4e13 100644
--- a/packages/SystemUI/log/src/com/android/systemui/log/ConstantStringsLoggerImpl.kt
+++ b/packages/SystemUI/log/src/com/android/systemui/log/ConstantStringsLoggerImpl.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.log
 
+import com.android.systemui.log.core.LogLevel
 import com.google.errorprone.annotations.CompileTimeConstant
 
 class ConstantStringsLoggerImpl(val buffer: LogBuffer, val tag: String) : ConstantStringsLogger {
diff --git a/packages/SystemUI/log/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/log/src/com/android/systemui/log/LogBuffer.kt
index 2007e76..e0051f5 100644
--- a/packages/SystemUI/log/src/com/android/systemui/log/LogBuffer.kt
+++ b/packages/SystemUI/log/src/com/android/systemui/log/LogBuffer.kt
@@ -19,6 +19,11 @@
 import android.os.Trace
 import android.util.Log
 import com.android.systemui.common.buffer.RingBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogMessage
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.log.core.MessageInitializer
+import com.android.systemui.log.core.MessagePrinter
 import com.google.errorprone.annotations.CompileTimeConstant
 import java.io.PrintWriter
 import java.util.concurrent.ArrayBlockingQueue
@@ -73,7 +78,7 @@
     private val maxSize: Int,
     private val logcatEchoTracker: LogcatEchoTracker,
     private val systrace: Boolean = true,
-) {
+) : MessageBuffer {
     private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() }
 
     private val echoMessageQueue: BlockingQueue<LogMessage>? =
@@ -174,11 +179,11 @@
      * store any relevant data on the message and then call [commit].
      */
     @Synchronized
-    fun obtain(
+    override fun obtain(
         tag: String,
         level: LogLevel,
         messagePrinter: MessagePrinter,
-        exception: Throwable? = null,
+        exception: Throwable?,
     ): LogMessage {
         if (!mutable) {
             return FROZEN_MESSAGE
@@ -195,7 +200,7 @@
      * have finished filling in its data fields. The message will be echoed to logcat if necessary.
      */
     @Synchronized
-    fun commit(message: LogMessage) {
+    override fun commit(message: LogMessage) {
         if (!mutable) {
             return
         }
@@ -292,11 +297,5 @@
     }
 }
 
-/**
- * A function that will be called immediately to store relevant data on the log message. The value
- * of `this` will be the LogMessage to be initialized.
- */
-typealias MessageInitializer = LogMessage.() -> Unit
-
 private const val TAG = "LogBuffer"
 private val FROZEN_MESSAGE = LogMessageImpl.create()
diff --git a/packages/SystemUI/log/src/com/android/systemui/log/LogLevel.kt b/packages/SystemUI/log/src/com/android/systemui/log/LogLevel.kt
deleted file mode 100644
index 7d9647a..0000000
--- a/packages/SystemUI/log/src/com/android/systemui/log/LogLevel.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.log
-
-import android.util.Log
-
-/** Enum version of @Log.Level */
-enum class LogLevel(@Log.Level val nativeLevel: Int, val shortString: String) {
-    VERBOSE(Log.VERBOSE, "V"),
-    DEBUG(Log.DEBUG, "D"),
-    INFO(Log.INFO, "I"),
-    WARNING(Log.WARN, "W"),
-    ERROR(Log.ERROR, "E"),
-    WTF(Log.ASSERT, "WTF")
-}
diff --git a/packages/SystemUI/log/src/com/android/systemui/log/LogMessage.kt b/packages/SystemUI/log/src/com/android/systemui/log/LogMessage.kt
deleted file mode 100644
index 8c3988b..0000000
--- a/packages/SystemUI/log/src/com/android/systemui/log/LogMessage.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.log
-
-import android.icu.text.SimpleDateFormat
-import java.io.PrintWriter
-import java.util.Locale
-
-/**
- * Generic data class for storing messages logged to a [LogBuffer]
- *
- * Each LogMessage has a few standard fields ([level], [tag], and [timestamp]). The rest are generic
- * data slots that may or may not be used, depending on the nature of the specific message being
- * logged.
- *
- * When a message is logged, the code doing the logging stores data in one or more of the generic
- * fields ([str1], [int1], etc). When it comes time to dump the message to logcat/bugreport/etc, the
- * [messagePrinter] function reads the data stored in the generic fields and converts that to a
- * human- readable string. Thus, for every log type there must be a specialized initializer function
- * that stores data specific to that log type and a specialized printer function that prints that
- * data.
- *
- * See [LogBuffer.log] for more information.
- */
-interface LogMessage {
-    val level: LogLevel
-    val tag: String
-    val timestamp: Long
-    val messagePrinter: MessagePrinter
-    val exception: Throwable?
-
-    var str1: String?
-    var str2: String?
-    var str3: String?
-    var int1: Int
-    var int2: Int
-    var long1: Long
-    var long2: Long
-    var double1: Double
-    var bool1: Boolean
-    var bool2: Boolean
-    var bool3: Boolean
-    var bool4: Boolean
-
-    /** Function that dumps the [LogMessage] to the provided [writer]. */
-    fun dump(writer: PrintWriter) {
-        val formattedTimestamp = DATE_FORMAT.format(timestamp)
-        val shortLevel = level.shortString
-        val messageToPrint = messagePrinter(this)
-        printLikeLogcat(writer, formattedTimestamp, shortLevel, tag, messageToPrint)
-        exception?.printStackTrace(writer)
-    }
-}
-
-/**
- * A function that will be called if and when the message needs to be dumped to logcat or a bug
- * report. It should read the data stored by the initializer and convert it to a human-readable
- * string. The value of `this` will be the LogMessage to be printed. **IMPORTANT:** The printer
- * should ONLY ever reference fields on the LogMessage and NEVER any variables in its enclosing
- * scope. Otherwise, the runtime will need to allocate a new instance of the printer for each call,
- * thwarting our attempts at avoiding any sort of allocation.
- */
-typealias MessagePrinter = LogMessage.() -> String
-
-private fun printLikeLogcat(
-    pw: PrintWriter,
-    formattedTimestamp: String,
-    shortLogLevel: String,
-    tag: String,
-    message: String
-) {
-    pw.print(formattedTimestamp)
-    pw.print(" ")
-    pw.print(shortLogLevel)
-    pw.print(" ")
-    pw.print(tag)
-    pw.print(": ")
-    pw.println(message)
-}
-
-private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
diff --git a/packages/SystemUI/log/src/com/android/systemui/log/LogMessageImpl.kt b/packages/SystemUI/log/src/com/android/systemui/log/LogMessageImpl.kt
index 5e10f78..33cc199 100644
--- a/packages/SystemUI/log/src/com/android/systemui/log/LogMessageImpl.kt
+++ b/packages/SystemUI/log/src/com/android/systemui/log/LogMessageImpl.kt
@@ -16,6 +16,10 @@
 
 package com.android.systemui.log
 
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogMessage
+import com.android.systemui.log.core.MessagePrinter
+
 /** Recyclable implementation of [LogMessage]. */
 data class LogMessageImpl(
     override var level: LogLevel,
diff --git a/packages/SystemUI/log/src/com/android/systemui/log/LogcatEchoTracker.kt b/packages/SystemUI/log/src/com/android/systemui/log/LogcatEchoTracker.kt
index 55f3a73..ae717df 100644
--- a/packages/SystemUI/log/src/com/android/systemui/log/LogcatEchoTracker.kt
+++ b/packages/SystemUI/log/src/com/android/systemui/log/LogcatEchoTracker.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.log
 
+import com.android.systemui.log.core.LogLevel
+
 /** Keeps track of which [LogBuffer] messages should also appear in logcat. */
 interface LogcatEchoTracker {
     /** Whether [bufferName] should echo messages of [level] or higher to logcat. */
diff --git a/packages/SystemUI/log/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/log/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
index d0ad28f..9ff48ca 100644
--- a/packages/SystemUI/log/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
+++ b/packages/SystemUI/log/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
@@ -23,6 +23,7 @@
 import android.os.Looper
 import android.os.Trace
 import android.provider.Settings
+import com.android.systemui.log.core.LogLevel
 
 /**
  * Version of [LogcatEchoTracker] for debuggable builds
diff --git a/packages/SystemUI/log/src/com/android/systemui/log/LogcatEchoTrackerProd.kt b/packages/SystemUI/log/src/com/android/systemui/log/LogcatEchoTrackerProd.kt
index 56966773..044d97f 100644
--- a/packages/SystemUI/log/src/com/android/systemui/log/LogcatEchoTrackerProd.kt
+++ b/packages/SystemUI/log/src/com/android/systemui/log/LogcatEchoTrackerProd.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.log
 
+import com.android.systemui.log.core.LogLevel
+
 /** Production version of [LogcatEchoTracker] that isn't configurable. */
 class LogcatEchoTrackerProd : LogcatEchoTracker {
     override val logInBackgroundThread = false
diff --git a/packages/SystemUI/log/src/com/android/systemui/log/core/LogLevel.kt b/packages/SystemUI/log/src/com/android/systemui/log/core/LogLevel.kt
new file mode 100644
index 0000000..d30d8e9
--- /dev/null
+++ b/packages/SystemUI/log/src/com/android/systemui/log/core/LogLevel.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log.core
+
+import android.util.Log
+
+/** Enum version of @Log.Level */
+enum class LogLevel(@Log.Level val nativeLevel: Int, val shortString: String) {
+    VERBOSE(Log.VERBOSE, "V"),
+    DEBUG(Log.DEBUG, "D"),
+    INFO(Log.INFO, "I"),
+    WARNING(Log.WARN, "W"),
+    ERROR(Log.ERROR, "E"),
+    WTF(Log.ASSERT, "WTF")
+}
diff --git a/packages/SystemUI/log/src/com/android/systemui/log/core/LogMessage.kt b/packages/SystemUI/log/src/com/android/systemui/log/core/LogMessage.kt
new file mode 100644
index 0000000..3bd6473
--- /dev/null
+++ b/packages/SystemUI/log/src/com/android/systemui/log/core/LogMessage.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log.core
+
+import android.icu.text.SimpleDateFormat
+import java.io.PrintWriter
+import java.util.Locale
+
+/**
+ * Generic data class for storing messages logged to a [LogBuffer]
+ *
+ * Each LogMessage has a few standard fields ([level], [tag], and [timestamp]). The rest are generic
+ * data slots that may or may not be used, depending on the nature of the specific message being
+ * logged.
+ *
+ * When a message is logged, the code doing the logging stores data in one or more of the generic
+ * fields ([str1], [int1], etc). When it comes time to dump the message to logcat/bugreport/etc, the
+ * [messagePrinter] function reads the data stored in the generic fields and converts that to a
+ * human- readable string. Thus, for every log type there must be a specialized initializer function
+ * that stores data specific to that log type and a specialized printer function that prints that
+ * data.
+ *
+ * See [LogBuffer.log] for more information.
+ */
+interface LogMessage {
+    val level: LogLevel
+    val tag: String
+    val timestamp: Long
+    val messagePrinter: MessagePrinter
+    val exception: Throwable?
+
+    var str1: String?
+    var str2: String?
+    var str3: String?
+    var int1: Int
+    var int2: Int
+    var long1: Long
+    var long2: Long
+    var double1: Double
+    var bool1: Boolean
+    var bool2: Boolean
+    var bool3: Boolean
+    var bool4: Boolean
+
+    /** Function that dumps the [LogMessage] to the provided [writer]. */
+    fun dump(writer: PrintWriter) {
+        val formattedTimestamp = DATE_FORMAT.format(timestamp)
+        val shortLevel = level.shortString
+        val messageToPrint = messagePrinter(this)
+        printLikeLogcat(writer, formattedTimestamp, shortLevel, tag, messageToPrint)
+        exception?.printStackTrace(writer)
+    }
+}
+
+/**
+ * A function that will be called immediately to store relevant data on the log message. The value
+ * of `this` will be the LogMessage to be initialized.
+ */
+typealias MessageInitializer = LogMessage.() -> Unit
+
+/**
+ * A function that will be called if and when the message needs to be dumped to logcat or a bug
+ * report. It should read the data stored by the initializer and convert it to a human-readable
+ * string. The value of `this` will be the LogMessage to be printed. **IMPORTANT:** The printer
+ * should ONLY ever reference fields on the LogMessage and NEVER any variables in its enclosing
+ * scope. Otherwise, the runtime will need to allocate a new instance of the printer for each call,
+ * thwarting our attempts at avoiding any sort of allocation.
+ */
+typealias MessagePrinter = LogMessage.() -> String
+
+private fun printLikeLogcat(
+    pw: PrintWriter,
+    formattedTimestamp: String,
+    shortLogLevel: String,
+    tag: String,
+    message: String
+) {
+    pw.print(formattedTimestamp)
+    pw.print(" ")
+    pw.print(shortLogLevel)
+    pw.print(" ")
+    pw.print(tag)
+    pw.print(": ")
+    pw.println(message)
+}
+
+private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
diff --git a/packages/SystemUI/log/src/com/android/systemui/log/core/Logger.kt b/packages/SystemUI/log/src/com/android/systemui/log/core/Logger.kt
new file mode 100644
index 0000000..5729ab2
--- /dev/null
+++ b/packages/SystemUI/log/src/com/android/systemui/log/core/Logger.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.core
+
+import com.google.errorprone.annotations.CompileTimeConstant
+
+/** Logs messages to the [MessageBuffer] with [tag]. */
+open class Logger(val buffer: MessageBuffer, val tag: String) {
+    /**
+     * Logs a message to the buffer.
+     *
+     * The actual string of the log message is not constructed until it is needed. To accomplish
+     * this, logging a message is a two-step process. First, a fresh instance of [LogMessage] is
+     * obtained and is passed to the [messageInitializer]. The initializer stores any relevant data
+     * on the message's fields. The message is then inserted into the buffer where it waits until it
+     * is either pushed out by newer messages or it needs to printed. If and when this latter moment
+     * occurs, the [messagePrinter] function is called on the message. It reads whatever data the
+     * initializer stored and converts it to a human-readable log message.
+     *
+     * @param level Which level to log the message at, both to the buffer and to logcat if it's
+     *   echoed. In general, a module should split most of its logs into either INFO or DEBUG level.
+     *   INFO level should be reserved for information that other parts of the system might care
+     *   about, leaving the specifics of code's day-to-day operations to DEBUG.
+     * @param messagePrinter A function that will be called if and when the message needs to be
+     *   dumped to logcat or a bug report. It should read the data stored by the initializer and
+     *   convert it to a human-readable string. The value of `this` will be the [LogMessage] to be
+     *   printed. **IMPORTANT:** The printer should ONLY ever reference fields on the [LogMessage]
+     *   and NEVER any variables in its enclosing scope. Otherwise, the runtime will need to
+     *   allocate a new instance of the printer for each call, thwarting our attempts at avoiding
+     *   any sort of allocation.
+     * @param exception Provide any exception that need to be logged. This is saved as
+     *   [LogMessage.exception]
+     * @param messageInitializer A function that will be called immediately to store relevant data
+     *   on the log message. The value of `this` will be the [LogMessage] to be initialized.
+     */
+    @JvmOverloads
+    inline fun log(
+        level: LogLevel,
+        noinline messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
+        messageInitializer: MessageInitializer,
+    ) {
+        val message = buffer.obtain(tag, level, messagePrinter, exception)
+        messageInitializer(message)
+        buffer.commit(message)
+    }
+
+    /**
+     * Logs a compile-time string constant [message] to the log buffer. Use sparingly.
+     *
+     * This is for simpler use-cases where [message] is a compile time string constant. For
+     * use-cases where the log message is built during runtime, use the [log] overloaded method that
+     * takes in an initializer and a message printer.
+     *
+     * Buffers are limited by the number of entries, so logging more frequently will limit the time
+     * window that the [MessageBuffer] covers in a bug report. Richer logs, on the other hand, make
+     * a bug report more actionable, so using the [log] with a [MessagePrinter] to add more details
+     * to every log may do more to improve overall logging than adding more logs with this method.
+     */
+    @JvmOverloads
+    fun log(
+        level: LogLevel,
+        @CompileTimeConstant message: String,
+        exception: Throwable? = null,
+    ) = log(level, { str1!! }, exception) { str1 = message }
+
+    /**
+     * Logs a message to the buffer at [LogLevel.VERBOSE].
+     *
+     * @see log
+     */
+    @JvmOverloads
+    inline fun v(
+        noinline messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
+        messageInitializer: MessageInitializer,
+    ) = log(LogLevel.VERBOSE, messagePrinter, exception, messageInitializer)
+
+    /**
+     * Logs a compile-time string constant [message] to the log buffer at [LogLevel.VERBOSE]. Use
+     * sparingly.
+     *
+     * @see log
+     */
+    @JvmOverloads
+    fun v(
+        @CompileTimeConstant message: String,
+        exception: Throwable? = null,
+    ) = log(LogLevel.VERBOSE, message, exception)
+
+    /**
+     * Logs a message to the buffer at [LogLevel.DEBUG].
+     *
+     * @see log
+     */
+    @JvmOverloads
+    inline fun d(
+        noinline messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
+        messageInitializer: MessageInitializer,
+    ) = log(LogLevel.DEBUG, messagePrinter, exception, messageInitializer)
+
+    /**
+     * Logs a compile-time string constant [message] to the log buffer at [LogLevel.DEBUG]. Use
+     * sparingly.
+     *
+     * @see log
+     */
+    @JvmOverloads
+    fun d(
+        @CompileTimeConstant message: String,
+        exception: Throwable? = null,
+    ) = log(LogLevel.DEBUG, message, exception)
+
+    /**
+     * Logs a message to the buffer at [LogLevel.INFO].
+     *
+     * @see log
+     */
+    @JvmOverloads
+    inline fun i(
+        noinline messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
+        messageInitializer: MessageInitializer,
+    ) = log(LogLevel.INFO, messagePrinter, exception, messageInitializer)
+
+    /**
+     * Logs a compile-time string constant [message] to the log buffer at [LogLevel.INFO]. Use
+     * sparingly.
+     *
+     * @see log
+     */
+    @JvmOverloads
+    fun i(
+        @CompileTimeConstant message: String,
+        exception: Throwable? = null,
+    ) = log(LogLevel.INFO, message, exception)
+
+    /**
+     * Logs a message to the buffer at [LogLevel.WARNING].
+     *
+     * @see log
+     */
+    @JvmOverloads
+    inline fun w(
+        noinline messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
+        messageInitializer: MessageInitializer,
+    ) = log(LogLevel.WARNING, messagePrinter, exception, messageInitializer)
+
+    /**
+     * Logs a compile-time string constant [message] to the log buffer at [LogLevel.WARNING]. Use
+     * sparingly.
+     *
+     * @see log
+     */
+    @JvmOverloads
+    fun w(
+        @CompileTimeConstant message: String,
+        exception: Throwable? = null,
+    ) = log(LogLevel.WARNING, message, exception)
+
+    /**
+     * Logs a message to the buffer at [LogLevel.ERROR].
+     *
+     * @see log
+     */
+    @JvmOverloads
+    inline fun e(
+        noinline messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
+        messageInitializer: MessageInitializer,
+    ) = log(LogLevel.ERROR, messagePrinter, exception, messageInitializer)
+
+    /**
+     * Logs a compile-time string constant [message] to the log buffer at [LogLevel.ERROR]. Use
+     * sparingly.
+     *
+     * @see log
+     */
+    @JvmOverloads
+    fun e(
+        @CompileTimeConstant message: String,
+        exception: Throwable? = null,
+    ) = log(LogLevel.ERROR, message, exception)
+
+    /**
+     * Logs a message to the buffer at [LogLevel.WTF].
+     *
+     * @see log
+     */
+    @JvmOverloads
+    inline fun wtf(
+        noinline messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
+        messageInitializer: MessageInitializer,
+    ) = log(LogLevel.WTF, messagePrinter, exception, messageInitializer)
+
+    /**
+     * Logs a compile-time string constant [message] to the log buffer at [LogLevel.WTF]. Use
+     * sparingly.
+     *
+     * @see log
+     */
+    @JvmOverloads
+    fun wtf(
+        @CompileTimeConstant message: String,
+        exception: Throwable? = null,
+    ) = log(LogLevel.WTF, message, exception)
+}
diff --git a/packages/SystemUI/log/src/com/android/systemui/log/core/MessageBuffer.kt b/packages/SystemUI/log/src/com/android/systemui/log/core/MessageBuffer.kt
new file mode 100644
index 0000000..bb91633
--- /dev/null
+++ b/packages/SystemUI/log/src/com/android/systemui/log/core/MessageBuffer.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.log.core
+
+/**
+ * [MessageBuffer] is an interface that represents a buffer of log messages, and provides methods to
+ * [obtain] a log message and [commit] it to the buffer.
+ */
+interface MessageBuffer {
+    /**
+     * Obtains the next [LogMessage] from the buffer.
+     *
+     * After calling [obtain], the caller must store any relevant data on the message and then call
+     * [commit].
+     */
+    fun obtain(
+        tag: String,
+        level: LogLevel,
+        messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
+    ): LogMessage
+
+    /**
+     * After acquiring a log message via [obtain], call this method to signal to the buffer that
+     * data fields have been filled.
+     */
+    fun commit(message: LogMessage)
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
index 2baeaf6..9cc87fd 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
@@ -133,6 +133,9 @@
             boolean willAnimateOnKeyguard,
             @Nullable String customMessage);
 
+    /** Whether we should animate an activity launch. */
+    boolean shouldAnimateLaunch(boolean isActivityIntent);
+
     interface Callback {
         void onActivityStarted(int resultCode);
     }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockPlugin.java
deleted file mode 100644
index bef61b8..0000000
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockPlugin.java
+++ /dev/null
@@ -1,145 +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.plugins;
-
-import android.graphics.Bitmap;
-import android.graphics.Paint.Style;
-import android.view.View;
-
-import com.android.systemui.plugins.annotations.ProvidesInterface;
-
-import java.util.TimeZone;
-
-/**
- * Plugin used to replace main clock in keyguard.
- * @deprecated Migrating to ClockProviderPlugin
- */
-@Deprecated
-@ProvidesInterface(action = ClockPlugin.ACTION, version = ClockPlugin.VERSION)
-public interface ClockPlugin extends Plugin {
-
-    String ACTION = "com.android.systemui.action.PLUGIN_CLOCK";
-    int VERSION = 5;
-
-    /**
-     * Get the name of the clock face.
-     *
-     * This name should not be translated.
-     */
-    String getName();
-
-    /**
-     * Get the title of the clock face to be shown in the picker app.
-     */
-    String getTitle();
-
-    /**
-     * Get thumbnail of clock face to be shown in the picker app.
-     */
-    Bitmap getThumbnail();
-
-    /**
-     * Get preview images of clock face to be shown in the picker app.
-     *
-     * Preview image should be realistic and show what the clock face will look like on AOD and lock
-     * screen.
-     *
-     * @param width width of the preview image, should be the same as device width in pixels.
-     * @param height height of the preview image, should be the same as device height in pixels.
-     */
-    Bitmap getPreview(int width, int height);
-
-    /**
-     * Get clock view.
-     * @return clock view from plugin.
-     */
-    View getView();
-
-    /**
-     * Get clock view for a large clock that appears behind NSSL.
-     */
-    default View getBigClockView() {
-        return null;
-    }
-
-    /**
-     * Returns the preferred Y position of the clock.
-     *
-     * @param totalHeight Height of the parent container.
-     * @return preferred Y position.
-     */
-    int getPreferredY(int totalHeight);
-
-    /**
-     * Allows the plugin to clean up resources when no longer needed.
-     *
-     * Called when the view previously created by {@link ClockPlugin#getView()} has been detached
-     * from the view hierarchy.
-     */
-    void onDestroyView();
-
-    /**
-     * Set clock paint style.
-     * @param style The new style to set in the paint.
-     */
-    void setStyle(Style style);
-
-    /**
-     * Set clock text color.
-     * @param color A color value.
-     */
-    void setTextColor(int color);
-
-    /**
-     * Sets the color palette for the clock face.
-     * @param supportsDarkText Whether dark text can be displayed.
-     * @param colors Colors that should be used on the clock face, ordered from darker to lighter.
-     */
-    default void setColorPalette(boolean supportsDarkText, int[] colors) {}
-
-    /**
-     * Set the amount (ratio) that the device has transitioned to doze.
-     * @param darkAmount Amount of transition to doze: 1f for doze and 0f for awake.
-     */
-    default void setDarkAmount(float darkAmount) {}
-
-    /**
-     * Notifies that time tick alarm from doze service fired.
-     *
-     * Implement this method instead of registering a broadcast listener for TIME_TICK.
-     */
-    default void onTimeTick() {}
-
-    /**
-     * Notifies that the time zone has changed.
-     *
-     * Implement this method instead of registering a broadcast listener for TIME_ZONE_CHANGED.
-     */
-    default void onTimeZoneChanged(TimeZone timeZone) {}
-
-    /**
-     * Notifies that the time format has changed.
-     *
-     * @param timeFormat "12" for 12-hour format, "24" for 24-hour format
-     */
-    default void onTimeFormatChanged(String timeFormat) {}
-
-    /**
-     * Indicates whether the keyguard status area (date) should be shown below
-     * the clock.
-     */
-    default boolean shouldShowStatusArea() {
-        return true;
-    }
-}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt
index a4b1cee..f83fa33 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/WeatherData.kt
@@ -2,14 +2,18 @@
 
 import android.os.Bundle
 import android.util.Log
+import android.view.View
 import androidx.annotation.VisibleForTesting
 
+typealias WeatherTouchAction = (View) -> Unit
+
 class WeatherData
 constructor(
     val description: String,
     val state: WeatherStateIcon,
     val useCelsius: Boolean,
     val temperature: Int,
+    val touchAction: WeatherTouchAction? = null,
 ) {
     companion object {
         const val DEBUG = true
@@ -20,7 +24,7 @@
         @VisibleForTesting const val TEMPERATURE_KEY = "temperature"
         private const val INVALID_WEATHER_ICON_STATE = -1
 
-        fun fromBundle(extras: Bundle): WeatherData? {
+        fun fromBundle(extras: Bundle, touchAction: WeatherTouchAction? = null): WeatherData? {
             val description = extras.getString(DESCRIPTION_KEY)
             val state =
                 WeatherStateIcon.fromInt(extras.getInt(STATE_KEY, INVALID_WEATHER_ICON_STATE))
@@ -41,7 +45,8 @@
                         description = description,
                         state = state,
                         useCelsius = extras.getBoolean(USE_CELSIUS_KEY),
-                        temperature = temperature
+                        temperature = temperature,
+                        touchAction = touchAction
                     )
                 if (DEBUG) {
                     Log.i(TAG, "Weather data parsed $result from $extras")
diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags
index a8ed843..b534fcec 100644
--- a/packages/SystemUI/proguard.flags
+++ b/packages/SystemUI/proguard.flags
@@ -1,13 +1,7 @@
 -include proguard_common.flags
 
--keep class com.android.systemui.statusbar.tv.TvStatusBar
 -keep class com.android.systemui.SystemUIInitializerImpl {
     *;
 }
 
--keep class com.android.systemui.tv.TvSystemUIInitializer {
-    *;
-}
-
--keep,allowoptimization,allowaccessmodification class com.android.systemui.dagger.DaggerReferenceGlobalRootComponent** { !synthetic *; }
--keep,allowoptimization,allowaccessmodification class com.android.systemui.tv.DaggerTvGlobalRootComponent** { !synthetic *; }
\ No newline at end of file
+-keep,allowoptimization,allowaccessmodification class com.android.systemui.dagger.DaggerReferenceGlobalRootComponent** { !synthetic *; }
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/color/shade_disabled.xml b/packages/SystemUI/res-keyguard/color/shade_disabled.xml
new file mode 100644
index 0000000..241f203
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/color/shade_disabled.xml
@@ -0,0 +1,19 @@
+<?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:color="@android:color/system_neutral1_500" android:lStar="4" />
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml b/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml
index a7ffe9c..c09607d 100644
--- a/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml
+++ b/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml
@@ -26,7 +26,7 @@
         android:layout_height="wrap_content"
         android:textAppearance="@style/TextAppearance.QS.SecurityFooter"
         android:layout_gravity="center"
-        android:textColor="?android:attr/textColorPrimary"
+        android:textColor="?attr/onShadeInactiveVariant"
         android:textSize="18sp"/>
     <ImageView
         android:id="@+id/new_dot"
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 6fe7d39..1c31f1d 100644
--- a/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml
+++ b/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml
@@ -33,7 +33,7 @@
         android:layout_marginEnd="12dp"
         android:contentDescription="@null"
         android:src="@drawable/ic_info_outline"
-        android:tint="?android:attr/textColorSecondary" />
+        android:tint="?attr/onSurfaceVariant" />
 
     <TextView
         android:id="@+id/text"
@@ -43,7 +43,7 @@
         android:maxLines="1"
         android:ellipsize="end"
         android:textAppearance="@style/TextAppearance.QS.SecurityFooter"
-        android:textColor="?android:attr/textColorSecondary"/>
+        android:textColor="?attr/onSurfaceVariant"/>
 
     <ImageView
         android:id="@+id/new_dot"
@@ -62,5 +62,5 @@
         android:contentDescription="@null"
         android:src="@*android:drawable/ic_chevron_end"
         android:autoMirrored="true"
-        android:tint="?android:attr/textColorSecondary" />
+        android:tint="?attr/onSurfaceVariant" />
 </com.android.systemui.animation.view.LaunchableLinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
index 3412a30..2fc1d2e 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
@@ -29,7 +29,7 @@
     >
     <include layout="@layout/keyguard_bouncer_message_area"/>
 
-    <com.android.systemui.keyguard.bouncer.ui.BouncerMessageView
+    <com.android.systemui.bouncer.ui.BouncerMessageView
         android:id="@+id/bouncer_message_view"
         android:layout_width="match_parent"
         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 78a7c17..d9011c2 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
@@ -33,7 +33,7 @@
     android:clipToPadding="false">
     <include layout="@layout/keyguard_bouncer_message_area"/>
 
-    <com.android.systemui.keyguard.bouncer.ui.BouncerMessageView
+    <com.android.systemui.bouncer.ui.BouncerMessageView
         android:id="@+id/bouncer_message_view"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
index 01c5443..c7d2d81 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
@@ -29,7 +29,7 @@
     androidprv:layout_maxWidth="@dimen/keyguard_security_width">
 <include layout="@layout/keyguard_bouncer_message_area"/>
 
-<com.android.systemui.keyguard.bouncer.ui.BouncerMessageView
+<com.android.systemui.bouncer.ui.BouncerMessageView
     android:id="@+id/bouncer_message_view"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res-keyguard/layout/udfps_keyguard_view.xml b/packages/SystemUI/res-keyguard/layout/udfps_keyguard_view.xml
new file mode 100644
index 0000000..360ef26
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/layout/udfps_keyguard_view.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.
+  -->
+<com.android.systemui.biometrics.UdfpsKeyguardView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/udfps_animation_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <!-- Add fingerprint views here. See udfps_keyguard_view_internal.xml. -->
+
+</com.android.systemui.biometrics.UdfpsKeyguardView>
diff --git a/packages/SystemUI/res/drawable/brightness_mirror_background.xml b/packages/SystemUI/res/drawable/brightness_mirror_background.xml
index 2095103..b5c181b 100644
--- a/packages/SystemUI/res/drawable/brightness_mirror_background.xml
+++ b/packages/SystemUI/res/drawable/brightness_mirror_background.xml
@@ -15,6 +15,6 @@
   ~ limitations under the License
   -->
 <shape xmlns:android="http://schemas.android.com/apk/res/android">
-    <solid android:color="?attr/underSurfaceColor" />
+    <solid android:color="?attr/underSurface" />
     <corners android:radius="@dimen/rounded_slider_background_rounded_corner" />
 </shape>
diff --git a/packages/SystemUI/res/drawable/brightness_progress_drawable.xml b/packages/SystemUI/res/drawable/brightness_progress_drawable.xml
index 569ee76..95c7778c 100644
--- a/packages/SystemUI/res/drawable/brightness_progress_drawable.xml
+++ b/packages/SystemUI/res/drawable/brightness_progress_drawable.xml
@@ -24,7 +24,7 @@
             <shape>
                 <size android:height="@dimen/rounded_slider_track_width" />
                 <corners android:radius="@dimen/rounded_slider_track_corner_radius" />
-                <solid android:color="?attr/offStateColor" />
+                <solid android:color="?attr/shadeInactive" />
             </shape>
         </inset>
     </item>
diff --git a/packages/SystemUI/res/drawable/brightness_progress_full_drawable.xml b/packages/SystemUI/res/drawable/brightness_progress_full_drawable.xml
index 4d9188c..2ea90c7 100644
--- a/packages/SystemUI/res/drawable/brightness_progress_full_drawable.xml
+++ b/packages/SystemUI/res/drawable/brightness_progress_full_drawable.xml
@@ -22,7 +22,7 @@
         android:height="@dimen/rounded_slider_height">
         <shape>
             <size android:height="@dimen/rounded_slider_height" />
-            <solid android:color="?priv-android:attr/colorAccentPrimary" />
+            <solid android:color="?attr/shadeActive" />
             <corners android:radius="@dimen/rounded_slider_corner_radius"/>
         </shape>
     </item>
@@ -34,7 +34,7 @@
         android:right="@dimen/rounded_slider_icon_inset">
         <com.android.systemui.util.AlphaTintDrawableWrapper
             android:drawable="@drawable/ic_brightness"
-            android:tint="?android:attr/textColorPrimaryInverse"
+            android:tint="?attr/onShadeActive"
         />
     </item>
 </layer-list>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/fgs_dot.xml b/packages/SystemUI/res/drawable/fgs_dot.xml
index 3669e1d..0881d7c 100644
--- a/packages/SystemUI/res/drawable/fgs_dot.xml
+++ b/packages/SystemUI/res/drawable/fgs_dot.xml
@@ -19,5 +19,5 @@
     android:shape="oval"
     android:width="12dp"
     android:height="12dp">
-    <solid android:color="?androidprv:attr/colorAccentTertiary" />
+    <solid android:color="?attr/tertiary" />
 </shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/pin_dot_avd.xml b/packages/SystemUI/res/drawable/pin_dot_avd.xml
index 1c16251..710ba83 100644
--- a/packages/SystemUI/res/drawable/pin_dot_avd.xml
+++ b/packages/SystemUI/res/drawable/pin_dot_avd.xml
@@ -1 +1,40 @@
-<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="30dp" android:width="30dp" android:viewportHeight="30" android:viewportWidth="30"><group android:name="_R_G"><group android:name="_R_G_L_0_G" android:translateX="28.237000000000002" android:translateY="23.112000000000002"><path android:name="_R_G_L_0_G_D_0_P_0" android:strokeColor="#ffffff" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M-13.24 -12.11 C-11.03,-12.11 -9.24,-10.32 -9.24,-8.11 C-9.24,-5.9 -11.03,-4.11 -13.24,-4.11 C-15.44,-4.11 -17.24,-5.9 -17.24,-8.11 C-17.24,-10.32 -15.44,-12.11 -13.24,-12.11c "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="500" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:height="30dp"
+            android:width="30dp"
+            android:viewportHeight="30"
+            android:viewportWidth="30">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:translateX="28.237000000000002"
+                    android:translateY="23.112000000000002">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:strokeColor="#ffffff"
+                        android:strokeLineCap="round"
+                        android:strokeLineJoin="round"
+                        android:strokeWidth="2"
+                        android:strokeAlpha="1"
+                        android:pathData=" M-13.24 -12.11 C-11.03,-12.11 -9.24,-10.32 -9.24,-8.11 C-9.24,-5.9 -11.03,-4.11 -13.24,-4.11 C-15.44,-4.11 -17.24,-5.9 -17.24,-8.11 C-17.24,-10.32 -15.44,-12.11 -13.24,-12.11c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="translateX"
+                    android:duration="500"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/pin_dot_delete_avd.xml b/packages/SystemUI/res/drawable/pin_dot_delete_avd.xml
index 0f8703f..72a03bf 100644
--- a/packages/SystemUI/res/drawable/pin_dot_delete_avd.xml
+++ b/packages/SystemUI/res/drawable/pin_dot_delete_avd.xml
@@ -1 +1,130 @@
-<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="30dp" android:width="30dp" android:viewportHeight="30" android:viewportWidth="30"><group android:name="_R_G"><group android:name="_R_G_L_0_G" android:translateX="28.54" android:translateY="23.54"><path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-13.54 -16.54 C-9.12,-16.54 -5.54,-12.96 -5.54,-8.54 C-5.54,-4.12 -9.12,-0.54 -13.54,-0.54 C-17.96,-0.54 -21.54,-4.12 -21.54,-8.54 C-21.54,-12.96 -17.96,-16.54 -13.54,-16.54c "/><path android:name="_R_G_L_0_G_D_1_P_0" android:strokeColor="#ffffff" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0" android:strokeAlpha="1" android:pathData=" M-13.54 -16.54 C-9.12,-16.54 -5.54,-12.96 -5.54,-8.54 C-5.54,-4.12 -9.12,-0.54 -13.54,-0.54 C-17.96,-0.54 -21.54,-4.12 -21.54,-8.54 C-21.54,-12.96 -17.96,-16.54 -13.54,-16.54c "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="fillAlpha" android:duration="150" android:startOffset="0" android:valueFrom="1" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="fillAlpha" android:duration="200" android:startOffset="150" android:valueFrom="1" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="150" android:startOffset="0" android:valueFrom="M-13.54 -16.54 C-9.12,-16.54 -5.54,-12.96 -5.54,-8.54 C-5.54,-4.12 -9.12,-0.54 -13.54,-0.54 C-17.96,-0.54 -21.54,-4.12 -21.54,-8.54 C-21.54,-12.96 -17.96,-16.54 -13.54,-16.54c " android:valueTo="M-13.54 -11.54 C-11.88,-11.54 -10.54,-10.2 -10.54,-8.54 C-10.54,-6.88 -11.88,-5.54 -13.54,-5.54 C-15.2,-5.54 -16.54,-6.88 -16.54,-8.54 C-16.54,-10.2 -15.2,-11.54 -13.54,-11.54c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="200" android:startOffset="150" android:valueFrom="M-13.54 -11.54 C-11.88,-11.54 -10.54,-10.2 -10.54,-8.54 C-10.54,-6.88 -11.88,-5.54 -13.54,-5.54 C-15.2,-5.54 -16.54,-6.88 -16.54,-8.54 C-16.54,-10.2 -15.2,-11.54 -13.54,-11.54c " android:valueTo="M-13.54 -12.54 C-11.33,-12.54 -9.54,-10.75 -9.54,-8.54 C-9.54,-6.33 -11.33,-4.54 -13.54,-4.54 C-15.75,-4.54 -17.54,-6.33 -17.54,-8.54 C-17.54,-10.75 -15.75,-12.54 -13.54,-12.54c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_1_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="strokeWidth" android:duration="150" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="strokeWidth" android:duration="50" android:startOffset="150" android:valueFrom="0" android:valueTo="2" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_1_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="150" android:startOffset="0" android:valueFrom="M-13.54 -16.54 C-9.12,-16.54 -5.54,-12.96 -5.54,-8.54 C-5.54,-4.12 -9.12,-0.54 -13.54,-0.54 C-17.96,-0.54 -21.54,-4.12 -21.54,-8.54 C-21.54,-12.96 -17.96,-16.54 -13.54,-16.54c " android:valueTo="M-13.54 -11.54 C-11.88,-11.54 -10.54,-10.2 -10.54,-8.54 C-10.54,-6.88 -11.88,-5.54 -13.54,-5.54 C-15.2,-5.54 -16.54,-6.88 -16.54,-8.54 C-16.54,-10.2 -15.2,-11.54 -13.54,-11.54c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="200" android:startOffset="150" android:valueFrom="M-13.54 -11.54 C-11.88,-11.54 -10.54,-10.2 -10.54,-8.54 C-10.54,-6.88 -11.88,-5.54 -13.54,-5.54 C-15.2,-5.54 -16.54,-6.88 -16.54,-8.54 C-16.54,-10.2 -15.2,-11.54 -13.54,-11.54c " android:valueTo="M-13.54 -12.54 C-11.33,-12.54 -9.54,-10.75 -9.54,-8.54 C-9.54,-6.33 -11.33,-4.54 -13.54,-4.54 C-15.75,-4.54 -17.54,-6.33 -17.54,-8.54 C-17.54,-10.75 -15.75,-12.54 -13.54,-12.54c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="500" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_1_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="150"
+                    android:propertyName="scaleX"
+                    android:startOffset="0"
+                    android:valueFrom="1"
+                    android:valueTo="0.43"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="150"
+                    android:propertyName="scaleY"
+                    android:startOffset="0"
+                    android:valueFrom="1"
+                    android:valueTo="0.43"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="fillAlpha"
+                    android:duration="200"
+                    android:startOffset="150"
+                    android:valueFrom="1"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="200"
+                    android:propertyName="scaleX"
+                    android:startOffset="150"
+                    android:valueFrom="0.65"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="200"
+                    android:propertyName="scaleY"
+                    android:startOffset="150"
+                    android:valueFrom="0.65"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="500"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="30dp"
+            android:height="30dp"
+            android:viewportHeight="30"
+            android:viewportWidth="30">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:pivotX="-13.54"
+                    android:pivotY="-8.54"
+                    android:scaleX="1"
+                    android:scaleY="1"
+                    android:translateX="28.54"
+                    android:translateY="23.54">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M-13.54 -16.54 C-9.12,-16.54 -5.54,-12.96 -5.54,-8.54 C-5.54,-4.12 -9.12,-0.54 -13.54,-0.54 C-17.96,-0.54 -21.54,-4.12 -21.54,-8.54 C-21.54,-12.96 -17.96,-16.54 -13.54,-16.54c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:pivotX="317.509"
+                    android:pivotY="-826.986"
+                    android:scaleX="0.65"
+                    android:scaleY="0.65"
+                    android:translateX="-302.509"
+                    android:translateY="841.986">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M317.51 -821.99 C314.75,-821.99 312.51,-824.23 312.51,-826.99 C312.51,-829.74 314.75,-831.99 317.51,-831.99 C320.27,-831.99 322.51,-829.74 322.51,-826.99 C322.51,-824.23 320.27,-821.99 317.51,-821.99z  M317.51 -829.99 C315.86,-829.99 314.51,-828.64 314.51,-826.99 C314.51,-825.33 315.86,-823.99 317.51,-823.99 C319.16,-823.99 320.51,-825.33 320.51,-826.99 C320.51,-828.64 319.16,-829.99 317.51,-829.99z " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/pin_dot_shape_1_avd.xml b/packages/SystemUI/res/drawable/pin_dot_shape_1_avd.xml
index f1fb2aa..02abb99 100644
--- a/packages/SystemUI/res/drawable/pin_dot_shape_1_avd.xml
+++ b/packages/SystemUI/res/drawable/pin_dot_shape_1_avd.xml
@@ -1 +1,141 @@
-<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="30dp" android:width="30dp" android:viewportHeight="30" android:viewportWidth="30"><group android:name="_R_G"><group android:name="_R_G_L_1_G" android:translateX="15.441" android:translateY="15.691" android:scaleY="0"><path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-0.44 -12.06 C13.5,-14.63 13.5,-14.63 10.9,-0.69 C13.5,13.25 13.5,13.25 -0.44,10.53 C-14.38,13.25 -14.38,13.25 -11.86,-0.69 C-14.38,-14.63 -14.38,-14.63 -0.44,-12.06c "/></group><group android:name="_R_G_L_0_G" android:translateX="15" android:translateY="15" android:scaleX="0" android:scaleY="0"><path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-13.65 -3.95 C-16.31,-10.09 -10.09,-16.31 -3.95,-13.65 C-3.95,-13.65 -2.94,-13.21 -2.94,-13.21 C-1.06,-12.39 1.06,-12.39 2.94,-13.21 C2.94,-13.21 3.95,-13.65 3.95,-13.65 C10.09,-16.31 16.31,-10.09 13.65,-3.95 C13.65,-3.95 13.21,-2.94 13.21,-2.94 C12.39,-1.06 12.39,1.06 13.21,2.94 C13.21,2.94 13.65,3.95 13.65,3.95 C16.31,10.09 10.09,16.31 3.95,13.65 C3.95,13.65 2.94,13.21 2.94,13.21 C1.06,12.39 -1.06,12.39 -2.94,13.21 C-2.94,13.21 -3.95,13.65 -3.95,13.65 C-10.09,16.31 -16.31,10.09 -13.65,3.95 C-13.65,3.95 -13.21,2.94 -13.21,2.94 C-12.39,1.06 -12.39,-1.06 -13.21,-2.94 C-13.21,-2.94 -13.65,-3.95 -13.65,-3.95c "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_1_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="67" android:startOffset="0" android:valueFrom="M-0.44 -12.06 C13.5,-14.63 13.5,-14.63 10.9,-0.69 C13.5,13.25 13.5,13.25 -0.44,10.53 C-14.38,13.25 -14.38,13.25 -11.86,-0.69 C-14.38,-14.63 -14.38,-14.63 -0.44,-12.06c " android:valueTo="M-0.44 -12.06 C13.5,-14.63 13.5,-14.63 10.9,-0.69 C13.5,13.25 13.5,13.25 -0.44,10.53 C-14.38,13.25 -14.38,13.25 -11.86,-0.69 C-14.38,-14.63 -14.38,-14.63 -0.44,-12.06c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="283" android:startOffset="67" android:valueFrom="M-0.44 -12.06 C13.5,-14.63 13.5,-14.63 10.9,-0.69 C13.5,13.25 13.5,13.25 -0.44,10.53 C-14.38,13.25 -14.38,13.25 -11.86,-0.69 C-14.38,-14.63 -14.38,-14.63 -0.44,-12.06c " android:valueTo="M-0.44 -8.69 C3.98,-8.69 7.56,-5.11 7.56,-0.69 C7.56,3.73 3.98,7.31 -0.44,7.31 C-4.86,7.31 -8.44,3.73 -8.44,-0.69 C-8.44,-5.11 -4.86,-8.69 -0.44,-8.69c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleX" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleX" android:duration="283" android:startOffset="67" android:valueFrom="1" android:valueTo="0.3" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.7,0 0.6,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="283" android:startOffset="67" android:valueFrom="1" android:valueTo="0.3" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.7,0 0.6,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="367" android:valueFrom="1" android:valueTo="0" android:valueType="floatType"/></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="500" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="67"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M-0.44 -12.06 C13.5,-14.63 13.5,-14.63 10.9,-0.69 C13.5,13.25 13.5,13.25 -0.44,10.53 C-14.38,13.25 -14.38,13.25 -11.86,-0.69 C-14.38,-14.63 -14.38,-14.63 -0.44,-12.06c "
+                    android:valueTo="M-0.44 -12.06 C13.5,-14.63 13.5,-14.63 10.9,-0.69 C13.5,13.25 13.5,13.25 -0.44,10.53 C-14.38,13.25 -14.38,13.25 -11.86,-0.69 C-14.38,-14.63 -14.38,-14.63 -0.44,-12.06c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="283"
+                    android:propertyName="pathData"
+                    android:startOffset="67"
+                    android:valueFrom="M-0.44 -12.06 C13.5,-14.63 13.5,-14.63 10.9,-0.69 C13.5,13.25 13.5,13.25 -0.44,10.53 C-14.38,13.25 -14.38,13.25 -11.86,-0.69 C-14.38,-14.63 -14.38,-14.63 -0.44,-12.06c "
+                    android:valueTo="M-0.44 -8.69 C3.98,-8.69 7.56,-5.11 7.56,-0.69 C7.56,3.73 3.98,7.31 -0.44,7.31 C-4.86,7.31 -8.44,3.73 -8.44,-0.69 C-8.44,-5.11 -4.86,-8.69 -0.44,-8.69c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="0"
+                    android:propertyName="scaleY"
+                    android:startOffset="67"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="67"
+                    android:propertyName="scaleX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="67"
+                    android:propertyName="scaleY"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="283"
+                    android:propertyName="scaleX"
+                    android:startOffset="67"
+                    android:valueFrom="1"
+                    android:valueTo="0.3"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.7,0 0.6,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="283"
+                    android:propertyName="scaleY"
+                    android:startOffset="67"
+                    android:valueFrom="1"
+                    android:valueTo="0.3"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.7,0 0.6,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="500"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="30dp"
+            android:height="30dp"
+            android:viewportHeight="30"
+            android:viewportWidth="30">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:scaleY="0"
+                    android:translateX="15.441"
+                    android:translateY="15.691">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M-0.44 -12.06 C13.5,-14.63 13.5,-14.63 10.9,-0.69 C13.5,13.25 13.5,13.25 -0.44,10.53 C-14.38,13.25 -14.38,13.25 -11.86,-0.69 C-14.38,-14.63 -14.38,-14.63 -0.44,-12.06c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:translateX="15"
+                    android:translateY="15">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M-13.65 -3.95 C-16.31,-10.09 -10.09,-16.31 -3.95,-13.65 C-3.95,-13.65 -2.94,-13.21 -2.94,-13.21 C-1.06,-12.39 1.06,-12.39 2.94,-13.21 C2.94,-13.21 3.95,-13.65 3.95,-13.65 C10.09,-16.31 16.31,-10.09 13.65,-3.95 C13.65,-3.95 13.21,-2.94 13.21,-2.94 C12.39,-1.06 12.39,1.06 13.21,2.94 C13.21,2.94 13.65,3.95 13.65,3.95 C16.31,10.09 10.09,16.31 3.95,13.65 C3.95,13.65 2.94,13.21 2.94,13.21 C1.06,12.39 -1.06,12.39 -2.94,13.21 C-2.94,13.21 -3.95,13.65 -3.95,13.65 C-10.09,16.31 -16.31,10.09 -13.65,3.95 C-13.65,3.95 -13.21,2.94 -13.21,2.94 C-12.39,1.06 -12.39,-1.06 -13.21,-2.94 C-13.21,-2.94 -13.65,-3.95 -13.65,-3.95c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/pin_dot_shape_2_avd.xml b/packages/SystemUI/res/drawable/pin_dot_shape_2_avd.xml
index 3717db8..1607327 100644
--- a/packages/SystemUI/res/drawable/pin_dot_shape_2_avd.xml
+++ b/packages/SystemUI/res/drawable/pin_dot_shape_2_avd.xml
@@ -1 +1,148 @@
-<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="30dp" android:width="30dp" android:viewportHeight="30" android:viewportWidth="30"><group android:name="_R_G"><group android:name="_R_G_L_1_G" android:translateX="15.397" android:translateY="15.691" android:scaleY="0"><path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-0.56 -14.03 C3.65,-13.99 14.58,7.64 11.51,10.42 C8.45,13.2 5.92,9.56 -0.46,9.61 C-6.85,9.65 -9.27,12.76 -12.33,10.46 C-15.39,8.15 -4.77,-14.07 -0.56,-14.03c "/></group><group android:name="_R_G_L_0_G" android:translateX="15" android:translateY="13.205" android:pivotY="1.795" android:scaleX="0" android:scaleY="0"><path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M12.78 7.57 C12.78,7.57 4.72,-8.55 4.72,-8.55 C2.77,-12.44 -2.77,-12.44 -4.72,-8.55 C-4.72,-8.55 -12.78,7.57 -12.78,7.57 C-15,12.01 -10.42,16.78 -5.89,14.74 C-5.89,14.74 -2.17,13.07 -2.17,13.07 C-0.79,12.45 0.79,12.45 2.17,13.07 C2.17,13.07 5.9,14.74 5.9,14.74 C10.42,16.78 15,12.01 12.78,7.57c "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_1_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="67" android:startOffset="0" android:valueFrom="M-0.56 -14.03 C3.65,-13.99 14.58,7.64 11.51,10.42 C8.45,13.2 5.92,9.56 -0.46,9.61 C-6.85,9.65 -9.27,12.76 -12.33,10.46 C-15.39,8.15 -4.77,-14.07 -0.56,-14.03c " android:valueTo="M-0.56 -14.03 C3.65,-13.99 14.58,7.64 11.51,10.42 C8.45,13.2 5.92,9.56 -0.46,9.61 C-6.85,9.65 -9.27,12.76 -12.33,10.46 C-15.39,8.15 -4.77,-14.07 -0.56,-14.03c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="283" android:startOffset="67" android:valueFrom="M-0.56 -14.03 C3.65,-13.99 14.58,7.64 11.51,10.42 C8.45,13.2 5.92,9.56 -0.46,9.61 C-6.85,9.65 -9.27,12.76 -12.33,10.46 C-15.39,8.15 -4.77,-14.07 -0.56,-14.03c " android:valueTo="M-0.44 -8.69 C3.98,-8.69 7.56,-5.11 7.56,-0.69 C7.56,3.73 3.98,7.31 -0.44,7.31 C-4.86,7.31 -8.44,3.73 -8.44,-0.69 C-8.44,-5.11 -4.86,-8.69 -0.44,-8.69c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="67" android:startOffset="0" android:valueFrom="M12.78 7.57 C12.78,7.57 4.72,-8.55 4.72,-8.55 C2.77,-12.44 -2.77,-12.44 -4.72,-8.55 C-4.72,-8.55 -12.78,7.57 -12.78,7.57 C-15,12.01 -10.42,16.78 -5.89,14.74 C-5.89,14.74 -2.17,13.07 -2.17,13.07 C-0.79,12.45 0.79,12.45 2.17,13.07 C2.17,13.07 5.9,14.74 5.9,14.74 C10.42,16.78 15,12.01 12.78,7.57c " android:valueTo="M12.78 7.57 C12.78,7.57 4.72,-8.55 4.72,-8.55 C2.77,-12.44 -2.77,-12.44 -4.72,-8.55 C-4.72,-8.55 -12.78,7.57 -12.78,7.57 C-15,12.01 -10.42,16.78 -5.89,14.74 C-5.89,14.74 -2.17,13.07 -2.17,13.07 C-0.79,12.45 0.79,12.45 2.17,13.07 C2.17,13.07 5.9,14.74 5.9,14.74 C10.42,16.78 15,12.01 12.78,7.57c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.833,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="283" android:startOffset="67" android:valueFrom="M12.78 7.57 C12.78,7.57 4.72,-8.55 4.72,-8.55 C2.77,-12.44 -2.77,-12.44 -4.72,-8.55 C-4.72,-8.55 -12.78,7.57 -12.78,7.57 C-15,12.01 -10.42,16.78 -5.89,14.74 C-5.89,14.74 -2.17,13.07 -2.17,13.07 C-0.79,12.45 0.79,12.45 2.17,13.07 C2.17,13.07 5.9,14.74 5.9,14.74 C10.42,16.78 15,12.01 12.78,7.57c " android:valueTo="M6.12 0.21 C6.12,0.21 2.27,-4.35 2.27,-4.35 C1.34,-6.2 -1.3,-6.2 -2.23,-4.35 C-2.23,-4.35 -6.06,0.21 -6.06,0.21 C-7.12,2.33 -5.46,5.79 -4.03,6.54 C-2.28,7.45 -1.01,7.48 -1.01,7.48 C-1.01,7.48 1.05,7.48 1.05,7.48 C1.05,7.48 3.28,7.36 4.23,6.66 C4.92,6.15 7.18,2.33 6.12,0.21c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.833,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleX" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="367" android:valueFrom="1" android:valueTo="0" android:valueType="floatType"/></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="500" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:height="30dp"
+            android:width="30dp"
+            android:viewportHeight="30"
+            android:viewportWidth="30">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:translateX="15.397"
+                    android:translateY="15.691"
+                    android:scaleY="0">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillColor="#ffffff"
+                        android:fillAlpha="1"
+                        android:fillType="nonZero"
+                        android:pathData=" M-0.56 -14.03 C3.65,-13.99 14.58,7.64 11.51,10.42 C8.45,13.2 5.92,9.56 -0.46,9.61 C-6.85,9.65 -9.27,12.76 -12.33,10.46 C-15.39,8.15 -4.77,-14.07 -0.56,-14.03c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:translateX="15"
+                    android:translateY="13.205"
+                    android:pivotY="1.795">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillColor="#ffffff"
+                        android:fillAlpha="1"
+                        android:fillType="nonZero"
+                        android:pathData=" M12.78 7.57 C12.78,7.57 4.72,-8.55 4.72,-8.55 C2.77,-12.44 -2.77,-12.44 -4.72,-8.55 C-4.72,-8.55 -12.78,7.57 -12.78,7.57 C-15,12.01 -10.42,16.78 -5.89,14.74 C-5.89,14.74 -2.17,13.07 -2.17,13.07 C-0.79,12.45 0.79,12.45 2.17,13.07 C2.17,13.07 5.9,14.74 5.9,14.74 C10.42,16.78 15,12.01 12.78,7.57c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="67"
+                    android:startOffset="0"
+                    android:valueFrom="M-0.56 -14.03 C3.65,-13.99 14.58,7.64 11.51,10.42 C8.45,13.2 5.92,9.56 -0.46,9.61 C-6.85,9.65 -9.27,12.76 -12.33,10.46 C-15.39,8.15 -4.77,-14.07 -0.56,-14.03c "
+                    android:valueTo="M-0.56 -14.03 C3.65,-13.99 14.58,7.64 11.51,10.42 C8.45,13.2 5.92,9.56 -0.46,9.61 C-6.85,9.65 -9.27,12.76 -12.33,10.46 C-15.39,8.15 -4.77,-14.07 -0.56,-14.03c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="283"
+                    android:startOffset="67"
+                    android:valueFrom="M-0.56 -14.03 C3.65,-13.99 14.58,7.64 11.51,10.42 C8.45,13.2 5.92,9.56 -0.46,9.61 C-6.85,9.65 -9.27,12.76 -12.33,10.46 C-15.39,8.15 -4.77,-14.07 -0.56,-14.03c "
+                    android:valueTo="M-0.44 -8.69 C3.98,-8.69 7.56,-5.11 7.56,-0.69 C7.56,3.73 3.98,7.31 -0.44,7.31 C-4.86,7.31 -8.44,3.73 -8.44,-0.69 C-8.44,-5.11 -4.86,-8.69 -0.44,-8.69c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="scaleY"
+                    android:duration="0"
+                    android:startOffset="67"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="67"
+                    android:startOffset="0"
+                    android:valueFrom="M12.78 7.57 C12.78,7.57 4.72,-8.55 4.72,-8.55 C2.77,-12.44 -2.77,-12.44 -4.72,-8.55 C-4.72,-8.55 -12.78,7.57 -12.78,7.57 C-15,12.01 -10.42,16.78 -5.89,14.74 C-5.89,14.74 -2.17,13.07 -2.17,13.07 C-0.79,12.45 0.79,12.45 2.17,13.07 C2.17,13.07 5.9,14.74 5.9,14.74 C10.42,16.78 15,12.01 12.78,7.57c "
+                    android:valueTo="M12.78 7.57 C12.78,7.57 4.72,-8.55 4.72,-8.55 C2.77,-12.44 -2.77,-12.44 -4.72,-8.55 C-4.72,-8.55 -12.78,7.57 -12.78,7.57 C-15,12.01 -10.42,16.78 -5.89,14.74 C-5.89,14.74 -2.17,13.07 -2.17,13.07 C-0.79,12.45 0.79,12.45 2.17,13.07 C2.17,13.07 5.9,14.74 5.9,14.74 C10.42,16.78 15,12.01 12.78,7.57c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="283"
+                    android:startOffset="67"
+                    android:valueFrom="M12.78 7.57 C12.78,7.57 4.72,-8.55 4.72,-8.55 C2.77,-12.44 -2.77,-12.44 -4.72,-8.55 C-4.72,-8.55 -12.78,7.57 -12.78,7.57 C-15,12.01 -10.42,16.78 -5.89,14.74 C-5.89,14.74 -2.17,13.07 -2.17,13.07 C-0.79,12.45 0.79,12.45 2.17,13.07 C2.17,13.07 5.9,14.74 5.9,14.74 C10.42,16.78 15,12.01 12.78,7.57c "
+                    android:valueTo="M6.12 0.21 C6.12,0.21 2.27,-4.35 2.27,-4.35 C1.34,-6.2 -1.3,-6.2 -2.23,-4.35 C-2.23,-4.35 -6.06,0.21 -6.06,0.21 C-7.12,2.33 -5.46,5.79 -4.03,6.54 C-2.28,7.45 -1.01,7.48 -1.01,7.48 C-1.01,7.48 1.05,7.48 1.05,7.48 C1.05,7.48 3.28,7.36 4.23,6.66 C4.92,6.15 7.18,2.33 6.12,0.21c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="scaleX"
+                    android:duration="67"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="scaleY"
+                    android:duration="67"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="translateX"
+                    android:duration="500"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/pin_dot_shape_3_avd.xml b/packages/SystemUI/res/drawable/pin_dot_shape_3_avd.xml
index 95b8044..78e2249 100644
--- a/packages/SystemUI/res/drawable/pin_dot_shape_3_avd.xml
+++ b/packages/SystemUI/res/drawable/pin_dot_shape_3_avd.xml
@@ -1 +1,141 @@
-<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="30dp" android:width="30dp" android:viewportHeight="30" android:viewportWidth="30"><group android:name="_R_G"><group android:name="_R_G_L_1_G" android:translateX="15.441" android:translateY="15.691" android:scaleY="0"><path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-10.2 -11.16 C-1.58,-18.94 4.25,-12.72 8.06,-8.64 C11.88,-4.57 17.93,1.89 9.39,9.74 C0.85,17.6 -5.06,11.3 -8.87,7.22 C-12.69,3.14 -18.81,-3.39 -10.2,-11.16c "/></group><group android:name="_R_G_L_0_G" android:translateX="15" android:translateY="15" android:scaleX="0" android:scaleY="0"><path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M10.71 10.71 C5.92,15.5 -1.85,15.5 -6.64,10.71 C-6.64,10.71 -10.71,6.64 -10.71,6.64 C-15.5,1.85 -15.5,-5.92 -10.71,-10.71 C-5.92,-15.5 1.85,-15.5 6.64,-10.71 C6.64,-10.71 10.71,-6.64 10.71,-6.64 C15.5,-1.85 15.5,5.92 10.71,10.71c "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_1_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="67" android:startOffset="0" android:valueFrom="M-10.2 -11.16 C-1.58,-18.94 4.25,-12.72 8.06,-8.64 C11.88,-4.57 17.93,1.89 9.39,9.74 C0.85,17.6 -5.06,11.3 -8.87,7.22 C-12.69,3.14 -18.81,-3.39 -10.2,-11.16c " android:valueTo="M-10.2 -11.16 C-1.58,-18.94 4.25,-12.72 8.06,-8.64 C11.88,-4.57 17.93,1.89 9.39,9.74 C0.85,17.6 -5.06,11.3 -8.87,7.22 C-12.69,3.14 -18.81,-3.39 -10.2,-11.16c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="283" android:startOffset="67" android:valueFrom="M-10.2 -11.16 C-1.58,-18.94 4.25,-12.72 8.06,-8.64 C11.88,-4.57 17.93,1.89 9.39,9.74 C0.85,17.6 -5.06,11.3 -8.87,7.22 C-12.69,3.14 -18.81,-3.39 -10.2,-11.16c " android:valueTo="M-0.44 -8.69 C3.98,-8.69 7.56,-5.11 7.56,-0.69 C7.56,3.73 3.98,7.31 -0.44,7.31 C-4.86,7.31 -8.44,3.73 -8.44,-0.69 C-8.44,-5.11 -4.86,-8.69 -0.44,-8.69c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleX" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleX" android:duration="283" android:startOffset="67" android:valueFrom="1" android:valueTo="0.4" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.7,0 0.6,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="283" android:startOffset="67" android:valueFrom="1" android:valueTo="0.4" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.7,0 0.6,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="367" android:valueFrom="1" android:valueTo="0" android:valueType="floatType"/></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="500" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:height="30dp"
+            android:width="30dp"
+            android:viewportHeight="30"
+            android:viewportWidth="30">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:translateX="15.441"
+                    android:translateY="15.691"
+                    android:scaleY="0">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillColor="#ffffff"
+                        android:fillAlpha="1"
+                        android:fillType="nonZero"
+                        android:pathData=" M-10.2 -11.16 C-1.58,-18.94 4.25,-12.72 8.06,-8.64 C11.88,-4.57 17.93,1.89 9.39,9.74 C0.85,17.6 -5.06,11.3 -8.87,7.22 C-12.69,3.14 -18.81,-3.39 -10.2,-11.16c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:translateX="15"
+                    android:translateY="15">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillColor="#ffffff"
+                        android:fillAlpha="1"
+                        android:fillType="nonZero"
+                        android:pathData=" M10.71 10.71 C5.92,15.5 -1.85,15.5 -6.64,10.71 C-6.64,10.71 -10.71,6.64 -10.71,6.64 C-15.5,1.85 -15.5,-5.92 -10.71,-10.71 C-5.92,-15.5 1.85,-15.5 6.64,-10.71 C6.64,-10.71 10.71,-6.64 10.71,-6.64 C15.5,-1.85 15.5,5.92 10.71,10.71c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="67"
+                    android:startOffset="0"
+                    android:valueFrom="M-10.2 -11.16 C-1.58,-18.94 4.25,-12.72 8.06,-8.64 C11.88,-4.57 17.93,1.89 9.39,9.74 C0.85,17.6 -5.06,11.3 -8.87,7.22 C-12.69,3.14 -18.81,-3.39 -10.2,-11.16c "
+                    android:valueTo="M-10.2 -11.16 C-1.58,-18.94 4.25,-12.72 8.06,-8.64 C11.88,-4.57 17.93,1.89 9.39,9.74 C0.85,17.6 -5.06,11.3 -8.87,7.22 C-12.69,3.14 -18.81,-3.39 -10.2,-11.16c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="283"
+                    android:startOffset="67"
+                    android:valueFrom="M-10.2 -11.16 C-1.58,-18.94 4.25,-12.72 8.06,-8.64 C11.88,-4.57 17.93,1.89 9.39,9.74 C0.85,17.6 -5.06,11.3 -8.87,7.22 C-12.69,3.14 -18.81,-3.39 -10.2,-11.16c "
+                    android:valueTo="M-0.44 -8.69 C3.98,-8.69 7.56,-5.11 7.56,-0.69 C7.56,3.73 3.98,7.31 -0.44,7.31 C-4.86,7.31 -8.44,3.73 -8.44,-0.69 C-8.44,-5.11 -4.86,-8.69 -0.44,-8.69c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="scaleY"
+                    android:duration="0"
+                    android:startOffset="67"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="scaleX"
+                    android:duration="67"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="scaleY"
+                    android:duration="67"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="scaleX"
+                    android:duration="283"
+                    android:startOffset="67"
+                    android:valueFrom="1"
+                    android:valueTo="0.4"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.7,0 0.6,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="scaleY"
+                    android:duration="283"
+                    android:startOffset="67"
+                    android:valueFrom="1"
+                    android:valueTo="0.4"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.7,0 0.6,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="translateX"
+                    android:duration="500"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/pin_dot_shape_4_avd.xml b/packages/SystemUI/res/drawable/pin_dot_shape_4_avd.xml
index 8ea8f85..35c7210 100644
--- a/packages/SystemUI/res/drawable/pin_dot_shape_4_avd.xml
+++ b/packages/SystemUI/res/drawable/pin_dot_shape_4_avd.xml
@@ -1 +1,119 @@
-<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="30dp" android:width="30dp" android:viewportHeight="30" android:viewportWidth="30"><group android:name="_R_G"><group android:name="_R_G_L_1_G" android:translateX="15.441" android:translateY="15.691" android:scaleY="0"><path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-0.44 -8.69 C3.97,-8.69 7.56,-5.11 7.56,-0.69 C7.56,3.72 3.97,7.31 -0.44,7.31 C-4.86,7.31 -8.44,3.72 -8.44,-0.69 C-8.44,-5.11 -4.86,-8.69 -0.44,-8.69c "/></group><group android:name="_R_G_L_0_G" android:translateX="15" android:translateY="15" android:scaleX="0" android:scaleY="0"><path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M8 0 C8,-1.35 7.97,-1.94 7.41,-3.02 C6.85,-4.09 6.61,-4.79 5.66,-5.66 C4.7,-6.52 4.2,-6.97 3.15,-7.36 C2.09,-7.74 1.39,-8 0,-8 C-1.39,-8 -2.18,-7.78 -3.12,-7.37 C-4.07,-6.96 -4.67,-6.63 -5.66,-5.66 C-6.64,-4.68 -6.98,-4.1 -7.37,-3.13 C-7.78,-2.08 -8,-1.39 -8,0 C-8,1.4 -7.86,1.98 -7.47,2.87 C-7.08,3.76 -6.68,4.66 -5.66,5.66 C-4.63,6.65 -4,6.96 -3.12,7.37 C-2.25,7.78 -1.32,8 0,8 C1.32,8 1.86,7.88 2.9,7.46 C3.95,7.03 4.85,6.63 5.66,5.66 C6.46,4.69 6.78,4.45 7.29,3.29 C7.81,2.14 8,1.35 8,0c "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_1_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="67" android:startOffset="0" android:valueFrom="M8 0 C8,-1.35 7.97,-1.94 7.41,-3.02 C6.85,-4.09 6.61,-4.79 5.66,-5.66 C4.7,-6.52 4.2,-6.97 3.15,-7.36 C2.09,-7.74 1.39,-8 0,-8 C-1.39,-8 -2.18,-7.78 -3.12,-7.37 C-4.07,-6.96 -4.67,-6.63 -5.66,-5.66 C-6.64,-4.68 -6.98,-4.1 -7.37,-3.13 C-7.78,-2.08 -8,-1.39 -8,0 C-8,1.4 -7.86,1.98 -7.47,2.87 C-7.08,3.76 -6.68,4.66 -5.66,5.66 C-4.63,6.65 -4,6.96 -3.12,7.37 C-2.25,7.78 -1.32,8 0,8 C1.32,8 1.86,7.88 2.9,7.46 C3.95,7.03 4.85,6.63 5.66,5.66 C6.46,4.69 6.78,4.45 7.29,3.29 C7.81,2.14 8,1.35 8,0c " android:valueTo="M15 0 C15,-2.52 11.99,-2.3 10.94,-4.32 C9.89,-6.33 12.4,-8.98 10.61,-10.61 C8.82,-12.23 6.44,-10.15 4.46,-10.86 C2.48,-11.58 2.61,-15 0,-15 C-2.61,-15 -2.68,-11.61 -4.46,-10.84 C-6.23,-10.08 -8.76,-12.44 -10.61,-10.61 C-12.45,-8.78 -10.31,-6.69 -10.87,-4.64 C-11.43,-2.61 -15,-2.61 -15,0 C-15,2.62 -11.67,2.75 -10.94,4.42 C-10.21,6.08 -12.53,8.74 -10.61,10.61 C-8.68,12.47 -6.18,10.19 -4.54,10.96 C-2.89,11.72 -2.48,15 0,15 C2.48,15 2.49,11.78 4.45,10.99 C6.4,10.19 9.09,12.43 10.61,10.61 C12.12,8.79 10,6.52 10.97,4.36 C11.94,2.2 15,2.52 15,0c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="283" android:startOffset="67" android:valueFrom="M15 0 C15,-2.52 11.99,-2.3 10.94,-4.32 C9.89,-6.33 12.4,-8.98 10.61,-10.61 C8.82,-12.23 6.44,-10.15 4.46,-10.86 C2.48,-11.58 2.61,-15 0,-15 C-2.61,-15 -2.68,-11.61 -4.46,-10.84 C-6.23,-10.08 -8.76,-12.44 -10.61,-10.61 C-12.45,-8.78 -10.31,-6.69 -10.87,-4.64 C-11.43,-2.61 -15,-2.61 -15,0 C-15,2.62 -11.67,2.75 -10.94,4.42 C-10.21,6.08 -12.53,8.74 -10.61,10.61 C-8.68,12.47 -6.18,10.19 -4.54,10.96 C-2.89,11.72 -2.48,15 0,15 C2.48,15 2.49,11.78 4.45,10.99 C6.4,10.19 9.09,12.43 10.61,10.61 C12.12,8.79 10,6.52 10.97,4.36 C11.94,2.2 15,2.52 15,0c " android:valueTo="M7.73 0 C7.73,-1.3 7.71,-1.88 7.16,-2.92 C6.62,-3.95 6.39,-4.63 5.47,-5.47 C4.55,-6.31 4.06,-6.74 3.04,-7.11 C2.02,-7.48 1.35,-7.73 0,-7.73 C-1.34,-7.73 -2.1,-7.52 -3.02,-7.12 C-3.93,-6.73 -4.52,-6.41 -5.47,-5.47 C-6.42,-4.53 -6.75,-3.96 -7.12,-3.02 C-7.52,-2.01 -7.73,-1.35 -7.73,0 C-7.73,1.35 -7.6,1.91 -7.22,2.77 C-6.85,3.63 -6.46,4.51 -5.47,5.47 C-4.48,6.43 -3.87,6.73 -3.02,7.12 C-2.17,7.52 -1.28,7.73 0,7.73 C1.28,7.73 1.8,7.62 2.81,7.21 C3.81,6.8 4.69,6.41 5.47,5.47 C6.25,4.53 6.55,4.3 7.05,3.19 C7.55,2.07 7.73,1.3 7.73,0c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.7,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleX" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="367" android:valueFrom="1" android:valueTo="0" android:valueType="floatType"/></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="500" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_1_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="0"
+                    android:propertyName="scaleY"
+                    android:startOffset="67"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="67"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M8 0 C8,-1.35 7.97,-1.94 7.41,-3.02 C6.85,-4.09 6.61,-4.79 5.66,-5.66 C4.7,-6.52 4.2,-6.97 3.15,-7.36 C2.09,-7.74 1.39,-8 0,-8 C-1.39,-8 -2.18,-7.78 -3.12,-7.37 C-4.07,-6.96 -4.67,-6.63 -5.66,-5.66 C-6.64,-4.68 -6.98,-4.1 -7.37,-3.13 C-7.78,-2.08 -8,-1.39 -8,0 C-8,1.4 -7.86,1.98 -7.47,2.87 C-7.08,3.76 -6.68,4.66 -5.66,5.66 C-4.63,6.65 -4,6.96 -3.12,7.37 C-2.25,7.78 -1.32,8 0,8 C1.32,8 1.86,7.88 2.9,7.46 C3.95,7.03 4.85,6.63 5.66,5.66 C6.46,4.69 6.78,4.45 7.29,3.29 C7.81,2.14 8,1.35 8,0c "
+                    android:valueTo="M15 0 C15,-2.52 11.99,-2.3 10.94,-4.32 C9.89,-6.33 12.4,-8.98 10.61,-10.61 C8.82,-12.23 6.44,-10.15 4.46,-10.86 C2.48,-11.58 2.61,-15 0,-15 C-2.61,-15 -2.68,-11.61 -4.46,-10.84 C-6.23,-10.08 -8.76,-12.44 -10.61,-10.61 C-12.45,-8.78 -10.31,-6.69 -10.87,-4.64 C-11.43,-2.61 -15,-2.61 -15,0 C-15,2.62 -11.67,2.75 -10.94,4.42 C-10.21,6.08 -12.53,8.74 -10.61,10.61 C-8.68,12.47 -6.18,10.19 -4.54,10.96 C-2.89,11.72 -2.48,15 0,15 C2.48,15 2.49,11.78 4.45,10.99 C6.4,10.19 9.09,12.43 10.61,10.61 C12.12,8.79 10,6.52 10.97,4.36 C11.94,2.2 15,2.52 15,0c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="283"
+                    android:propertyName="pathData"
+                    android:startOffset="67"
+                    android:valueFrom="M15 0 C15,-2.52 11.99,-2.3 10.94,-4.32 C9.89,-6.33 12.4,-8.98 10.61,-10.61 C8.82,-12.23 6.44,-10.15 4.46,-10.86 C2.48,-11.58 2.61,-15 0,-15 C-2.61,-15 -2.68,-11.61 -4.46,-10.84 C-6.23,-10.08 -8.76,-12.44 -10.61,-10.61 C-12.45,-8.78 -10.31,-6.69 -10.87,-4.64 C-11.43,-2.61 -15,-2.61 -15,0 C-15,2.62 -11.67,2.75 -10.94,4.42 C-10.21,6.08 -12.53,8.74 -10.61,10.61 C-8.68,12.47 -6.18,10.19 -4.54,10.96 C-2.89,11.72 -2.48,15 0,15 C2.48,15 2.49,11.78 4.45,10.99 C6.4,10.19 9.09,12.43 10.61,10.61 C12.12,8.79 10,6.52 10.97,4.36 C11.94,2.2 15,2.52 15,0c "
+                    android:valueTo="M7.73 0 C7.73,-1.3 7.71,-1.88 7.16,-2.92 C6.62,-3.95 6.39,-4.63 5.47,-5.47 C4.55,-6.31 4.06,-6.74 3.04,-7.11 C2.02,-7.48 1.35,-7.73 0,-7.73 C-1.34,-7.73 -2.1,-7.52 -3.02,-7.12 C-3.93,-6.73 -4.52,-6.41 -5.47,-5.47 C-6.42,-4.53 -6.75,-3.96 -7.12,-3.02 C-7.52,-2.01 -7.73,-1.35 -7.73,0 C-7.73,1.35 -7.6,1.91 -7.22,2.77 C-6.85,3.63 -6.46,4.51 -5.47,5.47 C-4.48,6.43 -3.87,6.73 -3.02,7.12 C-2.17,7.52 -1.28,7.73 0,7.73 C1.28,7.73 1.8,7.62 2.81,7.21 C3.81,6.8 4.69,6.41 5.47,5.47 C6.25,4.53 6.55,4.3 7.05,3.19 C7.55,2.07 7.73,1.3 7.73,0c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.7,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="67"
+                    android:propertyName="scaleX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="67"
+                    android:propertyName="scaleY"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="500"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="30dp"
+            android:height="30dp"
+            android:viewportHeight="30"
+            android:viewportWidth="30">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:scaleY="0"
+                    android:translateX="15.441"
+                    android:translateY="15.691">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M-0.44 -8.69 C3.97,-8.69 7.56,-5.11 7.56,-0.69 C7.56,3.72 3.97,7.31 -0.44,7.31 C-4.86,7.31 -8.44,3.72 -8.44,-0.69 C-8.44,-5.11 -4.86,-8.69 -0.44,-8.69c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:translateX="15"
+                    android:translateY="15">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M8 0 C8,-1.35 7.97,-1.94 7.41,-3.02 C6.85,-4.09 6.61,-4.79 5.66,-5.66 C4.7,-6.52 4.2,-6.97 3.15,-7.36 C2.09,-7.74 1.39,-8 0,-8 C-1.39,-8 -2.18,-7.78 -3.12,-7.37 C-4.07,-6.96 -4.67,-6.63 -5.66,-5.66 C-6.64,-4.68 -6.98,-4.1 -7.37,-3.13 C-7.78,-2.08 -8,-1.39 -8,0 C-8,1.4 -7.86,1.98 -7.47,2.87 C-7.08,3.76 -6.68,4.66 -5.66,5.66 C-4.63,6.65 -4,6.96 -3.12,7.37 C-2.25,7.78 -1.32,8 0,8 C1.32,8 1.86,7.88 2.9,7.46 C3.95,7.03 4.85,6.63 5.66,5.66 C6.46,4.69 6.78,4.45 7.29,3.29 C7.81,2.14 8,1.35 8,0c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/pin_dot_shape_5_avd.xml b/packages/SystemUI/res/drawable/pin_dot_shape_5_avd.xml
index 3779c80..9458b2e 100644
--- a/packages/SystemUI/res/drawable/pin_dot_shape_5_avd.xml
+++ b/packages/SystemUI/res/drawable/pin_dot_shape_5_avd.xml
@@ -1 +1,148 @@
-<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="30dp" android:width="30dp" android:viewportHeight="30" android:viewportWidth="30"><group android:name="_R_G"><group android:name="_R_G_L_1_G" android:translateX="15.397" android:translateY="15.691" android:scaleY="0"><path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-0.48 -12.86 C2.96,-12.87 14.59,5.93 11.98,9.12 C9.42,12.26 5.76,11.36 -0.48,11.41 C-6.72,11.45 -10.24,11.91 -12.78,9.16 C-15.4,6.32 -3.91,-12.85 -0.48,-12.86c "/></group><group android:name="_R_G_L_0_G" android:translateX="15" android:translateY="17.64" android:pivotY="-2.64" android:scaleX="0" android:scaleY="0"><path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-4.68 -13.34 C-2.59,-17.01 2.64,-17 4.72,-13.33 C4.72,-13.33 13.65,2.45 13.65,2.45 C15.72,6.11 13.11,10.66 8.94,10.65 C8.94,10.65 -8.98,10.62 -8.98,10.62 C-13.15,10.61 -15.75,6.05 -13.67,2.4 C-13.67,2.4 -4.68,-13.34 -4.68,-13.34c "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_1_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="67" android:startOffset="0" android:valueFrom="M-0.48 -12.86 C2.96,-12.87 14.59,5.93 11.98,9.12 C9.42,12.26 5.76,11.36 -0.48,11.41 C-6.72,11.45 -10.24,11.91 -12.78,9.16 C-15.4,6.32 -3.91,-12.85 -0.48,-12.86c " android:valueTo="M-0.48 -12.86 C2.96,-12.87 14.59,5.93 11.98,9.12 C9.42,12.26 5.76,11.36 -0.48,11.41 C-6.72,11.45 -10.24,11.91 -12.78,9.16 C-15.4,6.32 -3.91,-12.85 -0.48,-12.86c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.7,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="283" android:startOffset="67" android:valueFrom="M-0.48 -12.86 C2.96,-12.87 14.59,5.93 11.98,9.12 C9.42,12.26 5.76,11.36 -0.48,11.41 C-6.72,11.45 -10.24,11.91 -12.78,9.16 C-15.4,6.32 -3.91,-12.85 -0.48,-12.86c " android:valueTo="M-0.44 -8.69 C3.98,-8.69 7.56,-5.11 7.56,-0.69 C7.56,3.73 3.98,7.31 -0.44,7.31 C-4.86,7.31 -8.44,3.73 -8.44,-0.69 C-8.44,-5.11 -4.86,-8.69 -0.44,-8.69c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.7,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="67" android:startOffset="0" android:valueFrom="M-4.68 -13.34 C-2.59,-17.01 2.64,-17 4.72,-13.33 C4.72,-13.33 13.65,2.45 13.65,2.45 C15.72,6.11 13.11,10.66 8.94,10.65 C8.94,10.65 -8.98,10.62 -8.98,10.62 C-13.15,10.61 -15.75,6.05 -13.67,2.4 C-13.67,2.4 -4.68,-13.34 -4.68,-13.34c " android:valueTo="M-4.68 -13.34 C-2.59,-17.01 2.64,-17 4.72,-13.33 C4.72,-13.33 13.65,2.45 13.65,2.45 C15.72,6.11 13.11,10.66 8.94,10.65 C8.94,10.65 -8.98,10.62 -8.98,10.62 C-13.15,10.61 -15.75,6.05 -13.67,2.4 C-13.67,2.4 -4.68,-13.34 -4.68,-13.34c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.7,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="283" android:startOffset="67" android:valueFrom="M-4.68 -13.34 C-2.59,-17.01 2.64,-17 4.72,-13.33 C4.72,-13.33 13.65,2.45 13.65,2.45 C15.72,6.11 13.11,10.66 8.94,10.65 C8.94,10.65 -8.98,10.62 -8.98,10.62 C-13.15,10.61 -15.75,6.05 -13.67,2.4 C-13.67,2.4 -4.68,-13.34 -4.68,-13.34c " android:valueTo="M-2.99 -8.52 C-1.33,-9.83 1.52,-9.34 3.25,-8.28 C4.98,-7.22 6.77,-5.88 6.7,-2.4 C6.64,1.08 4.48,2.26 3.2,3.05 C1.92,3.83 -1.1,3.72 -3.03,2.76 C-4.97,1.79 -6.38,0.3 -6.37,-2.59 C-6.36,-5.49 -4.65,-7.2 -2.99,-8.52c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.7,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleX" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="367" android:valueFrom="1" android:valueTo="0" android:valueType="floatType"/></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="500" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:height="30dp"
+            android:width="30dp"
+            android:viewportHeight="30"
+            android:viewportWidth="30">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:translateX="15.397"
+                    android:translateY="15.691"
+                    android:scaleY="0">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillColor="#ffffff"
+                        android:fillAlpha="1"
+                        android:fillType="nonZero"
+                        android:pathData=" M-0.48 -12.86 C2.96,-12.87 14.59,5.93 11.98,9.12 C9.42,12.26 5.76,11.36 -0.48,11.41 C-6.72,11.45 -10.24,11.91 -12.78,9.16 C-15.4,6.32 -3.91,-12.85 -0.48,-12.86c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:translateX="15"
+                    android:translateY="17.64"
+                    android:pivotY="-2.64">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillColor="#ffffff"
+                        android:fillAlpha="1"
+                        android:fillType="nonZero"
+                        android:pathData=" M-4.68 -13.34 C-2.59,-17.01 2.64,-17 4.72,-13.33 C4.72,-13.33 13.65,2.45 13.65,2.45 C15.72,6.11 13.11,10.66 8.94,10.65 C8.94,10.65 -8.98,10.62 -8.98,10.62 C-13.15,10.61 -15.75,6.05 -13.67,2.4 C-13.67,2.4 -4.68,-13.34 -4.68,-13.34c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="67"
+                    android:startOffset="0"
+                    android:valueFrom="M-0.48 -12.86 C2.96,-12.87 14.59,5.93 11.98,9.12 C9.42,12.26 5.76,11.36 -0.48,11.41 C-6.72,11.45 -10.24,11.91 -12.78,9.16 C-15.4,6.32 -3.91,-12.85 -0.48,-12.86c "
+                    android:valueTo="M-0.48 -12.86 C2.96,-12.87 14.59,5.93 11.98,9.12 C9.42,12.26 5.76,11.36 -0.48,11.41 C-6.72,11.45 -10.24,11.91 -12.78,9.16 C-15.4,6.32 -3.91,-12.85 -0.48,-12.86c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.7,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="283"
+                    android:startOffset="67"
+                    android:valueFrom="M-0.48 -12.86 C2.96,-12.87 14.59,5.93 11.98,9.12 C9.42,12.26 5.76,11.36 -0.48,11.41 C-6.72,11.45 -10.24,11.91 -12.78,9.16 C-15.4,6.32 -3.91,-12.85 -0.48,-12.86c "
+                    android:valueTo="M-0.44 -8.69 C3.98,-8.69 7.56,-5.11 7.56,-0.69 C7.56,3.73 3.98,7.31 -0.44,7.31 C-4.86,7.31 -8.44,3.73 -8.44,-0.69 C-8.44,-5.11 -4.86,-8.69 -0.44,-8.69c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.7,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="scaleY"
+                    android:duration="0"
+                    android:startOffset="67"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="67"
+                    android:startOffset="0"
+                    android:valueFrom="M-4.68 -13.34 C-2.59,-17.01 2.64,-17 4.72,-13.33 C4.72,-13.33 13.65,2.45 13.65,2.45 C15.72,6.11 13.11,10.66 8.94,10.65 C8.94,10.65 -8.98,10.62 -8.98,10.62 C-13.15,10.61 -15.75,6.05 -13.67,2.4 C-13.67,2.4 -4.68,-13.34 -4.68,-13.34c "
+                    android:valueTo="M-4.68 -13.34 C-2.59,-17.01 2.64,-17 4.72,-13.33 C4.72,-13.33 13.65,2.45 13.65,2.45 C15.72,6.11 13.11,10.66 8.94,10.65 C8.94,10.65 -8.98,10.62 -8.98,10.62 C-13.15,10.61 -15.75,6.05 -13.67,2.4 C-13.67,2.4 -4.68,-13.34 -4.68,-13.34c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.7,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="283"
+                    android:startOffset="67"
+                    android:valueFrom="M-4.68 -13.34 C-2.59,-17.01 2.64,-17 4.72,-13.33 C4.72,-13.33 13.65,2.45 13.65,2.45 C15.72,6.11 13.11,10.66 8.94,10.65 C8.94,10.65 -8.98,10.62 -8.98,10.62 C-13.15,10.61 -15.75,6.05 -13.67,2.4 C-13.67,2.4 -4.68,-13.34 -4.68,-13.34c "
+                    android:valueTo="M-2.99 -8.52 C-1.33,-9.83 1.52,-9.34 3.25,-8.28 C4.98,-7.22 6.77,-5.88 6.7,-2.4 C6.64,1.08 4.48,2.26 3.2,3.05 C1.92,3.83 -1.1,3.72 -3.03,2.76 C-4.97,1.79 -6.38,0.3 -6.37,-2.59 C-6.36,-5.49 -4.65,-7.2 -2.99,-8.52c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.7,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="scaleX"
+                    android:duration="67"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="scaleY"
+                    android:duration="67"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="translateX"
+                    android:duration="500"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/pin_dot_shape_6_avd.xml b/packages/SystemUI/res/drawable/pin_dot_shape_6_avd.xml
index ddda94a..06e27df 100644
--- a/packages/SystemUI/res/drawable/pin_dot_shape_6_avd.xml
+++ b/packages/SystemUI/res/drawable/pin_dot_shape_6_avd.xml
@@ -1 +1,142 @@
-<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="30dp" android:width="30dp" android:viewportHeight="30" android:viewportWidth="30"><group android:name="_R_G"><group android:name="_R_G_L_1_G" android:translateX="15.397" android:translateY="15.691" android:scaleY="0"><path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-0.46 -13.65 C3.05,-13.65 12.63,-6.57 12.9,-3.63 C12.98,-2.7 12.65,12.85 -0.46,12.85 C-13.57,12.85 -13.77,-2.84 -13.76,-3.63 C-13.72,-6.87 -3.96,-13.65 -0.46,-13.65c "/></group><group android:name="_R_G_L_0_G" android:translateX="15" android:translateY="15.859" android:pivotY="-0.859" android:scaleX="0" android:scaleY="0"><path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-2.82 -14.07 C-1.14,-15.29 1.14,-15.29 2.82,-14.07 C2.82,-14.07 7.73,-10.53 7.73,-10.53 C7.73,-10.53 12.58,-7 12.58,-7 C14.29,-5.76 15,-3.55 14.35,-1.54 C14.35,-1.54 12.51,4.14 12.51,4.14 C12.51,4.14 10.64,9.85 10.64,9.85 C9.99,11.84 8.14,13.19 6.05,13.19 C6.05,13.19 0,13.2 0,13.2 C0,13.2 -6.05,13.19 -6.05,13.19 C-8.14,13.19 -9.98,11.84 -10.64,9.85 C-10.64,9.85 -12.51,4.14 -12.51,4.14 C-12.51,4.14 -14.35,-1.54 -14.35,-1.54 C-15,-3.55 -14.29,-5.76 -12.58,-7 C-12.58,-7 -7.73,-10.53 -7.73,-10.53 C-7.73,-10.53 -2.82,-14.07 -2.82,-14.07c "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_1_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="67" android:startOffset="0" android:valueFrom="M-0.46 -13.65 C3.05,-13.65 12.63,-6.57 12.9,-3.63 C12.98,-2.7 12.65,12.85 -0.46,12.85 C-13.57,12.85 -13.77,-2.84 -13.76,-3.63 C-13.72,-6.87 -3.96,-13.65 -0.46,-13.65c " android:valueTo="M-0.46 -13.65 C3.05,-13.65 12.63,-6.57 12.9,-3.63 C12.98,-2.7 12.65,12.85 -0.46,12.85 C-13.57,12.85 -13.77,-2.84 -13.76,-3.63 C-13.72,-6.87 -3.96,-13.65 -0.46,-13.65c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.8,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="283" android:startOffset="67" android:valueFrom="M-0.46 -13.65 C3.05,-13.65 12.63,-6.57 12.9,-3.63 C12.98,-2.7 12.65,12.85 -0.46,12.85 C-13.57,12.85 -13.77,-2.84 -13.76,-3.63 C-13.72,-6.87 -3.96,-13.65 -0.46,-13.65c " android:valueTo="M-0.44 -8.69 C3.98,-8.69 7.56,-5.11 7.56,-0.69 C7.56,3.73 3.98,7.31 -0.44,7.31 C-4.86,7.31 -8.44,3.73 -8.44,-0.69 C-8.44,-5.11 -4.86,-8.69 -0.44,-8.69c " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.8,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleX" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleX" android:duration="283" android:startOffset="67" android:valueFrom="1" android:valueTo="0.35000000000000003" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.7,0 0.6,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="283" android:startOffset="67" android:valueFrom="1" android:valueTo="0.35000000000000003" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.7,0 0.6,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="367" android:valueFrom="1" android:valueTo="0" android:valueType="floatType"/></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="500" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:height="30dp"
+            android:width="30dp"
+            android:viewportHeight="30"
+            android:viewportWidth="30">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:translateX="15.397"
+                    android:translateY="15.691"
+                    android:scaleY="0">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillColor="#ffffff"
+                        android:fillAlpha="1"
+                        android:fillType="nonZero"
+                        android:pathData=" M-0.46 -13.65 C3.05,-13.65 12.63,-6.57 12.9,-3.63 C12.98,-2.7 12.65,12.85 -0.46,12.85 C-13.57,12.85 -13.77,-2.84 -13.76,-3.63 C-13.72,-6.87 -3.96,-13.65 -0.46,-13.65c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:translateX="15"
+                    android:translateY="15.859"
+                    android:pivotY="-0.859">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillColor="#ffffff"
+                        android:fillAlpha="1"
+                        android:fillType="nonZero"
+                        android:pathData=" M-2.82 -14.07 C-1.14,-15.29 1.14,-15.29 2.82,-14.07 C2.82,-14.07 7.73,-10.53 7.73,-10.53 C7.73,-10.53 12.58,-7 12.58,-7 C14.29,-5.76 15,-3.55 14.35,-1.54 C14.35,-1.54 12.51,4.14 12.51,4.14 C12.51,4.14 10.64,9.85 10.64,9.85 C9.99,11.84 8.14,13.19 6.05,13.19 C6.05,13.19 0,13.2 0,13.2 C0,13.2 -6.05,13.19 -6.05,13.19 C-8.14,13.19 -9.98,11.84 -10.64,9.85 C-10.64,9.85 -12.51,4.14 -12.51,4.14 C-12.51,4.14 -14.35,-1.54 -14.35,-1.54 C-15,-3.55 -14.29,-5.76 -12.58,-7 C-12.58,-7 -7.73,-10.53 -7.73,-10.53 C-7.73,-10.53 -2.82,-14.07 -2.82,-14.07c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="67"
+                    android:startOffset="0"
+                    android:valueFrom="M-0.46 -13.65 C3.05,-13.65 12.63,-6.57 12.9,-3.63 C12.98,-2.7 12.65,12.85 -0.46,12.85 C-13.57,12.85 -13.77,-2.84 -13.76,-3.63 C-13.72,-6.87 -3.96,-13.65 -0.46,-13.65c "
+                    android:valueTo="M-0.46 -13.65 C3.05,-13.65 12.63,-6.57 12.9,-3.63 C12.98,-2.7 12.65,12.85 -0.46,12.85 C-13.57,12.85 -13.77,-2.84 -13.76,-3.63 C-13.72,-6.87 -3.96,-13.65 -0.46,-13.65c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.8,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="283"
+                    android:startOffset="67"
+                    android:valueFrom="M-0.46 -13.65 C3.05,-13.65 12.63,-6.57 12.9,-3.63 C12.98,-2.7 12.65,12.85 -0.46,12.85 C-13.57,12.85 -13.77,-2.84 -13.76,-3.63 C-13.72,-6.87 -3.96,-13.65 -0.46,-13.65c "
+                    android:valueTo="M-0.44 -8.69 C3.98,-8.69 7.56,-5.11 7.56,-0.69 C7.56,3.73 3.98,7.31 -0.44,7.31 C-4.86,7.31 -8.44,3.73 -8.44,-0.69 C-8.44,-5.11 -4.86,-8.69 -0.44,-8.69c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0 0.8,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_1_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="scaleY"
+                    android:duration="0"
+                    android:startOffset="67"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="scaleX"
+                    android:duration="67"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="scaleY"
+                    android:duration="67"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="scaleX"
+                    android:duration="283"
+                    android:startOffset="67"
+                    android:valueFrom="1"
+                    android:valueTo="0.35000000000000003"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.7,0 0.6,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="scaleY"
+                    android:duration="283"
+                    android:startOffset="67"
+                    android:valueFrom="1"
+                    android:valueTo="0.35000000000000003"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.7,0 0.6,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="translateX"
+                    android:duration="500"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/qs_customizer_background_primary.xml b/packages/SystemUI/res/drawable/qs_customizer_background_primary.xml
index ea0aafd..e138d09 100644
--- a/packages/SystemUI/res/drawable/qs_customizer_background_primary.xml
+++ b/packages/SystemUI/res/drawable/qs_customizer_background_primary.xml
@@ -15,7 +15,7 @@
 -->
 <inset xmlns:android="http://schemas.android.com/apk/res/android">
     <shape>
-        <solid android:color="?attr/underSurfaceColor"/>
+        <solid android:color="?attr/underSurface"/>
         <corners android:radius="?android:attr/dialogCornerRadius" />
     </shape>
 </inset>
diff --git a/packages/SystemUI/res/drawable/qs_customizer_toolbar.xml b/packages/SystemUI/res/drawable/qs_customizer_toolbar.xml
index ef950fe..f1a24aa 100644
--- a/packages/SystemUI/res/drawable/qs_customizer_toolbar.xml
+++ b/packages/SystemUI/res/drawable/qs_customizer_toolbar.xml
@@ -15,6 +15,6 @@
 -->
 <inset xmlns:android="http://schemas.android.com/apk/res/android">
     <shape>
-        <solid android:color="?attr/underSurfaceColor"/>
+        <solid android:color="?attr/underSurface"/>
     </shape>
 </inset>
diff --git a/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml b/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
index 14cb1de9..c4e45bf 100644
--- a/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
+++ b/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
@@ -28,7 +28,7 @@
         <item>
             <shape android:shape="rectangle">
                 <corners android:radius="?android:attr/buttonCornerRadius"/>
-                <solid android:color="?androidprv:attr/colorAccentPrimary"/>
+                <solid android:color="?androidprv:attr/materialColorPrimary"/>
                 <padding android:left="@dimen/dialog_button_horizontal_padding"
                          android:top="@dimen/dialog_button_vertical_padding"
                          android:right="@dimen/dialog_button_horizontal_padding"
diff --git a/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml b/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml
index 0544b871..1590daa 100644
--- a/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml
+++ b/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml
@@ -26,7 +26,7 @@
     <item>
         <shape android:shape="rectangle">
             <corners android:radius="18dp"/>
-            <solid android:color="?androidprv:attr/colorAccentPrimary"/>
+            <solid android:color="?androidprv:attr/materialColorPrimaryFixed"/>
         </shape>
     </item>
 </ripple>
diff --git a/packages/SystemUI/res/drawable/qs_dialog_btn_outline.xml b/packages/SystemUI/res/drawable/qs_dialog_btn_outline.xml
index a47299d..b0dc652 100644
--- a/packages/SystemUI/res/drawable/qs_dialog_btn_outline.xml
+++ b/packages/SystemUI/res/drawable/qs_dialog_btn_outline.xml
@@ -29,7 +29,7 @@
             <shape android:shape="rectangle">
                 <corners android:radius="?android:attr/buttonCornerRadius"/>
                 <solid android:color="@android:color/transparent"/>
-                <stroke android:color="?androidprv:attr/colorAccentPrimaryVariant"
+                <stroke android:color="?androidprv:attr/materialColorPrimary"
                         android:width="1dp"
                 />
                 <padding android:left="@dimen/dialog_button_horizontal_padding"
diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle.xml
index c8c36b0..4a5d4af 100644
--- a/packages/SystemUI/res/drawable/qs_footer_action_circle.xml
+++ b/packages/SystemUI/res/drawable/qs_footer_action_circle.xml
@@ -28,7 +28,7 @@
         </item>
         <item>
             <shape android:shape="rectangle">
-                <solid android:color="?attr/offStateColor"/>
+                <solid android:color="?attr/shadeInactive"/>
                 <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
             </shape>
         </item>
diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml
index 6a365000..a8c0349 100644
--- a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml
+++ b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml
@@ -28,7 +28,7 @@
         </item>
         <item>
             <shape android:shape="rectangle">
-                <solid android:color="?android:attr/colorAccent"/>
+                <solid android:color="?attr/shadeActive"/>
                 <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
             </shape>
         </item>
diff --git a/packages/SystemUI/res/drawable/qs_footer_actions_background.xml b/packages/SystemUI/res/drawable/qs_footer_actions_background.xml
index c9517cd9..a7e8762 100644
--- a/packages/SystemUI/res/drawable/qs_footer_actions_background.xml
+++ b/packages/SystemUI/res/drawable/qs_footer_actions_background.xml
@@ -15,7 +15,7 @@
 -->
 <inset xmlns:android="http://schemas.android.com/apk/res/android">
     <shape>
-        <solid android:color="?attr/underSurfaceColor"/>
+        <solid android:color="?attr/underSurface"/>
         <corners android:topLeftRadius="@dimen/qs_corner_radius"
                  android:topRightRadius="@dimen/qs_corner_radius"/>
     </shape>
diff --git a/packages/SystemUI/res/drawable/qs_security_footer_background.xml b/packages/SystemUI/res/drawable/qs_security_footer_background.xml
index 381af50..0b0055b 100644
--- a/packages/SystemUI/res/drawable/qs_security_footer_background.xml
+++ b/packages/SystemUI/res/drawable/qs_security_footer_background.xml
@@ -29,7 +29,7 @@
         <item>
             <shape android:shape="rectangle">
                 <stroke android:width="1dp"
-                        android:color="?android:attr/colorBackground"/>
+                        android:color="?attr/shadeInactive"/>
                 <corners android:radius="@dimen/qs_security_footer_corner_radius"/>
             </shape>
         </item>
diff --git a/packages/SystemUI/res/drawable/stat_sys_connected_display.xml b/packages/SystemUI/res/drawable/stat_sys_connected_display.xml
new file mode 100644
index 0000000..3f3d6f5
--- /dev/null
+++ b/packages/SystemUI/res/drawable/stat_sys_connected_display.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:fillColor="@android:color/white"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M320,840L320,760L400,760L400,680L160,680Q127,680 103.5,656.5Q80,633 80,600L80,200Q80,167 103.5,143.5Q127,120 160,120L800,120Q833,120 856.5,143.5Q880,167 880,200L880,600Q880,633 856.5,656.5Q833,680 800,680L560,680L560,760L640,760L640,840L320,840ZM160,600L800,600Q800,600 800,600Q800,600 800,600L800,200Q800,200 800,200Q800,200 800,200L160,200Q160,200 160,200Q160,200 160,200L160,600Q160,600 160,600Q160,600 160,600ZM160,600Q160,600 160,600Q160,600 160,600L160,200Q160,200 160,200Q160,200 160,200L160,200Q160,200 160,200Q160,200 160,200L160,600Q160,600 160,600Q160,600 160,600L160,600Z" />
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/alert_dialog_title_systemui.xml b/packages/SystemUI/res/layout/alert_dialog_title_systemui.xml
index 88f13b4..ca7df86 100644
--- a/packages/SystemUI/res/layout/alert_dialog_title_systemui.xml
+++ b/packages/SystemUI/res/layout/alert_dialog_title_systemui.xml
@@ -42,7 +42,7 @@
             android:layout_marginBottom="16dp"
             android:scaleType="fitCenter"
             android:src="@null"
-            android:tint="?androidprv:attr/colorAccentPrimaryVariant"
+            android:tint="?androidprv:attr/materialColorPrimary"
             />
 
         <TextView
diff --git a/packages/SystemUI/res/layout/battery_percentage_view.xml b/packages/SystemUI/res/layout/battery_percentage_view.xml
index b9b1bb1..82facd0 100644
--- a/packages/SystemUI/res/layout/battery_percentage_view.xml
+++ b/packages/SystemUI/res/layout/battery_percentage_view.xml
@@ -20,7 +20,7 @@
         xmlns:android="http://schemas.android.com/apk/res/android"
         android:id="@+id/battery_percentage_view"
         android:layout_width="wrap_content"
-        android:layout_height="match_parent"
+        android:layout_height="wrap_content"
         android:singleLine="true"
         android:textAppearance="@style/TextAppearance.StatusBar.Clock"
         android:textColor="?android:attr/textColorPrimary"
diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml
index 386c9d6..665c612 100644
--- a/packages/SystemUI/res/layout/combined_qs_header.xml
+++ b/packages/SystemUI/res/layout/combined_qs_header.xml
@@ -74,7 +74,7 @@
         android:layout_height="wrap_content"
         android:id="@+id/barrier"
         app:barrierDirection="start"
-        app:constraint_referenced_ids="statusIcons,privacy_container" />
+        app:constraint_referenced_ids="shade_header_system_icons,privacy_container" />
 
     <com.android.systemui.statusbar.policy.Clock
         android:id="@+id/clock"
@@ -108,46 +108,39 @@
     <include
         android:id="@+id/carrier_group"
         layout="@layout/shade_carrier_group"
-        app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
-        android:minHeight="@dimen/large_screen_shade_header_min_height"
-        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_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/shade_header_system_icons"
+        app:layout_constraintHorizontal_bias="1"
         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"
-    />
+        app:layout_constraintTop_toTopOf="@id/clock" />
 
-    <com.android.systemui.statusbar.phone.StatusIconContainer
-        android:id="@+id/statusIcons"
-        app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
-        android:paddingEnd="@dimen/signal_cluster_battery_padding"
+    <LinearLayout
+        android:id="@+id/shade_header_system_icons"
         android:layout_width="wrap_content"
+        app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
         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"
+        android:clickable="true"
+        android:orientation="horizontal"
         app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintHorizontal_bias="1"
-    />
+        app:layout_constraintEnd_toEndOf="@id/privacy_container"
+        app:layout_constraintTop_toTopOf="@id/clock">
 
-    <com.android.systemui.battery.BatteryMeterView
-        android:id="@+id/batteryRemainingIcon"
-        android:layout_width="wrap_content"
-        android:layout_height="@dimen/large_screen_shade_header_min_height"
-        app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
-        app:layout_constrainedWidth="true"
-        app:textAppearance="@style/TextAppearance.QS.Status"
-        app:layout_constraintStart_toEndOf="@id/statusIcons"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toTopOf="@id/clock"
-        app:layout_constraintBottom_toBottomOf="parent"
-    />
+        <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"
+            app:textAppearance="@style/TextAppearance.QS.Status" />
+    </LinearLayout>
 
     <FrameLayout
         android:id="@+id/privacy_container"
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 50dcaf3..bb32022 100644
--- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
@@ -57,7 +57,6 @@
             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"
@@ -68,7 +67,6 @@
             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"
@@ -79,7 +77,6 @@
             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/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
index a3a7135..66c57fc 100644
--- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml
+++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
@@ -23,7 +23,7 @@
     android:outlineProvider="none" >
 
     <LinearLayout
-        android:id="@+id/keyguard_indication_area"
+        android:id="@id/keyguard_indication_area"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginBottom="@dimen/keyguard_indication_margin_bottom"
@@ -31,7 +31,7 @@
         android:orientation="vertical">
 
         <com.android.systemui.statusbar.phone.KeyguardIndicationTextView
-            android:id="@+id/keyguard_indication_text"
+            android:id="@id/keyguard_indication_text"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:gravity="center"
@@ -41,13 +41,12 @@
             android:accessibilityLiveRegion="polite"/>
 
         <com.android.systemui.statusbar.phone.KeyguardIndicationTextView
-            android:id="@+id/keyguard_indication_text_bottom"
+            android:id="@id/keyguard_indication_text_bottom"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:gravity="center"
-            android:minHeight="48dp"
+            android:minHeight="@dimen/keyguard_indication_text_min_height"
             android:layout_gravity="center_horizontal"
-            android:layout_centerHorizontal="true"
             android:paddingStart="@dimen/keyguard_indication_text_padding"
             android:paddingEnd="@dimen/keyguard_indication_text_padding"
             android:textAppearance="@style/TextAppearance.Keyguard.BottomArea"
diff --git a/packages/SystemUI/res/layout/keyguard_qs_user_switch.xml b/packages/SystemUI/res/layout/keyguard_qs_user_switch.xml
index 07c428b..6194311 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. -->
-    <com.android.systemui.animation.LaunchableFrameLayout
+    <com.android.systemui.animation.view.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>
-    </com.android.systemui.animation.LaunchableFrameLayout>
+    </com.android.systemui.animation.view.LaunchableFrameLayout>
 </FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/keyguard_status_bar.xml b/packages/SystemUI/res/layout/keyguard_status_bar.xml
index 64c4eff..fc0bf24 100644
--- a/packages/SystemUI/res/layout/keyguard_status_bar.xml
+++ b/packages/SystemUI/res/layout/keyguard_status_bar.xml
@@ -47,7 +47,10 @@
             android:layout_height="match_parent"
             android:layout_marginEnd="@dimen/status_bar_padding_end"
             android:gravity="center_vertical|end">
-            <include layout="@layout/system_icons" />
+            <include layout="@layout/system_icons"
+                android:layout_gravity="center_vertical"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
         </FrameLayout>
 
         <ImageView android:id="@+id/multi_user_avatar"
diff --git a/packages/SystemUI/res/layout/qs_footer_impl.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml
index 745cfc6..b8f4c0f 100644
--- a/packages/SystemUI/res/layout/qs_footer_impl.xml
+++ b/packages/SystemUI/res/layout/qs_footer_impl.xml
@@ -53,6 +53,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
                 android:layout_gravity="center_vertical"
+                android:tint="?attr/shadeActive"
                 android:visibility="gone" />
 
             <FrameLayout
@@ -70,7 +71,7 @@
                     android:focusable="true"
                     android:padding="@dimen/qs_footer_icon_padding"
                     android:src="@*android:drawable/ic_mode_edit"
-                    android:tint="?android:attr/textColorPrimary" />
+                    android:tint="?attr/onSurfaceVariant" />
             </FrameLayout>
 
         </LinearLayout>
diff --git a/packages/SystemUI/res/layout/qs_tile_label.xml b/packages/SystemUI/res/layout/qs_tile_label.xml
index c124aea..974cad3 100644
--- a/packages/SystemUI/res/layout/qs_tile_label.xml
+++ b/packages/SystemUI/res/layout/qs_tile_label.xml
@@ -54,6 +54,6 @@
         android:focusable="false"
         android:importantForAccessibility="no"
         android:textAppearance="@style/TextAppearance.QS.TileLabel.Secondary"
-        android:textColor="?android:attr/textColorSecondary"/>
+        android:textColor="?attr/onShadeInactive"/>
 
 </com.android.systemui.qs.tileimpl.IgnorableChildLinearLayout>
diff --git a/packages/SystemUI/res/layout/scene_window_root.xml b/packages/SystemUI/res/layout/scene_window_root.xml
new file mode 100644
index 0000000..0dcd15b
--- /dev/null
+++ b/packages/SystemUI/res/layout/scene_window_root.xml
@@ -0,0 +1,32 @@
+<?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.
+*/
+-->
+
+<!-- The root view of the scene window. -->
+<com.android.systemui.scene.ui.view.SceneWindowRootView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:sysui="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/scene_window_root"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fitsSystemWindows="true">
+
+    <include layout="@layout/super_notification_shade"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+</com.android.systemui.scene.ui.view.SceneWindowRootView>
diff --git a/packages/SystemUI/res/layout/screen_record_dialog.xml b/packages/SystemUI/res/layout/screen_record_dialog.xml
index bbf3adf..f6ce70d 100644
--- a/packages/SystemUI/res/layout/screen_record_dialog.xml
+++ b/packages/SystemUI/res/layout/screen_record_dialog.xml
@@ -15,6 +15,7 @@
   limitations under the License.
   -->
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:orientation="vertical">
@@ -47,7 +48,7 @@
                 <TextView
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:textAppearance="?android:attr/textAppearanceLarge"
+                    android:textAppearance="@style/TextAppearance.Dialog.Title"
                     android:fontFamily="@*android:string/config_headlineFontFamily"
                     android:text="@string/screenrecord_permission_dialog_title"
                     android:layout_marginTop="22dp"
@@ -56,8 +57,7 @@
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:text="@string/screenrecord_permission_dialog_warning_entire_screen"
-                    android:textAppearance="?android:attr/textAppearanceSmall"
-                    android:textColor="?android:textColorSecondary"
+                    android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
                     android:gravity="center"
                     android:layout_marginBottom="20dp"/>
 
@@ -81,6 +81,7 @@
                         android:minHeight="48dp"
                         android:layout_weight="1"
                         android:popupBackground="@drawable/screenrecord_spinner_background"
+                        android:textColor="?androidprv:attr/materialColorOnSurface"
                         android:dropDownWidth="274dp"
                         android:prompt="@string/screenrecord_audio_label"/>
                     <Switch
@@ -117,7 +118,7 @@
                         android:text="@string/screenrecord_taps_label"
                         android:textAppearance="?android:attr/textAppearanceMedium"
                         android:fontFamily="@*android:string/config_headlineFontFamily"
-                        android:textColor="?android:attr/textColorPrimary"
+                        android:textColor="?androidprv:attr/materialColorOnSurface"
                         android:importantForAccessibility="no"/>
                     <Switch
                         android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/layout/screen_record_options.xml b/packages/SystemUI/res/layout/screen_record_options.xml
index 6cc72dd..d9f4b79 100644
--- a/packages/SystemUI/res/layout/screen_record_options.xml
+++ b/packages/SystemUI/res/layout/screen_record_options.xml
@@ -55,14 +55,13 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="horizontal"
+        android:gravity="center_vertical"
         android:layout_marginTop="@dimen/screenrecord_option_padding">
         <ImageView
             android:layout_width="@dimen/screenrecord_option_icon_size"
             android:layout_height="@dimen/screenrecord_option_icon_size"
-            android:layout_weight="0"
             android:src="@drawable/ic_touch"
             android:tint="?android:attr/textColorSecondary"
-            android:layout_gravity="center_vertical"
             android:layout_marginRight="@dimen/screenrecord_option_padding"
             android:importantForAccessibility="no"/>
         <TextView
@@ -70,7 +69,6 @@
             android:layout_height="wrap_content"
             android:minHeight="48dp"
             android:layout_weight="1"
-            android:gravity="center_vertical"
             android:text="@string/screenrecord_taps_label"
             android:textAppearance="?android:attr/textAppearanceMedium"
             android:fontFamily="@*android:string/config_bodyFontFamily"
@@ -80,7 +78,6 @@
             android:layout_width="wrap_content"
             android:minWidth="48dp"
             android:layout_height="48dp"
-            android:layout_weight="0"
             android:id="@+id/screenrecord_taps_switch"
             style="@style/ScreenRecord.Switch"
             android:importantForAccessibility="yes"/>
diff --git a/packages/SystemUI/res/layout/screen_share_dialog.xml b/packages/SystemUI/res/layout/screen_share_dialog.xml
index ab522a3..9af46c5 100644
--- a/packages/SystemUI/res/layout/screen_share_dialog.xml
+++ b/packages/SystemUI/res/layout/screen_share_dialog.xml
@@ -36,14 +36,13 @@
             android:layout_width="@dimen/screenrecord_logo_size"
             android:layout_height="@dimen/screenrecord_logo_size"
             android:src="@drawable/ic_media_projection_permission"
-            android:tint="?androidprv:attr/colorAccentPrimaryVariant"
+            android:tint="?androidprv:attr/materialColorPrimary"
             android:importantForAccessibility="no"/>
         <TextView
             android:id="@+id/screen_share_dialog_title"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:textAppearance="?android:attr/textAppearanceLarge"
-            android:fontFamily="@*android:string/config_headlineFontFamily"
+            android:textAppearance="@style/TextAppearance.Dialog.Title"
             android:layout_marginTop="@dimen/screenrecord_title_margin_top"
             android:gravity="center"/>
         <Spinner
@@ -64,8 +63,7 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:text="@string/screenrecord_permission_dialog_warning_entire_screen"
-            android:textAppearance="?android:attr/textAppearanceSmall"
-            android:textColor="?android:textColorSecondary"
+            style="@style/TextAppearance.Dialog.Body.Message"
             android:gravity="start"
             android:lineHeight="@dimen/screenrecord_warning_line_height"/>
 
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index d710676..e214666 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -132,21 +132,6 @@
         android:id="@+id/lock_icon_view"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content">
-        <!-- Background protection -->
-        <ImageView
-            android:id="@+id/lock_icon_bg"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:src="@drawable/fingerprint_bg"
-            android:visibility="invisible"/>
-
-        <ImageView
-            android:id="@+id/lock_icon"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_gravity="center"
-            android:scaleType="centerCrop"/>
-
     </com.android.keyguard.LockIconView>
 
     <include
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index d9fe949..2fde947 100644
--- a/packages/SystemUI/res/layout/super_notification_shade.xml
+++ b/packages/SystemUI/res/layout/super_notification_shade.xml
@@ -21,6 +21,7 @@
 <com.android.systemui.shade.NotificationShadeWindowView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:sysui="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/legacy_window_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:fitsSystemWindows="true">
@@ -64,6 +65,12 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
 
+    <!-- Root for all keyguard content. It was previously located within the shade. -->
+    <com.android.systemui.keyguard.ui.view.KeyguardRootView
+        android:id="@id/keyguard_root_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
     <include layout="@layout/status_bar_expanded"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
diff --git a/packages/SystemUI/res/layout/system_icons.xml b/packages/SystemUI/res/layout/system_icons.xml
index 816dfd3..6a14c21 100644
--- a/packages/SystemUI/res/layout/system_icons.xml
+++ b/packages/SystemUI/res/layout/system_icons.xml
@@ -18,20 +18,23 @@
               xmlns:systemui="http://schemas.android.com/apk/res-auto"
     android:id="@+id/system_icons"
     android:layout_width="wrap_content"
-    android:layout_height="match_parent"
-    android:layout_gravity="center_vertical|end"
+    android:layout_height="wrap_content"
+    android:paddingBottom="@dimen/status_bar_icons_padding_bottom"
+    android:paddingTop="@dimen/status_bar_icons_padding_top"
+    android:paddingStart="@dimen/status_bar_icons_padding_start"
+    android:paddingEnd="@dimen/status_bar_icons_padding_end"
     android:gravity="center_vertical">
 
     <com.android.systemui.statusbar.phone.StatusIconContainer android:id="@+id/statusIcons"
         android:layout_width="0dp"
         android:layout_weight="1"
-        android:layout_height="match_parent"
+        android:layout_height="wrap_content"
         android:paddingEnd="@dimen/signal_cluster_battery_padding"
         android:gravity="center_vertical"
         android:orientation="horizontal"/>
 
     <com.android.systemui.battery.BatteryMeterView android:id="@+id/battery"
-        android:layout_height="match_parent"
+        android:layout_height="wrap_content"
         android:layout_width="wrap_content"
         android:clipToPadding="false"
         android:clipChildren="false"
diff --git a/packages/SystemUI/res/layout/udfps_keyguard_view_legacy.xml b/packages/SystemUI/res/layout/udfps_keyguard_view_legacy.xml
index 00af7f4..530d752 100644
--- a/packages/SystemUI/res/layout/udfps_keyguard_view_legacy.xml
+++ b/packages/SystemUI/res/layout/udfps_keyguard_view_legacy.xml
@@ -16,8 +16,7 @@
   -->
 <com.android.systemui.biometrics.UdfpsKeyguardViewLegacy
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/udfps_animation_view"
+    android:id="@+id/udfps_animation_view_legacy"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
diff --git a/packages/SystemUI/res/layout/window_magnification_settings_view.xml b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
index a8febe7..efdb0a3 100644
--- a/packages/SystemUI/res/layout/window_magnification_settings_view.xml
+++ b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
@@ -147,10 +147,12 @@
         android:id="@+id/magnifier_zoom_slider"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        app:max="6"
         app:progress="0"
         app:iconStartContentDescription="@string/accessibility_control_zoom_out"
-        app:iconEndContentDescription="@string/accessibility_control_zoom_in"/>
+        app:iconEndContentDescription="@string/accessibility_control_zoom_in"
+        app:tickMark="@android:color/transparent"
+        app:seekBarChangeMagnitude="10"
+    />
 
     <Button
         android:id="@+id/magnifier_done_button"
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index 59becc6..9a2db6bfd 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -20,6 +20,13 @@
          On large screens should be the same as the regular status bar. -->
     <dimen name="status_bar_header_height_keyguard">@dimen/status_bar_height</dimen>
 
+    <!-- padding for container with status icons and battery -->
+    <dimen name="status_bar_icons_padding_end">12dp</dimen>
+    <!-- start padding is smaller to account for status icon margins coming from drawable itself -->
+    <dimen name="status_bar_icons_padding_start">11dp</dimen>
+
+    <dimen name="status_bar_padding_end">0dp</dimen>
+
     <!-- Size of user icon + frame in the qs user picker (incl. frame) -->
     <dimen name="qs_framed_avatar_size">60dp</dimen>
     <!-- Size of user icon + frame in the keyguard user picker (incl. frame) -->
diff --git a/packages/SystemUI/res/values-sw720dp/dimens.xml b/packages/SystemUI/res/values-sw720dp/dimens.xml
index 2086459..d053a7a 100644
--- a/packages/SystemUI/res/values-sw720dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp/dimens.xml
@@ -16,8 +16,11 @@
 */
 -->
 <resources>
+    <!-- it's a bit smaller on 720dp to account for status_bar_icon_horizontal_margin -->
+    <dimen name="status_bar_icons_padding_start">10dp</dimen>
+
     <!-- gap on either side of status bar notification icons -->
-    <dimen name="status_bar_icon_padding">1dp</dimen>
+    <dimen name="status_bar_icon_horizontal_margin">1dp</dimen>
 
     <dimen name="controls_header_horizontal_padding">28dp</dimen>
     <dimen name="controls_content_margin_horizontal">40dp</dimen>
diff --git a/packages/SystemUI/res/values-television/config.xml b/packages/SystemUI/res/values-television/config.xml
index 0ca154e..2ace86f 100644
--- a/packages/SystemUI/res/values-television/config.xml
+++ b/packages/SystemUI/res/values-television/config.xml
@@ -20,11 +20,6 @@
 <!-- These resources are around just to allow their values to be customized
      for different hardware and product builds. -->
 <resources>
-    <!-- SystemUIFactory component -->
-    <string name="config_systemUIFactoryComponent" translatable="false">
-        com.android.systemui.tv.TvSystemUIInitializer
-    </string>
-
     <!-- Svelte specific logic, see RecentsConfiguration.SVELTE_* constants. -->
     <integer name="recents_svelte_level">3</integer>
 
diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml
index bd86e51..d693631 100644
--- a/packages/SystemUI/res/values/attrs.xml
+++ b/packages/SystemUI/res/values/attrs.xml
@@ -118,8 +118,25 @@
     <attr name="wallpaperTextColorSecondary" format="reference|color" />
     <attr name="wallpaperTextColorAccent" format="reference|color" />
     <attr name="backgroundProtectedStyle" format="reference" />
-    <attr name="offStateColor" format="reference|color" />
-    <attr name="underSurfaceColor" format="reference|color" />
+
+    <!-- color attribute tokens for QS -->
+    <attr name="isQsTheme" format="boolean" />
+    <attr name="underSurface" format="reference|color"/>
+    <attr name="shadeActive" format="reference|color" />
+    <attr name="onShadeActive" format="reference|color" />
+    <attr name="onShadeActiveVariant" format="reference|color" />
+    <attr name="shadeInactive" format="reference|color" />
+    <attr name="onShadeInactive" format="reference|color" />
+    <attr name="onShadeInactiveVariant" format="reference|color" />
+    <attr name="shadeDisabled" format="reference|color" />
+    <attr name="surfaceBright" format="reference|color" />
+    <attr name="scHigh" format="reference|color" />
+    <attr name="tertiary" format="reference|color" />
+    <attr name="onSurface" format="reference|color" />
+    <attr name="onSurfaceVariant" format="reference|color" />
+    <attr name="outline" format="reference|color" />
+    <attr name="primary" format="reference|color" />
+
 
     <declare-styleable name="SmartReplyView">
         <attr name="spacing" format="dimension" />
@@ -216,6 +233,8 @@
         <attr name="progress" format="integer" />
         <attr name="iconStartContentDescription" format="reference" />
         <attr name="iconEndContentDescription" format="reference" />
+        <attr name="tickMark" format="reference" />
+        <attr name="seekBarChangeMagnitude" format="integer" />
     </declare-styleable>
 </resources>
 
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 166bd2a..9c864ab 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -302,9 +302,6 @@
     <!-- Determines whether the shell features all run on another thread. -->
     <bool name="config_enableShellMainThread">true</bool>
 
-    <!-- SystemUIFactory component -->
-    <string name="config_systemUIFactoryComponent" translatable="false">com.android.systemui.SystemUIInitializerImpl</string>
-
     <!-- QS tile shape store width. negative implies fill configuration instead of stroke-->
     <dimen name="config_qsTileStrokeWidthActive">-1dp</dimen>
     <dimen name="config_qsTileStrokeWidthInactive">-1dp</dimen>
@@ -701,6 +698,10 @@
     -->
     <integer name="config_face_auth_supported_posture">0</integer>
 
+    <!-- Components to allow running fingerprint listening if their activity is occluding the lock screen. -->
+    <string-array name="config_fingerprint_listen_on_occluding_activity_packages" translatable="false">
+    </string-array>
+
     <!-- Whether the communal service should be enabled -->
     <bool name="config_communalServiceEnabled">false</bool>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 4f768cc..d9466bf 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -275,6 +275,9 @@
     <!-- The padding at start and end of indication text shown on AOD -->
     <dimen name="keyguard_indication_text_padding">16dp</dimen>
 
+    <!-- The min height on the indication text shown on AOD -->
+    <dimen name="keyguard_indication_text_min_height">48dp</dimen>
+
     <!-- Shadows under the clock, date and other keyguard text fields -->
     <dimen name="keyguard_shadow_radius">5</dimen>
 
@@ -323,8 +326,16 @@
     <!-- opacity at which Notification icons will be drawn in the status bar -->
     <item type="dimen" name="status_bar_icon_drawing_alpha">90%</item>
 
+    <!-- paddings for container with status icons and battery -->
+    <!-- padding start is a bit smaller than end to account for status icon margin-->
+    <dimen name="status_bar_icons_padding_start">11dp</dimen>
+
+    <dimen name="status_bar_icons_padding_end">0dp</dimen>
+    <dimen name="status_bar_icons_padding_bottom">8dp</dimen>
+    <dimen name="status_bar_icons_padding_top">8dp</dimen>
+
     <!-- gap on either side of status bar notification icons -->
-    <dimen name="status_bar_icon_padding">0dp</dimen>
+    <dimen name="status_bar_icon_horizontal_margin">0dp</dimen>
 
     <!-- the padding on the start of the statusbar -->
     <dimen name="status_bar_padding_start">8dp</dimen>
@@ -1557,7 +1568,8 @@
 
     <!-- Status bar user chip -->
     <dimen name="status_bar_user_chip_avatar_size">16dp</dimen>
-    <dimen name="status_bar_user_chip_end_margin">12dp</dimen>
+    <!-- below also works as break between user chip and hover state of status icons -->
+    <dimen name="status_bar_user_chip_end_margin">4dp</dimen>
     <dimen name="status_bar_user_chip_text_size">12sp</dimen>
 
     <!-- System UI Dialog -->
@@ -1781,7 +1793,6 @@
     <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>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index e5c9461..134a7a9 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -206,5 +206,11 @@
 
     <!-- keyboard backlight indicator-->
     <item type="id" name="backlight_icon" />
-</resources>
 
+    <item type="id" name="keyguard_root_view" />
+    <item type="id" name="keyguard_indication_area" />
+    <item type="id" name="keyguard_indication_text" />
+    <item type="id" name="keyguard_indication_text_bottom" />
+    <item type="id" name="lock_icon" />
+    <item type="id" name="lock_icon_bg" />
+</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 070748c..a36f318 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -613,8 +613,8 @@
     <string name="quick_settings_bluetooth_secondary_label_headset">Headset</string>
     <!-- QuickSettings: Bluetooth secondary label for an input/IO device being connected [CHAR LIMIT=20]-->
     <string name="quick_settings_bluetooth_secondary_label_input">Input</string>
-    <!-- QuickSettings: Bluetooth secondary label for a Hearing Aids device being connected [CHAR LIMIT=20]-->
-    <string name="quick_settings_bluetooth_secondary_label_hearing_aids">Hearing Aids</string>
+    <!-- QuickSettings: Bluetooth secondary label for a Hearing aids device being connected [CHAR LIMIT=20]-->
+    <string name="quick_settings_bluetooth_secondary_label_hearing_aids">Hearing aids</string>
     <!-- QuickSettings: Bluetooth secondary label shown when bluetooth is being enabled [CHAR LIMIT=NONE] -->
     <string name="quick_settings_bluetooth_secondary_label_transient">Turning on&#8230;</string>
     <!-- QuickSettings: Brightness [CHAR LIMIT=NONE] -->
@@ -2378,6 +2378,8 @@
     <string name="magnification_mode_switch_state_window">Magnify part of screen</string>
     <!-- Click action label for magnification settings panel. [CHAR LIMIT=NONE] -->
     <string name="magnification_open_settings_click_label">Open magnification settings</string>
+    <!-- Click action label for magnification settings panel. [CHAR LIMIT=NONE] -->
+    <string name="magnification_close_settings_click_label">Close magnification settings</string>
     <!-- Label of the corner of a rectangle that you can tap and drag to resize the magnification area. [CHAR LIMIT=NONE] -->
     <string name="magnification_drag_corner_to_resize">Drag corner to resize</string>
 
@@ -2803,6 +2805,8 @@
     <!-- Secondary label for alarm tile when there is no next alarm information [CHAR LIMIT=20] -->
     <string name="qs_alarm_tile_no_alarm">No alarm set</string>
 
+    <!-- Accessibility label for a11y action to show the bouncer (pin/pattern/password) screen lock [CHAR LIMIT=NONE] -->
+    <string name="accessibility_bouncer">enter screen lock</string>
     <!-- Accessibility label for fingerprint sensor [CHAR LIMIT=NONE] -->
     <string name="accessibility_fingerprint_label">Fingerprint sensor</string>
     <!-- Accessibility action for tapping on an affordance that will bring up the user's
@@ -2959,9 +2963,11 @@
         <xliff:g id="weather_condition" example="Partly cloudy">%1$s</xliff:g>, <xliff:g id="temperature" example="7°C">%2$s</xliff:g>
     </string>
 
-    <!-- TODO(b/259369672): Replace with final resource. -->
     <!-- [CHAR LIMIT=30] Label used to open Note Task -->
-    <string name="note_task_button_label">Notetaking</string>
+    <string name="note_task_button_label">Note-taking</string>
+
+    <!-- [CHAR LIMIT=25] Long label used by Note Task Shortcut -->
+    <string name="note_task_shortcut_long_label">Note-taking, <xliff:g id="note_taking_app" example="Note-taking App">%1$s</xliff:g></string>
 
     <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, media app is broadcasting -->
     <string name="broadcasting_description_is_broadcasting">Broadcasting</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index cb5342a..6b85621 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -83,7 +83,7 @@
 
     <style name="TextAppearance.QS">
         <item name="android:textStyle">normal</item>
-        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textColor">?attr/onShadeInactive</item>
         <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
     </style>
 
@@ -93,7 +93,7 @@
 
     <style name="TextAppearance.QS.DetailItemSecondary">
         <item name="android:textSize">@dimen/qs_tile_text_size</item>
-        <item name="android:textColor">?android:attr/colorAccent</item>
+        <item name="android:textColor">?attr/shadeActive</item>
     </style>
 
     <style name="TextAppearance.QS.Introduction">
@@ -117,11 +117,11 @@
 
     <style name="TextAppearance.QS.DataUsage.Usage">
         <item name="android:textSize">@dimen/qs_data_usage_usage_text_size</item>
-        <item name="android:textColor">?android:attr/colorAccent</item>
+        <item name="android:textColor">?attr/shadeActive</item>
     </style>
 
     <style name="TextAppearance.QS.DataUsage.Secondary">
-        <item name="android:textColor">?android:attr/textColorSecondary</item>
+        <item name="android:textColor">?attr/onShadeInactiveVariant</item>
     </style>
 
     <style name="TextAppearance.QS.TileLabel">
@@ -137,31 +137,31 @@
 
     <style name="TextAppearance.QS.UserSwitcher">
         <item name="android:textSize">@dimen/qs_tile_text_size</item>
-        <item name="android:textColor">?android:attr/textColorSecondary</item>
+        <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
     </style>
 
     <!-- This is hard coded to be sans-serif-condensed to match the icons -->
 
     <style name="TextAppearance.QS.Status">
         <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
-        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textColor">?attr/onSurface</item>
         <item name="android:textSize">14sp</item>
         <item name="android:letterSpacing">0.01</item>
     </style>
 
     <style name="TextAppearance.QS.SecurityFooter" parent="@style/TextAppearance.QS.Status">
         <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
-        <item name="android:textColor">?android:attr/textColorSecondary</item>
+        <item name="android:textColor">?attr/onSurface</item>
     </style>
 
     <style name="TextAppearance.QS.Status.Carriers" />
 
     <style name="TextAppearance.QS.Status.Carriers.NoCarrierText">
-        <item name="android:textColor">?android:attr/textColorSecondary</item>
+        <item name="android:textColor">?attr/onSurfaceVariant</item>
     </style>
 
     <style name="TextAppearance.QS.Status.Build">
-        <item name="android:textColor">?android:attr/textColorSecondary</item>
+        <item name="android:textColor">?attr/onSurfaceVariant</item>
     </style>
 
     <style name="TextAppearance.DeviceManagementDialog.Title" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle"/>
@@ -278,10 +278,10 @@
 
     <style name="DeviceManagementDialogTitle">
         <item name="android:gravity">center</item>
-        <item name="android:textAppearance">@style/TextAppearance.DeviceManagementDialog.Title</item>
+        <item name="android:textAppearance">@style/TextAppearance.Dialog.Title</item>
     </style>
 
-    <style name="TextAppearance.DeviceManagementDialog.Content" parent="@*android:style/TextAppearance.DeviceDefault.Subhead"/>
+    <style name="TextAppearance.DeviceManagementDialog.Content" parent="@style/TextAppearance.Dialog.Body.Message"/>
 
     <style name="BaseBrightnessDialogContainer" parent="@style/Theme.SystemUI">
         <item name="android:layout_width">match_parent</item>
@@ -371,18 +371,35 @@
     </style>
 
     <style name="Theme.SystemUI.QuickSettings" parent="@*android:style/Theme.DeviceDefault">
+        <item name="isQsTheme">true</item>
         <item name="lightIconTheme">@style/QSIconTheme</item>
         <item name="darkIconTheme">@style/QSIconTheme</item>
         <item name="android:colorError">@*android:color/error_color_material_dark</item>
         <item name="android:windowIsFloating">true</item>
         <item name="android:homeAsUpIndicator">@drawable/ic_arrow_back</item>
-        <item name="offStateColor">@color/material_dynamic_neutral20</item>
-        <item name="underSurfaceColor">@color/material_dynamic_neutral0</item>
-        <item name="android:colorBackground">@color/material_dynamic_neutral10</item>
+
+        <item name="surfaceBright">?androidprv:attr/materialColorSurfaceBright</item>
+        <item name="android:colorBackground">?attr/surfaceBright</item>
+        <item name="scHigh">?androidprv:attr/materialColorSurfaceContainerHigh</item>
+        <item name="primary">?androidprv:attr/materialColorPrimary</item>
+        <item name="tertiary">?androidprv:attr/materialColorTertiary</item>
+        <item name="onSurface">?androidprv:attr/materialColorOnSurface</item>
+        <item name="onSurfaceVariant">?androidprv:attr/materialColorOnSurfaceVariant</item>
+        <item name="outline">?androidprv:attr/materialColorOutline</item>
+
+        <item name="shadeActive">@color/material_dynamic_primary90</item>
+        <item name="onShadeActive">@color/material_dynamic_primary10</item>
+        <item name="onShadeActiveVariant">@color/material_dynamic_primary30</item>
+        <item name="shadeInactive">@color/material_dynamic_neutral20</item>
+        <item name="onShadeInactive">@color/material_dynamic_neutral90</item>
+        <item name="onShadeInactiveVariant">@color/material_dynamic_neutral_variant80</item>
+        <item name="shadeDisabled">@color/shade_disabled</item>
+        <item name="underSurface">@color/material_dynamic_neutral0</item>
         <item name="android:itemTextAppearance">@style/Control.MenuItem</item>
     </style>
 
-    <style name="Theme.SystemUI.QuickSettings.BrightnessDialog" parent="@android:style/Theme.DeviceDefault.Dialog">
+    <!-- Cannot double inherit. Use Theme.SystemUI.QuickSettings in code to match -->
+    <style name="BrightnessDialog" parent="@android:style/Theme.DeviceDefault.Dialog">
         <item name="android:windowBackground">@android:color/transparent</item>
     </style>
 
@@ -390,8 +407,15 @@
         <item name="android:windowBackground">@android:color/transparent</item>
     </style>
 
-    <style name="Theme.SystemUI.QuickSettings.Dialog" parent="@android:style/Theme.DeviceDefault.Dialog">
+    <style name="Theme.SystemUI.QuickSettings.Dialog" parent="@style/Theme.SystemUI.Dialog.QuickSettings">
+    </style>
+
+    <!-- Parent style overrides style in the dot inheritance -->
+    <style name="Theme.SystemUI.Dialog.QuickSettings" parent="@style/Theme.SystemUI.QuickSettings">
         <item name="android:dialogCornerRadius">@dimen/notification_corner_radius</item>
+        <item name="android:buttonBarPositiveButtonStyle">@style/Widget.Dialog.Button.QuickSettings</item>
+        <item name="android:buttonBarNegativeButtonStyle">@style/Widget.Dialog.Button.QuickSettings</item>
+        <item name="android:buttonBarNeutralButtonStyle">@style/Widget.Dialog.Button.QuickSettings</item>
     </style>
 
     <!-- Overridden by values-television/styles.xml with tv-specific settings -->
@@ -406,7 +430,7 @@
         <item name="android:buttonBarPositiveButtonStyle">@style/Widget.Dialog.Button</item>
         <item name="android:buttonBarNegativeButtonStyle">@style/Widget.Dialog.Button.BorderButton</item>
         <item name="android:buttonBarNeutralButtonStyle">@style/Widget.Dialog.Button.BorderButton</item>
-        <item name="android:colorBackground">?androidprv:attr/colorSurface</item>
+        <item name="android:colorBackground">?androidprv:attr/materialColorSurfaceBright</item>
         <item name="android:alertDialogStyle">@style/ScrollableAlertDialogStyle</item>
         <item name="android:buttonBarStyle">@style/ButtonBarStyle</item>
         <item name="android:buttonBarButtonStyle">@style/Widget.Dialog.Button.Large</item>
@@ -605,11 +629,11 @@
         <item name="android:letterSpacing">0.01</item>
         <item name="android:lineHeight">20sp</item>
         <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
-        <item name="android:textColor">?android:attr/textColorSecondary</item>
+        <item name="android:textColor">?attr/onSurfaceVariant</item>
     </style>
 
     <style name="QSCustomizeToolbar" parent="@*android:style/Widget.DeviceDefault.Toolbar">
-        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textColor">?attr/onSurface</item>
         <item name="android:elevation">10dp</item>
     </style>
 
@@ -1055,7 +1079,7 @@
     </style>
 
     <style name="TextAppearance.Dialog.Title" parent="@android:style/TextAppearance.DeviceDefault.Large">
-        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
         <item name="android:textSize">@dimen/dialog_title_text_size</item>
         <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
         <item name="android:lineHeight">32sp</item>
@@ -1064,7 +1088,7 @@
     </style>
 
     <style name="TextAppearance.Dialog.Body" parent="@android:style/TextAppearance.DeviceDefault.Medium">
-        <item name="android:textColor">?android:attr/textColorSecondary</item>
+        <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
         <item name="android:textSize">14sp</item>
         <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
         <item name="android:lineHeight">20sp</item>
@@ -1092,7 +1116,7 @@
     <style name="Widget.Dialog.Button">
         <item name="android:buttonCornerRadius">28dp</item>
         <item name="android:background">@drawable/qs_dialog_btn_filled</item>
-        <item name="android:textColor">?androidprv:attr/textColorOnAccent</item>
+        <item name="android:textColor">?androidprv:attr/materialColorOnPrimary</item>
         <item name="android:textSize">14sp</item>
         <item name="android:lineHeight">20sp</item>
         <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
@@ -1102,12 +1126,18 @@
 
     <style name="Widget.Dialog.Button.BorderButton">
         <item name="android:background">@drawable/qs_dialog_btn_outline</item>
-        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
     </style>
 
     <style name="Widget.Dialog.Button.Large">
         <item name="android:background">@drawable/qs_dialog_btn_filled_large</item>
         <item name="android:minHeight">56dp</item>
+        <item name="android:textColor">?androidprv:attr/materialColorOnPrimaryFixed</item>
+    </style>
+
+    <style name="Widget.Dialog.Button.QuickSettings">
+        <item name="android:textColor">?attr/primary</item>
+        <item name="android:background">?android:attr/selectableItemBackground</item>
     </style>
 
     <style name="MainSwitch.Settingslib" parent="@android:style/Theme.DeviceDefault">
diff --git a/packages/SystemUI/res/xml/combined_qs_header_scene.xml b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
index 8512f6f..c167256 100644
--- a/packages/SystemUI/res/xml/combined_qs_header_scene.xml
+++ b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
@@ -53,56 +53,28 @@
                 android:alpha="0"
                 />
             <KeyPosition
+                app:motionTarget="@id/shade_header_system_icons"
                 app:keyPositionType="deltaRelative"
                 app:percentX="0"
                 app:percentY="@dimen/percent_displacement_at_fade_out"
                 app:framePosition="@integer/fade_out_complete_frame"
                 app:sizePercent="0"
-                app:curveFit="linear"
-                app:motionTarget="@id/statusIcons" />
+                app:curveFit="linear" />
             <KeyPosition
+                app:motionTarget="@id/shade_header_system_icons"
                 app:keyPositionType="deltaRelative"
                 app:percentX="1"
                 app:percentY="0.5"
                 app:framePosition="50"
                 app:sizePercent="1"
-                app:curveFit="linear"
-                app:motionTarget="@id/statusIcons" />
+                app:curveFit="linear" />
             <KeyAttribute
-                app:motionTarget="@id/statusIcons"
+                app:motionTarget="@id/shade_header_system_icons"
                 app:framePosition="@integer/fade_out_complete_frame"
                 android:alpha="0"
                 />
             <KeyAttribute
-                app:motionTarget="@id/statusIcons"
-                app:framePosition="@integer/fade_in_start_frame"
-                android:alpha="0"
-                />
-            <KeyPosition
-                app:keyPositionType="deltaRelative"
-                app:percentX="0"
-                app:percentY="@dimen/percent_displacement_at_fade_out"
-                app:framePosition="@integer/fade_out_complete_frame"
-                app:percentWidth="1"
-                app:percentHeight="1"
-                app:curveFit="linear"
-                app:motionTarget="@id/batteryRemainingIcon" />
-            <KeyPosition
-                app:keyPositionType="deltaRelative"
-                app:percentX="1"
-                app:percentY="0.5"
-                app:framePosition="50"
-                app:percentWidth="1"
-                app:percentHeight="1"
-                app:curveFit="linear"
-                app:motionTarget="@id/batteryRemainingIcon" />
-            <KeyAttribute
-                app:motionTarget="@id/batteryRemainingIcon"
-                app:framePosition="@integer/fade_out_complete_frame"
-                android:alpha="0"
-                />
-            <KeyAttribute
-                app:motionTarget="@id/batteryRemainingIcon"
+                app:motionTarget="@id/shade_header_system_icons"
                 app:framePosition="@integer/fade_in_start_frame"
                 android:alpha="0"
                 />
diff --git a/packages/SystemUI/res/xml/large_screen_shade_header.xml b/packages/SystemUI/res/xml/large_screen_shade_header.xml
index 3881e8c..39f4c81 100644
--- a/packages/SystemUI/res/xml/large_screen_shade_header.xml
+++ b/packages/SystemUI/res/xml/large_screen_shade_header.xml
@@ -45,7 +45,7 @@
             android:layout_height="0dp"
             android:layout_gravity="end|center_vertical"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toStartOf="@id/statusIcons"
+            app:layout_constraintEnd_toStartOf="@id/shade_header_system_icons"
             app:layout_constraintStart_toEndOf="@id/date"
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintWidth_default="wrap"
@@ -53,25 +53,14 @@
         <PropertySet android:alpha="1" />
     </Constraint>
 
-    <Constraint android:id="@+id/statusIcons">
+    <Constraint android:id="@+id/shade_header_system_icons">
         <Layout
             android:layout_width="wrap_content"
             android:layout_height="@dimen/large_screen_shade_header_min_height"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintStart_toEndOf="@id/carrier_group"/>
-        <PropertySet android:alpha="1" />
-    </Constraint>
-
-    <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_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toStartOf="@id/privacy_container"
-            app:layout_constraintTop_toTopOf="parent" />
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintEnd_toEndOf="@id/carrier_group"/>
         <PropertySet android:alpha="1" />
     </Constraint>
 
diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml
index 1950965..50a388d 100644
--- a/packages/SystemUI/res/xml/qqs_header.xml
+++ b/packages/SystemUI/res/xml/qqs_header.xml
@@ -54,27 +54,12 @@
     </Constraint>
 
     <Constraint
-        android:id="@+id/statusIcons">
+        android:id="@+id/shade_header_system_icons">
         <Layout
             android:layout_width="wrap_content"
             android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/date"
-            app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="@id/qqs_header_bottom_guideline"
-            app:layout_constraintHorizontal_bias="1"
-            app:layout_constraintHorizontal_chainStyle="packed"
-            />
-    </Constraint>
-
-    <Constraint
-        android:id="@+id/batteryRemainingIcon">
-        <Layout
-            android:layout_width="wrap_content"
-            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
-            app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
-            app:layout_constraintStart_toEndOf="@id/statusIcons"
             app:layout_constraintEnd_toEndOf="@id/end_guide"
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="@id/qqs_header_bottom_guideline"
diff --git a/packages/SystemUI/res/xml/qs_header.xml b/packages/SystemUI/res/xml/qs_header.xml
index 8039c68..7b4282f 100644
--- a/packages/SystemUI/res/xml/qs_header.xml
+++ b/packages/SystemUI/res/xml/qs_header.xml
@@ -59,7 +59,7 @@
         />
     </Constraint>
 
-    <!-- LargeScreenShadeHeaderController helps with managing clock width to layout this view -->
+    <!-- ShadeHeaderController helps with managing clock width to layout this view -->
     <Constraint
         android:id="@+id/carrier_group">
         <Layout
@@ -78,25 +78,11 @@
     </Constraint>
 
     <Constraint
-        android:id="@+id/statusIcons">
+        android:id="@+id/shade_header_system_icons">
         <Layout
             android:layout_width="0dp"
             android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintWidth_default="wrap"
-            app:layout_constraintStart_toEndOf="@id/date"
-            app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
-            app:layout_constraintTop_toTopOf="@id/date"
-            app:layout_constraintBottom_toBottomOf="@id/date"
-            />
-    </Constraint>
-
-    <Constraint
-        android:id="@+id/batteryRemainingIcon">
-        <Layout
-            android:layout_width="0dp"
-            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
-            app:layout_constraintWidth_default="wrap"
-            app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintTop_toTopOf="@id/date"
             app:layout_constraintBottom_toBottomOf="@id/date"
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 1f61c64..97d6099 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
@@ -215,7 +215,7 @@
             mSubscriptions.put(token, state);
 
             // Add and associate conditions.
-            normalizedCondition.getConditions().stream().forEach(condition -> {
+            normalizedCondition.getConditions().forEach(condition -> {
                 if (!mConditions.containsKey(condition)) {
                     mConditions.put(condition, new ArraySet<>());
                     condition.addCallback(mConditionCallback);
@@ -321,7 +321,6 @@
             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}.
@@ -337,8 +336,7 @@
             private Builder(Subscription nestedSubscription, Callback callback) {
                 mNestedSubscription = nestedSubscription;
                 mCallback = callback;
-                mConditions = new ArraySet();
-                mPreconditions = new ArraySet();
+                mConditions = new ArraySet<>();
             }
 
             /**
@@ -352,29 +350,6 @@
             }
 
             /**
-             * 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}.
@@ -394,11 +369,7 @@
              * @return The resulting {@link Subscription}.
              */
             public Subscription build() {
-                final Subscription subscription =
-                        new Subscription(mConditions, mCallback, mNestedSubscription);
-                return !mPreconditions.isEmpty()
-                        ? new Subscription(mPreconditions, null, subscription)
-                        : subscription;
+                return new Subscription(mConditions, mCallback, mNestedSubscription);
             }
         }
     }
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 117cf78a..4b31498 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
@@ -48,7 +48,7 @@
      *
      * Normal gesture: DOWN, MOVE/POINTER_DOWN/POINTER_UP)*, UP or CANCLE
      */
-    oneway void onStatusBarMotionEvent(in MotionEvent event) = 9;
+    oneway void onStatusBarTouchEvent(in MotionEvent event) = 9;
 
     /**
      * Proxies the assistant gesture's progress started from navigation bar.
@@ -125,5 +125,15 @@
      */
     oneway void takeScreenshot(in ScreenshotRequest request) = 51;
 
-    // Next id = 52
+    /**
+     * Dispatches trackpad status bar motion event to the notification shade. Currently these events
+     * are from the input monitor in {@link TouchInteractionService}. This is different from
+     * {@link #onStatusBarTouchEvent} above in that, this directly dispatches motion events to the
+     * notification shade, while {@link #onStatusBarTouchEvent} relies on setting the launcher
+     * window slippery to allow the frameworks to route those events after passing the initial
+     * threshold.
+     */
+    oneway void onStatusBarTrackpadEvent(in MotionEvent event) = 52;
+
+    // Next id = 53
 }
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 9a00447..0fbeb1a 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
@@ -202,8 +202,6 @@
 
     fun calculateScreenLocation(sampledView: View): RectF? {
 
-        if (!sampledView.isLaidOut) return null
-
         val screenLocation = tmpScreenLocation
         /**
          * The method getLocationOnScreen is used to obtain the view coordinates relative to its
@@ -219,6 +217,10 @@
         samplingBounds.right = left + sampledView.width
         samplingBounds.bottom = top + sampledView.height
 
+        // ensure never go out of bounds
+        if (samplingBounds.right > displaySize.x) samplingBounds.right = displaySize.x
+        if (samplingBounds.bottom > displaySize.y) samplingBounds.bottom = displaySize.y
+
         return RectF(samplingBounds)
     }
 
@@ -263,6 +265,8 @@
                 (colors?.colorHints?.and(WallpaperColors.HINT_SUPPORTS_DARK_TEXT)) !=
                     WallpaperColors.HINT_SUPPORTS_DARK_TEXT
             )
+        if (DEBUG)
+            Log.d(TAG, "onColorsChanged() | region darkness = $regionDarkness for region $area")
         updateForegroundColor()
     }
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
index 4269530..c844db7 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
@@ -61,6 +61,8 @@
             InteractionJankMonitor.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS;
     public static final int CUJ_OPEN_SEARCH_RESULT =
             InteractionJankMonitor.CUJ_LAUNCHER_OPEN_SEARCH_RESULT;
+    public static final int CUJ_SHADE_EXPAND_FROM_STATUS_BAR =
+            InteractionJankMonitor.CUJ_SHADE_EXPAND_FROM_STATUS_BAR;
 
     @IntDef({
             CUJ_APP_LAUNCH_FROM_RECENTS,
@@ -76,6 +78,7 @@
             CUJ_CLOSE_ALL_APPS_SWIPE,
             CUJ_CLOSE_ALL_APPS_TO_HOME,
             CUJ_OPEN_SEARCH_RESULT,
+            CUJ_SHADE_EXPAND_FROM_STATUS_BAR,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
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 3c447a8..ca064ef 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
@@ -324,7 +324,7 @@
      * Returns whether the specified sysui state is such that the back gesture should be
      * disabled.
      */
-    public static boolean isBackGestureDisabled(int sysuiStateFlags) {
+    public static boolean isBackGestureDisabled(int sysuiStateFlags, boolean forTrackpad) {
         // Always allow when the bouncer/global actions/voice session is showing (even on top of
         // the keyguard)
         if ((sysuiStateFlags & SYSUI_STATE_BOUNCER_SHOWING) != 0
@@ -335,16 +335,23 @@
         if ((sysuiStateFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0) {
             sysuiStateFlags &= ~SYSUI_STATE_NAV_BAR_HIDDEN;
         }
+
+        return (sysuiStateFlags & getBackGestureDisabledMask(forTrackpad)) != 0;
+    }
+
+    private static int getBackGestureDisabledMask(boolean forTrackpad) {
         // Disable when in immersive, or the notifications are interactive
-        int disableFlags = SYSUI_STATE_NAV_BAR_HIDDEN | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
+        int disableFlags = SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
+        if (!forTrackpad) {
+            disableFlags |= SYSUI_STATE_NAV_BAR_HIDDEN;
+        }
 
         // EdgeBackGestureHandler ignores Back gesture when SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED.
         // To allow Shade to respond to Back, we're bypassing this check (behind a flag).
         if (!ALLOW_BACK_GESTURE_IN_SHADE) {
             disableFlags |= SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
         }
-
-        return (sysuiStateFlags & disableFlags) != 0;
+        return disableFlags;
     }
 
     /**
diff --git a/packages/SystemUI/shared/src/com/android/systemui/util/TraceUtils.kt b/packages/SystemUI/shared/src/com/android/systemui/util/TraceUtils.kt
index 64234c2..e02e592 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/util/TraceUtils.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/util/TraceUtils.kt
@@ -18,22 +18,23 @@
 
 import android.os.Trace
 import android.os.TraceNameSupplier
+import java.util.concurrent.atomic.AtomicInteger
 
 /**
- * Run a block within a [Trace] section.
- * Calls [Trace.beginSection] before and [Trace.endSection] after the passed block.
+ * Run a block within a [Trace] section. Calls [Trace.beginSection] before and [Trace.endSection]
+ * after the passed block.
  */
 inline fun <T> traceSection(tag: String, block: () -> T): T =
-        if (Trace.isTagEnabled(Trace.TRACE_TAG_APP)) {
-            Trace.traceBegin(Trace.TRACE_TAG_APP, tag)
-            try {
-                block()
-            } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_APP)
-            }
-        } else {
+    if (Trace.isTagEnabled(Trace.TRACE_TAG_APP)) {
+        Trace.traceBegin(Trace.TRACE_TAG_APP, tag)
+        try {
             block()
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_APP)
         }
+    } else {
+        block()
+    }
 
 class TraceUtils {
     companion object {
@@ -43,6 +44,7 @@
 
         /**
          * Helper function for creating a Runnable object that implements TraceNameSupplier.
+         *
          * This is useful for posting Runnables to Handlers with meaningful names.
          */
         inline fun namedRunnable(tag: String, crossinline block: () -> Unit): Runnable {
@@ -51,5 +53,37 @@
                 override fun run() = block()
             }
         }
+
+        /**
+         * Cookie used for async traces. Shouldn't be public, but to use it inside inline methods
+         * there is no other way around.
+         */
+        val lastCookie = AtomicInteger(0)
+
+        /**
+         * Creates an async slice in a track called "AsyncTraces".
+         *
+         * This can be used to trace coroutine code. Note that all usages of this method will appear
+         * under a single track.
+         */
+        inline fun <T> traceAsync(method: String, block: () -> T): T =
+            traceAsync(method, "AsyncTraces", block)
+
+        /**
+         * Creates an async slice in a track with [trackName] while [block] runs.
+         *
+         * This can be used to trace coroutine code. [method] will be the name of the slice,
+         * [trackName] of the track. The track is one of the rows visible in a perfetto trace inside
+         * SystemUI process.
+         */
+        inline fun <T> traceAsync(method: String, trackName: String, block: () -> T): T {
+            val cookie = lastCookie.incrementAndGet()
+            Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, trackName, method, cookie)
+            try {
+                return block()
+            } finally {
+                Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, trackName, cookie)
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
index 0842953..78a5c98 100644
--- a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
+++ b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
@@ -26,7 +26,7 @@
 import android.view.View
 import com.android.app.animation.Interpolators
 import com.android.settingslib.Utils
-import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.ColorId.TITLE
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.ColorId.TITLE
 
 /** Displays security messages for the keyguard bouncer. */
 open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) :
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 8ea4c31a..91937af 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -15,18 +15,18 @@
  */
 package com.android.keyguard
 
-import android.app.WallpaperManager
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
 import android.content.res.Resources
 import android.text.format.DateFormat
-import android.util.TypedValue
 import android.util.Log
+import android.util.TypedValue
 import android.view.View
 import android.view.View.OnAttachStateChangeListener
 import android.view.ViewTreeObserver
+import android.widget.FrameLayout
 import androidx.annotation.VisibleForTesting
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
@@ -42,7 +42,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.DEBUG
 import com.android.systemui.log.dagger.KeyguardLargeClockLog
 import com.android.systemui.log.dagger.KeyguardSmallClockLog
 import com.android.systemui.plugins.ClockController
@@ -99,6 +99,28 @@
 
                 if (!regionSamplingEnabled) {
                     updateColors()
+                } else {
+                    clock?.let {
+                        smallRegionSampler = createRegionSampler(
+                                it.smallClock.view,
+                                mainExecutor,
+                                bgExecutor,
+                                regionSamplingEnabled,
+                                isLockscreen = true,
+                                ::updateColors
+                        )?.apply { startRegionSampler() }
+
+                        largeRegionSampler = createRegionSampler(
+                                it.largeClock.view,
+                                mainExecutor,
+                                bgExecutor,
+                                regionSamplingEnabled,
+                                isLockscreen = true,
+                                ::updateColors
+                        )?.apply { startRegionSampler() }
+
+                        updateColors()
+                    }
                 }
                 updateFontSizes()
                 updateTimeListeners()
@@ -110,8 +132,25 @@
                 }
                 value.smallClock.view.addOnAttachStateChangeListener(
                     object : OnAttachStateChangeListener {
-                        override fun onViewAttachedToWindow(p0: View?) {
+                        var pastVisibility: Int? = null
+                        override fun onViewAttachedToWindow(view: View?) {
                             value.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
+                            if (view != null) {
+                                val smallClockFrame = view.parent as FrameLayout
+                                pastVisibility = smallClockFrame.visibility
+                                smallClockFrame.viewTreeObserver.addOnGlobalLayoutListener(
+                                        ViewTreeObserver.OnGlobalLayoutListener {
+                                    val currentVisibility = smallClockFrame.visibility
+                                    if (pastVisibility != currentVisibility) {
+                                        pastVisibility = currentVisibility
+                                        // when small clock  visible, recalculate bounds and sample
+                                        if (currentVisibility == View.VISIBLE) {
+                                            smallRegionSampler?.stopRegionSampler()
+                                            smallRegionSampler?.startRegionSampler()
+                                        }
+                                    }
+                                })
+                            }
                         }
 
                         override fun onViewDetachedFromWindow(p0: View?) {
@@ -141,21 +180,19 @@
 
 
     private fun updateColors() {
-        val wallpaperManager = WallpaperManager.getInstance(context)
         if (regionSamplingEnabled) {
-            regionSampler?.let { regionSampler ->
-                clock?.let { clock ->
-                    if (regionSampler.sampledView == clock.smallClock.view) {
-                        smallClockIsDark = regionSampler.currentRegionDarkness().isDark
-                        clock.smallClock.events.onRegionDarknessChanged(smallClockIsDark)
-                        return@updateColors
-                    } else if (regionSampler.sampledView == clock.largeClock.view) {
-                        largeClockIsDark = regionSampler.currentRegionDarkness().isDark
-                        clock.largeClock.events.onRegionDarknessChanged(largeClockIsDark)
-                        return@updateColors
-                    }
+            clock?.let { clock ->
+                smallRegionSampler?.let {
+                    smallClockIsDark = it.currentRegionDarkness().isDark
+                    clock.smallClock.events.onRegionDarknessChanged(smallClockIsDark)
+                }
+
+                largeRegionSampler?.let {
+                    largeClockIsDark = it.currentRegionDarkness().isDark
+                    clock.largeClock.events.onRegionDarknessChanged(largeClockIsDark)
                 }
             }
+            return
         }
 
         val isLightTheme = TypedValue()
@@ -168,23 +205,6 @@
             largeClock.events.onRegionDarknessChanged(largeClockIsDark)
         }
     }
-
-    private fun updateRegionSampler(sampledRegion: View) {
-        regionSampler?.stopRegionSampler()
-        regionSampler =
-            createRegionSampler(
-                    sampledRegion,
-                    mainExecutor,
-                    bgExecutor,
-                    regionSamplingEnabled,
-                    isLockscreen = true,
-                    ::updateColors
-                )
-                ?.apply { startRegionSampler() }
-
-        updateColors()
-    }
-
     protected open fun createRegionSampler(
         sampledView: View,
         mainExecutor: Executor?,
@@ -202,7 +222,10 @@
         ) { updateColors() }
     }
 
-    var regionSampler: RegionSampler? = null
+    var smallRegionSampler: RegionSampler? = null
+        private set
+    var largeRegionSampler: RegionSampler? = null
+        private set
     var smallTimeListener: TimeListener? = null
     var largeTimeListener: TimeListener? = null
     val shouldTimeListenerRun: Boolean
@@ -319,7 +342,8 @@
         configurationController.removeCallback(configListener)
         batteryController.removeCallback(batteryCallback)
         keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
-        regionSampler?.stopRegionSampler()
+        smallRegionSampler?.stopRegionSampler()
+        largeRegionSampler?.stopRegionSampler()
         smallTimeListener?.stop()
         largeTimeListener?.stop()
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
index d7e8616..bb11217 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
@@ -143,6 +143,7 @@
         long elapsedRealtime = SystemClock.elapsedRealtime();
         long secondsInFuture = (long) Math.ceil(
                 (elapsedRealtimeDeadline - elapsedRealtime) / 1000.0);
+        getKeyguardSecurityCallback().onAttemptLockoutStart(secondsInFuture);
         mCountdownTimer = new CountDownTimer(secondsInFuture * 1000, 1000) {
 
             @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 376e27c..4793b4f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -29,7 +29,7 @@
 import com.android.keyguard.dagger.KeyguardStatusViewScope;
 import com.android.systemui.R;
 import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.LogLevel;
+import com.android.systemui.log.core.LogLevel;
 import com.android.systemui.plugins.ClockController;
 import com.android.systemui.shared.clocks.DefaultClockController;
 
@@ -303,9 +303,10 @@
         if (!animate) {
             out.setAlpha(0f);
             out.setTranslationY(clockOutYTranslation);
+            out.setVisibility(INVISIBLE);
             in.setAlpha(1f);
             in.setTranslationY(clockInYTranslation);
-            in.setVisibility(View.VISIBLE);
+            in.setVisibility(VISIBLE);
             mStatusArea.setScaleX(statusAreaClockScale);
             mStatusArea.setScaleY(statusAreaClockScale);
             mStatusArea.setTranslateXFromClockDesign(statusAreaClockTranslateX);
@@ -323,7 +324,10 @@
                 ObjectAnimator.ofFloat(out, TRANSLATION_Y, clockOutYTranslation));
         mClockOutAnim.addListener(new AnimatorListenerAdapter() {
             public void onAnimationEnd(Animator animation) {
-                mClockOutAnim = null;
+                if (mClockOutAnim == animation) {
+                    out.setVisibility(INVISIBLE);
+                    mClockOutAnim = null;
+                }
             }
         });
 
@@ -338,7 +342,9 @@
         mClockInAnim.setStartDelay(CLOCK_IN_START_DELAY_MILLIS);
         mClockInAnim.addListener(new AnimatorListenerAdapter() {
             public void onAnimationEnd(Animator animation) {
-                mClockInAnim = null;
+                if (mClockInAnim == animation) {
+                    mClockInAnim = null;
+                }
             }
         });
 
@@ -359,7 +365,9 @@
                         statusAreaClockTranslateY));
         mStatusAreaAnim.addListener(new AnimatorListenerAdapter() {
             public void onAnimationEnd(Animator animation) {
-                mStatusAreaAnim = null;
+                if (mStatusAreaAnim == animation) {
+                    mStatusAreaAnim = null;
+                }
             }
         });
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index a026130..6c98376 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -40,7 +40,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.LogLevel;
+import com.android.systemui.log.core.LogLevel;
 import com.android.systemui.log.dagger.KeyguardClockLog;
 import com.android.systemui.plugins.ClockController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -444,10 +444,6 @@
             int clockHeight = clock.getLargeClock().getView().getHeight();
             return frameHeight / 2 + clockHeight / 2 + mKeyguardLargeClockTopMargin / -2;
         } else {
-            // This is only called if we've never shown the large clock as the frame is inflated
-            // with 'gone', but then the visibility is never set when it is animated away by
-            // KeyguardClockSwitch, instead it is removed from the view hierarchy.
-            // TODO(b/261755021): Cleanup Large Frame Visibility
             int clockHeight = clock.getSmallClock().getView().getHeight();
             return clockHeight + statusBarHeaderHeight + mKeyguardSmallClockTopMargin;
         }
@@ -465,15 +461,11 @@
         if (mLargeClockFrame.getVisibility() == View.VISIBLE) {
             return clock.getLargeClock().getView().getHeight();
         } else {
-            // Is not called except in certain edge cases, see comment in getClockBottom
-            // TODO(b/261755021): Cleanup Large Frame Visibility
             return clock.getSmallClock().getView().getHeight();
         }
     }
 
     boolean isClockTopAligned() {
-        // Returns false except certain edge cases, see comment in getClockBottom
-        // TODO(b/261755021): Cleanup Large Frame Visibility
         return mLargeClockFrame.getVisibility() != View.VISIBLE;
     }
 
@@ -552,9 +544,13 @@
         if (clock != null) {
             clock.dump(pw);
         }
-        final RegionSampler regionSampler = mClockEventController.getRegionSampler();
-        if (regionSampler != null) {
-            regionSampler.dump(pw);
+        final RegionSampler smallRegionSampler = mClockEventController.getSmallRegionSampler();
+        if (smallRegionSampler != null) {
+            smallRegionSampler.dump(pw);
+        }
+        final RegionSampler largeRegionSampler = mClockEventController.getLargeRegionSampler();
+        if (largeRegionSampler != null) {
+            largeRegionSampler.dump(pw);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt
index f2685c5..f23ae67 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 allowOnCurrentOccludingActivity: Boolean = false,
     var alternateBouncerShowing: Boolean = false,
     var biometricEnabledForUser: Boolean = false,
     var bouncerIsOrWillShow: Boolean = false,
@@ -58,6 +59,7 @@
             userId.toString(),
             listening.toString(),
             // keep sorted
+            allowOnCurrentOccludingActivity.toString(),
             alternateBouncerShowing.toString(),
             biometricEnabledForUser.toString(),
             bouncerIsOrWillShow.toString(),
@@ -98,6 +100,7 @@
                 userId = model.userId
                 listening = model.listening
                 // keep sorted
+                allowOnCurrentOccludingActivity = model.allowOnCurrentOccludingActivity
                 alternateBouncerShowing = model.alternateBouncerShowing
                 biometricEnabledForUser = model.biometricEnabledForUser
                 bouncerIsOrWillShow = model.bouncerIsOrWillShow
@@ -144,6 +147,7 @@
                 "userId",
                 "listening",
                 // keep sorted
+                "allowOnCurrentOccludingActivity",
                 "alternateBouncerShowing",
                 "biometricAllowedForUser",
                 "bouncerIsOrWillShow",
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index 3c05299..20e4656 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -17,6 +17,7 @@
 package com.android.keyguard;
 
 import android.annotation.CallSuper;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
@@ -33,6 +34,10 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
+import com.android.systemui.bouncer.ui.BouncerMessageView;
+import com.android.systemui.bouncer.ui.binder.BouncerMessageViewBinder;
+import com.android.systemui.log.BouncerLogger;
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -168,6 +173,24 @@
     /** Determines the message to show in the bouncer when it first appears. */
     protected abstract int getInitialMessageResId();
 
+    /**
+     * Binds the {@link KeyguardInputView#getBouncerMessageView()} view with the provided context.
+     */
+    public void bindMessageView(
+            @NonNull BouncerMessageInteractor bouncerMessageInteractor,
+            KeyguardMessageAreaController.Factory messageAreaControllerFactory,
+            BouncerLogger bouncerLogger,
+            FeatureFlags featureFlags) {
+        BouncerMessageView bouncerMessageView = (BouncerMessageView) mView.getBouncerMessageView();
+        if (bouncerMessageView != null) {
+            BouncerMessageViewBinder.bind(bouncerMessageView,
+                    bouncerMessageInteractor,
+                    messageAreaControllerFactory,
+                    bouncerLogger,
+                    featureFlags);
+        }
+    }
+
     /** Factory for a {@link KeyguardInputViewController}. */
     public static class Factory {
         private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index 3d255a5..49f788c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -236,6 +236,7 @@
                 getKeyguardSecurityCallback().onCancelClicked();
             });
         }
+        mView.onDevicePostureChanged(mPostureController.getDevicePosture());
         mPostureController.addCallback(mPostureCallback);
     }
 
@@ -368,6 +369,7 @@
         final long elapsedRealtime = SystemClock.elapsedRealtime();
         final long secondsInFuture = (long) Math.ceil(
                 (elapsedRealtimeDeadline - elapsedRealtime) / 1000.0);
+        getKeyguardSecurityCallback().onAttemptLockoutStart(secondsInFuture);
         mCountdownTimer = new CountDownTimer(secondsInFuture * 1000, 1000) {
 
             @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java
index bf9c3bb..2878df2 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java
@@ -87,6 +87,11 @@
     default void onUserInput() {
     }
 
+    /**
+     * Invoked when the auth input is disabled for specified number of seconds.
+     * @param seconds Number of seconds for which the auth input is disabled.
+     */
+    default void onAttemptLockoutStart(long seconds) {}
 
     /**
      * Dismisses keyguard and go to unlocked state.
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 841b5b3..6853f81 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -281,6 +281,8 @@
 
     public interface SwipeListener {
         void onSwipeUp();
+        /** */
+        void onSwipeDown();
     }
 
     @VisibleForTesting
@@ -543,6 +545,11 @@
                 if (mSwipeListener != null) {
                     mSwipeListener.onSwipeUp();
                 }
+            } else if (getTranslationY() > TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                    MIN_DRAG_SIZE, getResources().getDisplayMetrics())) {
+                if (mSwipeListener != null) {
+                    mSwipeListener.onSwipeDown();
+                }
             }
         }
         return true;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 74b29a7..458ca2b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -70,6 +70,7 @@
 import com.android.systemui.R;
 import com.android.systemui.biometrics.SideFpsController;
 import com.android.systemui.biometrics.SideFpsUiRequestSource;
+import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
 import com.android.systemui.classifier.FalsingA11yDelegate;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.flags.FeatureFlags;
@@ -116,6 +117,7 @@
     private final Optional<SideFpsController> mSideFpsController;
     private final FalsingA11yDelegate mFalsingA11yDelegate;
     private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor;
+    private final BouncerMessageInteractor mBouncerMessageInteractor;
     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
@@ -178,6 +180,7 @@
 
         @Override
         public void onUserInput() {
+            mBouncerMessageInteractor.onPrimaryBouncerUserInput();
             mKeyguardFaceAuthInteractor.onPrimaryBouncerUserInput();
             mUpdateMonitor.cancelFaceAuth();
         }
@@ -207,7 +210,15 @@
         }
 
         @Override
+        public void onAttemptLockoutStart(long seconds) {
+            mBouncerMessageInteractor.onPrimaryAuthLockedOut(seconds);
+        }
+
+        @Override
         public void reportUnlockAttempt(int userId, boolean success, int timeoutMs) {
+            if (timeoutMs == 0 && !success) {
+                mBouncerMessageInteractor.onPrimaryAuthIncorrectAttempt();
+            }
             int bouncerSide = SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__DEFAULT;
             if (mView.isSidedSecurityMode()) {
                 bouncerSide = mView.isSecurityLeftAligned()
@@ -308,6 +319,11 @@
                         "swipeUpOnBouncer");
             }
         }
+
+        @Override
+        public void onSwipeDown() {
+            mViewMediatorCallback.onBouncerSwipeDown();
+        }
     };
     private final ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
@@ -385,7 +401,8 @@
             TelephonyManager telephonyManager,
             ViewMediatorCallback viewMediatorCallback,
             AudioManager audioManager,
-            KeyguardFaceAuthInteractor keyguardFaceAuthInteractor
+            KeyguardFaceAuthInteractor keyguardFaceAuthInteractor,
+            BouncerMessageInteractor bouncerMessageInteractor
     ) {
         super(view);
         mLockPatternUtils = lockPatternUtils;
@@ -411,6 +428,7 @@
         mViewMediatorCallback = viewMediatorCallback;
         mAudioManager = audioManager;
         mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor;
+        mBouncerMessageInteractor = bouncerMessageInteractor;
     }
 
     @Override
@@ -431,6 +449,7 @@
         // Update ViewMediator with the current input method requirements
         mViewMediatorCallback.setNeedsInput(needsInput());
         mView.setOnKeyListener(mOnKeyListener);
+
         showPrimarySecurityScreen(false);
     }
 
@@ -525,7 +544,6 @@
     public void setOnDismissAction(ActivityStarter.OnDismissAction action, Runnable cancelAction) {
         if (mCancelAction != null) {
             mCancelAction.run();
-            mCancelAction = null;
         }
         mDismissAction = action;
         mCancelAction = cancelAction;
@@ -763,9 +781,10 @@
                 case SimPuk:
                     // Shortcut for SIM PIN/PUK to go to directly to user's security screen or home
                     SecurityMode securityMode = mSecurityModel.getSecurityMode(targetUserId);
-                    if (securityMode == SecurityMode.None || mLockPatternUtils.isLockScreenDisabled(
-                            KeyguardUpdateMonitor.getCurrentUser())) {
-                        finish = true;
+                    boolean isLockscreenDisabled = mLockPatternUtils.isLockScreenDisabled(
+                            KeyguardUpdateMonitor.getCurrentUser());
+                    if (securityMode == SecurityMode.None || isLockscreenDisabled) {
+                        finish = isLockscreenDisabled;
                         eventSubtype = BOUNCER_DISMISS_SIM;
                         uiEvent = BouncerUiEvent.BOUNCER_DISMISS_SIM;
                     } else {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimInputView.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardSimInputView.kt
index 1461dbe..7c8d91f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimInputView.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimInputView.kt
@@ -22,7 +22,7 @@
 import androidx.core.graphics.drawable.DrawableCompat
 import com.android.settingslib.Utils
 import com.android.systemui.R
-import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.ColorId.EMERGENCY_BUTTON
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.ColorId.EMERGENCY_BUTTON
 
 abstract class KeyguardSimInputView(context: Context, attrs: AttributeSet) :
     KeyguardPinBasedInputView(context, attrs) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index de24024..518baec 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -18,10 +18,14 @@
 
 import static android.app.StatusBarManager.SESSION_KEYGUARD;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.content.Intent.ACTION_USER_REMOVED;
 import static android.content.Intent.ACTION_USER_STOPPED;
 import static android.content.Intent.ACTION_USER_UNLOCKED;
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
 import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_NONE;
 import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_PERMANENT;
 import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_TIMED;
@@ -76,9 +80,9 @@
 import android.annotation.AnyThread;
 import android.annotation.MainThread;
 import android.annotation.SuppressLint;
-import android.app.ActivityTaskManager;
 import android.app.ActivityTaskManager.RootTaskInfo;
 import android.app.AlarmManager;
+import android.app.IActivityTaskManager;
 import android.app.admin.DevicePolicyManager;
 import android.app.trust.TrustManager;
 import android.content.BroadcastReceiver;
@@ -155,6 +159,8 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.dump.DumpsysTableLogger;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.domain.interactor.FaceAuthenticationListener;
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.keyguard.shared.constants.TrustAgentUiEvent;
@@ -247,6 +253,7 @@
     private static final int MSG_REQUIRE_NFC_UNLOCK = 345;
     private static final int MSG_KEYGUARD_DISMISS_ANIMATION_FINISHED = 346;
     private static final int MSG_SERVICE_PROVIDERS_UPDATED = 347;
+    private static final int MSG_BIOMETRIC_ENROLLMENT_STATE_CHANGED = 348;
 
     /** Biometric authentication state: Not listening. */
     private static final int BIOMETRIC_STATE_STOPPED = 0;
@@ -308,6 +315,7 @@
     private final AuthController mAuthController;
     private final UiEventLogger mUiEventLogger;
     private final Set<Integer> mFaceAcquiredInfoIgnoreList;
+    private final Set<String> mAllowFingerprintOnOccludingActivitiesFromPackage;
     private final PackageManager mPackageManager;
     private int mStatusBarState;
     private final StatusBarStateController.StateListener mStatusBarStateControllerListener =
@@ -349,6 +357,7 @@
     private boolean mSecureCameraLaunched;
     @VisibleForTesting
     protected boolean mTelephonyCapable;
+    private boolean mAllowFingerprintOnCurrentOccludingActivity;
 
     // Device provisioning state
     private boolean mDeviceProvisioned;
@@ -385,12 +394,15 @@
     private final ActiveUnlockConfig mActiveUnlockConfig;
     private final IDreamManager mDreamManager;
     private final TelephonyManager mTelephonyManager;
+    private final FeatureFlags mFeatureFlags;
     @Nullable
     private final FingerprintManager mFpm;
     @Nullable
     private final FaceManager mFaceManager;
     @Nullable
     private KeyguardFaceAuthInteractor mFaceAuthInteractor;
+    private final TaskStackChangeListeners mTaskStackChangeListeners;
+    private final IActivityTaskManager mActivityTaskManager;
     private final LockPatternUtils mLockPatternUtils;
     @VisibleForTesting
     @DevicePostureInt
@@ -1467,14 +1479,6 @@
                         ErrorAuthenticationStatus error = (ErrorAuthenticationStatus) status;
                         handleFaceError(error.getMsgId(), error.getMsg());
                     } else if (status instanceof FailedAuthenticationStatus) {
-                        if (isFaceLockedOut()) {
-                            // TODO b/270090188: remove this hack when biometrics fixes this issue.
-                            // FailedAuthenticationStatus is emitted after ErrorAuthenticationStatus
-                            // for lockout error is received
-                            mLogger.d("onAuthenticationFailed called after"
-                                    + " face has been locked out");
-                            return;
-                        }
                         handleFaceAuthFailed();
                     } else if (status instanceof HelpAuthenticationStatus) {
                         HelpAuthenticationStatus helpMsg = (HelpAuthenticationStatus) status;
@@ -1985,13 +1989,6 @@
 
                 @Override
                 public void onAuthenticationFailed() {
-                    if (isFaceLockedOut()) {
-                        // TODO b/270090188: remove this hack when biometrics fixes this issue.
-                        // onAuthenticationFailed is called after onAuthenticationError
-                        // for lockout error is received
-                        mLogger.d("onAuthenticationFailed called after face has been locked out");
-                        return;
-                    }
                     handleFaceAuthFailed();
                 }
 
@@ -2338,7 +2335,10 @@
             @Nullable BiometricManager biometricManager,
             FaceWakeUpTriggersConfig faceWakeUpTriggersConfig,
             DevicePostureController devicePostureController,
-            Optional<FingerprintInteractiveToAuthProvider> interactiveToAuthProvider) {
+            Optional<FingerprintInteractiveToAuthProvider> interactiveToAuthProvider,
+            FeatureFlags featureFlags,
+            TaskStackChangeListeners taskStackChangeListeners,
+            IActivityTaskManager activityTaskManagerService) {
         mContext = context;
         mSubscriptionManager = subscriptionManager;
         mUserTracker = userTracker;
@@ -2370,6 +2370,7 @@
         mPackageManager = packageManager;
         mFpm = fingerprintManager;
         mFaceManager = faceManager;
+        mFeatureFlags = featureFlags;
         mActiveUnlockConfig.setKeyguardUpdateMonitor(this);
         mFaceAcquiredInfoIgnoreList = Arrays.stream(
                 mContext.getResources().getIntArray(
@@ -2379,6 +2380,12 @@
         mConfigFaceAuthSupportedPosture = mContext.getResources().getInteger(
                 R.integer.config_face_auth_supported_posture);
         mFaceWakeUpTriggersConfig = faceWakeUpTriggersConfig;
+        mAllowFingerprintOnOccludingActivitiesFromPackage = Arrays.stream(
+                mContext.getResources().getStringArray(
+                        R.array.config_fingerprint_listen_on_occluding_activity_packages))
+                .collect(Collectors.toSet());
+        mTaskStackChangeListeners = taskStackChangeListeners;
+        mActivityTaskManager = activityTaskManagerService;
 
         mHandler = new Handler(mainLooper) {
             @Override
@@ -2485,6 +2492,9 @@
                     case MSG_KEYGUARD_DISMISS_ANIMATION_FINISHED:
                         handleKeyguardDismissAnimationFinished();
                         break;
+                    case MSG_BIOMETRIC_ENROLLMENT_STATE_CHANGED:
+                        notifyAboutEnrollmentChange(msg.arg1);
+                        break;
                     default:
                         super.handleMessage(msg);
                         break;
@@ -2585,6 +2595,8 @@
 
             @Override
             public void onEnrollmentsChanged(@BiometricAuthenticator.Modality int modality) {
+                mHandler.obtainMessage(MSG_BIOMETRIC_ENROLLMENT_STATE_CHANGED, modality, 0)
+                        .sendToTarget();
                 mainExecutor.execute(() -> updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
                         FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED));
             }
@@ -2594,7 +2606,7 @@
         }
         updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_ON_KEYGUARD_INIT);
 
-        TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
+        mTaskStackChangeListeners.registerTaskStackListener(mTaskStackListener);
         mIsSystemUser = mUserManager.isSystemUser();
         int user = mUserTracker.getUserId();
         mUserIsUnlocked.put(user, mUserManager.isUserUnlocked(user));
@@ -3061,7 +3073,12 @@
                         || shouldListenForFingerprintAssistant
                         || (mKeyguardOccluded && mIsDreaming)
                         || (mKeyguardOccluded && userDoesNotHaveTrust && mKeyguardShowing
-                            && (mOccludingAppRequestingFp || isUdfps || mAlternateBouncerShowing));
+                        && (mOccludingAppRequestingFp
+                        || isUdfps
+                        || mAlternateBouncerShowing
+                        || mAllowFingerprintOnCurrentOccludingActivity
+                )
+            );
 
         // Only listen if this KeyguardUpdateMonitor belongs to the system user. There is an
         // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware.
@@ -3102,6 +3119,7 @@
                     System.currentTimeMillis(),
                     user,
                     shouldListen,
+                    mAllowFingerprintOnCurrentOccludingActivity,
                     mAlternateBouncerShowing,
                     biometricEnabledForUser,
                     mPrimaryBouncerIsOrWillBeShowing,
@@ -3218,6 +3236,13 @@
                 || (posture == mConfigFaceAuthSupportedPosture);
     }
 
+    /**
+     * If the current device posture allows face auth to run.
+     */
+    public boolean doesCurrentPostureAllowFaceAuth() {
+        return doesPostureAllowFaceAuth(mPostureState);
+    }
+
     private void logListenerModelData(@NonNull KeyguardListenModel model) {
         mLogger.logKeyguardListenerModel(model);
         if (model instanceof KeyguardFingerprintListenModel) {
@@ -3421,6 +3446,25 @@
         return mIsFaceEnrolled;
     }
 
+    private void notifyAboutEnrollmentChange(@BiometricAuthenticator.Modality int modality) {
+        BiometricSourceType biometricSourceType;
+        if (modality == TYPE_FINGERPRINT) {
+            biometricSourceType = FINGERPRINT;
+        } else if (modality == TYPE_FACE) {
+            biometricSourceType = FACE;
+        } else {
+            return;
+        }
+        mLogger.notifyAboutEnrollmentsChanged(biometricSourceType);
+        Assert.isMainThread();
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+            if (cb != null) {
+                cb.onBiometricEnrollmentStateChanged(biometricSourceType);
+            }
+        }
+    }
+
     private void stopListeningForFingerprint() {
         mLogger.v("stopListeningForFingerprint()");
         if (mFingerprintRunningState == BIOMETRIC_STATE_RUNNING) {
@@ -4122,19 +4166,35 @@
         return mSimDatas.get(subId).slotId;
     }
 
-    private final TaskStackChangeListener
-            mTaskStackListener = new TaskStackChangeListener() {
+    private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
         @Override
         public void onTaskStackChangedBackground() {
             try {
-                RootTaskInfo info = ActivityTaskManager.getService().getRootTaskInfo(
+                if (mFeatureFlags.isEnabled(Flags.FP_LISTEN_OCCLUDING_APPS)) {
+                    RootTaskInfo standardTask = mActivityTaskManager.getRootTaskInfo(
+                            WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+                    final boolean previousState = mAllowFingerprintOnCurrentOccludingActivity;
+                    mAllowFingerprintOnCurrentOccludingActivity =
+                            standardTask.topActivity != null
+                                    && !TextUtils.isEmpty(standardTask.topActivity.getPackageName())
+                                    && mAllowFingerprintOnOccludingActivitiesFromPackage.contains(
+                                            standardTask.topActivity.getPackageName())
+                                    && standardTask.visible;
+                    if (mAllowFingerprintOnCurrentOccludingActivity != previousState) {
+                        mLogger.allowFingerprintOnCurrentOccludingActivityChanged(
+                                mAllowFingerprintOnCurrentOccludingActivity);
+                        updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
+                    }
+                }
+
+                RootTaskInfo assistantTask = mActivityTaskManager.getRootTaskInfo(
                         WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_ASSISTANT);
-                if (info == null) {
+                if (assistantTask == null) {
                     return;
                 }
-                mLogger.logTaskStackChangedForAssistant(info.visible);
+                mLogger.logTaskStackChangedForAssistant(assistantTask.visible);
                 mHandler.sendMessage(mHandler.obtainMessage(MSG_ASSISTANT_STACK_CHANGED,
-                        info.visible));
+                        assistantTask.visible));
             } catch (RemoteException e) {
                 mLogger.logException(e, "unable to check task stack ");
             }
@@ -4326,7 +4386,7 @@
 
         mUserTracker.removeCallback(mUserChangedCallback);
 
-        TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
+        mTaskStackChangeListeners.unregisterTaskStackListener(mTaskStackListener);
 
         mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver);
         mBroadcastDispatcher.unregisterReceiver(mBroadcastAllReceiver);
@@ -4385,6 +4445,8 @@
             pw.println("    enabledByUser=" + mBiometricEnabledForUser.get(userId));
             pw.println("    mKeyguardOccluded=" + mKeyguardOccluded);
             pw.println("    mIsDreaming=" + mIsDreaming);
+            pw.println("    mFingerprintListenOnOccludingActivitiesFromPackage="
+                    + mAllowFingerprintOnOccludingActivitiesFromPackage);
             if (isUdfpsSupported()) {
                 pw.println("        udfpsEnrolled=" + isUdfpsEnrolled());
                 pw.println("        shouldListenForUdfps=" + shouldListenForFingerprint(true));
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index 7394005..7b59632 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -327,4 +327,9 @@
      * Called when the enabled trust agents associated with the specified user.
      */
     public void onEnabledTrustAgentsChanged(int userId) { }
+
+    /**
+     * On biometric enrollment state changed
+     */
+    public void onBiometricEnrollmentStateChanged(BiometricSourceType biometricSourceType) { }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
index 281067d..bc12aee 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
@@ -119,6 +119,14 @@
     boolean isUnlockWithWallpaper();
 
     /**
+     * @return Whether the bouncer over dream is showing. Note that the bouncer over dream is
+     * handled independently of the rest of the notification panel. As a result, setting this state
+     * via {@link CentralSurfaces#setBouncerShowing(boolean)} leads to unintended side effects from
+     * states modified behind the dream.
+     */
+    boolean isBouncerShowingOverDream();
+
+    /**
      * @return Whether subtle animation should be used for unlocking the device.
      */
     boolean shouldSubtleWindowAnimationsForUnlock();
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index 61af722..71f78c3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -23,7 +23,7 @@
 
 import com.android.app.animation.Interpolators;
 import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.LogLevel;
+import com.android.systemui.log.core.LogLevel;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconView.java b/packages/SystemUI/src/com/android/keyguard/LockIconView.java
index abad0be..76b073e 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconView.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconView.java
@@ -16,6 +16,8 @@
 
 package com.android.keyguard;
 
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.graphics.Color;
@@ -23,13 +25,13 @@
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
+import android.view.Gravity;
 import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.graphics.ColorUtils;
 import com.android.settingslib.Utils;
@@ -68,13 +70,9 @@
     public LockIconView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mSensorRect = new RectF();
-    }
 
-    @Override
-    public void onFinishInflate() {
-        super.onFinishInflate();
-        mLockIcon = findViewById(R.id.lock_icon);
-        mBgView = findViewById(R.id.lock_icon_bg);
+        addBgImageView(context, attrs);
+        addLockIconImageView(context, attrs);
     }
 
     void setDozeAmount(float dozeAmount) {
@@ -127,7 +125,6 @@
     /**
      * Set the location of the lock icon.
      */
-    @VisibleForTesting
     public void setCenterLocation(@NonNull Point center, float radius, int drawablePadding) {
         mLockIconCenter = center;
         mRadius = radius;
@@ -184,6 +181,30 @@
         mLockIcon.setImageState(getLockIconState(mIconType, mAod), true);
     }
 
+    private void addLockIconImageView(Context context, AttributeSet attrs) {
+        mLockIcon = new ImageView(context, attrs);
+        mLockIcon.setId(R.id.lock_icon);
+        mLockIcon.setScaleType(ImageView.ScaleType.CENTER_CROP);
+        addView(mLockIcon);
+        LayoutParams lp = (LayoutParams) mLockIcon.getLayoutParams();
+        lp.height = MATCH_PARENT;
+        lp.width = MATCH_PARENT;
+        lp.gravity = Gravity.CENTER;
+        mLockIcon.setLayoutParams(lp);
+    }
+
+    private void addBgImageView(Context context, AttributeSet attrs) {
+        mBgView = new ImageView(context, attrs);
+        mBgView.setId(R.id.lock_icon_bg);
+        mBgView.setImageDrawable(context.getDrawable(R.drawable.fingerprint_bg));
+        mBgView.setVisibility(View.INVISIBLE);
+        addView(mBgView);
+        LayoutParams lp = (LayoutParams) mBgView.getLayoutParams();
+        lp.height = MATCH_PARENT;
+        lp.width = MATCH_PARENT;
+        mBgView.setLayoutParams(lp);
+    }
+
     private static int[] getLockIconState(@IconType int icon, boolean aod) {
         if (icon == ICON_NONE) {
             return new int[0];
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 9003c43..8ef6c2e 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -56,13 +56,14 @@
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.biometrics.AuthRippleController;
 import com.android.systemui.biometrics.UdfpsController;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
-import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -397,15 +398,17 @@
     private void updateLockIconLocation() {
         final float scaleFactor = mAuthController.getScaleFactor();
         final int scaledPadding = (int) (mDefaultPaddingPx * scaleFactor);
-        if (mUdfpsSupported) {
-            mView.setCenterLocation(mAuthController.getUdfpsLocation(),
-                    mAuthController.getUdfpsRadius(), scaledPadding);
-        } else {
-            mView.setCenterLocation(
-                    new Point((int) mWidthPixels / 2,
-                            (int) (mHeightPixels
-                                    - ((mBottomPaddingPx + sLockIconRadiusPx) * scaleFactor))),
+        if (!mFeatureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON)) {
+            if (mUdfpsSupported) {
+                mView.setCenterLocation(mAuthController.getUdfpsLocation(),
+                        mAuthController.getUdfpsRadius(), scaledPadding);
+            } else {
+                mView.setCenterLocation(
+                        new Point((int) mWidthPixels / 2,
+                                (int) (mHeightPixels
+                                        - ((mBottomPaddingPx + sLockIconRadiusPx) * scaleFactor))),
                         sLockIconRadiusPx * scaleFactor, scaledPadding);
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java b/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java
index 7d76f12..a04a48d 100644
--- a/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java
+++ b/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java
@@ -16,11 +16,11 @@
 package com.android.keyguard;
 
 import static com.android.settingslib.Utils.getColorAttrDefaultColor;
-import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_BACKGROUND;
-import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_BACKGROUND_PRESSED;
-import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_BUTTON;
-import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_KEY;
-import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_PRESSED;
+import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_BACKGROUND;
+import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_BACKGROUND_PRESSED;
+import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_BUTTON;
+import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_KEY;
+import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_PRESSED;
 import static com.android.systemui.util.ColorUtilKt.getPrivateAttrColorIfUnset;
 
 import android.animation.AnimatorSet;
diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
index ebd234f..3f1741a6 100644
--- a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
+++ b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java
@@ -15,8 +15,8 @@
  */
 package com.android.keyguard;
 
-import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_BUTTON;
-import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_KEY;
+import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_BUTTON;
+import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_KEY;
 
 import android.content.Context;
 import android.content.res.ColorStateList;
diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
index e22fc30..edc298c 100644
--- a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
+++ b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java
@@ -15,7 +15,7 @@
  */
 package com.android.keyguard;
 
-import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_KEY;
+import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_KEY;
 
 import android.content.Context;
 import android.content.res.Configuration;
diff --git a/packages/SystemUI/src/com/android/keyguard/PinShapeHintingView.java b/packages/SystemUI/src/com/android/keyguard/PinShapeHintingView.java
index 32f06dc..7c129b4 100644
--- a/packages/SystemUI/src/com/android/keyguard/PinShapeHintingView.java
+++ b/packages/SystemUI/src/com/android/keyguard/PinShapeHintingView.java
@@ -16,7 +16,7 @@
 
 package com.android.keyguard;
 
-import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.ColorId.PIN_SHAPES;
+import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.ColorId.PIN_SHAPES;
 
 import android.content.Context;
 import android.graphics.drawable.AnimatedVectorDrawable;
diff --git a/packages/SystemUI/src/com/android/keyguard/PinShapeNonHintingView.java b/packages/SystemUI/src/com/android/keyguard/PinShapeNonHintingView.java
index 500910c..56c0953 100644
--- a/packages/SystemUI/src/com/android/keyguard/PinShapeNonHintingView.java
+++ b/packages/SystemUI/src/com/android/keyguard/PinShapeNonHintingView.java
@@ -16,7 +16,7 @@
 
 package com.android.keyguard;
 
-import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.ColorId.PIN_SHAPES;
+import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.ColorId.PIN_SHAPES;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
diff --git a/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java b/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java
index 50f8f7e..14ec27a 100644
--- a/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java
@@ -104,4 +104,9 @@
      * Call when cancel button is pressed in bouncer.
      */
     void onCancelClicked();
+
+    /**
+     * Determines if bouncer has swiped down.
+     */
+    void onBouncerSwipeDown();
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/AnalogClockController.java b/packages/SystemUI/src/com/android/keyguard/clock/AnalogClockController.java
deleted file mode 100644
index 7517dee..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/AnalogClockController.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import android.app.WallpaperManager;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
-import android.graphics.Paint.Style;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.TextClock;
-
-import com.android.internal.colorextraction.ColorExtractor;
-import com.android.systemui.R;
-import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.plugins.ClockPlugin;
-
-import java.util.TimeZone;
-
-/**
- * Controller for Stretch clock that can appear on lock screen and AOD.
- */
-public class AnalogClockController implements ClockPlugin {
-
-    /**
-     * Resources used to get title and thumbnail.
-     */
-    private final Resources mResources;
-
-    /**
-     * LayoutInflater used to inflate custom clock views.
-     */
-    private final LayoutInflater mLayoutInflater;
-
-    /**
-     * Extracts accent color from wallpaper.
-     */
-    private final SysuiColorExtractor mColorExtractor;
-
-    /**
-     * Computes preferred position of clock.
-     */
-    private final SmallClockPosition mClockPosition;
-
-    /**
-     * Renders preview from clock view.
-     */
-    private final ViewPreviewer mRenderer = new ViewPreviewer();
-
-    /**
-     * Custom clock shown on AOD screen and behind stack scroller on lock.
-     */
-    private ClockLayout mBigClockView;
-    private ImageClock mAnalogClock;
-
-    /**
-     * Small clock shown on lock screen above stack scroller.
-     */
-    private View mView;
-    private TextClock mLockClock;
-
-    /**
-     * Helper to extract colors from wallpaper palette for clock face.
-     */
-    private final ClockPalette mPalette = new ClockPalette();
-
-    /**
-     * Create a BubbleClockController instance.
-     *
-     * @param res Resources contains title and thumbnail.
-     * @param inflater Inflater used to inflate custom clock views.
-     * @param colorExtractor Extracts accent color from wallpaper.
-     */
-    public AnalogClockController(Resources res, LayoutInflater inflater,
-            SysuiColorExtractor colorExtractor) {
-        mResources = res;
-        mLayoutInflater = inflater;
-        mColorExtractor = colorExtractor;
-        mClockPosition = new SmallClockPosition(inflater.getContext());
-    }
-
-    private void createViews() {
-        mBigClockView = (ClockLayout) mLayoutInflater.inflate(R.layout.analog_clock, null);
-        mAnalogClock = mBigClockView.findViewById(R.id.analog_clock);
-
-        mView = mLayoutInflater.inflate(R.layout.digital_clock, null);
-        mLockClock = mView.findViewById(R.id.lock_screen_clock);
-    }
-
-    @Override
-    public void onDestroyView() {
-        mBigClockView = null;
-        mAnalogClock = null;
-        mView = null;
-        mLockClock = null;
-    }
-
-    @Override
-    public String getName() {
-        return "analog";
-    }
-
-    @Override
-    public String getTitle() {
-        return mResources.getString(R.string.clock_title_analog);
-    }
-
-    @Override
-    public Bitmap getThumbnail() {
-        return BitmapFactory.decodeResource(mResources, R.drawable.analog_thumbnail);
-    }
-
-    @Override
-    public Bitmap getPreview(int width, int height) {
-
-        // Use the big clock view for the preview
-        View view = getBigClockView();
-
-        // Initialize state of plugin before generating preview.
-        setDarkAmount(1f);
-        setTextColor(Color.WHITE);
-        ColorExtractor.GradientColors colors = mColorExtractor.getColors(
-                WallpaperManager.FLAG_LOCK);
-        setColorPalette(colors.supportsDarkText(), colors.getColorPalette());
-        onTimeTick();
-
-        return mRenderer.createPreview(view, width, height);
-    }
-
-    @Override
-    public View getView() {
-        if (mView == null) {
-            createViews();
-        }
-        return mView;
-    }
-
-    @Override
-    public View getBigClockView() {
-        if (mBigClockView == null) {
-            createViews();
-        }
-        return mBigClockView;
-    }
-
-    @Override
-    public int getPreferredY(int totalHeight) {
-        return mClockPosition.getPreferredY();
-    }
-
-    @Override
-    public void setStyle(Style style) {}
-
-    @Override
-    public void setTextColor(int color) {
-        updateColor();
-    }
-
-    @Override
-    public void setColorPalette(boolean supportsDarkText, int[] colorPalette) {
-        mPalette.setColorPalette(supportsDarkText, colorPalette);
-        updateColor();
-    }
-
-    private void updateColor() {
-        final int primary = mPalette.getPrimaryColor();
-        final int secondary = mPalette.getSecondaryColor();
-        mLockClock.setTextColor(secondary);
-        mAnalogClock.setClockColors(primary, secondary);
-    }
-
-    @Override
-    public void onTimeTick() {
-        mAnalogClock.onTimeChanged();
-        mBigClockView.onTimeChanged();
-        mLockClock.refreshTime();
-    }
-
-    @Override
-    public void setDarkAmount(float darkAmount) {
-        mPalette.setDarkAmount(darkAmount);
-        mClockPosition.setDarkAmount(darkAmount);
-        mBigClockView.setDarkAmount(darkAmount);
-    }
-
-    @Override
-    public void onTimeZoneChanged(TimeZone timeZone) {
-        mAnalogClock.onTimeZoneChanged(timeZone);
-    }
-
-    @Override
-    public boolean shouldShowStatusArea() {
-        return true;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/BubbleClockController.java b/packages/SystemUI/src/com/android/keyguard/clock/BubbleClockController.java
deleted file mode 100644
index 1add1a3..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/BubbleClockController.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import android.app.WallpaperManager;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
-import android.graphics.Paint.Style;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.TextClock;
-
-import com.android.internal.colorextraction.ColorExtractor;
-import com.android.systemui.R;
-import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.plugins.ClockPlugin;
-
-import java.util.TimeZone;
-
-/**
- * Controller for Bubble clock that can appear on lock screen and AOD.
- */
-public class BubbleClockController implements ClockPlugin {
-
-    /**
-     * Resources used to get title and thumbnail.
-     */
-    private final Resources mResources;
-
-    /**
-     * LayoutInflater used to inflate custom clock views.
-     */
-    private final LayoutInflater mLayoutInflater;
-
-    /**
-     * Extracts accent color from wallpaper.
-     */
-    private final SysuiColorExtractor mColorExtractor;
-
-    /**
-     * Computes preferred position of clock.
-     */
-    private final SmallClockPosition mClockPosition;
-
-    /**
-     * Renders preview from clock view.
-     */
-    private final ViewPreviewer mRenderer = new ViewPreviewer();
-
-    /**
-     * Custom clock shown on AOD screen and behind stack scroller on lock.
-     */
-    private ClockLayout mView;
-    private ImageClock mAnalogClock;
-
-    /**
-     * Small clock shown on lock screen above stack scroller.
-     */
-    private View mLockClockContainer;
-    private TextClock mLockClock;
-
-    /**
-     * Helper to extract colors from wallpaper palette for clock face.
-     */
-    private final ClockPalette mPalette = new ClockPalette();
-
-    /**
-     * Create a BubbleClockController instance.
-     *
-     * @param res Resources contains title and thumbnail.
-     * @param inflater Inflater used to inflate custom clock views.
-     * @param colorExtractor Extracts accent color from wallpaper.
-     */
-    public BubbleClockController(Resources res, LayoutInflater inflater,
-            SysuiColorExtractor colorExtractor) {
-        mResources = res;
-        mLayoutInflater = inflater;
-        mColorExtractor = colorExtractor;
-        mClockPosition = new SmallClockPosition(inflater.getContext());
-    }
-
-    private void createViews() {
-        mView = (ClockLayout) mLayoutInflater.inflate(R.layout.bubble_clock, null);
-        mAnalogClock = (ImageClock) mView.findViewById(R.id.analog_clock);
-
-        mLockClockContainer = mLayoutInflater.inflate(R.layout.digital_clock, null);
-        mLockClock = (TextClock) mLockClockContainer.findViewById(R.id.lock_screen_clock);
-    }
-
-    @Override
-    public void onDestroyView() {
-        mView = null;
-        mAnalogClock = null;
-        mLockClockContainer = null;
-        mLockClock = null;
-    }
-
-    @Override
-    public String getName() {
-        return "bubble";
-    }
-
-    @Override
-    public String getTitle() {
-        return mResources.getString(R.string.clock_title_bubble);
-    }
-
-    @Override
-    public Bitmap getThumbnail() {
-        return BitmapFactory.decodeResource(mResources, R.drawable.bubble_thumbnail);
-    }
-
-    @Override
-    public Bitmap getPreview(int width, int height) {
-
-        // Use the big clock view for the preview
-        View view = getBigClockView();
-
-        // Initialize state of plugin before generating preview.
-        setDarkAmount(1f);
-        setTextColor(Color.WHITE);
-        ColorExtractor.GradientColors colors = mColorExtractor.getColors(
-                WallpaperManager.FLAG_LOCK);
-        setColorPalette(colors.supportsDarkText(), colors.getColorPalette());
-        onTimeTick();
-
-        return mRenderer.createPreview(view, width, height);
-    }
-
-    @Override
-    public View getView() {
-        if (mLockClockContainer == null) {
-            createViews();
-        }
-        return mLockClockContainer;
-    }
-
-    @Override
-    public View getBigClockView() {
-        if (mView == null) {
-            createViews();
-        }
-        return mView;
-    }
-
-    @Override
-    public int getPreferredY(int totalHeight) {
-        return mClockPosition.getPreferredY();
-    }
-
-    @Override
-    public void setStyle(Style style) {}
-
-    @Override
-    public void setTextColor(int color) {
-        updateColor();
-    }
-
-    @Override
-    public void setColorPalette(boolean supportsDarkText, int[] colorPalette) {
-        mPalette.setColorPalette(supportsDarkText, colorPalette);
-        updateColor();
-    }
-
-    private void updateColor() {
-        final int primary = mPalette.getPrimaryColor();
-        final int secondary = mPalette.getSecondaryColor();
-        mLockClock.setTextColor(secondary);
-        mAnalogClock.setClockColors(primary, secondary);
-    }
-
-    @Override
-    public void setDarkAmount(float darkAmount) {
-        mPalette.setDarkAmount(darkAmount);
-        mClockPosition.setDarkAmount(darkAmount);
-        mView.setDarkAmount(darkAmount);
-    }
-
-    @Override
-    public void onTimeTick() {
-        mAnalogClock.onTimeChanged();
-        mView.onTimeChanged();
-        mLockClock.refreshTime();
-    }
-
-    @Override
-    public void onTimeZoneChanged(TimeZone timeZone) {
-        mAnalogClock.onTimeZoneChanged(timeZone);
-    }
-
-    @Override
-    public boolean shouldShowStatusArea() {
-        return true;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockInfo.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfo.java
deleted file mode 100644
index 0210e08..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/ClockInfo.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import android.graphics.Bitmap;
-
-import java.util.function.Supplier;
-
-/**
- * Metadata about an available clock face.
- */
-final class ClockInfo {
-
-    private final String mName;
-    private final Supplier<String> mTitle;
-    private final String mId;
-    private final Supplier<Bitmap> mThumbnail;
-    private final Supplier<Bitmap> mPreview;
-
-    private ClockInfo(String name, Supplier<String> title, String id,
-            Supplier<Bitmap> thumbnail, Supplier<Bitmap> preview) {
-        mName = name;
-        mTitle = title;
-        mId = id;
-        mThumbnail = thumbnail;
-        mPreview = preview;
-    }
-
-    /**
-     * Gets the non-internationalized name for the clock face.
-     */
-    String getName() {
-        return mName;
-    }
-
-    /**
-     * Gets the name (title) of the clock face to be shown in the picker app.
-     */
-    String getTitle() {
-        return mTitle.get();
-    }
-
-    /**
-     * Gets the ID of the clock face, used by the picker to set the current selection.
-     */
-    String getId() {
-        return mId;
-    }
-
-    /**
-     * Gets a thumbnail image of the clock.
-     */
-    Bitmap getThumbnail() {
-        return mThumbnail.get();
-    }
-
-    /**
-     * Gets a potentially realistic preview image of the clock face.
-     */
-    Bitmap getPreview() {
-        return mPreview.get();
-    }
-
-    static Builder builder() {
-        return new Builder();
-    }
-
-    static class Builder {
-        private String mName;
-        private Supplier<String> mTitle;
-        private String mId;
-        private Supplier<Bitmap> mThumbnail;
-        private Supplier<Bitmap> mPreview;
-
-        public ClockInfo build() {
-            return new ClockInfo(mName, mTitle, mId, mThumbnail, mPreview);
-        }
-
-        public Builder setName(String name) {
-            mName = name;
-            return this;
-        }
-
-        public Builder setTitle(Supplier<String> title) {
-            mTitle = title;
-            return this;
-        }
-
-        public Builder setId(String id) {
-            mId = id;
-            return this;
-        }
-
-        public Builder setThumbnail(Supplier<Bitmap> thumbnail) {
-            mThumbnail = thumbnail;
-            return this;
-        }
-
-        public Builder setPreview(Supplier<Bitmap> preview) {
-            mPreview = preview;
-            return this;
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java
deleted file mode 100644
index 72a44bd..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java
+++ /dev/null
@@ -1,38 +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.keyguard.clock;
-
-import java.util.List;
-
-import dagger.Module;
-import dagger.Provides;
-
-/**
- * Dagger Module for clock package.
- *
- * @deprecated Migrate to ClockRegistry
- */
-@Module
-@Deprecated
-public abstract class ClockInfoModule {
-
-    /** */
-    @Provides
-    public static List<ClockInfo> provideClockInfoList(ClockManager clockManager) {
-        return clockManager.getClockInfos();
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockLayout.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockLayout.java
deleted file mode 100644
index d44d89e..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/ClockLayout.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.util.AttributeSet;
-import android.util.MathUtils;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import com.android.systemui.R;
-
-/**
- * Positions clock faces (analog, digital, typographic) and handles pixel shifting
- * to prevent screen burn-in.
- */
-public class ClockLayout extends FrameLayout {
-
-    private static final int ANALOG_CLOCK_SHIFT_FACTOR = 3;
-    /**
-     * Clock face views.
-     */
-    private View mAnalogClock;
-
-    /**
-     * Pixel shifting amplitudes used to prevent screen burn-in.
-     */
-    private int mBurnInPreventionOffsetX;
-    private int mBurnInPreventionOffsetY;
-
-    private float mDarkAmount;
-
-    public ClockLayout(Context context) {
-        this(context, null);
-    }
-
-    public ClockLayout(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public ClockLayout(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mAnalogClock = findViewById(R.id.analog_clock);
-
-        // Get pixel shifting X, Y amplitudes from resources.
-        Resources resources = getResources();
-        mBurnInPreventionOffsetX = resources.getDimensionPixelSize(
-            R.dimen.burn_in_prevention_offset_x);
-        mBurnInPreventionOffsetY = resources.getDimensionPixelSize(
-            R.dimen.burn_in_prevention_offset_y);
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        super.onLayout(changed, left, top, right, bottom);
-        positionChildren();
-    }
-
-    void onTimeChanged() {
-        positionChildren();
-    }
-
-    /**
-     * See {@link com.android.systemui.plugins.ClockPlugin#setDarkAmount(float)}.
-     */
-    void setDarkAmount(float darkAmount) {
-        mDarkAmount = darkAmount;
-        positionChildren();
-    }
-
-    private void positionChildren() {
-        final float offsetX = MathUtils.lerp(0f,
-                getBurnInOffset(mBurnInPreventionOffsetX * 2, true) - mBurnInPreventionOffsetX,
-                mDarkAmount);
-        final float offsetY = MathUtils.lerp(0f,
-                getBurnInOffset(mBurnInPreventionOffsetY * 2, false)
-                        - 0.5f * mBurnInPreventionOffsetY,
-                mDarkAmount);
-
-        // Put the analog clock in the middle of the screen.
-        if (mAnalogClock != null) {
-            mAnalogClock.setX(Math.max(0f, 0.5f * (getWidth() - mAnalogClock.getWidth()))
-                    + ANALOG_CLOCK_SHIFT_FACTOR * offsetX);
-            mAnalogClock.setY(Math.max(0f, 0.5f * (getHeight() - mAnalogClock.getHeight()))
-                    + ANALOG_CLOCK_SHIFT_FACTOR * offsetY);
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java
deleted file mode 100644
index 122c521..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java
+++ /dev/null
@@ -1,392 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import android.annotation.Nullable;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.res.Resources;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.provider.Settings;
-import android.util.ArrayMap;
-import android.util.DisplayMetrics;
-import android.view.LayoutInflater;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dock.DockManager;
-import com.android.systemui.dock.DockManager.DockEventListener;
-import com.android.systemui.plugins.ClockPlugin;
-import com.android.systemui.plugins.PluginListener;
-import com.android.systemui.plugins.PluginManager;
-import com.android.systemui.settings.UserTracker;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.Executor;
-import java.util.function.Supplier;
-
-import javax.inject.Inject;
-
-/**
- * Manages custom clock faces for AOD and lock screen.
- *
- * @deprecated Migrate to ClockRegistry
- */
-@SysUISingleton
-@Deprecated
-public final class ClockManager {
-
-    private static final String TAG = "ClockOptsProvider";
-
-    private final AvailableClocks mPreviewClocks;
-    private final List<Supplier<ClockPlugin>> mBuiltinClocks = new ArrayList<>();
-
-    private final Context mContext;
-    private final ContentResolver mContentResolver;
-    private final SettingsWrapper mSettingsWrapper;
-    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
-    private final UserTracker mUserTracker;
-    private final Executor mMainExecutor;
-
-    /**
-     * Observe settings changes to know when to switch the clock face.
-     */
-    private final ContentObserver mContentObserver =
-            new ContentObserver(mMainHandler) {
-                @Override
-                public void onChange(boolean selfChange, Collection<Uri> uris,
-                        int flags, int userId) {
-                    if (Objects.equals(userId,
-                            mUserTracker.getUserId())) {
-                        reload();
-                    }
-                }
-            };
-
-    /**
-     * Observe user changes and react by potentially loading the custom clock for the new user.
-     */
-    private final UserTracker.Callback mUserChangedCallback =
-            new UserTracker.Callback() {
-                @Override
-                public void onUserChanged(int newUser, @NonNull Context userContext) {
-                    reload();
-                }
-            };
-
-    private final PluginManager mPluginManager;
-    @Nullable private final DockManager mDockManager;
-
-    /**
-     * Observe changes to dock state to know when to switch the clock face.
-     */
-    private final DockEventListener mDockEventListener =
-            new DockEventListener() {
-                @Override
-                public void onEvent(int event) {
-                    mIsDocked = (event == DockManager.STATE_DOCKED
-                            || event == DockManager.STATE_DOCKED_HIDE);
-                    reload();
-                }
-            };
-
-    /**
-     * When docked, the DOCKED_CLOCK_FACE setting will be checked for the custom clock face
-     * to show.
-     */
-    private boolean mIsDocked;
-
-    /**
-     * Listeners for onClockChanged event.
-     *
-     * Each listener must receive a separate clock plugin instance. Otherwise, there could be
-     * problems like attempting to attach a view that already has a parent. To deal with this issue,
-     * each listener is associated with a collection of available clocks. When onClockChanged is
-     * fired the current clock plugin instance is retrieved from that listeners available clocks.
-     */
-    private final Map<ClockChangedListener, AvailableClocks> mListeners = new ArrayMap<>();
-
-    private final int mWidth;
-    private final int mHeight;
-
-    @Inject
-    public ClockManager(Context context, LayoutInflater layoutInflater,
-            PluginManager pluginManager, SysuiColorExtractor colorExtractor,
-            @Nullable DockManager dockManager, UserTracker userTracker,
-            @Main Executor mainExecutor) {
-        this(context, layoutInflater, pluginManager, colorExtractor,
-                context.getContentResolver(), userTracker, mainExecutor,
-                new SettingsWrapper(context.getContentResolver()), dockManager);
-    }
-
-    @VisibleForTesting
-    ClockManager(Context context, LayoutInflater layoutInflater,
-            PluginManager pluginManager, SysuiColorExtractor colorExtractor,
-            ContentResolver contentResolver, UserTracker userTracker, Executor mainExecutor,
-            SettingsWrapper settingsWrapper, DockManager dockManager) {
-        mContext = context;
-        mPluginManager = pluginManager;
-        mContentResolver = contentResolver;
-        mSettingsWrapper = settingsWrapper;
-        mUserTracker = userTracker;
-        mMainExecutor = mainExecutor;
-        mDockManager = dockManager;
-        mPreviewClocks = new AvailableClocks();
-
-        Resources res = context.getResources();
-
-        addBuiltinClock(() -> new DefaultClockController(res, layoutInflater, colorExtractor));
-
-        // Store the size of the display for generation of clock preview.
-        DisplayMetrics dm = res.getDisplayMetrics();
-        mWidth = dm.widthPixels;
-        mHeight = dm.heightPixels;
-    }
-
-    /**
-     * Add listener to be notified when clock implementation should change.
-     */
-    public void addOnClockChangedListener(ClockChangedListener listener) {
-        if (mListeners.isEmpty()) {
-            register();
-        }
-        AvailableClocks availableClocks = new AvailableClocks();
-        for (int i = 0; i < mBuiltinClocks.size(); i++) {
-            availableClocks.addClockPlugin(mBuiltinClocks.get(i).get());
-        }
-        mListeners.put(listener, availableClocks);
-        mPluginManager.addPluginListener(availableClocks, ClockPlugin.class, true);
-        reload();
-    }
-
-    /**
-     * Remove listener added with {@link addOnClockChangedListener}.
-     */
-    public void removeOnClockChangedListener(ClockChangedListener listener) {
-        AvailableClocks availableClocks = mListeners.remove(listener);
-        mPluginManager.removePluginListener(availableClocks);
-        if (mListeners.isEmpty()) {
-            unregister();
-        }
-    }
-
-    /**
-     * Get information about available clock faces.
-     */
-    List<ClockInfo> getClockInfos() {
-        return mPreviewClocks.getInfo();
-    }
-
-    /**
-     * Get the current clock.
-     * @return current custom clock or null for default.
-     */
-    @Nullable
-    ClockPlugin getCurrentClock() {
-        return mPreviewClocks.getCurrentClock();
-    }
-
-    @VisibleForTesting
-    boolean isDocked() {
-        return mIsDocked;
-    }
-
-    @VisibleForTesting
-    ContentObserver getContentObserver() {
-        return mContentObserver;
-    }
-
-    @VisibleForTesting
-    void addBuiltinClock(Supplier<ClockPlugin> pluginSupplier) {
-        ClockPlugin plugin = pluginSupplier.get();
-        mPreviewClocks.addClockPlugin(plugin);
-        mBuiltinClocks.add(pluginSupplier);
-    }
-
-    private void register() {
-        mPluginManager.addPluginListener(mPreviewClocks, ClockPlugin.class, true);
-        mContentResolver.registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
-                false, mContentObserver, UserHandle.USER_ALL);
-        mContentResolver.registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.DOCKED_CLOCK_FACE),
-                false, mContentObserver, UserHandle.USER_ALL);
-        mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
-        if (mDockManager != null) {
-            mDockManager.addListener(mDockEventListener);
-        }
-    }
-
-    private void unregister() {
-        mPluginManager.removePluginListener(mPreviewClocks);
-        mContentResolver.unregisterContentObserver(mContentObserver);
-        mUserTracker.removeCallback(mUserChangedCallback);
-        if (mDockManager != null) {
-            mDockManager.removeListener(mDockEventListener);
-        }
-    }
-
-    private void reload() {
-        mPreviewClocks.reloadCurrentClock();
-        mListeners.forEach((listener, clocks) -> {
-            clocks.reloadCurrentClock();
-            final ClockPlugin clock = clocks.getCurrentClock();
-            if (Looper.myLooper() == Looper.getMainLooper()) {
-                listener.onClockChanged(clock instanceof DefaultClockController ? null : clock);
-            } else {
-                mMainHandler.post(() -> listener.onClockChanged(
-                        clock instanceof DefaultClockController ? null : clock));
-            }
-        });
-    }
-
-    /**
-     * Listener for events that should cause the custom clock face to change.
-     */
-    public interface ClockChangedListener {
-        /**
-         * Called when custom clock should change.
-         *
-         * @param clock Custom clock face to use. A null value indicates the default clock face.
-         */
-        void onClockChanged(ClockPlugin clock);
-    }
-
-    /**
-     * Collection of available clocks.
-     */
-    private final class AvailableClocks implements PluginListener<ClockPlugin> {
-
-        /**
-         * Map from expected value stored in settings to plugin for custom clock face.
-         */
-        private final Map<String, ClockPlugin> mClocks = new ArrayMap<>();
-
-        /**
-         * Metadata about available clocks, such as name and preview images.
-         */
-        private final List<ClockInfo> mClockInfo = new ArrayList<>();
-
-        /**
-         * Active ClockPlugin.
-         */
-        @Nullable private ClockPlugin mCurrentClock;
-
-        @Override
-        public void onPluginConnected(ClockPlugin plugin, Context pluginContext) {
-            addClockPlugin(plugin);
-            reloadIfNeeded(plugin);
-        }
-
-        @Override
-        public void onPluginDisconnected(ClockPlugin plugin) {
-            removeClockPlugin(plugin);
-            reloadIfNeeded(plugin);
-        }
-
-        /**
-         * Get the current clock.
-         * @return current custom clock or null for default.
-         */
-        @Nullable
-        ClockPlugin getCurrentClock() {
-            return mCurrentClock;
-        }
-
-        /**
-         * Get information about available clock faces.
-         */
-        List<ClockInfo> getInfo() {
-            return mClockInfo;
-        }
-
-        /**
-         * Adds a clock plugin to the collection of available clocks.
-         *
-         * @param plugin The plugin to add.
-         */
-        void addClockPlugin(ClockPlugin plugin) {
-            final String id = plugin.getClass().getName();
-            mClocks.put(plugin.getClass().getName(), plugin);
-            mClockInfo.add(ClockInfo.builder()
-                    .setName(plugin.getName())
-                    .setTitle(plugin::getTitle)
-                    .setId(id)
-                    .setThumbnail(plugin::getThumbnail)
-                    .setPreview(() -> plugin.getPreview(mWidth, mHeight))
-                    .build());
-        }
-
-        private void removeClockPlugin(ClockPlugin plugin) {
-            final String id = plugin.getClass().getName();
-            mClocks.remove(id);
-            for (int i = 0; i < mClockInfo.size(); i++) {
-                if (id.equals(mClockInfo.get(i).getId())) {
-                    mClockInfo.remove(i);
-                    break;
-                }
-            }
-        }
-
-        private void reloadIfNeeded(ClockPlugin plugin) {
-            final boolean wasCurrentClock = plugin == mCurrentClock;
-            reloadCurrentClock();
-            final boolean isCurrentClock = plugin == mCurrentClock;
-            if (wasCurrentClock || isCurrentClock) {
-                ClockManager.this.reload();
-            }
-        }
-
-        /**
-         * Update the current clock.
-         */
-        void reloadCurrentClock() {
-            mCurrentClock = getClockPlugin();
-        }
-
-        private ClockPlugin getClockPlugin() {
-            ClockPlugin plugin = null;
-            if (ClockManager.this.isDocked()) {
-                final String name = mSettingsWrapper.getDockedClockFace(
-                        mUserTracker.getUserId());
-                if (name != null) {
-                    plugin = mClocks.get(name);
-                    if (plugin != null) {
-                        return plugin;
-                    }
-                }
-            }
-            final String name = mSettingsWrapper.getLockScreenCustomClockFace(
-                    mUserTracker.getUserId());
-            if (name != null) {
-                plugin = mClocks.get(name);
-            }
-            return plugin;
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockOptionsProvider.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockOptionsProvider.java
deleted file mode 100644
index b6413cb..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/ClockOptionsProvider.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.ParcelFileDescriptor;
-import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.io.FileNotFoundException;
-import java.util.List;
-
-import javax.inject.Inject;
-import javax.inject.Provider;
-
-/**
- * Exposes custom clock face options and provides realistic preview images.
- *
- * APIs:
- *
- *   /list_options: List the available clock faces, which has the following columns
- *     name: name of the clock face
- *     title: title of the clock face
- *     id: value used to set the clock face
- *     thumbnail: uri of the thumbnail image, should be /thumbnail/{name}
- *     preview: uri of the preview image, should be /preview/{name}
- *
- *   /thumbnail/{id}: Opens a file stream for the thumbnail image for clock face {id}.
- *
- *   /preview/{id}: Opens a file stream for the preview image for clock face {id}.
- */
-public final class ClockOptionsProvider extends ContentProvider {
-
-    private static final String TAG = "ClockOptionsProvider";
-    private static final String KEY_LIST_OPTIONS = "/list_options";
-    private static final String KEY_PREVIEW = "preview";
-    private static final String KEY_THUMBNAIL = "thumbnail";
-    private static final String COLUMN_NAME = "name";
-    private static final String COLUMN_TITLE = "title";
-    private static final String COLUMN_ID = "id";
-    private static final String COLUMN_THUMBNAIL = "thumbnail";
-    private static final String COLUMN_PREVIEW = "preview";
-    private static final String MIME_TYPE_PNG = "image/png";
-    private static final String CONTENT_SCHEME = "content";
-    private static final String AUTHORITY = "com.android.keyguard.clock";
-
-    @Inject
-    public Provider<List<ClockInfo>> mClockInfosProvider;
-
-    @VisibleForTesting
-    ClockOptionsProvider(Provider<List<ClockInfo>> clockInfosProvider) {
-        mClockInfosProvider = clockInfosProvider;
-    }
-
-    @Override
-    public boolean onCreate() {
-        return true;
-    }
-
-    @Override
-    public String getType(Uri uri) {
-        List<String> segments = uri.getPathSegments();
-        if (segments.size() > 0 && (KEY_PREVIEW.equals(segments.get(0))
-                || KEY_THUMBNAIL.equals(segments.get(0)))) {
-            return MIME_TYPE_PNG;
-        }
-        return "vnd.android.cursor.dir/clock_faces";
-    }
-
-    @Override
-    public Cursor query(Uri uri, String[] projection, String selection,
-            String[] selectionArgs, String sortOrder) {
-        if (!KEY_LIST_OPTIONS.equals(uri.getPath())) {
-            return null;
-        }
-        MatrixCursor cursor = new MatrixCursor(new String[] {
-                COLUMN_NAME, COLUMN_TITLE, COLUMN_ID, COLUMN_THUMBNAIL, COLUMN_PREVIEW});
-        List<ClockInfo> clocks = mClockInfosProvider.get();
-        for (int i = 0; i < clocks.size(); i++) {
-            ClockInfo clock = clocks.get(i);
-            cursor.newRow()
-                    .add(COLUMN_NAME, clock.getName())
-                    .add(COLUMN_TITLE, clock.getTitle())
-                    .add(COLUMN_ID, clock.getId())
-                    .add(COLUMN_THUMBNAIL, createThumbnailUri(clock))
-                    .add(COLUMN_PREVIEW, createPreviewUri(clock));
-        }
-        return cursor;
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues initialValues) {
-        return null;
-    }
-
-    @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        return 0;
-    }
-
-    @Override
-    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
-        return 0;
-    }
-
-    @Override
-    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
-        List<String> segments = uri.getPathSegments();
-        if (segments.size() != 2 || !(KEY_PREVIEW.equals(segments.get(0))
-                || KEY_THUMBNAIL.equals(segments.get(0)))) {
-            throw new FileNotFoundException("Invalid preview url");
-        }
-        String id = segments.get(1);
-        if (TextUtils.isEmpty(id)) {
-            throw new FileNotFoundException("Invalid preview url, missing id");
-        }
-        ClockInfo clock = null;
-        List<ClockInfo> clocks = mClockInfosProvider.get();
-        for (int i = 0; i < clocks.size(); i++) {
-            if (id.equals(clocks.get(i).getId())) {
-                clock = clocks.get(i);
-                break;
-            }
-        }
-        if (clock == null) {
-            throw new FileNotFoundException("Invalid preview url, id not found");
-        }
-        return openPipeHelper(uri, MIME_TYPE_PNG, null, KEY_PREVIEW.equals(segments.get(0))
-                ? clock.getPreview() : clock.getThumbnail(), new MyWriter());
-    }
-
-    private Uri createThumbnailUri(ClockInfo clock) {
-        return new Uri.Builder()
-                .scheme(CONTENT_SCHEME)
-                .authority(AUTHORITY)
-                .appendPath(KEY_THUMBNAIL)
-                .appendPath(clock.getId())
-                .build();
-    }
-
-    private Uri createPreviewUri(ClockInfo clock) {
-        return new Uri.Builder()
-                .scheme(CONTENT_SCHEME)
-                .authority(AUTHORITY)
-                .appendPath(KEY_PREVIEW)
-                .appendPath(clock.getId())
-                .build();
-    }
-
-    private static class MyWriter implements ContentProvider.PipeDataWriter<Bitmap> {
-        @Override
-        public void writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType,
-                Bundle opts, Bitmap bitmap) {
-            try (AutoCloseOutputStream os = new AutoCloseOutputStream(output)) {
-                bitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
-            } catch (Exception e) {
-                Log.w(TAG, "fail to write to pipe", e);
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockPalette.kt b/packages/SystemUI/src/com/android/keyguard/clock/ClockPalette.kt
deleted file mode 100644
index 5c5493a..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/ClockPalette.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock
-
-import android.graphics.Color
-import android.util.MathUtils
-
-private const val PRIMARY_INDEX = 5
-private const val SECONDARY_DARK_INDEX = 8
-private const val SECONDARY_LIGHT_INDEX = 2
-
-/**
- * A helper class to extract colors from a clock face.
- */
-class ClockPalette {
-
-    private var darkAmount: Float = 0f
-    private var accentPrimary: Int = Color.WHITE
-    private var accentSecondaryLight: Int = Color.WHITE
-    private var accentSecondaryDark: Int = Color.BLACK
-    private val lightHSV: FloatArray = FloatArray(3)
-    private val darkHSV: FloatArray = FloatArray(3)
-    private val hsv: FloatArray = FloatArray(3)
-
-    /** Returns a color from the palette as an RGB packed int. */
-    fun getPrimaryColor(): Int {
-        return accentPrimary
-    }
-
-    /** Returns either a light or dark color from the palette as an RGB packed int. */
-    fun getSecondaryColor(): Int {
-        Color.colorToHSV(accentSecondaryLight, lightHSV)
-        Color.colorToHSV(accentSecondaryDark, darkHSV)
-        for (i in 0..2) {
-            hsv[i] = MathUtils.lerp(darkHSV[i], lightHSV[i], darkAmount)
-        }
-        return Color.HSVToColor(hsv)
-    }
-
-    /** See {@link ClockPlugin#setColorPalette}. */
-    fun setColorPalette(supportsDarkText: Boolean, colorPalette: IntArray?) {
-        if (colorPalette == null || colorPalette.isEmpty()) {
-            accentPrimary = Color.WHITE
-            accentSecondaryLight = Color.WHITE
-            accentSecondaryDark = if (supportsDarkText) Color.BLACK else Color.WHITE
-            return
-        }
-        val length = colorPalette.size
-        accentPrimary = colorPalette[Math.max(0, length - PRIMARY_INDEX)]
-        accentSecondaryLight = colorPalette[Math.max(0, length - SECONDARY_LIGHT_INDEX)]
-        accentSecondaryDark = colorPalette[Math.max(0,
-                length - if (supportsDarkText) SECONDARY_DARK_INDEX else SECONDARY_LIGHT_INDEX)]
-    }
-
-    /** See {@link ClockPlugin#setDarkAmount}. */
-    fun setDarkAmount(darkAmount: Float) {
-        this.darkAmount = darkAmount
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/CrossFadeDarkController.java b/packages/SystemUI/src/com/android/keyguard/clock/CrossFadeDarkController.java
deleted file mode 100644
index 3c3f475..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/CrossFadeDarkController.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import android.view.View;
-
-/**
- * Controls transition to dark state by cross fading between views.
- */
-final class CrossFadeDarkController {
-
-    private final View mFadeInView;
-    private final View mFadeOutView;
-
-    /**
-     * Creates a new controller that fades between views.
-     *
-     * @param fadeInView View to fade in when transitioning to AOD.
-     * @param fadeOutView View to fade out when transitioning to AOD.
-     */
-    CrossFadeDarkController(View fadeInView, View fadeOutView) {
-        mFadeInView = fadeInView;
-        mFadeOutView = fadeOutView;
-    }
-
-    /**
-     * Sets the amount the system has transitioned to the dark state.
-     *
-     * @param darkAmount Amount of transition to dark state: 1f for AOD and 0f for lock screen.
-     */
-    void setDarkAmount(float darkAmount) {
-        mFadeInView.setAlpha(Math.max(0f, 2f * darkAmount - 1f));
-        if (darkAmount == 0f) {
-            mFadeInView.setVisibility(View.GONE);
-        } else {
-            if (mFadeInView.getVisibility() == View.GONE) {
-                mFadeInView.setVisibility(View.VISIBLE);
-            }
-        }
-        mFadeOutView.setAlpha(Math.max(0f, 1f - 2f * darkAmount));
-        if (darkAmount == 1f) {
-            mFadeOutView.setVisibility(View.GONE);
-        } else {
-            if (mFadeOutView.getVisibility() == View.GONE) {
-                mFadeOutView.setVisibility(View.VISIBLE);
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/DefaultClockController.java b/packages/SystemUI/src/com/android/keyguard/clock/DefaultClockController.java
deleted file mode 100644
index c81935a..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/DefaultClockController.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import android.app.WallpaperManager;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
-import android.graphics.Paint.Style;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.TextView;
-
-import com.android.internal.colorextraction.ColorExtractor;
-import com.android.systemui.R;
-import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.plugins.ClockPlugin;
-
-import java.util.TimeZone;
-
-/**
- * Plugin for the default clock face used only to provide a preview.
- */
-public class DefaultClockController implements ClockPlugin {
-
-    /**
-     * Resources used to get title and thumbnail.
-     */
-    private final Resources mResources;
-
-    /**
-     * LayoutInflater used to inflate custom clock views.
-     */
-    private final LayoutInflater mLayoutInflater;
-
-    /**
-     * Extracts accent color from wallpaper.
-     */
-    private final SysuiColorExtractor mColorExtractor;
-
-    /**
-     * Renders preview from clock view.
-     */
-    private final ViewPreviewer mRenderer = new ViewPreviewer();
-
-    /**
-     * Root view of preview.
-     */
-    private View mView;
-
-    /**
-     * Text clock in preview view hierarchy.
-     */
-    private TextView mTextTime;
-
-    /**
-     * Date showing below time in preview view hierarchy.
-     */
-    private TextView mTextDate;
-
-    /**
-     * Create a DefaultClockController instance.
-     *
-     * @param res Resources contains title and thumbnail.
-     * @param inflater Inflater used to inflate custom clock views.
-     * @param colorExtractor Extracts accent color from wallpaper.
-     */
-    public DefaultClockController(Resources res, LayoutInflater inflater,
-            SysuiColorExtractor colorExtractor) {
-        mResources = res;
-        mLayoutInflater = inflater;
-        mColorExtractor = colorExtractor;
-    }
-
-    private void createViews() {
-        mView = mLayoutInflater.inflate(R.layout.default_clock_preview, null);
-        mTextTime = mView.findViewById(R.id.time);
-        mTextDate = mView.findViewById(R.id.date);
-    }
-
-    @Override
-    public void onDestroyView() {
-        mView = null;
-        mTextTime = null;
-        mTextDate = null;
-    }
-
-    @Override
-    public String getName() {
-        return "default";
-    }
-
-    @Override
-    public String getTitle() {
-        return mResources.getString(R.string.clock_title_default);
-    }
-
-    @Override
-    public Bitmap getThumbnail() {
-        return BitmapFactory.decodeResource(mResources, R.drawable.default_thumbnail);
-    }
-
-    @Override
-    public Bitmap getPreview(int width, int height) {
-
-        // Use the big clock view for the preview
-        View view = getBigClockView();
-
-        // Initialize state of plugin before generating preview.
-        setDarkAmount(1f);
-        setTextColor(Color.WHITE);
-        ColorExtractor.GradientColors colors = mColorExtractor.getColors(
-                WallpaperManager.FLAG_LOCK);
-        setColorPalette(colors.supportsDarkText(), colors.getColorPalette());
-        onTimeTick();
-
-        return mRenderer.createPreview(view, width, height);
-    }
-
-    @Override
-    public View getView() {
-        return null;
-    }
-
-    @Override
-    public View getBigClockView() {
-        if (mView == null) {
-            createViews();
-        }
-        return mView;
-    }
-
-    @Override
-    public int getPreferredY(int totalHeight) {
-        return totalHeight / 2;
-    }
-
-    @Override
-    public void setStyle(Style style) {}
-
-    @Override
-    public void setTextColor(int color) {
-        mTextTime.setTextColor(color);
-        mTextDate.setTextColor(color);
-    }
-
-    @Override
-    public void setColorPalette(boolean supportsDarkText, int[] colorPalette) {}
-
-    @Override
-    public void onTimeTick() {
-    }
-
-    @Override
-    public void setDarkAmount(float darkAmount) {}
-
-    @Override
-    public void onTimeZoneChanged(TimeZone timeZone) {}
-
-    @Override
-    public boolean shouldShowStatusArea() {
-        return true;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ImageClock.java b/packages/SystemUI/src/com/android/keyguard/clock/ImageClock.java
deleted file mode 100644
index 34c041b..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/ImageClock.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import android.content.Context;
-import android.text.format.DateFormat;
-import android.util.AttributeSet;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-
-import com.android.systemui.R;
-
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.TimeZone;
-
-/**
- * Clock composed of two images that rotate with the time.
- *
- * The images are the clock hands. ImageClock expects two child ImageViews
- * with ids hour_hand and minute_hand.
- */
-public class ImageClock extends FrameLayout {
-
-    private ImageView mHourHand;
-    private ImageView mMinuteHand;
-    private final Calendar mTime = Calendar.getInstance(TimeZone.getDefault());
-    private String mDescFormat;
-    private TimeZone mTimeZone;
-
-    public ImageClock(Context context) {
-        this(context, null);
-    }
-
-    public ImageClock(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public ImageClock(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        mDescFormat = ((SimpleDateFormat) DateFormat.getTimeFormat(context)).toLocalizedPattern();
-    }
-
-    /**
-     * Call when the time changes to update the rotation of the clock hands.
-     */
-    public void onTimeChanged() {
-        mTime.setTimeInMillis(System.currentTimeMillis());
-        final float hourAngle = mTime.get(Calendar.HOUR) * 30f + mTime.get(Calendar.MINUTE) * 0.5f;
-        mHourHand.setRotation(hourAngle);
-        final float minuteAngle = mTime.get(Calendar.MINUTE) * 6f;
-        mMinuteHand.setRotation(minuteAngle);
-        setContentDescription(DateFormat.format(mDescFormat, mTime));
-        invalidate();
-    }
-
-    /**
-     * Call when the time zone has changed to update clock hands.
-     *
-     * @param timeZone The updated time zone that will be used.
-     */
-    public void onTimeZoneChanged(TimeZone timeZone) {
-        mTimeZone = timeZone;
-        mTime.setTimeZone(timeZone);
-    }
-
-    /**
-     * Sets the colors to use on the clock face.
-     * @param dark Darker color obtained from color palette.
-     * @param light Lighter color obtained from color palette.
-     */
-    public void setClockColors(int dark, int light) {
-        mHourHand.setColorFilter(dark);
-        mMinuteHand.setColorFilter(light);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mHourHand = findViewById(R.id.hour_hand);
-        mMinuteHand = findViewById(R.id.minute_hand);
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        mTime.setTimeZone(mTimeZone != null ? mTimeZone : TimeZone.getDefault());
-        onTimeChanged();
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/SettingsWrapper.java b/packages/SystemUI/src/com/android/keyguard/clock/SettingsWrapper.java
deleted file mode 100644
index 096e943..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/SettingsWrapper.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import android.annotation.Nullable;
-import android.content.ContentResolver;
-import android.provider.Settings;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-/**
- * Wrapper around Settings used for testing.
- */
-public class SettingsWrapper {
-
-    private static final String TAG = "ClockFaceSettings";
-    private static final String CUSTOM_CLOCK_FACE = Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE;
-    private static final String DOCKED_CLOCK_FACE = Settings.Secure.DOCKED_CLOCK_FACE;
-    private static final String CLOCK_FIELD = "clock";
-
-    private final ContentResolver mContentResolver;
-    private final Migration mMigration;
-
-    SettingsWrapper(ContentResolver contentResolver) {
-        this(contentResolver, new Migrator(contentResolver));
-    }
-
-    @VisibleForTesting
-    SettingsWrapper(ContentResolver contentResolver, Migration migration) {
-        mContentResolver = contentResolver;
-        mMigration = migration;
-    }
-
-    /**
-     * Gets the value stored in settings for the custom clock face.
-     *
-     * @param userId ID of the user.
-     */
-    String getLockScreenCustomClockFace(int userId) {
-        return decode(
-                Settings.Secure.getStringForUser(mContentResolver, CUSTOM_CLOCK_FACE, userId),
-                userId);
-    }
-
-    /**
-     * Gets the value stored in settings for the clock face to use when docked.
-     *
-     * @param userId ID of the user.
-     */
-    String getDockedClockFace(int userId) {
-        return Settings.Secure.getStringForUser(mContentResolver, DOCKED_CLOCK_FACE, userId);
-    }
-
-    /**
-     * Decodes the string stored in settings, which should be formatted as JSON.
-     * @param value String stored in settings. If value is not JSON, then the settings is
-     *              overwritten with JSON containing the prior value.
-     * @return ID of the clock face to show on AOD and lock screen. If value is not JSON, the value
-     *         is returned.
-     */
-    @VisibleForTesting
-    String decode(@Nullable String value, int userId) {
-        if (value == null) {
-            return value;
-        }
-        JSONObject json;
-        try {
-            json = new JSONObject(value);
-        } catch (JSONException ex) {
-            Log.e(TAG, "Settings value is not valid JSON", ex);
-            // The settings value isn't JSON since it didn't parse so migrate the value to JSON.
-            // TODO(b/135674383): Remove this migration path in the following release.
-            mMigration.migrate(value, userId);
-            return value;
-        }
-        try {
-            return json.getString(CLOCK_FIELD);
-        } catch (JSONException ex) {
-            Log.e(TAG, "JSON object does not contain clock field.", ex);
-            return null;
-        }
-    }
-
-    interface Migration {
-        void migrate(String value, int userId);
-    }
-
-    /**
-     * Implementation of {@link Migration} that writes valid JSON back to Settings.
-     */
-    private static final class Migrator implements Migration {
-
-        private final ContentResolver mContentResolver;
-
-        Migrator(ContentResolver contentResolver) {
-            mContentResolver = contentResolver;
-        }
-
-        /**
-         * Migrate settings values that don't parse by converting to JSON format.
-         *
-         * Values in settings must be JSON to be backed up and restored. To help users maintain
-         * their current settings, convert existing values into the JSON format.
-         *
-         * TODO(b/135674383): Remove this migration code in the following release.
-         */
-        @Override
-        public void migrate(String value, int userId) {
-            try {
-                JSONObject json = new JSONObject();
-                json.put(CLOCK_FIELD, value);
-                Settings.Secure.putStringForUser(mContentResolver, CUSTOM_CLOCK_FACE,
-                        json.toString(),
-                        userId);
-            } catch (JSONException ex) {
-                Log.e(TAG, "Failed migrating settings value to JSON format", ex);
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/SmallClockPosition.java b/packages/SystemUI/src/com/android/keyguard/clock/SmallClockPosition.java
deleted file mode 100644
index 4e51b98..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/SmallClockPosition.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import android.content.Context;
-import android.util.MathUtils;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.policy.SystemBarUtils;
-import com.android.systemui.R;
-
-/**
- * Computes preferred position of clock by considering height of status bar and lock icon.
- */
-class SmallClockPosition {
-
-    /**
-     * Dimensions used to determine preferred clock position.
-     */
-    private final int mStatusBarHeight;
-    private final int mKeyguardLockPadding;
-    private final int mKeyguardLockHeight;
-    private final int mBurnInOffsetY;
-
-    /**
-     * Amount of transition between AOD and lock screen.
-     */
-    private float mDarkAmount;
-
-    SmallClockPosition(Context context) {
-        this(SystemBarUtils.getStatusBarHeight(context),
-                context.getResources().getDimensionPixelSize(R.dimen.keyguard_lock_padding),
-                context.getResources().getDimensionPixelSize(R.dimen.keyguard_lock_height),
-                context.getResources().getDimensionPixelSize(R.dimen.burn_in_prevention_offset_y)
-        );
-    }
-
-    @VisibleForTesting
-    SmallClockPosition(int statusBarHeight, int lockPadding, int lockHeight, int burnInY) {
-        mStatusBarHeight = statusBarHeight;
-        mKeyguardLockPadding = lockPadding;
-        mKeyguardLockHeight = lockHeight;
-        mBurnInOffsetY = burnInY;
-    }
-
-    /**
-     * See {@link ClockPlugin#setDarkAmount}.
-     */
-    void setDarkAmount(float darkAmount) {
-        mDarkAmount = darkAmount;
-    }
-
-    /**
-     * Gets the preferred Y position accounting for status bar and lock icon heights.
-     */
-    int getPreferredY() {
-        // On AOD, clock needs to appear below the status bar with enough room for pixel shifting
-        int aodY = mStatusBarHeight + mKeyguardLockHeight + 2 * mKeyguardLockPadding
-                + mBurnInOffsetY;
-        // On lock screen, clock needs to appear below the lock icon
-        int lockY =  mStatusBarHeight + mKeyguardLockHeight + 2 * mKeyguardLockPadding;
-        return (int) MathUtils.lerp(lockY, aodY, mDarkAmount);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ViewPreviewer.java b/packages/SystemUI/src/com/android/keyguard/clock/ViewPreviewer.java
deleted file mode 100644
index abd0dd2..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/ViewPreviewer.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import android.annotation.Nullable;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
-
-import java.util.concurrent.Callable;
-import java.util.concurrent.FutureTask;
-
-/**
- * Creates a preview image ({@link Bitmap}) of a {@link View} for a custom clock face.
- */
-final class ViewPreviewer {
-
-    private static final String TAG = "ViewPreviewer";
-
-    /**
-     * Handler used to run {@link View#draw(Canvas)} on the main thread.
-     */
-    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
-
-    /**
-     * Generate a realistic preview of a clock face.
-     *
-     * @param view view is used to generate preview image.
-     * @param width width of the preview image, should be the same as device width in pixels.
-     * @param height height of the preview image, should be the same as device height in pixels.
-     * @return bitmap of view.
-     */
-    @Nullable
-    Bitmap createPreview(View view, int width, int height) {
-        if (view == null) {
-            return null;
-        }
-        FutureTask<Bitmap> task = new FutureTask<>(new Callable<Bitmap>() {
-            @Override
-            public Bitmap call() {
-                Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
-
-                // Draw clock view hierarchy to canvas.
-                Canvas canvas = new Canvas(bitmap);
-                canvas.drawColor(Color.BLACK);
-                dispatchVisibilityAggregated(view, true);
-                view.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
-                        View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
-                view.layout(0, 0, width, height);
-                view.draw(canvas);
-
-                return bitmap;
-            }
-        });
-
-        if (Looper.myLooper() == Looper.getMainLooper()) {
-            task.run();
-        } else {
-            mMainHandler.post(task);
-        }
-
-        try {
-            return task.get();
-        } catch (Exception e) {
-            Log.e(TAG, "Error completing task", e);
-            return null;
-        }
-    }
-
-    private void dispatchVisibilityAggregated(View view, boolean isVisible) {
-        // Similar to View.dispatchVisibilityAggregated implementation.
-        final boolean thisVisible = view.getVisibility() == View.VISIBLE;
-        if (thisVisible || !isVisible) {
-            view.onVisibilityAggregated(isVisible);
-        }
-
-        if (view instanceof ViewGroup) {
-            isVisible = thisVisible && isVisible;
-            ViewGroup vg = (ViewGroup) view;
-            int count = vg.getChildCount();
-
-            for (int i = 0; i < count; i++) {
-                dispatchVisibilityAggregated(vg.getChildAt(i), isVisible);
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
index d2b10d6..83fc278 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -71,7 +71,8 @@
                 context.getString(R.string.lockscreen_clock_id_fallback),
                 logBuffer,
                 /* keepAllLoaded = */ false,
-                /* subTag = */ "System");
+                /* subTag = */ "System",
+                /* isTransitClockEnabled = */ featureFlags.isEnabled(Flags.TRANSIT_CLOCK));
         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 154b0ed..b57c2b5 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerComponent.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerComponent.java
@@ -20,7 +20,7 @@
 
 import com.android.keyguard.KeyguardSecurityContainerController;
 import com.android.systemui.dagger.qualifiers.RootView;
-import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 
 import dagger.BindsInstance;
 import dagger.Subcomponent;
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java
index 38f252a..893239b 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java
@@ -28,7 +28,7 @@
 import com.android.systemui.R;
 import com.android.systemui.biometrics.SideFpsController;
 import com.android.systemui.dagger.qualifiers.RootView;
-import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 
 import java.util.Optional;
 
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
index e7295ef..54738c6 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.DEBUG
 import com.android.systemui.log.dagger.BiometricLog
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt
index c00b2c6..cfad614 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt
@@ -19,9 +19,9 @@
 import android.hardware.biometrics.BiometricSourceType
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.INFO
 import com.android.systemui.log.dagger.BiometricLog
 import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_DISMISS_BOUNCER
 import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_NONE
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt
index 1978715..d02b72f 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt
@@ -19,7 +19,7 @@
 import androidx.annotation.IntDef
 import com.android.keyguard.CarrierTextManager.CarrierTextCallbackInfo
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.dagger.CarrierTextManagerLog
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
index 8b925b1..bddf3b0 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -19,7 +19,7 @@
 import com.android.systemui.biometrics.AuthRippleController
 import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.dagger.KeyguardLog
 import com.android.systemui.statusbar.KeyguardIndicationController
 import com.google.errorprone.annotations.CompileTimeConstant
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index b596331..eec5b3e 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -18,6 +18,7 @@
 
 import android.content.Intent
 import android.hardware.biometrics.BiometricConstants.LockoutMode
+import android.hardware.biometrics.BiometricSourceType
 import android.os.PowerManager
 import android.os.PowerManager.WakeReason
 import android.telephony.ServiceState
@@ -32,12 +33,12 @@
 import com.android.keyguard.TrustGrantFlags
 import com.android.settingslib.fuelgauge.BatteryStatus
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.ERROR
+import com.android.systemui.log.core.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.VERBOSE
+import com.android.systemui.log.core.LogLevel.WARNING
 import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
@@ -370,16 +371,14 @@
 
     fun logServiceProvidersUpdated(intent: Intent) {
         logBuffer.log(
-                TAG,
-                VERBOSE,
-                {
-                    int1 = intent.getIntExtra(EXTRA_SUBSCRIPTION_INDEX, INVALID_SUBSCRIPTION_ID)
-                    str1 = intent.getStringExtra(TelephonyManager.EXTRA_SPN)
-                    str2 = intent.getStringExtra(TelephonyManager.EXTRA_PLMN)
-                },
-                {
-                    "action SERVICE_PROVIDERS_UPDATED subId=$int1 spn=$str1 plmn=$str2"
-                }
+            TAG,
+            VERBOSE,
+            {
+                int1 = intent.getIntExtra(EXTRA_SUBSCRIPTION_INDEX, INVALID_SUBSCRIPTION_ID)
+                str1 = intent.getStringExtra(TelephonyManager.EXTRA_SPN)
+                str2 = intent.getStringExtra(TelephonyManager.EXTRA_PLMN)
+            },
+            { "action SERVICE_PROVIDERS_UPDATED subId=$int1 spn=$str1 plmn=$str2" }
         )
     }
 
@@ -601,6 +600,15 @@
         )
     }
 
+    fun allowFingerprintOnCurrentOccludingActivityChanged(allow: Boolean) {
+        logBuffer.log(
+                TAG,
+                VERBOSE,
+                { bool1 = allow },
+                { "allowFingerprintOnCurrentOccludingActivityChanged: $bool1" }
+        )
+    }
+
     fun logAssistantVisible(assistantVisible: Boolean) {
         logBuffer.log(
             TAG,
@@ -710,4 +718,13 @@
     fun scheduleWatchdog(@CompileTimeConstant watchdogType: String) {
         logBuffer.log(TAG, DEBUG, "Scheduling biometric watchdog for $watchdogType")
     }
+
+    fun notifyAboutEnrollmentsChanged(biometricSourceType: BiometricSourceType) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            { str1 = "$biometricSourceType" },
+            { "notifying about enrollments changed: $str1" }
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt
index cb764a8..57b5db1 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt
@@ -21,8 +21,8 @@
 import com.android.systemui.keyguard.shared.model.TrustManagedModel
 import com.android.systemui.keyguard.shared.model.TrustModel
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogLevel.DEBUG
 import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index be5bb07..1f1b154 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -33,7 +33,6 @@
 import com.android.internal.util.Preconditions;
 import com.android.keyguard.KeyguardSecurityModel;
 import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.clock.ClockManager;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
 import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
@@ -323,7 +322,6 @@
     @Inject @Named(LEAK_REPORT_EMAIL_NAME) Lazy<String> mLeakReportEmail;
     @Inject @Main Lazy<Executor> mMainExecutor;
     @Inject @Background Lazy<Executor> mBackgroundExecutor;
-    @Inject Lazy<ClockManager> mClockManager;
     @Inject Lazy<ActivityManagerWrapper> mActivityManagerWrapper;
     @Inject Lazy<DevicePolicyManagerWrapper> mDevicePolicyManagerWrapper;
     @Inject Lazy<PackageManagerWrapper> mPackageManagerWrapper;
@@ -517,7 +515,6 @@
         mProviders.put(SmartReplyController.class, mSmartReplyController::get);
         mProviders.put(RemoteInputQuickSettingsDisabler.class,
                 mRemoteInputQuickSettingsDisabler::get);
-        mProviders.put(ClockManager.class, mClockManager::get);
         mProviders.put(PrivacyItemController.class, mPrivacyItemController::get);
         mProviders.put(ActivityManagerWrapper.class, mActivityManagerWrapper::get);
         mProviders.put(DevicePolicyManagerWrapper.class, mDevicePolicyManagerWrapper::get);
diff --git a/packages/SystemUI/src/com/android/systemui/PhoneSystemUIAppComponentFactory.kt b/packages/SystemUI/src/com/android/systemui/PhoneSystemUIAppComponentFactory.kt
new file mode 100644
index 0000000..f06cb41
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/PhoneSystemUIAppComponentFactory.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui
+
+import android.content.Context
+
+class PhoneSystemUIAppComponentFactory : SystemUIAppComponentFactoryBase() {
+    override fun createSystemUIInitializer(context: Context) = SystemUIInitializerImpl(context)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt b/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt
index 99dd6b6..c465585 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt
@@ -49,13 +49,21 @@
  * When the HWC of the device supports Composition.DISPLAY_DECORATION, we use this layer to draw
  * screen decorations.
  */
-class ScreenDecorHwcLayer(context: Context, displayDecorationSupport: DisplayDecorationSupport) :
-        DisplayCutoutBaseView(context) {
+class ScreenDecorHwcLayer(
+    context: Context,
+    displayDecorationSupport: DisplayDecorationSupport,
+    private val debug: Boolean,
+) : DisplayCutoutBaseView(context) {
     val colorMode: Int
     private val useInvertedAlphaColor: Boolean
-    private val color: Int
+    private var color: Int = Color.BLACK
+        set(value) {
+            field = value
+            paint.color = value
+        }
+
     private val bgColor: Int
-    private val cornerFilter: ColorFilter
+    private var cornerFilter: ColorFilter
     private val cornerBgFilter: ColorFilter
     private val clearPaint: Paint
     @JvmField val transparentRect: Rect = Rect()
@@ -74,7 +82,7 @@
             throw IllegalArgumentException("Attempting to use unsupported mode " +
                     "${PixelFormat.formatToString(displayDecorationSupport.format)}")
         }
-        if (DEBUG_COLOR) {
+        if (debug) {
             color = Color.GREEN
             bgColor = Color.TRANSPARENT
             colorMode = ActivityInfo.COLOR_MODE_DEFAULT
@@ -106,10 +114,16 @@
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
         parent.requestTransparentRegion(this)
-        if (!DEBUG_COLOR) {
+        updateColors()
+    }
+
+    private fun updateColors() {
+        if (!debug) {
             viewRootImpl.setDisplayDecoration(true)
         }
 
+        cornerFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
+
         if (useInvertedAlphaColor) {
             paint.set(clearPaint)
         } else {
@@ -118,6 +132,21 @@
         }
     }
 
+    fun setDebugColor(color: Int) {
+        if (!debug) {
+            return
+        }
+
+        if (this.color == color) {
+            return
+        }
+
+        this.color = color
+
+        updateColors()
+        invalidate()
+    }
+
     override fun onUpdate() {
         parent.requestTransparentRegion(this)
     }
@@ -143,12 +172,12 @@
     override fun gatherTransparentRegion(region: Region?): Boolean {
         region?.let {
             calculateTransparentRect()
-            if (DEBUG_COLOR) {
+            if (debug) {
                 // Since we're going to draw a rectangle where the layer would
                 // normally be transparent, treat the transparent region as
                 // empty. We still want this method to be called, though, so
                 // that it calculates the transparent rect at the right time
-                // to match !DEBUG_COLOR.
+                // to match ![debug]
                 region.setEmpty()
             } else {
                 region.op(transparentRect, Region.Op.INTERSECT)
@@ -364,7 +393,7 @@
     /**
      * Update the rounded corner drawables.
      */
-    fun updateRoundedCornerDrawable(top: Drawable, bottom: Drawable) {
+    fun updateRoundedCornerDrawable(top: Drawable?, bottom: Drawable?) {
         roundedCornerDrawableTop = top
         roundedCornerDrawableBottom = bottom
         updateRoundedCornerDrawableBounds()
@@ -421,8 +450,4 @@
         ipw.println("roundedCornerBottomSize=$roundedCornerBottomSize")
         ipw.decreaseIndent()
     }
-
-    companion object {
-        private val DEBUG_COLOR = ScreenDecorations.DEBUG_COLOR
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index ea0f343..ff395da 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -36,6 +36,7 @@
 import android.graphics.Paint;
 import android.graphics.Path;
 import android.graphics.PixelFormat;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.hardware.graphics.common.AlphaInterpretation;
@@ -69,8 +70,9 @@
 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;
+import com.android.systemui.decor.DebugRoundedCornerDelegate;
+import com.android.systemui.decor.DebugRoundedCornerModel;
 import com.android.systemui.decor.DecorProvider;
 import com.android.systemui.decor.DecorProviderFactory;
 import com.android.systemui.decor.DecorProviderKt;
@@ -78,14 +80,14 @@
 import com.android.systemui.decor.OverlayWindow;
 import com.android.systemui.decor.PrivacyDotDecorProviderFactory;
 import com.android.systemui.decor.RoundedCornerDecorProviderFactory;
-import com.android.systemui.decor.RoundedCornerResDelegate;
+import com.android.systemui.decor.RoundedCornerResDelegateImpl;
+import com.android.systemui.decor.ScreenDecorCommand;
 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.commandline.CommandRegistry;
 import com.android.systemui.statusbar.events.PrivacyDotViewController;
-import com.android.systemui.tuner.TunerService;
-import com.android.systemui.tuner.TunerService.Tunable;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.concurrency.ThreadFactory;
 import com.android.systemui.util.settings.SecureSettings;
@@ -96,7 +98,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
-import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
@@ -105,19 +106,17 @@
  * for antialiasing and emulation purposes.
  */
 @SysUISingleton
-public class ScreenDecorations implements CoreStartable, Tunable , Dumpable {
-    private static final boolean DEBUG = false;
+public class ScreenDecorations implements CoreStartable, Dumpable {
+    private static final boolean DEBUG_LOGGING = false;
     private static final String TAG = "ScreenDecorations";
 
-    public static final String SIZE = "sysui_rounded_size";
-    public static final String PADDING = "sysui_rounded_content_padding";
     // Provide a way for factory to disable ScreenDecorations to run the Display tests.
     private static final boolean DEBUG_DISABLE_SCREEN_DECORATIONS =
             SystemProperties.getBoolean("debug.disable_screen_decorations", false);
     private static final boolean DEBUG_SCREENSHOT_ROUNDED_CORNERS =
             SystemProperties.getBoolean("debug.screenshot_rounded_corners", false);
-    private static final boolean VERBOSE = false;
-    static final boolean DEBUG_COLOR = DEBUG_SCREENSHOT_ROUNDED_CORNERS;
+    private boolean mDebug = DEBUG_SCREENSHOT_ROUNDED_CORNERS;
+    private int mDebugColor = Color.RED;
 
     private static final int[] DISPLAY_CUTOUT_IDS = {
             R.id.display_cutout,
@@ -133,8 +132,7 @@
     @VisibleForTesting
     protected boolean mIsRegistered;
     private final Context mContext;
-    private final Executor mMainExecutor;
-    private final TunerService mTunerService;
+    private final CommandRegistry mCommandRegistry;
     private final SecureSettings mSecureSettings;
     @VisibleForTesting
     DisplayTracker.Callback mDisplayListener;
@@ -147,9 +145,13 @@
     public final int mFaceScanningViewId;
 
     @VisibleForTesting
-    protected RoundedCornerResDelegate mRoundedCornerResDelegate;
+    protected RoundedCornerResDelegateImpl mRoundedCornerResDelegate;
     @VisibleForTesting
     protected DecorProviderFactory mRoundedCornerFactory;
+    @VisibleForTesting
+    protected DebugRoundedCornerDelegate mDebugRoundedCornerDelegate =
+            new DebugRoundedCornerDelegate();
+    protected DecorProviderFactory mDebugRoundedCornerFactory;
     private CutoutDecorProviderFactory mCutoutFactory;
     private int mProviderRefreshToken = 0;
     @VisibleForTesting
@@ -170,7 +172,7 @@
     private int mTintColor = Color.BLACK;
     @VisibleForTesting
     protected DisplayDecorationSupport mHwcScreenDecorationSupport;
-    private Display.Mode mDisplayMode;
+    private final Point mDisplaySize = new Point();
     @VisibleForTesting
     protected DisplayInfo mDisplayInfo = new DisplayInfo();
     private DisplayCutout mDisplayCutout;
@@ -313,9 +315,8 @@
 
     @Inject
     public ScreenDecorations(Context context,
-            @Main Executor mainExecutor,
             SecureSettings secureSettings,
-            TunerService tunerService,
+            CommandRegistry commandRegistry,
             UserTracker userTracker,
             DisplayTracker displayTracker,
             PrivacyDotViewController dotViewController,
@@ -325,9 +326,8 @@
             ScreenDecorationsLogger logger,
             AuthController authController) {
         mContext = context;
-        mMainExecutor = mainExecutor;
         mSecureSettings = secureSettings;
-        mTunerService = tunerService;
+        mCommandRegistry = commandRegistry;
         mUserTracker = userTracker;
         mDisplayTracker = displayTracker;
         mDotViewController = dotViewController;
@@ -352,6 +352,45 @@
         }
     };
 
+    private final ScreenDecorCommand.Callback mScreenDecorCommandCallback = (cmd, pw) -> {
+        // If we are exiting debug mode, we can set it (false) and bail, otherwise we will
+        // ensure that debug mode is set
+        if (cmd.getDebug() != null && !cmd.getDebug()) {
+            setDebug(false);
+            return;
+        } else {
+            // setDebug is idempotent
+            setDebug(true);
+        }
+
+        if (cmd.getColor() != null) {
+            mDebugColor = cmd.getColor();
+            mExecutor.execute(() -> {
+                if (mScreenDecorHwcLayer != null) {
+                    mScreenDecorHwcLayer.setDebugColor(cmd.getColor());
+                }
+                updateColorInversionDefault();
+            });
+        }
+
+        DebugRoundedCornerModel roundedTop = null;
+        DebugRoundedCornerModel roundedBottom = null;
+        if (cmd.getRoundedTop() != null) {
+            roundedTop = cmd.getRoundedTop().toRoundedCornerDebugModel();
+        }
+        if (cmd.getRoundedBottom() != null) {
+            roundedBottom = cmd.getRoundedBottom().toRoundedCornerDebugModel();
+        }
+        if (roundedTop != null || roundedBottom != null) {
+            mDebugRoundedCornerDelegate.applyNewDebugCorners(roundedTop, roundedBottom);
+            mExecutor.execute(() -> {
+                removeAllOverlays();
+                removeHwcOverlay();
+                setupDecorations();
+            });
+        }
+    };
+
     @Override
     public void start() {
         if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
@@ -363,6 +402,34 @@
         mExecutor.execute(this::startOnScreenDecorationsThread);
         mDotViewController.setUiExecutor(mExecutor);
         mAuthController.addCallback(mAuthControllerCallback);
+        mCommandRegistry.registerCommand(ScreenDecorCommand.SCREEN_DECOR_CMD_NAME,
+                () -> new ScreenDecorCommand(mScreenDecorCommandCallback));
+    }
+
+    /**
+     * Change the value of {@link ScreenDecorations#mDebug}. This operation is heavyweight, since
+     * it requires essentially re-init-ing this screen decorations process with the debug
+     * information taken into account.
+     */
+    @VisibleForTesting
+    protected void setDebug(boolean debug) {
+        if (mDebug == debug) {
+            return;
+        }
+
+        mDebug = debug;
+        if (!mDebug) {
+            mDebugRoundedCornerDelegate.removeDebugState();
+        }
+
+        mExecutor.execute(() -> {
+            // Re-trigger all of the screen decorations setup here so that the debug values
+            // can be picked up
+            removeAllOverlays();
+            removeHwcOverlay();
+            startOnScreenDecorationsThread();
+            updateColorInversionDefault();
+        });
     }
 
     private boolean isPrivacyDotEnabled() {
@@ -370,11 +437,16 @@
     }
 
     @NonNull
-    private List<DecorProvider> getProviders(boolean hasHwLayer) {
+    @VisibleForTesting
+    protected List<DecorProvider> getProviders(boolean hasHwLayer) {
         List<DecorProvider> decorProviders = new ArrayList<>(mDotFactory.getProviders());
         decorProviders.addAll(mFaceScanningFactory.getProviders());
         if (!hasHwLayer) {
-            decorProviders.addAll(mRoundedCornerFactory.getProviders());
+            if (mDebug && mDebugRoundedCornerFactory.getHasProviders()) {
+                decorProviders.addAll(mDebugRoundedCornerFactory.getProviders());
+            } else {
+                decorProviders.addAll(mRoundedCornerFactory.getProviders());
+            }
             decorProviders.addAll(mCutoutFactory.getProviders());
         }
         return decorProviders;
@@ -413,14 +485,17 @@
         mWindowManager = mContext.getSystemService(WindowManager.class);
         mContext.getDisplay().getDisplayInfo(mDisplayInfo);
         mRotation = mDisplayInfo.rotation;
-        mDisplayMode = mDisplayInfo.getMode();
+        mDisplaySize.x = mDisplayInfo.getNaturalWidth();
+        mDisplaySize.y = mDisplayInfo.getNaturalHeight();
         mDisplayUniqueId = mDisplayInfo.uniqueId;
         mDisplayCutout = mDisplayInfo.displayCutout;
-        mRoundedCornerResDelegate = new RoundedCornerResDelegate(mContext.getResources(),
-                mDisplayUniqueId);
+        mRoundedCornerResDelegate =
+                new RoundedCornerResDelegateImpl(mContext.getResources(), mDisplayUniqueId);
         mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(
                 getPhysicalPixelDisplaySizeRatio());
         mRoundedCornerFactory = new RoundedCornerDecorProviderFactory(mRoundedCornerResDelegate);
+        mDebugRoundedCornerFactory =
+                new RoundedCornerDecorProviderFactory(mDebugRoundedCornerDelegate);
         mCutoutFactory = getCutoutFactory();
         mHwcScreenDecorationSupport = mContext.getDisplay().getDisplayDecorationSupport();
         updateHwLayerRoundedCornerDrawable();
@@ -432,10 +507,12 @@
             public void onDisplayChanged(int displayId) {
                 mContext.getDisplay().getDisplayInfo(mDisplayInfo);
                 final int newRotation = mDisplayInfo.rotation;
-                final Display.Mode newDisplayMode = mDisplayInfo.getMode();
                 if ((mOverlays != null || mScreenDecorHwcWindow != null)
                         && (mRotation != newRotation
-                        || displayModeChanged(mDisplayMode, newDisplayMode))) {
+                        || displaySizeChanged(mDisplaySize, mDisplayInfo))) {
+                    final Point newSize = new Point();
+                    newSize.x = mDisplayInfo.getNaturalWidth();
+                    newSize.y = mDisplayInfo.getNaturalHeight();
                     // We cannot immediately update the orientation. Otherwise
                     // WindowManager is still deferring layout until it has finished dispatching
                     // the config changes, which may cause divergence between what we draw
@@ -444,15 +521,11 @@
                     // - we are trying to redraw. This because WM resized our window and told us to.
                     // - the config change has been dispatched, so WM is no longer deferring layout.
                     mPendingConfigChange = true;
-                    if (DEBUG) {
-                        if (mRotation != newRotation) {
-                            Log.i(TAG, "Rotation changed, deferring " + newRotation
-                                            + ", staying at " + mRotation);
-                        }
-                        if (displayModeChanged(mDisplayMode, newDisplayMode)) {
-                            Log.i(TAG, "Resolution changed, deferring " + newDisplayMode
-                                    + ", staying at " + mDisplayMode);
-                        }
+                    if (mRotation != newRotation) {
+                        mLogger.logRotationChangeDeferred(mRotation, newRotation);
+                    }
+                    if (!mDisplaySize.equals(newSize)) {
+                        mLogger.logDisplaySizeChanged(mDisplaySize, newSize);
                     }
 
                     if (mOverlays != null) {
@@ -461,7 +534,7 @@
                                 final ViewGroup overlayView = mOverlays[i].getRootView();
                                 overlayView.getViewTreeObserver().addOnPreDrawListener(
                                         new RestartingPreDrawListener(
-                                                overlayView, i, newRotation, newDisplayMode));
+                                                overlayView, i, newRotation, newSize));
                             }
                         }
                     }
@@ -471,7 +544,7 @@
                                 new RestartingPreDrawListener(
                                         mScreenDecorHwcWindow,
                                         -1, // Pass -1 for views with no specific position.
-                                        newRotation, newDisplayMode));
+                                        newRotation, newSize));
                     }
                     if (mScreenDecorHwcLayer != null) {
                         mScreenDecorHwcLayer.pendingConfigChange = true;
@@ -608,12 +681,6 @@
                 return;
             }
 
-            mMainExecutor.execute(() -> {
-                Trace.beginSection("ScreenDecorations#addTunable");
-                mTunerService.addTunable(this, SIZE);
-                Trace.endSection();
-            });
-
             // Watch color inversion and invert the overlay as needed.
             if (mColorInversionSetting == null) {
                 mColorInversionSetting = new SettingObserver(mSecureSettings, mHandler,
@@ -632,12 +699,6 @@
             mUserTracker.addCallback(mUserChangedCallback, mExecutor);
             mIsRegistered = true;
         } else {
-            mMainExecutor.execute(() -> {
-                Trace.beginSection("ScreenDecorations#removeTunable");
-                mTunerService.removeTunable(this);
-                Trace.endSection();
-            });
-
             if (mColorInversionSetting != null) {
                 mColorInversionSetting.setListening(false);
             }
@@ -777,7 +838,8 @@
         }
         mScreenDecorHwcWindow = (ViewGroup) LayoutInflater.from(mContext).inflate(
                 R.layout.screen_decor_hwc_layer, null);
-        mScreenDecorHwcLayer = new ScreenDecorHwcLayer(mContext, mHwcScreenDecorationSupport);
+        mScreenDecorHwcLayer =
+                new ScreenDecorHwcLayer(mContext, mHwcScreenDecorationSupport, mDebug);
         mScreenDecorHwcWindow.addView(mScreenDecorHwcLayer, new FrameLayout.LayoutParams(
                 MATCH_PARENT, MATCH_PARENT, Gravity.TOP | Gravity.START));
         mWindowManager.addView(mScreenDecorHwcWindow, getHwcWindowLayoutParams());
@@ -825,7 +887,7 @@
         lp.height = MATCH_PARENT;
         lp.setTitle("ScreenDecorHwcOverlay");
         lp.gravity = Gravity.TOP | Gravity.START;
-        if (!DEBUG_COLOR) {
+        if (!mDebug) {
             lp.setColorMode(ActivityInfo.COLOR_MODE_A8);
         }
         return lp;
@@ -884,15 +946,8 @@
         }
     }
 
-    private static boolean displayModeChanged(Display.Mode oldMode, Display.Mode newMode) {
-        if (oldMode == null) {
-            return true;
-        }
-
-        // We purposely ignore refresh rate and id changes here, because we don't need to
-        // invalidate for those, and they can trigger the refresh rate to increase
-        return oldMode.getPhysicalWidth() != newMode.getPhysicalWidth()
-                || oldMode.getPhysicalHeight() != newMode.getPhysicalHeight();
+    private static boolean displaySizeChanged(Point size, DisplayInfo info) {
+        return size.x != info.getNaturalWidth() || size.y != info.getNaturalHeight();
     }
 
     private int getOverlayWindowGravity(@BoundsPosition int pos) {
@@ -933,19 +988,42 @@
             new UserTracker.Callback() {
                 @Override
                 public void onUserChanged(int newUser, @NonNull Context userContext) {
-                    if (DEBUG) {
-                        Log.d(TAG, "UserSwitched newUserId=" + newUser);
-                    }
+                    mLogger.logUserSwitched(newUser);
                     // update color inversion setting to the new user
                     mColorInversionSetting.setUserId(newUser);
                     updateColorInversion(mColorInversionSetting.getValue());
                 }
             };
 
+    /**
+     * Use the current value of {@link ScreenDecorations#mColorInversionSetting} and passes it
+     * to {@link ScreenDecorations#updateColorInversion}
+     */
+    private void updateColorInversionDefault() {
+        int inversion = 0;
+        if (mColorInversionSetting != null) {
+            inversion = mColorInversionSetting.getValue();
+        }
+
+        updateColorInversion(inversion);
+    }
+
+    /**
+     * Update the tint color of screen decoration assets. Defaults to Color.BLACK. In the case of
+     * a color inversion being set, use Color.WHITE (which inverts to black).
+     *
+     * When {@link ScreenDecorations#mDebug} is {@code true}, this value is updated to use
+     * {@link ScreenDecorations#mDebugColor}, and does not handle inversion.
+     *
+     * @param colorsInvertedValue if non-zero, assume that colors are inverted, and use Color.WHITE
+     *                            for screen decoration tint
+     */
     private void updateColorInversion(int colorsInvertedValue) {
         mTintColor = colorsInvertedValue != 0 ? Color.WHITE : Color.BLACK;
-        if (DEBUG_COLOR) {
-            mTintColor = Color.RED;
+        if (mDebug) {
+            mTintColor = mDebugColor;
+            mDebugRoundedCornerDelegate.setColor(mTintColor);
+            //TODO(b/285941724): update the hwc layer color here too (or disable it in debug mode)
         }
 
         updateOverlayProviderViews(new Integer[] {
@@ -986,7 +1064,9 @@
             int oldRotation = mRotation;
             mPendingConfigChange = false;
             updateConfiguration();
-            if (DEBUG) Log.i(TAG, "onConfigChanged from rot " + oldRotation + " to " + mRotation);
+            if (oldRotation != mRotation) {
+                mLogger.logRotationChanged(oldRotation, mRotation);
+            }
             setupDecorations();
             if (mOverlays != null) {
                 // Updating the layout params ensures that ViewRootImpl will call relayoutWindow(),
@@ -1016,6 +1096,7 @@
         if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
             return;
         }
+        ipw.println("mDebug:" + mDebug);
 
         ipw.println("mIsPrivacyDotEnabled:" + isPrivacyDotEnabled());
         ipw.println("shouldOptimizeOverlayVisibility:" + shouldOptimizeVisibility());
@@ -1071,6 +1152,7 @@
             }
         }
         mRoundedCornerResDelegate.dump(pw, args);
+        mDebugRoundedCornerDelegate.dump(pw);
     }
 
     @VisibleForTesting
@@ -1084,17 +1166,18 @@
         if (mRotation != newRotation) {
             mDotViewController.setNewRotation(newRotation);
         }
-        final Display.Mode newMod = mDisplayInfo.getMode();
         final DisplayCutout newCutout = mDisplayInfo.displayCutout;
 
         if (!mPendingConfigChange
-                && (newRotation != mRotation || displayModeChanged(mDisplayMode, newMod)
+                && (newRotation != mRotation || displaySizeChanged(mDisplaySize, mDisplayInfo)
                 || !Objects.equals(newCutout, mDisplayCutout))) {
             mRotation = newRotation;
-            mDisplayMode = newMod;
+            mDisplaySize.x = mDisplayInfo.getNaturalWidth();
+            mDisplaySize.y = mDisplayInfo.getNaturalHeight();
             mDisplayCutout = newCutout;
-            mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(
-                    getPhysicalPixelDisplaySizeRatio());
+            float ratio = getPhysicalPixelDisplaySizeRatio();
+            mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(ratio);
+            mDebugRoundedCornerDelegate.setPhysicalPixelDisplaySizeRatio(ratio);
             if (mScreenDecorHwcLayer != null) {
                 mScreenDecorHwcLayer.pendingConfigChange = false;
                 mScreenDecorHwcLayer.updateConfiguration(mDisplayUniqueId);
@@ -1117,7 +1200,8 @@
     }
 
     private boolean hasRoundedCorners() {
-        return mRoundedCornerFactory.getHasProviders();
+        return mRoundedCornerFactory.getHasProviders()
+                || mDebugRoundedCornerFactory.getHasProviders();
     }
 
     private boolean shouldOptimizeVisibility() {
@@ -1170,43 +1254,19 @@
         Trace.endSection();
     }
 
-    @Override
-    public void onTuningChanged(String key, String newValue) {
-        if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
-            Log.i(TAG, "ScreenDecorations is disabled");
-            return;
-        }
-        mExecutor.execute(() -> {
-            if (mOverlays == null || !SIZE.equals(key)) {
-                return;
-            }
-            Trace.beginSection("ScreenDecorations#onTuningChanged");
-            try {
-                final int sizeFactor = Integer.parseInt(newValue);
-                mRoundedCornerResDelegate.setTuningSizeFactor(sizeFactor);
-            } catch (NumberFormatException e) {
-                mRoundedCornerResDelegate.setTuningSizeFactor(null);
-            }
-            updateOverlayProviderViews(new Integer[] {
-                    R.id.rounded_corner_top_left,
-                    R.id.rounded_corner_top_right,
-                    R.id.rounded_corner_bottom_left,
-                    R.id.rounded_corner_bottom_right
-            });
-            updateHwLayerRoundedCornerExistAndSize();
-            Trace.endSection();
-        });
-    }
-
     private void updateHwLayerRoundedCornerDrawable() {
         if (mScreenDecorHwcLayer == null) {
             return;
         }
 
-        final Drawable topDrawable = mRoundedCornerResDelegate.getTopRoundedDrawable();
-        final Drawable bottomDrawable = mRoundedCornerResDelegate.getBottomRoundedDrawable();
+        Drawable topDrawable = mRoundedCornerResDelegate.getTopRoundedDrawable();
+        Drawable bottomDrawable = mRoundedCornerResDelegate.getBottomRoundedDrawable();
+        if (mDebug && (mDebugRoundedCornerFactory.getHasProviders())) {
+            topDrawable = mDebugRoundedCornerDelegate.getTopRoundedDrawable();
+            bottomDrawable = mDebugRoundedCornerDelegate.getBottomRoundedDrawable();
+        }
 
-        if (topDrawable == null || bottomDrawable == null) {
+        if (topDrawable == null && bottomDrawable == null) {
             return;
         }
         mScreenDecorHwcLayer.updateRoundedCornerDrawable(topDrawable, bottomDrawable);
@@ -1216,11 +1276,19 @@
         if (mScreenDecorHwcLayer == null) {
             return;
         }
-        mScreenDecorHwcLayer.updateRoundedCornerExistenceAndSize(
-                mRoundedCornerResDelegate.getHasTop(),
-                mRoundedCornerResDelegate.getHasBottom(),
-                mRoundedCornerResDelegate.getTopRoundedSize().getWidth(),
-                mRoundedCornerResDelegate.getBottomRoundedSize().getWidth());
+        if (mDebug && mDebugRoundedCornerFactory.getHasProviders()) {
+            mScreenDecorHwcLayer.updateRoundedCornerExistenceAndSize(
+                    mDebugRoundedCornerDelegate.getHasTop(),
+                    mDebugRoundedCornerDelegate.getHasBottom(),
+                    mDebugRoundedCornerDelegate.getTopRoundedSize().getWidth(),
+                    mDebugRoundedCornerDelegate.getBottomRoundedSize().getWidth());
+        } else {
+            mScreenDecorHwcLayer.updateRoundedCornerExistenceAndSize(
+                    mRoundedCornerResDelegate.getHasTop(),
+                    mRoundedCornerResDelegate.getHasBottom(),
+                    mRoundedCornerResDelegate.getTopRoundedSize().getWidth(),
+                    mRoundedCornerResDelegate.getBottomRoundedSize().getWidth());
+        }
     }
 
     @VisibleForTesting
@@ -1247,7 +1315,7 @@
 
             paint.setColor(mColor);
             paint.setStyle(Paint.Style.FILL);
-            if (DEBUG) {
+            if (DEBUG_LOGGING) {
                 getViewTreeObserver().addOnDrawListener(() -> Log.i(TAG,
                         getWindowTitleByPos(pos) + " drawn in rot " + mRotation));
             }
@@ -1422,31 +1490,29 @@
 
         private final View mView;
         private final int mTargetRotation;
-        private final Display.Mode mTargetDisplayMode;
+        private final Point mTargetDisplaySize;
         // Pass -1 for ScreenDecorHwcLayer since it's a fullscreen window and has no specific
         // position.
         private final int mPosition;
 
         private RestartingPreDrawListener(View view, @BoundsPosition int position,
-                int targetRotation, Display.Mode targetDisplayMode) {
+                int targetRotation, Point targetDisplaySize) {
             mView = view;
             mTargetRotation = targetRotation;
-            mTargetDisplayMode = targetDisplayMode;
+            mTargetDisplaySize = targetDisplaySize;
             mPosition = position;
         }
 
         @Override
         public boolean onPreDraw() {
             mView.getViewTreeObserver().removeOnPreDrawListener(this);
-            if (mTargetRotation == mRotation
-                    && !displayModeChanged(mDisplayMode, mTargetDisplayMode)) {
-                if (DEBUG) {
+            if (mTargetRotation == mRotation && mDisplaySize.equals(mTargetDisplaySize)) {
+                if (DEBUG_LOGGING) {
                     final String title = mPosition < 0 ? "ScreenDecorHwcLayer"
                             : getWindowTitleByPos(mPosition);
                     Log.i(TAG, title + " already in target rot "
                             + mTargetRotation + " and in target resolution "
-                            + mTargetDisplayMode.getPhysicalWidth() + "x"
-                            + mTargetDisplayMode.getPhysicalHeight()
+                            + mTargetDisplaySize.x + "x" + mTargetDisplaySize.y
                             + ", allow draw without restarting it");
                 }
                 return true;
@@ -1456,13 +1522,12 @@
             // This changes the window attributes - we need to restart the traversal for them to
             // take effect.
             updateConfiguration();
-            if (DEBUG) {
+            if (DEBUG_LOGGING) {
                 final String title = mPosition < 0 ? "ScreenDecorHwcLayer"
                         : getWindowTitleByPos(mPosition);
                 Log.i(TAG, title
                         + " restarting listener fired, restarting draw for rot " + mRotation
-                        + ", resolution " + mDisplayMode.getPhysicalWidth() + "x"
-                        + mDisplayMode.getPhysicalHeight());
+                        + ", resolution " + mDisplaySize.x + "x" + mDisplaySize.y);
             }
             mView.invalidate();
             return false;
@@ -1488,19 +1553,18 @@
         public boolean onPreDraw() {
             mContext.getDisplay().getDisplayInfo(mDisplayInfo);
             final int displayRotation = mDisplayInfo.rotation;
-            final Display.Mode displayMode = mDisplayInfo.getMode();
-            if ((displayRotation != mRotation || displayModeChanged(mDisplayMode, displayMode))
+            if ((displayRotation != mRotation || displaySizeChanged(mDisplaySize, mDisplayInfo))
                     && !mPendingConfigChange) {
-                if (DEBUG) {
+                if (DEBUG_LOGGING) {
                     if (displayRotation != mRotation) {
                         Log.i(TAG, "Drawing rot " + mRotation + ", but display is at rot "
                                 + displayRotation + ". Restarting draw");
                     }
-                    if (displayModeChanged(mDisplayMode, displayMode)) {
-                        Log.i(TAG, "Drawing at " + mDisplayMode.getPhysicalWidth()
-                                + "x" + mDisplayMode.getPhysicalHeight() + ", but display is at "
-                                + displayMode.getPhysicalWidth() + "x"
-                                + displayMode.getPhysicalHeight() + ". Restarting draw");
+                    if (displaySizeChanged(mDisplaySize, mDisplayInfo)) {
+                        Log.i(TAG, "Drawing at " + mDisplaySize.x + "x" + mDisplaySize.y
+                                + ", but display is at "
+                                + mDisplayInfo.getNaturalWidth() + "x"
+                                + mDisplayInfo.getNaturalHeight() + ". Restarting draw");
                     }
                 }
                 mView.invalidate();
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index 806d1af..3ca74ac 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -81,9 +81,6 @@
 
     protected final Handler mHandler;
 
-    private float mMinSwipeProgress = 0f;
-    private float mMaxSwipeProgress = 1f;
-
     private final SpringConfig mSnapBackSpringConfig =
             new SpringConfig(SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
 
@@ -227,18 +224,11 @@
         return v.getMeasuredWidth();
     }
 
-    public void setMinSwipeProgress(float minSwipeProgress) {
-        mMinSwipeProgress = minSwipeProgress;
-    }
-
-    public void setMaxSwipeProgress(float maxSwipeProgress) {
-        mMaxSwipeProgress = maxSwipeProgress;
-    }
-
     private float getSwipeProgressForOffset(View view, float translation) {
+        if (translation == 0) return 0;
         float viewSize = getSize(view);
         float result = Math.abs(translation / viewSize);
-        return Math.min(Math.max(mMinSwipeProgress, result), mMaxSwipeProgress);
+        return Math.min(Math.max(0, result), 1);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactory.java
deleted file mode 100644
index 527ce12..0000000
--- a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactory.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui;
-
-import android.content.Context;
-
-/**
- * Starts up SystemUI using the AOSP {@link SystemUIInitializerImpl}.
- *
- * This initializer relies on reflection to start everything up and should be considered deprecated.
- * Instead, create your own {@link SystemUIAppComponentFactoryBase}, specify it in your
- * AndroidManifest.xml and construct your own {@link SystemUIInitializer} directly.
- *
- * @deprecated Define your own SystemUIAppComponentFactoryBase implementation and use that. This
- *             implementation may be changed or removed in future releases.
- */
-@Deprecated
-public class SystemUIAppComponentFactory extends SystemUIAppComponentFactoryBase {
-    @Override
-    protected SystemUIInitializer createSystemUIInitializer(Context context) {
-        return SystemUIInitializerFactory.createWithContext(context);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 70c39df..453d1d1 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -54,7 +54,7 @@
  * Application class for SystemUI.
  */
 public class SystemUIApplication extends Application implements
-        SystemUIAppComponentFactory.ContextInitializer {
+        SystemUIAppComponentFactoryBase.ContextInitializer {
 
     public static final String TAG = "SystemUIService";
     private static final boolean DEBUG = false;
@@ -66,7 +66,7 @@
      */
     private CoreStartable[] mServices;
     private boolean mServicesStarted;
-    private SystemUIAppComponentFactory.ContextAvailableCallback mContextAvailableCallback;
+    private SystemUIAppComponentFactoryBase.ContextAvailableCallback mContextAvailableCallback;
     private SysUIComponent mSysUIComponent;
     private SystemUIInitializer mInitializer;
 
@@ -366,7 +366,7 @@
 
     @Override
     public void setContextAvailableCallback(
-            SystemUIAppComponentFactory.ContextAvailableCallback callback) {
+            SystemUIAppComponentFactoryBase.ContextAvailableCallback callback) {
         mContextAvailableCallback = callback;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerFactory.kt b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerFactory.kt
deleted file mode 100644
index b9454e8..0000000
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerFactory.kt
+++ /dev/null
@@ -1,72 +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
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.util.Log
-import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.util.Assert
-
-/**
- * Factory to reflectively lookup a [SystemUIInitializer] to start SystemUI with.
- */
-@Deprecated("Provide your own {@link SystemUIAppComponentFactoryBase} that doesn't need this.")
-object SystemUIInitializerFactory {
-    private const val TAG = "SysUIInitializerFactory"
-    @SuppressLint("StaticFieldLeak")
-    private var initializer: SystemUIInitializer? = null
-
-    /**
-     * Instantiate a [SystemUIInitializer] reflectively.
-     */
-    @JvmStatic
-    fun createWithContext(context: Context): SystemUIInitializer {
-        return createFromConfig(context)
-    }
-
-    /**
-     * Instantiate a [SystemUIInitializer] reflectively.
-     */
-    @JvmStatic
-    private fun createFromConfig(context: Context): SystemUIInitializer {
-        Assert.isMainThread()
-
-        return createFromConfigNoAssert(context)
-    }
-
-    @JvmStatic
-    @VisibleForTesting
-    fun createFromConfigNoAssert(context: Context): SystemUIInitializer {
-
-        return initializer ?: run {
-            val className = context.getString(R.string.config_systemUIFactoryComponent)
-            if (className.isEmpty()) {
-                throw RuntimeException("No SystemUIFactory component configured")
-            }
-            try {
-                val cls = context.classLoader.loadClass(className)
-                val constructor = cls.getConstructor(Context::class.java)
-                (constructor.newInstance(context) as SystemUIInitializer).apply {
-                    initializer = this
-                }
-            } catch (t: Throwable) {
-                Log.w(TAG, "Error creating SystemUIInitializer component: $className", t)
-                throw t
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
index 50e0399..6cf9eff 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
@@ -30,8 +30,10 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpHandler;
+import com.android.systemui.dump.LogBufferEulogizer;
 import com.android.systemui.dump.LogBufferFreezer;
 import com.android.systemui.dump.SystemUIAuxiliaryDumpService;
+import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager;
 import com.android.systemui.statusbar.policy.BatteryStateNotifier;
 
 import java.io.FileDescriptor;
@@ -44,22 +46,29 @@
     private final Handler mMainHandler;
     private final DumpHandler mDumpHandler;
     private final BroadcastDispatcher mBroadcastDispatcher;
+    private final LogBufferEulogizer mLogBufferEulogizer;
     private final LogBufferFreezer mLogBufferFreezer;
     private final BatteryStateNotifier mBatteryStateNotifier;
 
+    private final UncaughtExceptionPreHandlerManager mUncaughtExceptionPreHandlerManager;
+
     @Inject
     public SystemUIService(
             @Main Handler mainHandler,
             DumpHandler dumpHandler,
             BroadcastDispatcher broadcastDispatcher,
+            LogBufferEulogizer logBufferEulogizer,
             LogBufferFreezer logBufferFreezer,
-            BatteryStateNotifier batteryStateNotifier) {
+            BatteryStateNotifier batteryStateNotifier,
+            UncaughtExceptionPreHandlerManager uncaughtExceptionPreHandlerManager) {
         super();
         mMainHandler = mainHandler;
         mDumpHandler = dumpHandler;
         mBroadcastDispatcher = broadcastDispatcher;
+        mLogBufferEulogizer = logBufferEulogizer;
         mLogBufferFreezer = logBufferFreezer;
         mBatteryStateNotifier = batteryStateNotifier;
+        mUncaughtExceptionPreHandlerManager = uncaughtExceptionPreHandlerManager;
     }
 
     @Override
@@ -71,7 +80,10 @@
 
         // Finish initializing dump logic
         mLogBufferFreezer.attach(mBroadcastDispatcher);
-        mDumpHandler.init();
+
+        // Attempt to dump all LogBuffers for any uncaught exception
+        mUncaughtExceptionPreHandlerManager.registerHandler(
+                (thread, throwable) -> mLogBufferEulogizer.record(throwable));
 
         // If configured, set up a battery notification
         if (getResources().getBoolean(R.bool.config_showNotificationForUnknownBatteryState)) {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
index c3bb423..fd3c158 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
@@ -87,14 +87,15 @@
     }
 
     /**
-     * Shows magnification settings panel {@link WindowMagnificationSettings}.
+     * Toggles the visibility of magnification settings panel {@link WindowMagnificationSettings}.
+     * We show the panel if it is not visible. Otherwise, hide the panel.
      */
-    void showMagnificationSettings() {
+    void toggleSettingsPanelVisibility() {
         if (!mWindowMagnificationSettings.isSettingPanelShowing()) {
             onConfigurationChanged(mContext.getResources().getConfiguration());
             mContext.registerComponentCallbacks(this);
         }
-        mWindowMagnificationSettings.showSettingPanel();
+        mWindowMagnificationSettings.toggleSettingsPanelVisibility();
     }
 
     void closeMagnificationSettings() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index 4158390..2f6a68c 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
@@ -329,7 +329,9 @@
         // binder calls
         final Optional<CentralSurfaces> centralSurfacesOptional =
                 mCentralSurfacesOptionalLazy.get();
-        if (centralSurfacesOptional.map(CentralSurfaces::isPanelExpanded).orElse(false)
+        if (centralSurfacesOptional.isPresent()
+                && centralSurfacesOptional.get().getShadeViewController() != null
+                && centralSurfacesOptional.get().getShadeViewController().isPanelExpanded()
                 && !centralSurfacesOptional.get().isKeyguardShowing()) {
             if (!mDismissNotificationShadeActionRegistered) {
                 mA11yManager.registerSystemAction(
@@ -485,13 +487,11 @@
     }
 
     private void handleNotifications() {
-        mCentralSurfacesOptionalLazy.get().ifPresent(
-                CentralSurfaces::animateExpandNotificationsPanel);
+        mShadeController.animateExpandShade();
     }
 
     private void handleQuickSettings() {
-        mCentralSurfacesOptionalLazy.get().ifPresent(
-                centralSurfaces -> centralSurfaces.animateExpandSettingsPanel(null));
+        mShadeController.animateExpandQs();
     }
 
     private void handlePowerDialog() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
index e2b85fa..1739ba4 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
@@ -28,6 +28,7 @@
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
 import android.os.Handler;
+import android.util.SparseArray;
 import android.view.Display;
 import android.view.SurfaceControl;
 import android.view.WindowManagerGlobal;
@@ -72,6 +73,9 @@
     private WindowMagnificationConnectionImpl mWindowMagnificationConnectionImpl;
     private SysUiState mSysUiState;
 
+    @VisibleForTesting
+    SparseArray<SparseArray<Float>> mUsersScales = new SparseArray();
+
     private static class ControllerSupplier extends
             DisplayIdIndexSupplier<WindowMagnificationController> {
 
@@ -171,7 +175,7 @@
 
         mModeSwitchesController.setClickListenerDelegate(
                 displayId -> mHandler.post(() -> {
-                    showMagnificationSettingsPanel(displayId);
+                    toggleSettingsPanelVisibility(displayId);
                 }));
     }
 
@@ -254,11 +258,11 @@
     }
 
     @MainThread
-    void showMagnificationSettingsPanel(int displayId) {
+    void toggleSettingsPanelVisibility(int displayId) {
         final MagnificationSettingsController magnificationSettingsController =
                 mMagnificationSettingsSupplier.get(displayId);
         if (magnificationSettingsController != null) {
-            magnificationSettingsController.showMagnificationSettings();
+            magnificationSettingsController.toggleSettingsPanelVisibility();
         }
     }
 
@@ -295,6 +299,19 @@
         mModeSwitchesController.removeButton(displayId);
     }
 
+    @MainThread
+    void setUserMagnificationScale(int userId, int displayId, float scale) {
+        SparseArray<Float> scales = mUsersScales.get(userId);
+        if (scales == null) {
+            scales = new SparseArray<>();
+            mUsersScales.put(userId, scales);
+        }
+        if (scales.contains(displayId) && scales.get(displayId) == scale) {
+            return;
+        }
+        scales.put(displayId, scale);
+    }
+
     @VisibleForTesting
     final WindowMagnifierCallback mWindowMagnifierCallback = new WindowMagnifierCallback() {
         @Override
@@ -335,7 +352,7 @@
         @Override
         public void onClickSettingsButton(int displayId) {
             mHandler.post(() -> {
-                showMagnificationSettingsPanel(displayId);
+                toggleSettingsPanelVisibility(displayId);
             });
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationConnectionImpl.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationConnectionImpl.java
index c081893..f1d00ce2 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationConnectionImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationConnectionImpl.java
@@ -99,6 +99,12 @@
     }
 
     @Override
+    public void onUserMagnificationScaleChanged(int userId, int displayId, float scale) {
+        mHandler.post(() -> mWindowMagnification.setUserMagnificationScale(
+                userId, displayId, scale));
+    }
+
+    @Override
     public void setConnectionCallback(IWindowMagnificationConnectionCallback callback) {
         mConnectionCallback = callback;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index a67f706..e7eab7e 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -76,6 +76,7 @@
 
 import androidx.core.math.MathUtils;
 
+import com.android.internal.accessibility.common.MagnificationConstants;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.systemui.R;
@@ -101,7 +102,9 @@
     // Delay to avoid updating state description too frequently.
     private static final int UPDATE_STATE_DESCRIPTION_DELAY_MS = 100;
     // It should be consistent with the value defined in WindowMagnificationGestureHandler.
-    private static final Range<Float> A11Y_ACTION_SCALE_RANGE = new Range<>(1.0f, 8.0f);
+    private static final Range<Float> A11Y_ACTION_SCALE_RANGE = new Range<>(
+            MagnificationConstants.SCALE_MIN_VALUE,
+            MagnificationConstants.SCALE_MAX_VALUE);
     private static final float A11Y_CHANGE_SCALE_DIFFERENCE = 1.0f;
     private static final float ANIMATION_BOUNCE_EFFECT_SCALE = 1.05f;
     private final SparseArray<Float> mMagnificationSizeScaleOptions = new SparseArray<>();
@@ -221,6 +224,7 @@
 
     private boolean mAllowDiagonalScrolling = false;
     private boolean mEditSizeEnable = false;
+    private boolean mSettingsPanelVisibility = false;
 
     @Nullable
     private final MirrorWindowControl mMirrorWindowControl;
@@ -1399,6 +1403,8 @@
             return;
         }
 
+        mSettingsPanelVisibility = settingsPanelIsShown;
+
         mDragView.setBackground(mContext.getResources().getDrawable(settingsPanelIsShown
                 ? R.drawable.accessibility_window_magnification_drag_handle_background_change
                 : R.drawable.accessibility_window_magnification_drag_handle_background));
@@ -1439,12 +1445,19 @@
 
     private class MirrorWindowA11yDelegate extends View.AccessibilityDelegate {
 
+        private CharSequence getClickAccessibilityActionLabel() {
+            return mSettingsPanelVisibility
+                    ? mContext.getResources().getString(
+                            R.string.magnification_close_settings_click_label)
+                    : mContext.getResources().getString(
+                            R.string.magnification_open_settings_click_label);
+        }
+
         @Override
         public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
             super.onInitializeAccessibilityNodeInfo(host, info);
             final AccessibilityAction clickAction = new AccessibilityAction(
-                    AccessibilityAction.ACTION_CLICK.getId(), mContext.getResources().getString(
-                    R.string.magnification_open_settings_click_label));
+                    AccessibilityAction.ACTION_CLICK.getId(), getClickAccessibilityActionLabel());
             info.addAction(clickAction);
             info.setClickable(true);
             info.addAction(
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
index 3b1d695..1e1d4b7 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
@@ -18,10 +18,12 @@
 
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
-import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 
+import static com.android.internal.accessibility.common.MagnificationConstants.SCALE_MAX_VALUE;
+import static com.android.internal.accessibility.common.MagnificationConstants.SCALE_MIN_VALUE;
+
 import android.annotation.IntDef;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -79,14 +81,16 @@
     private final Runnable mWindowInsetChangeRunnable;
     private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider;
 
-    private final LayoutParams mParams;
+    @VisibleForTesting
+    final LayoutParams mParams;
     @VisibleForTesting
     final Rect mDraggableWindowBounds = new Rect();
     private boolean mIsVisible = false;
     private final MagnificationGestureDetector mGestureDetector;
     private boolean mSingleTapDetected = false;
 
-    private SeekBarWithIconButtonsView mZoomSeekbar;
+    @VisibleForTesting
+    SeekBarWithIconButtonsView mZoomSeekbar;
     private LinearLayout mAllowDiagonalScrollingView;
     private TextView mAllowDiagonalScrollingTitle;
     private Switch mAllowDiagonalScrollingSwitch;
@@ -101,11 +105,15 @@
     private ImageButton mFullScreenButton;
     private int mLastSelectedButtonIndex = MagnificationSize.NONE;
     private boolean mAllowDiagonalScrolling = false;
-    private static final float A11Y_CHANGE_SCALE_DIFFERENCE = 1.0f;
-    private static final float A11Y_SCALE_MIN_VALUE = 1.0f;
+    /**
+     * Amount by which magnification scale changes compared to seekbar in settings.
+     * magnitude = 10 means, for every 1 scale increase, 10 progress increase in seekbar.
+     */
+    private int mSeekBarMagnitude;
     private WindowMagnificationSettingsCallback mCallback;
 
     private ContentObserver mMagnificationCapabilityObserver;
+    private ContentObserver mMagnificationScaleObserver;
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
@@ -155,18 +163,27 @@
                 });
             }
         };
+        mMagnificationScaleObserver = new ContentObserver(
+                mContext.getMainThreadHandler()) {
+            @Override
+            public void onChange(boolean selfChange) {
+                setScaleSeekbar(getMagnificationScale());
+            }
+        };
     }
 
-    private class ZoomSeekbarChangeListener implements SeekBar.OnSeekBarChangeListener {
+    private class ZoomSeekbarChangeListener implements
+            SeekBarWithIconButtonsView.OnSeekBarWithIconButtonsChangeListener {
         @Override
         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
-            float scale = progress * A11Y_CHANGE_SCALE_DIFFERENCE + A11Y_SCALE_MIN_VALUE;
+            float scale = (progress / (float) mSeekBarMagnitude) + SCALE_MIN_VALUE;
             // Update persisted scale only when scale >= PERSISTED_SCALE_MIN_VALUE const.
             // We assume if the scale is lower than the PERSISTED_SCALE_MIN_VALUE, there will be
             // no obvious magnification effect.
             if (scale >= MagnificationConstants.PERSISTED_SCALE_MIN_VALUE) {
-                Settings.Secure.putFloatForUser(mContext.getContentResolver(),
-                        Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale,
+                mSecureSettings.putFloatForUser(
+                        Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
+                        scale,
                         UserHandle.USER_CURRENT);
             }
             mCallback.onMagnifierScale(scale);
@@ -181,6 +198,11 @@
         public void onStopTrackingTouch(SeekBar seekBar) {
             // Do nothing
         }
+
+        @Override
+        public void onUserInteractionFinalized(SeekBar seekBar, @ControlUnitType int control) {
+            // Do nothing
+        }
     }
 
     private final AccessibilityDelegate mPanelDelegate = new AccessibilityDelegate() {
@@ -300,6 +322,7 @@
 
         // Unregister observer before removing view
         mSecureSettings.unregisterContentObserver(mMagnificationCapabilityObserver);
+        mSecureSettings.unregisterContentObserver(mMagnificationScaleObserver);
         mWindowManager.removeView(mSettingView);
         mIsVisible = false;
         if (resetPosition) {
@@ -311,6 +334,14 @@
         mCallback.onSettingsPanelVisibilityChanged(/* shown= */ false);
     }
 
+    public void toggleSettingsPanelVisibility() {
+        if (!mIsVisible) {
+            showSettingPanel();
+        } else {
+            hideSettingPanel();
+        }
+    }
+
     public void showSettingPanel() {
         showSettingPanel(true);
     }
@@ -320,7 +351,13 @@
     }
 
     public void setScaleSeekbar(float scale) {
-        setSeekbarProgress(scale);
+        int index = (int) ((scale - SCALE_MIN_VALUE) * mSeekBarMagnitude);
+        if (index < 0) {
+            index = 0;
+        } else if (index > mZoomSeekbar.getMax()) {
+            index = mZoomSeekbar.getMax();
+        }
+        mZoomSeekbar.setProgress(index);
     }
 
     private void transitToMagnificationMode(int mode) {
@@ -337,6 +374,7 @@
     private void showSettingPanel(boolean resetPosition) {
         if (!mIsVisible) {
             updateUIControlsIfNeeded();
+            setScaleSeekbar(getMagnificationScale());
             if (resetPosition) {
                 mDraggableWindowBounds.set(getDraggableWindowBounds());
                 mParams.x = mDraggableWindowBounds.right;
@@ -349,6 +387,10 @@
                     Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
                     mMagnificationCapabilityObserver,
                     UserHandle.USER_CURRENT);
+            mSecureSettings.registerContentObserverForUser(
+                    Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
+                    mMagnificationScaleObserver,
+                    UserHandle.USER_CURRENT);
 
             // Exclude magnification switch button from system gesture area.
             setSystemGestureExclusion();
@@ -368,22 +410,35 @@
     }
 
     private int getMagnificationMode() {
+        // If current capability is window mode, we would like the default value of the mode to
+        // be WINDOW, otherwise, the default value would be FULLSCREEN.
+        int defaultValue =
+                (getMagnificationCapability() == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW)
+                        ? ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW
+                        : ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
+
         return mSecureSettings.getIntForUser(
                 Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
-                ACCESSIBILITY_MAGNIFICATION_MODE_NONE,
+                defaultValue,
                 UserHandle.USER_CURRENT);
     }
 
     private int getMagnificationCapability() {
         return mSecureSettings.getIntForUser(
                 Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
-                ACCESSIBILITY_MAGNIFICATION_MODE_NONE,
+                ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN,
+                UserHandle.USER_CURRENT);
+    }
+
+    private float getMagnificationScale() {
+        return mSecureSettings.getFloatForUser(
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
+                SCALE_MIN_VALUE,
                 UserHandle.USER_CURRENT);
     }
 
     private void updateUIControlsIfNeeded() {
         int capability = getMagnificationCapability();
-
         int selectedButtonIndex = mLastSelectedButtonIndex;
         switch (capability) {
             case ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW:
@@ -444,14 +499,6 @@
         }
     };
 
-    private void setSeekbarProgress(float scale) {
-        int index = (int) ((scale - A11Y_SCALE_MIN_VALUE) / A11Y_CHANGE_SCALE_DIFFERENCE);
-        if (index < 0) {
-            index = 0;
-        }
-        mZoomSeekbar.setProgress(index);
-    }
-
     void inflateView() {
         mSettingView = (LinearLayout) View.inflate(mContext,
                 R.layout.window_magnification_settings_view, null);
@@ -473,11 +520,14 @@
                 mSettingView.findViewById(R.id.magnifier_horizontal_lock_title);
 
         mZoomSeekbar = mSettingView.findViewById(R.id.magnifier_zoom_slider);
+        mZoomSeekbar.setMax((int) (mZoomSeekbar.getChangeMagnitude()
+                * (SCALE_MAX_VALUE - SCALE_MIN_VALUE)));
+        mSeekBarMagnitude = mZoomSeekbar.getChangeMagnitude();
         float scale = mSecureSettings.getFloatForUser(
                 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 0,
                 UserHandle.USER_CURRENT);
-        setSeekbarProgress(scale);
-        mZoomSeekbar.setOnSeekBarChangeListener(new ZoomSeekbarChangeListener());
+        setScaleSeekbar(scale);
+        mZoomSeekbar.setOnSeekBarWithIconButtonsChangeListener(new ZoomSeekbarChangeListener());
 
         mAllowDiagonalScrollingView =
                 (LinearLayout) mSettingView.findViewById(R.id.magnifier_horizontal_lock_view);
@@ -521,7 +571,6 @@
             // CONFIG_FONT_SCALE: font size change
             // CONFIG_LOCALE: language change
             // CONFIG_DENSITY: display size change
-
             mParams.accessibilityTitle = getAccessibilityWindowTitle(mContext);
 
             boolean showSettingPanelAfterConfigChange = mIsVisible;
@@ -533,16 +582,13 @@
             return;
         }
 
-        if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) {
-            final Rect previousDraggableBounds = new Rect(mDraggableWindowBounds);
+        if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0
+                || (configDiff & ActivityInfo.CONFIG_SCREEN_SIZE) != 0) {
             mDraggableWindowBounds.set(getDraggableWindowBounds());
-            // Keep the Y position with the same height ratio before the window bounds and
-            // draggable bounds are changed.
-            final float windowHeightFraction = (float) (mParams.y - previousDraggableBounds.top)
-                    / previousDraggableBounds.height();
-            mParams.y = (int) (windowHeightFraction * mDraggableWindowBounds.height())
-                    + mDraggableWindowBounds.top;
-            return;
+            // reset the panel position to the right-bottom corner
+            mParams.x = mDraggableWindowBounds.right;
+            mParams.y = mDraggableWindowBounds.bottom;
+            updateButtonViewLayoutIfNeeded();
         }
     }
 
@@ -554,7 +600,8 @@
         mDraggableWindowBounds.set(newBounds);
     }
 
-    private void updateButtonViewLayoutIfNeeded() {
+    @VisibleForTesting
+    void updateButtonViewLayoutIfNeeded() {
         if (mIsVisible) {
             mParams.x = MathUtils.constrain(mParams.x, mDraggableWindowBounds.left,
                     mDraggableWindowBounds.right);
@@ -595,7 +642,7 @@
 
     @VisibleForTesting
     void setDiagonalScrolling(boolean enabled) {
-        Settings.Secure.putIntForUser(mContext.getContentResolver(),
+        mSecureSettings.putIntForUser(
                 Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, enabled ? 1 : 0,
                 UserHandle.USER_CURRENT);
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
index 7f4e7844..225a2f7 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
@@ -25,7 +25,7 @@
 import androidx.dynamicanimation.animation.DynamicAnimation;
 
 import com.android.systemui.R;
-import com.android.wm.shell.bubbles.DismissView;
+import com.android.wm.shell.common.bubbles.DismissView;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index 3f41a76..6ba40d6 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -61,7 +61,8 @@
 import com.android.internal.util.Preconditions;
 import com.android.systemui.R;
 import com.android.systemui.util.settings.SecureSettings;
-import com.android.wm.shell.bubbles.DismissView;
+import com.android.wm.shell.bubbles.DismissViewUtils;
+import com.android.wm.shell.common.bubbles.DismissView;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 
 import java.lang.annotation.Retention;
@@ -184,6 +185,7 @@
         mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage);
         mMenuAnimationController.setSpringAnimationsEndAction(this::onSpringAnimationsEndAction);
         mDismissView = new DismissView(context);
+        DismissViewUtils.setup(mDismissView);
         mDismissAnimationController = new DismissAnimationController(mDismissView, mMenuView);
         mDismissAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() {
             @Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
index e79b3f4..783460c 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
@@ -26,14 +26,16 @@
 import android.view.LayoutInflater
 import android.widget.Button
 import android.widget.SeekBar
-import android.widget.SeekBar.OnSeekBarChangeListener
 import android.widget.TextView
 import androidx.annotation.MainThread
 import androidx.annotation.WorkerThread
 import com.android.systemui.R
 import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView
+import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView.OnSeekBarWithIconButtonsChangeListener
+import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView.OnSeekBarWithIconButtonsChangeListener.ControlUnitType
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.settings.SecureSettings
@@ -48,6 +50,7 @@
     private val systemSettings: SystemSettings,
     private val secureSettings: SecureSettings,
     private val systemClock: SystemClock,
+    private val userTracker: UserTracker,
     @Main mainHandler: Handler,
     @Background private val backgroundDelayableExecutor: DelayableExecutor
 ) : SystemUIDialog(context) {
@@ -98,32 +101,37 @@
 
         seekBarWithIconButtonsView.setMax((strEntryValues).size - 1)
 
-        val currentScale = systemSettings.getFloat(Settings.System.FONT_SCALE, 1.0f)
+        val currentScale =
+            systemSettings.getFloatForUser(Settings.System.FONT_SCALE, 1.0f, userTracker.userId)
         lastProgress.set(fontSizeValueToIndex(currentScale))
         seekBarWithIconButtonsView.setProgress(lastProgress.get())
 
-        seekBarWithIconButtonsView.setOnSeekBarChangeListener(
-            object : OnSeekBarChangeListener {
-                var isTrackingTouch = false
-
+        seekBarWithIconButtonsView.setOnSeekBarWithIconButtonsChangeListener(
+            object : OnSeekBarWithIconButtonsChangeListener {
                 override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
                     // Always provide preview configuration for text first when there is a change
                     // in the seekbar progress.
                     createTextPreview(progress)
-
-                    if (!isTrackingTouch) {
-                        // The seekbar progress is changed by icon buttons
-                        changeFontSize(progress, CHANGE_BY_BUTTON_DELAY_MS)
-                    }
                 }
 
                 override fun onStartTrackingTouch(seekBar: SeekBar) {
-                    isTrackingTouch = true
+                    // Do nothing
                 }
 
                 override fun onStopTrackingTouch(seekBar: SeekBar) {
-                    isTrackingTouch = false
-                    changeFontSize(seekBar.progress, CHANGE_BY_SEEKBAR_DELAY_MS)
+                    // Do nothing
+                }
+
+                override fun onUserInteractionFinalized(
+                    seekBar: SeekBar,
+                    @ControlUnitType control: Int
+                ) {
+                    if (control == ControlUnitType.BUTTON) {
+                        // The seekbar progress is changed by icon buttons
+                        changeFontSize(seekBar.progress, CHANGE_BY_BUTTON_DELAY_MS)
+                    } else {
+                        changeFontSize(seekBar.progress, CHANGE_BY_SEEKBAR_DELAY_MS)
+                    }
                 }
             }
         )
@@ -195,18 +203,25 @@
 
     @WorkerThread
     fun updateFontScale() {
-        systemSettings.putString(Settings.System.FONT_SCALE, strEntryValues[lastProgress.get()])
+        systemSettings.putStringForUser(
+            Settings.System.FONT_SCALE,
+            strEntryValues[lastProgress.get()],
+            userTracker.userId
+        )
     }
 
     @WorkerThread
     fun updateSecureSettingsIfNeeded() {
         if (
-            secureSettings.getString(Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED) !=
-                ON
-        ) {
-            secureSettings.putString(
+            secureSettings.getStringForUser(
                 Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED,
-                ON
+                userTracker.userId
+            ) != ON
+        ) {
+            secureSettings.putStringForUser(
+                Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED,
+                ON,
+                userTracker.userId
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
index 665a398..2b83e6b 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
@@ -105,6 +105,8 @@
             AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS;
     public static final int INVOCATION_TYPE_POWER_BUTTON_LONG_PRESS =
             AssistUtils.INVOCATION_TYPE_POWER_BUTTON_LONG_PRESS;
+    public static final int INVOCATION_TYPE_NAV_HANDLE_LONG_PRESS =
+            AssistUtils.INVOCATION_TYPE_NAV_HANDLE_LONG_PRESS;
 
     public static final int DISMISS_REASON_INVOCATION_CANCELLED = 1;
     public static final int DISMISS_REASON_TAP = 2;
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt b/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt
index 7c394a6..6128d91 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt
@@ -16,8 +16,11 @@
 
 package com.android.systemui.authentication
 
+import com.android.keyguard.KeyguardSecurityModel
 import com.android.systemui.authentication.data.repository.AuthenticationRepositoryModule
 import dagger.Module
+import dagger.Provides
+import java.util.function.Function
 
 @Module(
     includes =
@@ -25,4 +28,12 @@
             AuthenticationRepositoryModule::class,
         ],
 )
-object AuthenticationModule
+object AuthenticationModule {
+
+    @Provides
+    fun getSecurityMode(
+        model: KeyguardSecurityModel,
+    ): Function<Int, KeyguardSecurityModel.SecurityMode> {
+        return Function { userId -> model.getSecurityMode(userId) }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index c684dc5..0b25184 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -16,13 +16,25 @@
 
 package com.android.systemui.authentication.data.repository
 
+import com.android.internal.widget.LockPatternUtils
+import com.android.keyguard.KeyguardSecurityModel
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+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.user.data.repository.UserRepository
 import dagger.Binds
 import dagger.Module
+import java.util.function.Function
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
 
 /** Defines interface for classes that can access authentication-related application state. */
 interface AuthenticationRepository {
@@ -38,12 +50,6 @@
     val isUnlocked: StateFlow<Boolean>
 
     /**
-     * The currently-configured authentication method. This determines how the authentication
-     * challenge is completed in order to unlock an otherwise locked device.
-     */
-    val authenticationMethod: StateFlow<AuthenticationMethodModel>
-
-    /**
      * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
      * dismisses once the authentication challenge is completed. For example, completing a biometric
      * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
@@ -57,11 +63,11 @@
      */
     val failedAuthenticationAttempts: StateFlow<Int>
 
-    /** See [isUnlocked]. */
-    fun setUnlocked(isUnlocked: Boolean)
-
-    /** See [authenticationMethod]. */
-    fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel)
+    /**
+     * Returns the currently-configured authentication method. This determines how the
+     * authentication challenge is completed in order to unlock an otherwise locked device.
+     */
+    suspend fun getAuthenticationMethod(): AuthenticationMethodModel
 
     /** See [isBypassEnabled]. */
     fun setBypassEnabled(isBypassEnabled: Boolean)
@@ -70,16 +76,23 @@
     fun setFailedAuthenticationAttempts(failedAuthenticationAttempts: Int)
 }
 
-class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationRepository {
-    // TODO(b/280883900): get data from real data sources in SysUI.
+class AuthenticationRepositoryImpl
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    private val getSecurityMode: Function<Int, KeyguardSecurityModel.SecurityMode>,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val userRepository: UserRepository,
+    private val lockPatternUtils: LockPatternUtils,
+    keyguardRepository: KeyguardRepository,
+) : AuthenticationRepository {
 
-    private val _isUnlocked = MutableStateFlow(false)
-    override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow()
-
-    private val _authenticationMethod =
-        MutableStateFlow<AuthenticationMethodModel>(AuthenticationMethodModel.PIN(1234))
-    override val authenticationMethod: StateFlow<AuthenticationMethodModel> =
-        _authenticationMethod.asStateFlow()
+    override val isUnlocked: StateFlow<Boolean> =
+        keyguardRepository.isKeyguardUnlocked.stateIn(
+            scope = applicationScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = false,
+        )
 
     private val _isBypassEnabled = MutableStateFlow(false)
     override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow()
@@ -88,18 +101,45 @@
     override val failedAuthenticationAttempts: StateFlow<Int> =
         _failedAuthenticationAttempts.asStateFlow()
 
-    override fun setUnlocked(isUnlocked: Boolean) {
-        _isUnlocked.value = isUnlocked
+    override suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
+        return withContext(backgroundDispatcher) {
+            val selectedUserId = userRepository.getSelectedUserInfo().id
+            when (getSecurityMode.apply(selectedUserId)) {
+                KeyguardSecurityModel.SecurityMode.PIN,
+                KeyguardSecurityModel.SecurityMode.SimPin ->
+                    AuthenticationMethodModel.Pin(
+                        code = listOf(1, 2, 3, 4), // TODO(b/280883900): remove this
+                        autoConfirm = lockPatternUtils.isAutoPinConfirmEnabled(selectedUserId),
+                    )
+                KeyguardSecurityModel.SecurityMode.Password,
+                KeyguardSecurityModel.SecurityMode.SimPuk ->
+                    AuthenticationMethodModel.Password(
+                        password = "password", // TODO(b/280883900): remove this
+                    )
+                KeyguardSecurityModel.SecurityMode.Pattern ->
+                    AuthenticationMethodModel.Pattern(
+                        coordinates =
+                            listOf(
+                                AuthenticationMethodModel.Pattern.PatternCoordinate(2, 0),
+                                AuthenticationMethodModel.Pattern.PatternCoordinate(2, 1),
+                                AuthenticationMethodModel.Pattern.PatternCoordinate(2, 2),
+                                AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1),
+                                AuthenticationMethodModel.Pattern.PatternCoordinate(0, 0),
+                                AuthenticationMethodModel.Pattern.PatternCoordinate(0, 1),
+                                AuthenticationMethodModel.Pattern.PatternCoordinate(0, 2),
+                            ), // TODO(b/280883900): remove this
+                    )
+                KeyguardSecurityModel.SecurityMode.None -> AuthenticationMethodModel.None
+                KeyguardSecurityModel.SecurityMode.Invalid -> error("Invalid security mode!")
+                null -> error("Invalid security is null!")
+            }
+        }
     }
 
     override fun setBypassEnabled(isBypassEnabled: Boolean) {
         _isBypassEnabled.value = isBypassEnabled
     }
 
-    override fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
-        _authenticationMethod.value = authenticationMethod
-    }
-
     override fun setFailedAuthenticationAttempts(failedAuthenticationAttempts: Int) {
         _failedAuthenticationAttempts.value = failedAuthenticationAttempts
     }
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 3984627..15e579d 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.authentication.domain.interactor
 
+import android.app.admin.DevicePolicyManager
 import com.android.systemui.authentication.data.repository.AuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.dagger.SysUISingleton
@@ -24,9 +25,8 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
 
 /** Hosts application business logic related to authentication. */
 @SysUISingleton
@@ -37,12 +37,6 @@
     private val repository: AuthenticationRepository,
 ) {
     /**
-     * The currently-configured authentication method. This determines how the authentication
-     * challenge is completed in order to unlock an otherwise locked device.
-     */
-    val authenticationMethod: StateFlow<AuthenticationMethodModel> = repository.authenticationMethod
-
-    /**
      * Whether the device is unlocked.
      *
      * A device that is not yet unlocked requires unlocking by completing an authentication
@@ -51,20 +45,18 @@
      * Note that this state has no real bearing on whether the lock screen is showing or dismissed.
      */
     val isUnlocked: StateFlow<Boolean> =
-        combine(authenticationMethod, repository.isUnlocked) { authMethod, isUnlocked ->
-                isUnlockedWithAuthMethod(
-                    isUnlocked = isUnlocked,
-                    authMethod = authMethod,
-                )
+        repository.isUnlocked
+            .map { isUnlocked ->
+                if (getAuthenticationMethod() is AuthenticationMethodModel.None) {
+                    true
+                } else {
+                    isUnlocked
+                }
             }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.Eagerly,
-                initialValue =
-                    isUnlockedWithAuthMethod(
-                        isUnlocked = repository.isUnlocked.value,
-                        authMethod = repository.authenticationMethod.value,
-                    )
+                initialValue = true,
             )
 
     /**
@@ -81,55 +73,56 @@
      */
     val failedAuthenticationAttempts: StateFlow<Int> = repository.failedAuthenticationAttempts
 
-    init {
-        // UNLOCKS WHEN AUTH METHOD REMOVED.
-        //
-        // Unlocks the device if the auth method becomes None.
-        applicationScope.launch {
-            repository.authenticationMethod.collect {
-                if (it is AuthenticationMethodModel.None) {
-                    unlockDevice()
-                }
-            }
-        }
+    /**
+     * Returns the currently-configured authentication method. This determines how the
+     * authentication challenge is completed in order to unlock an otherwise locked device.
+     */
+    suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
+        return repository.getAuthenticationMethod()
     }
 
     /**
      * Returns `true` if the device currently requires authentication before content can be viewed;
      * `false` if content can be displayed without unlocking first.
      */
-    fun isAuthenticationRequired(): Boolean {
-        return !isUnlocked.value && authenticationMethod.value.isSecure
-    }
-
-    /**
-     * Unlocks the device, assuming that the authentication challenge has been completed
-     * successfully.
-     */
-    fun unlockDevice() {
-        repository.setUnlocked(true)
-    }
-
-    /**
-     * Locks the device. From now on, the device will remain locked until [authenticate] is called
-     * with the correct input.
-     */
-    fun lockDevice() {
-        repository.setUnlocked(false)
+    suspend fun isAuthenticationRequired(): Boolean {
+        return !isUnlocked.value && getAuthenticationMethod().isSecure
     }
 
     /**
      * Attempts to authenticate the user and unlock the device.
      *
+     * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method
+     * supports auto-confirming, and the input's length is at least the code's length. Otherwise,
+     * `null` is returned.
+     *
      * @param input The input from the user to try to authenticate with. This can be a list of
      *   different things, based on the current authentication method.
-     * @return `true` if the authentication succeeded and the device is now unlocked; `false`
-     *   otherwise.
+     * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit
+     *   request to validate.
+     * @return `true` if the authentication succeeded and the device is now unlocked; `false` when
+     *   authentication failed, `null` if the check was not performed.
      */
-    fun authenticate(input: List<Any>): Boolean {
+    suspend fun authenticate(input: List<Any>, tryAutoConfirm: Boolean = false): Boolean? {
+        val authMethod = getAuthenticationMethod()
+        if (tryAutoConfirm) {
+            if ((authMethod as? AuthenticationMethodModel.Pin)?.autoConfirm != true) {
+                // Do not attempt to authenticate unless the PIN lock is set to auto-confirm.
+                return null
+            }
+
+            if (input.size < authMethod.code.size) {
+                // Do not attempt to authenticate if the PIN has not yet the required amount of
+                // digits. This intentionally only skip for shorter PINs; if the PIN is longer, the
+                // layer above might have throttled this check, and the PIN should be rejected via
+                // the auth code below.
+                return null
+            }
+        }
+
         val isSuccessful =
-            when (val authMethod = this.authenticationMethod.value) {
-                is AuthenticationMethodModel.PIN -> input.asCode() == authMethod.code
+            when (authMethod) {
+                is AuthenticationMethodModel.Pin -> input.asCode() == authMethod.code
                 is AuthenticationMethodModel.Password -> input.asPassword() == authMethod.password
                 is AuthenticationMethodModel.Pattern -> input.asPattern() == authMethod.coordinates
                 else -> true
@@ -137,7 +130,6 @@
 
         if (isSuccessful) {
             repository.setFailedAuthenticationAttempts(0)
-            repository.setUnlocked(true)
         } else {
             repository.setFailedAuthenticationAttempts(
                 repository.failedAuthenticationAttempts.value + 1
@@ -147,47 +139,27 @@
         return isSuccessful
     }
 
-    /** Triggers a biometric-powered unlock of the device. */
-    fun biometricUnlock() {
-        // TODO(b/280883900): only allow this if the biometric is enabled and there's a match.
-        repository.setUnlocked(true)
-    }
-
-    /** See [authenticationMethod]. */
-    fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
-        repository.setAuthenticationMethod(authenticationMethod)
-    }
-
     /** See [isBypassEnabled]. */
     fun toggleBypassEnabled() {
         repository.setBypassEnabled(!repository.isBypassEnabled.value)
     }
 
     companion object {
-        private fun isUnlockedWithAuthMethod(
-            isUnlocked: Boolean,
-            authMethod: AuthenticationMethodModel,
-        ): Boolean {
-            return if (authMethod is AuthenticationMethodModel.None) {
-                true
-            } else {
-                isUnlocked
-            }
-        }
-
         /**
          * Returns a PIN code from the given list. It's assumed the given list elements are all
-         * [Int].
+         * [Int] in the range [0-9].
          */
-        private fun List<Any>.asCode(): Int? {
-            if (isEmpty()) {
+        private fun List<Any>.asCode(): List<Int>? {
+            if (isEmpty() || size > DevicePolicyManager.MAX_PASSWORD_LENGTH) {
                 return null
             }
 
-            var code = 0
-            map { it as Int }.forEach { integer -> code = code * 10 + integer }
-
-            return code
+            return map {
+                require(it is Int && it in 0..9) {
+                    "Pin is required to be Int in range [0..9], but got $it"
+                }
+                it
+            }
         }
 
         /**
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
index 6f008c3..1016b6b 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.authentication.shared.model
 
+import androidx.annotation.VisibleForTesting
+
 /** Enumerates all known authentication methods. */
 sealed class AuthenticationMethodModel(
     /**
@@ -32,7 +34,22 @@
     /** The most basic authentication method. The lock screen can be swiped away when displayed. */
     object Swipe : AuthenticationMethodModel(isSecure = false)
 
-    data class PIN(val code: Int) : AuthenticationMethodModel(isSecure = true)
+    /**
+     * Authentication method using a PIN.
+     *
+     * In practice, a pin is restricted to 16 decimal digits , see
+     * [android.app.admin.DevicePolicyManager.MAX_PASSWORD_LENGTH]
+     */
+    data class Pin(val code: List<Int>, val autoConfirm: Boolean) :
+        AuthenticationMethodModel(isSecure = true) {
+
+        /** Convenience constructor for tests only. */
+        @VisibleForTesting
+        constructor(
+            code: Long,
+            autoConfirm: Boolean = false
+        ) : this(code.toString(10).map { it - '0' }, autoConfirm) {}
+    }
 
     data class Password(val password: String) : AuthenticationMethodModel(isSecure = true)
 
diff --git a/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt
new file mode 100644
index 0000000..3c74bf4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.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 this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.back.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.QuickSettingsController
+import com.android.systemui.shade.ShadeController
+import com.android.systemui.shade.ShadeViewController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
+import javax.inject.Inject
+
+/** Handles requests to go back either from a button or gesture. */
+@SysUISingleton
+class BackActionInteractor
+@Inject
+constructor(
+    private val statusBarStateController: StatusBarStateController,
+    private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
+    private val shadeController: ShadeController
+) {
+    private lateinit var shadeViewController: ShadeViewController
+    private lateinit var qsController: QuickSettingsController
+
+    fun setup(qsController: QuickSettingsController, svController: ShadeViewController) {
+        this.qsController = qsController
+        this.shadeViewController = svController
+    }
+
+    fun shouldBackBeHandled(): Boolean {
+        return statusBarStateController.state != StatusBarState.KEYGUARD &&
+            statusBarStateController.state != StatusBarState.SHADE_LOCKED &&
+            !statusBarKeyguardViewManager.isBouncerShowingOverDream
+    }
+
+    fun onBackRequested(): Boolean {
+        if (statusBarKeyguardViewManager.canHandleBackPressed()) {
+            statusBarKeyguardViewManager.onBackPressed()
+            return true
+        }
+        if (qsController.isCustomizing) {
+            qsController.closeQsCustomizer()
+            return true
+        }
+        if (qsController.expanded) {
+            shadeViewController.animateCollapseQs(false)
+            return true
+        }
+        if (shadeViewController.closeUserSwitcherIfOpen()) {
+            return true
+        }
+        if (shouldBackBeHandled()) {
+            if (shadeViewController.canBeCollapsed()) {
+                // this is the Shade dismiss animation, so make sure QQS closes when it ends.
+                shadeViewController.onBackPressed()
+                shadeController.animateCollapseShade()
+            }
+            return true
+        }
+        return false
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index 98dd838..f5f47d0 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -350,7 +350,7 @@
                 updatePercentText();
                 addView(mBatteryPercentView, new LayoutParams(
                         LayoutParams.WRAP_CONTENT,
-                        LayoutParams.MATCH_PARENT));
+                        LayoutParams.WRAP_CONTENT));
             }
         } else {
             if (showing) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 5b746f1b..76e48e9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -201,7 +201,9 @@
     final TaskStackListener mTaskStackListener = new TaskStackListener() {
         @Override
         public void onTaskStackChanged() {
-            mHandler.post(AuthController.this::cancelIfOwnerIsNotInForeground);
+            if (!isOwnerInForeground()) {
+                mHandler.post(AuthController.this::cancelIfOwnerIsNotInForeground);
+            }
         }
     };
 
@@ -239,33 +241,39 @@
         }
     }
 
+    private boolean isOwnerInForeground() {
+        if (mCurrentDialog != null) {
+            final String clientPackage = mCurrentDialog.getOpPackageName();
+            final List<ActivityManager.RunningTaskInfo> runningTasks =
+                    mActivityTaskManager.getTasks(1);
+            if (!runningTasks.isEmpty()) {
+                final String topPackage = runningTasks.get(0).topActivity.getPackageName();
+                if (!topPackage.contentEquals(clientPackage)
+                        && !Utils.isSystem(mContext, clientPackage)) {
+                    Log.w(TAG, "Evicting client due to: " + topPackage);
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
     private void cancelIfOwnerIsNotInForeground() {
         mExecution.assertIsMainThread();
         if (mCurrentDialog != null) {
             try {
-                final String clientPackage = mCurrentDialog.getOpPackageName();
-                Log.w(TAG, "Task stack changed, current client: " + clientPackage);
-                final List<ActivityManager.RunningTaskInfo> runningTasks =
-                        mActivityTaskManager.getTasks(1);
-                if (!runningTasks.isEmpty()) {
-                    final String topPackage = runningTasks.get(0).topActivity.getPackageName();
-                    if (!topPackage.contentEquals(clientPackage)
-                            && !Utils.isSystem(mContext, clientPackage)) {
-                        Log.e(TAG, "Evicting client due to: " + topPackage);
-                        mCurrentDialog.dismissWithoutCallback(true /* animate */);
-                        mCurrentDialog = null;
+                mCurrentDialog.dismissWithoutCallback(true /* animate */);
+                mCurrentDialog = null;
 
-                        for (Callback cb : mCallbacks) {
-                            cb.onBiometricPromptDismissed();
-                        }
+                for (Callback cb : mCallbacks) {
+                    cb.onBiometricPromptDismissed();
+                }
 
-                        if (mReceiver != null) {
-                            mReceiver.onDialogDismissed(
-                                    BiometricPrompt.DISMISSED_REASON_USER_CANCEL,
-                                    null /* credentialAttestation */);
-                            mReceiver = null;
-                        }
-                    }
+                if (mReceiver != null) {
+                    mReceiver.onDialogDismissed(
+                            BiometricPrompt.DISMISSED_REASON_USER_CANCEL,
+                            null /* credentialAttestation */);
+                    mReceiver = null;
                 }
             } catch (RemoteException e) {
                 Log.e(TAG, "Remote exception", e);
@@ -1268,10 +1276,11 @@
             cb.onBiometricPromptShown();
         }
         mCurrentDialog = newDialog;
-        mCurrentDialog.show(mWindowManager, savedState);
 
-        if (!promptInfo.isAllowBackgroundAuthentication()) {
-            mHandler.post(this::cancelIfOwnerIsNotInForeground);
+        if (!promptInfo.isAllowBackgroundAuthentication() && !isOwnerInForeground()) {
+            cancelIfOwnerIsNotInForeground();
+        } else {
+            mCurrentDialog.show(mWindowManager, savedState);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index 6f0f633..946ddba 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -37,7 +37,7 @@
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.WakefulnessLifecycle
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.CircleReveal
 import com.android.systemui.statusbar.LiftReveal
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index d48b9c33..9d0cde1 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -54,11 +54,11 @@
 import com.android.systemui.Dumpable
 import com.android.systemui.R
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 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.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.util.boundsOnScreen
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.traceSection
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
index e6aeb43..802eea3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
@@ -37,4 +37,8 @@
     dumpManager
 ) {
     override val tag = "UdfpsBpViewController"
+
+    override fun shouldPauseAuth(): Boolean {
+        return false
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index aff08b6..ebff0b0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -75,6 +75,8 @@
 import com.android.systemui.biometrics.udfps.SinglePointerTouchProcessor;
 import com.android.systemui.biometrics.udfps.TouchProcessor;
 import com.android.systemui.biometrics.udfps.TouchProcessorResult;
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.doze.DozeReceiver;
@@ -82,9 +84,9 @@
 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.KeyguardFaceAuthInteractor;
-import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
+import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter;
+import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -153,6 +155,7 @@
     @NonNull private final SystemUIDialogManager mDialogManager;
     @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @NonNull private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor;
+    @NonNull private final Provider<UdfpsKeyguardViewModels> mUdfpsKeyguardViewModels;
     @NonNull private final VibratorHelper mVibrator;
     @NonNull private final FeatureFlags mFeatureFlags;
     @NonNull private final FalsingManager mFalsingManager;
@@ -173,6 +176,7 @@
     @NonNull private final SecureSettings mSecureSettings;
     @NonNull private final UdfpsUtils mUdfpsUtils;
     @NonNull private final InputManager mInputManager;
+    @NonNull private final UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate;
     private final boolean mIgnoreRefreshRate;
 
     // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple
@@ -259,10 +263,6 @@
         @Override
         public void showUdfpsOverlay(long requestId, int sensorId, int reason,
                 @NonNull IUdfpsOverlayControllerCallback callback) {
-            if (mFeatureFlags.isEnabled(Flags.NEW_UDFPS_OVERLAY)) {
-                return;
-            }
-
             mFgExecutor.execute(() -> UdfpsController.this.showUdfpsOverlay(
                     new UdfpsControllerOverlay(mContext, mFingerprintManager, mInflater,
                             mWindowManager, mAccessibilityManager, mStatusBarStateController,
@@ -274,15 +274,13 @@
                             mUdfpsDisplayMode, mSecureSettings, requestId, reason, callback,
                             (view, event, fromUdfpsView) -> onTouch(requestId, event,
                                     fromUdfpsView), mActivityLaunchAnimator, mFeatureFlags,
-                            mPrimaryBouncerInteractor, mAlternateBouncerInteractor, mUdfpsUtils)));
+                            mPrimaryBouncerInteractor, mAlternateBouncerInteractor, mUdfpsUtils,
+                            mUdfpsKeyguardAccessibilityDelegate,
+                            mUdfpsKeyguardViewModels)));
         }
 
         @Override
         public void hideUdfpsOverlay(int sensorId) {
-            if (mFeatureFlags.isEnabled(Flags.NEW_UDFPS_OVERLAY)) {
-                return;
-            }
-
             mFgExecutor.execute(() -> {
                 if (mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) {
                     // if we get here, we expect keyguardUpdateMonitor's fingerprintRunningState
@@ -597,6 +595,13 @@
 
                 // Pilfer if valid overlap, don't allow following events to reach keyguard
                 shouldPilfer = true;
+
+                // Touch is a valid UDFPS touch. Inform the falsing manager so that the touch
+                // isn't counted against the falsing algorithm as an accidental touch.
+                // We do this on the DOWN event instead of CANCEL/UP because the CANCEL/UP events
+                // get sent too late to this receiver (after the actual cancel/up motions occur),
+                // and therefore wouldn't end up being used as part of the falsing algo.
+                mFalsingManager.isFalseTouch(UDFPS_AUTHENTICATION);
                 break;
 
             case UP:
@@ -616,7 +621,6 @@
                         data.getTime(),
                         data.getGestureStart(),
                         mStatusBarStateController.isDozing());
-                mFalsingManager.isFalseTouch(UDFPS_AUTHENTICATION);
                 break;
 
             case UNCHANGED:
@@ -790,7 +794,7 @@
     private boolean shouldTryToDismissKeyguard() {
         return mOverlay != null
                 && mOverlay.getAnimationViewController()
-                instanceof UdfpsKeyguardViewControllerLegacy
+                instanceof UdfpsKeyguardViewControllerAdapter
                 && mKeyguardStateController.canDismissLockScreen()
                 && !mAttemptedToDismissKeyguard;
     }
@@ -834,7 +838,9 @@
             @NonNull SecureSettings secureSettings,
             @NonNull InputManager inputManager,
             @NonNull UdfpsUtils udfpsUtils,
-            @NonNull KeyguardFaceAuthInteractor keyguardFaceAuthInteractor) {
+            @NonNull KeyguardFaceAuthInteractor keyguardFaceAuthInteractor,
+            @NonNull UdfpsKeyguardAccessibilityDelegate udfpsKeyguardAccessibilityDelegate,
+            @NonNull Provider<UdfpsKeyguardViewModels> udfpsKeyguardViewModelsProvider) {
         mContext = context;
         mExecution = execution;
         mVibrator = vibrator;
@@ -880,6 +886,7 @@
         mSecureSettings = secureSettings;
         mUdfpsUtils = udfpsUtils;
         mInputManager = inputManager;
+        mUdfpsKeyguardAccessibilityDelegate = udfpsKeyguardAccessibilityDelegate;
 
         mTouchProcessor = mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)
                 ? singlePointerTouchProcessor : null;
@@ -899,6 +906,7 @@
                     return Unit.INSTANCE;
                 });
         mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor;
+        mUdfpsKeyguardViewModels = udfpsKeyguardViewModelsProvider;
 
         final UdfpsOverlayController mUdfpsOverlayController = new UdfpsOverlayController();
         mFingerprintManager.setUdfpsOverlayController(mUdfpsOverlayController);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index cabe900..d6ef94d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -46,15 +46,19 @@
 import androidx.annotation.LayoutRes
 import androidx.annotation.VisibleForTesting
 import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.settingslib.udfps.UdfpsUtils
 import com.android.settingslib.udfps.UdfpsOverlayParams
+import com.android.settingslib.udfps.UdfpsUtils
 import com.android.systemui.R
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.biometrics.ui.controller.UdfpsKeyguardViewController
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 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.flags.Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS
+import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
+import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -64,6 +68,8 @@
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.settings.SecureSettings
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import javax.inject.Provider
 
 private const val TAG = "UdfpsControllerOverlay"
 
@@ -75,6 +81,7 @@
  * request. This state can persist across configuration changes via the [show] and [hide]
  * methods.
  */
+@ExperimentalCoroutinesApi
 @UiThread
 class UdfpsControllerOverlay @JvmOverloads constructor(
         private val context: Context,
@@ -103,7 +110,9 @@
         private val primaryBouncerInteractor: PrimaryBouncerInteractor,
         private val alternateBouncerInteractor: AlternateBouncerInteractor,
         private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
-        private val udfpsUtils: UdfpsUtils
+        private val udfpsUtils: UdfpsUtils,
+        private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
+        private val udfpsKeyguardViewModels: Provider<UdfpsKeyguardViewModels>,
 ) {
     /** The view, when [isShowing], or null. */
     var overlayView: UdfpsView? = null
@@ -242,26 +251,40 @@
                 )
             }
             REASON_AUTH_KEYGUARD -> {
-                UdfpsKeyguardViewControllerLegacy(
-                    view.addUdfpsView(R.layout.udfps_keyguard_view_legacy) {
-                        updateSensorLocation(sensorBounds)
-                    },
-                    statusBarStateController,
-                    shadeExpansionStateManager,
-                    statusBarKeyguardViewManager,
-                    keyguardUpdateMonitor,
-                    dumpManager,
-                    transitionController,
-                    configurationController,
-                    keyguardStateController,
-                    unlockedScreenOffAnimationController,
-                    dialogManager,
-                    controller,
-                    activityLaunchAnimator,
-                    featureFlags,
-                    primaryBouncerInteractor,
-                    alternateBouncerInteractor,
-                )
+                if (featureFlags.isEnabled(REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
+                    udfpsKeyguardViewModels.get().setSensorBounds(sensorBounds)
+                    UdfpsKeyguardViewController(
+                        view.addUdfpsView(R.layout.udfps_keyguard_view),
+                        statusBarStateController,
+                        shadeExpansionStateManager,
+                        dialogManager,
+                        dumpManager,
+                        alternateBouncerInteractor,
+                        udfpsKeyguardViewModels.get(),
+                    )
+                } else {
+                    UdfpsKeyguardViewControllerLegacy(
+                        view.addUdfpsView(R.layout.udfps_keyguard_view_legacy) {
+                            updateSensorLocation(sensorBounds)
+                        },
+                        statusBarStateController,
+                        shadeExpansionStateManager,
+                        statusBarKeyguardViewManager,
+                        keyguardUpdateMonitor,
+                        dumpManager,
+                        transitionController,
+                        configurationController,
+                        keyguardStateController,
+                        unlockedScreenOffAnimationController,
+                        dialogManager,
+                        controller,
+                        activityLaunchAnimator,
+                        featureFlags,
+                        primaryBouncerInteractor,
+                        alternateBouncerInteractor,
+                        udfpsKeyguardAccessibilityDelegate,
+                    )
+                }
             }
             REASON_AUTH_BP -> {
                 // note: empty controller, currently shows no visual affordance
@@ -413,7 +436,7 @@
     }
 
     private fun shouldRotate(animation: UdfpsAnimationViewController<*>?): Boolean {
-        if (animation !is UdfpsKeyguardViewControllerLegacy) {
+        if (animation !is UdfpsKeyguardViewControllerAdapter) {
             // always rotate view if we're not on the keyguard
             return true
         }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegate.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegate.kt
new file mode 100644
index 0000000..fb7b56e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegate.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.content.res.Resources
+import android.os.Bundle
+import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
+import javax.inject.Inject
+
+@SysUISingleton
+class UdfpsKeyguardAccessibilityDelegate
+@Inject
+constructor(
+    @Main private val resources: Resources,
+    private val keyguardViewManager: StatusBarKeyguardViewManager,
+) : View.AccessibilityDelegate() {
+    override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo) {
+        super.onInitializeAccessibilityNodeInfo(host, info)
+        val clickAction =
+            AccessibilityNodeInfo.AccessibilityAction(
+                AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
+                resources.getString(R.string.accessibility_bouncer)
+            )
+        info.addAction(clickAction)
+    }
+
+    override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
+        // when an a11y service is enabled, double tapping on the fingerprint sensor should
+        // show the primary bouncer
+        return if (action == AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id) {
+            keyguardViewManager.showPrimaryBouncer(/* scrimmed */ true)
+            true
+        } else super.performAccessibilityAction(host, action, args)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.kt
new file mode 100644
index 0000000..8cc15da
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.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.biometrics
+
+import android.content.Context
+import android.util.AttributeSet
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/** View corresponding with udfps_keyguard_view.xml */
+@ExperimentalCoroutinesApi
+class UdfpsKeyguardView(
+    context: Context,
+    attrs: AttributeSet?,
+) :
+    UdfpsAnimationView(
+        context,
+        attrs,
+    ) {
+    private val fingerprintDrawablePlaceHolder = UdfpsFpDrawable(context)
+    private var visible = false
+
+    override fun calculateAlpha(): Int {
+        return if (mPauseAuth) {
+            0
+        } else 255 // ViewModels handle animating alpha values
+    }
+
+    override fun getDrawable(): UdfpsDrawable {
+        return fingerprintDrawablePlaceHolder
+    }
+
+    fun useExpandedOverlay(useExpandedOverlay: Boolean) {
+        mUseExpandedOverlay = useExpandedOverlay
+    }
+
+    fun isVisible(): Boolean {
+        return visible
+    }
+
+    fun setVisible(isVisible: Boolean) {
+        visible = isVisible
+        isPauseAuth = !visible
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
index 3fc3e82..15bd731 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
@@ -20,6 +20,7 @@
 import android.content.res.Configuration
 import android.util.MathUtils
 import android.view.MotionEvent
+import android.view.View
 import androidx.annotation.VisibleForTesting
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
@@ -28,11 +29,12 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.R
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 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.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionListener
@@ -71,6 +73,7 @@
     featureFlags: FeatureFlags,
     private val primaryBouncerInteractor: PrimaryBouncerInteractor,
     private val alternateBouncerInteractor: AlternateBouncerInteractor,
+    private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
 ) :
     UdfpsAnimationViewController<UdfpsKeyguardViewLegacy>(
         view,
@@ -78,7 +81,8 @@
         shadeExpansionStateManager,
         systemUIDialogManager,
         dumpManager,
-    ) {
+    ),
+    UdfpsKeyguardViewControllerAdapter {
     private val useExpandedOverlay: Boolean =
         featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)
     private var showingUdfpsBouncer = false
@@ -300,7 +304,10 @@
         lockScreenShadeTransitionController.mUdfpsKeyguardViewControllerLegacy = this
         activityLaunchAnimator.addListener(activityLaunchAnimatorListener)
         view.mUseExpandedOverlay = useExpandedOverlay
-        view.startIconAsyncInflate()
+        view.startIconAsyncInflate {
+            (view.findViewById(R.id.udfps_animation_view_internal) as View).accessibilityDelegate =
+                udfpsKeyguardAccessibilityDelegate
+        }
     }
 
     override fun onViewDetached() {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java
index 056d692..b916810c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java
@@ -79,6 +79,7 @@
     private float mInterpolatedDarkAmount;
     private int mAnimationType = ANIMATION_NONE;
     private boolean mFullyInflated;
+    private Runnable mOnFinishInflateRunnable;
 
     public UdfpsKeyguardViewLegacy(Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
@@ -90,7 +91,12 @@
             .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
     }
 
-    public void startIconAsyncInflate() {
+    /**
+     * Inflate internal udfps view on a background thread and call the onFinishRunnable
+     * when inflation is finished.
+     */
+    public void startIconAsyncInflate(Runnable onFinishInflate) {
+        mOnFinishInflateRunnable = onFinishInflate;
         // inflate Lottie views on a background thread in case it takes a while to inflate
         AsyncLayoutInflater inflater = new AsyncLayoutInflater(mContext);
         inflater.inflate(R.layout.udfps_keyguard_view_internal, this,
@@ -330,6 +336,7 @@
                     frameInfo -> new PorterDuffColorFilter(mTextColorPrimary,
                             PorterDuff.Mode.SRC_ATOP)
             );
+            mOnFinishInflateRunnable.run();
         }
     };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt
index 39199d1..2102a1f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt
@@ -17,10 +17,10 @@
 package com.android.systemui.biometrics
 
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogLevel.ERROR
+import com.android.systemui.log.core.LogLevel.VERBOSE
+import com.android.systemui.log.core.LogLevel.WARNING
 import com.android.systemui.log.dagger.UdfpsLog
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.kt
new file mode 100644
index 0000000..2a9f3ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/controller/UdfpsKeyguardViewController.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 this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.ui.controller
+
+import com.android.systemui.biometrics.UdfpsAnimationViewController
+import com.android.systemui.biometrics.UdfpsKeyguardView
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
+import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.statusbar.phone.SystemUIDialogManager
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/** Class that coordinates non-HBM animations during keyguard authentication. */
+@ExperimentalCoroutinesApi
+open class UdfpsKeyguardViewController(
+    val view: UdfpsKeyguardView,
+    statusBarStateController: StatusBarStateController,
+    shadeExpansionStateManager: ShadeExpansionStateManager,
+    systemUIDialogManager: SystemUIDialogManager,
+    dumpManager: DumpManager,
+    private val alternateBouncerInteractor: AlternateBouncerInteractor,
+    udfpsKeyguardViewModels: UdfpsKeyguardViewModels,
+) :
+    UdfpsAnimationViewController<UdfpsKeyguardView>(
+        view,
+        statusBarStateController,
+        shadeExpansionStateManager,
+        systemUIDialogManager,
+        dumpManager,
+    ),
+    UdfpsKeyguardViewControllerAdapter {
+    override val tag: String
+        get() = TAG
+
+    init {
+        udfpsKeyguardViewModels.bindViews(view)
+    }
+
+    public override fun onViewAttached() {
+        super.onViewAttached()
+        alternateBouncerInteractor.setAlternateBouncerUIAvailable(true)
+    }
+
+    public override fun onViewDetached() {
+        super.onViewDetached()
+        alternateBouncerInteractor.setAlternateBouncerUIAvailable(false)
+    }
+
+    override fun shouldPauseAuth(): Boolean {
+        return !view.isVisible()
+    }
+
+    override fun listenForTouchesOutsideView(): Boolean {
+        return true
+    }
+
+    companion object {
+        private const val TAG = "UdfpsKeyguardViewController"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
index 96af42b..2a457eb 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.dagger.BluetoothLog
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt
new file mode 100644
index 0000000..3206c00
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.data.factory
+
+import android.annotation.IntDef
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardSecurityModel.SecurityMode
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_AFTER_LOCKOUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEVICE_ADMIN
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FACE_LOCKED_OUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FINGERPRINT_LOCKED_OUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FACE_INPUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NONE
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.R.string.bouncer_face_not_recognized
+import com.android.systemui.R.string.keyguard_enter_password
+import com.android.systemui.R.string.keyguard_enter_pattern
+import com.android.systemui.R.string.keyguard_enter_pin
+import com.android.systemui.R.string.kg_bio_too_many_attempts_password
+import com.android.systemui.R.string.kg_bio_too_many_attempts_pattern
+import com.android.systemui.R.string.kg_bio_too_many_attempts_pin
+import com.android.systemui.R.string.kg_bio_try_again_or_password
+import com.android.systemui.R.string.kg_bio_try_again_or_pattern
+import com.android.systemui.R.string.kg_bio_try_again_or_pin
+import com.android.systemui.R.string.kg_face_locked_out
+import com.android.systemui.R.string.kg_fp_locked_out
+import com.android.systemui.R.string.kg_fp_not_recognized
+import com.android.systemui.R.string.kg_primary_auth_locked_out_password
+import com.android.systemui.R.string.kg_primary_auth_locked_out_pattern
+import com.android.systemui.R.string.kg_primary_auth_locked_out_pin
+import com.android.systemui.R.string.kg_prompt_after_dpm_lock
+import com.android.systemui.R.string.kg_prompt_after_user_lockdown_password
+import com.android.systemui.R.string.kg_prompt_after_user_lockdown_pattern
+import com.android.systemui.R.string.kg_prompt_after_user_lockdown_pin
+import com.android.systemui.R.string.kg_prompt_auth_timeout
+import com.android.systemui.R.string.kg_prompt_password_auth_timeout
+import com.android.systemui.R.string.kg_prompt_pattern_auth_timeout
+import com.android.systemui.R.string.kg_prompt_pin_auth_timeout
+import com.android.systemui.R.string.kg_prompt_reason_restart_password
+import com.android.systemui.R.string.kg_prompt_reason_restart_pattern
+import com.android.systemui.R.string.kg_prompt_reason_restart_pin
+import com.android.systemui.R.string.kg_prompt_unattended_update
+import com.android.systemui.R.string.kg_too_many_failed_attempts_countdown
+import com.android.systemui.R.string.kg_trust_agent_disabled
+import com.android.systemui.R.string.kg_unlock_with_password_or_fp
+import com.android.systemui.R.string.kg_unlock_with_pattern_or_fp
+import com.android.systemui.R.string.kg_unlock_with_pin_or_fp
+import com.android.systemui.R.string.kg_wrong_input_try_fp_suggestion
+import com.android.systemui.R.string.kg_wrong_password_try_again
+import com.android.systemui.R.string.kg_wrong_pattern_try_again
+import com.android.systemui.R.string.kg_wrong_pin_try_again
+import com.android.systemui.bouncer.shared.model.BouncerMessageModel
+import com.android.systemui.bouncer.shared.model.Message
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+@SysUISingleton
+class BouncerMessageFactory
+@Inject
+constructor(
+    private val updateMonitor: KeyguardUpdateMonitor,
+    private val securityModel: KeyguardSecurityModel,
+) {
+
+    fun createFromPromptReason(
+        @BouncerPromptReason reason: Int,
+        userId: Int,
+    ): BouncerMessageModel? {
+        val pair =
+            getBouncerMessage(
+                reason,
+                securityModel.getSecurityMode(userId),
+                updateMonitor.isFingerprintAllowedInBouncer
+            )
+        return pair?.let {
+            BouncerMessageModel(
+                message = Message(messageResId = pair.first),
+                secondaryMessage = Message(messageResId = pair.second)
+            )
+        }
+    }
+
+    fun createFromString(
+        primaryMsg: String? = null,
+        secondaryMsg: String? = null
+    ): BouncerMessageModel =
+        BouncerMessageModel(
+            message = primaryMsg?.let { Message(message = it) },
+            secondaryMessage = secondaryMsg?.let { Message(message = it) },
+        )
+
+    /**
+     * Helper method that provides the relevant bouncer message that should be shown for different
+     * scenarios indicated by [reason]. [securityMode] & [fpAllowedInBouncer] parameters are used to
+     * provide a more specific message.
+     */
+    private fun getBouncerMessage(
+        @BouncerPromptReason reason: Int,
+        securityMode: SecurityMode,
+        fpAllowedInBouncer: Boolean = false
+    ): Pair<Int, Int>? {
+        return when (reason) {
+            PROMPT_REASON_RESTART -> authRequiredAfterReboot(securityMode)
+            PROMPT_REASON_TIMEOUT -> authRequiredAfterPrimaryAuthTimeout(securityMode)
+            PROMPT_REASON_DEVICE_ADMIN -> authRequiredAfterAdminLockdown(securityMode)
+            PROMPT_REASON_USER_REQUEST -> authRequiredAfterUserLockdown(securityMode)
+            PROMPT_REASON_AFTER_LOCKOUT -> biometricLockout(securityMode)
+            PROMPT_REASON_PREPARE_FOR_UPDATE -> authRequiredForUnattendedUpdate(securityMode)
+            PROMPT_REASON_FINGERPRINT_LOCKED_OUT -> fingerprintUnlockUnavailable(securityMode)
+            PROMPT_REASON_FACE_LOCKED_OUT -> faceUnlockUnavailable(securityMode)
+            PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT ->
+                if (fpAllowedInBouncer) incorrectSecurityInputWithFingerprint(securityMode)
+                else incorrectSecurityInput(securityMode)
+            PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT ->
+                if (fpAllowedInBouncer) nonStrongAuthTimeoutWithFingerprintAllowed(securityMode)
+                else nonStrongAuthTimeout(securityMode)
+            PROMPT_REASON_TRUSTAGENT_EXPIRED ->
+                if (fpAllowedInBouncer) trustAgentDisabledWithFingerprintAllowed(securityMode)
+                else trustAgentDisabled(securityMode)
+            PROMPT_REASON_INCORRECT_FACE_INPUT ->
+                if (fpAllowedInBouncer) incorrectFaceInputWithFingerprintAllowed(securityMode)
+                else incorrectFaceInput(securityMode)
+            PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT -> incorrectFingerprintInput(securityMode)
+            PROMPT_REASON_DEFAULT ->
+                if (fpAllowedInBouncer) defaultMessageWithFingerprint(securityMode)
+                else defaultMessage(securityMode)
+            PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT -> primaryAuthLockedOut(securityMode)
+            else -> null
+        }
+    }
+}
+
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(
+    PROMPT_REASON_TIMEOUT,
+    PROMPT_REASON_DEVICE_ADMIN,
+    PROMPT_REASON_USER_REQUEST,
+    PROMPT_REASON_AFTER_LOCKOUT,
+    PROMPT_REASON_PREPARE_FOR_UPDATE,
+    PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT,
+    PROMPT_REASON_TRUSTAGENT_EXPIRED,
+    PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
+    PROMPT_REASON_INCORRECT_FACE_INPUT,
+    PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT,
+    PROMPT_REASON_FACE_LOCKED_OUT,
+    PROMPT_REASON_FINGERPRINT_LOCKED_OUT,
+    PROMPT_REASON_DEFAULT,
+    PROMPT_REASON_NONE,
+    PROMPT_REASON_RESTART,
+    PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT,
+)
+annotation class BouncerPromptReason
+
+private fun defaultMessage(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
+        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
+        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun defaultMessageWithFingerprint(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, 0)
+        SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, 0)
+        SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, 0)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun incorrectSecurityInput(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(kg_wrong_pattern_try_again, 0)
+        SecurityMode.Password -> Pair(kg_wrong_password_try_again, 0)
+        SecurityMode.PIN -> Pair(kg_wrong_pin_try_again, 0)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun incorrectSecurityInputWithFingerprint(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(kg_wrong_pattern_try_again, kg_wrong_input_try_fp_suggestion)
+        SecurityMode.Password -> Pair(kg_wrong_password_try_again, kg_wrong_input_try_fp_suggestion)
+        SecurityMode.PIN -> Pair(kg_wrong_pin_try_again, kg_wrong_input_try_fp_suggestion)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun incorrectFingerprintInput(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_pattern)
+        SecurityMode.Password -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_password)
+        SecurityMode.PIN -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_pin)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun incorrectFaceInput(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_pattern)
+        SecurityMode.Password -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_password)
+        SecurityMode.PIN -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_pin)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun incorrectFaceInputWithFingerprintAllowed(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, bouncer_face_not_recognized)
+        SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, bouncer_face_not_recognized)
+        SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, bouncer_face_not_recognized)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun biometricLockout(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_bio_too_many_attempts_pattern)
+        SecurityMode.Password -> Pair(keyguard_enter_password, kg_bio_too_many_attempts_password)
+        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_bio_too_many_attempts_pin)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun authRequiredAfterReboot(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_reason_restart_pattern)
+        SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_reason_restart_password)
+        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun authRequiredAfterAdminLockdown(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_dpm_lock)
+        SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_after_dpm_lock)
+        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun authRequiredAfterUserLockdown(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_user_lockdown_pattern)
+        SecurityMode.Password ->
+            Pair(keyguard_enter_password, kg_prompt_after_user_lockdown_password)
+        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun authRequiredForUnattendedUpdate(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_unattended_update)
+        SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_unattended_update)
+        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_unattended_update)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun authRequiredAfterPrimaryAuthTimeout(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_pattern_auth_timeout)
+        SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_password_auth_timeout)
+        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun nonStrongAuthTimeout(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_auth_timeout)
+        SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_auth_timeout)
+        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_auth_timeout)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun nonStrongAuthTimeoutWithFingerprintAllowed(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_prompt_auth_timeout)
+        SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_prompt_auth_timeout)
+        SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, kg_prompt_auth_timeout)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun faceUnlockUnavailable(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_face_locked_out)
+        SecurityMode.Password -> Pair(keyguard_enter_password, kg_face_locked_out)
+        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_face_locked_out)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun fingerprintUnlockUnavailable(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_fp_locked_out)
+        SecurityMode.Password -> Pair(keyguard_enter_password, kg_fp_locked_out)
+        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_fp_locked_out)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun trustAgentDisabled(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_trust_agent_disabled)
+        SecurityMode.Password -> Pair(keyguard_enter_password, kg_trust_agent_disabled)
+        SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_trust_agent_disabled)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun trustAgentDisabledWithFingerprintAllowed(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_trust_agent_disabled)
+        SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_trust_agent_disabled)
+        SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, kg_trust_agent_disabled)
+        else -> Pair(0, 0)
+    }
+}
+
+private fun primaryAuthLockedOut(securityMode: SecurityMode): Pair<Int, Int> {
+    return when (securityMode) {
+        SecurityMode.Pattern ->
+            Pair(kg_too_many_failed_attempts_countdown, kg_primary_auth_locked_out_pattern)
+        SecurityMode.Password ->
+            Pair(kg_too_many_failed_attempts_countdown, kg_primary_auth_locked_out_password)
+        SecurityMode.PIN ->
+            Pair(kg_too_many_failed_attempts_countdown, kg_primary_auth_locked_out_pin)
+        else -> Pair(0, 0)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt
deleted file mode 100644
index 49a0a3c..0000000
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.bouncer.data.repo
-
-import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
-import com.android.systemui.dagger.SysUISingleton
-import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-
-/** Provides access to bouncer-related application state. */
-@SysUISingleton
-class BouncerRepository @Inject constructor() {
-    private val _message = MutableStateFlow<String?>(null)
-    /** The user-facing message to show in the bouncer. */
-    val message: StateFlow<String?> = _message.asStateFlow()
-
-    private val _throttling = MutableStateFlow<AuthenticationThrottledModel?>(null)
-    /** The current authentication throttling state. If `null`, there's no throttling. */
-    val throttling: StateFlow<AuthenticationThrottledModel?> = _throttling.asStateFlow()
-
-    fun setMessage(message: String?) {
-        _message.value = message
-    }
-
-    fun setThrottling(throttling: AuthenticationThrottledModel?) {
-        _throttling.value = throttling
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt
new file mode 100644
index 0000000..7e420cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.data.repository
+
+import android.hardware.biometrics.BiometricSourceType
+import android.hardware.biometrics.BiometricSourceType.FACE
+import android.hardware.biometrics.BiometricSourceType.FINGERPRINT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEVICE_ADMIN
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FACE_LOCKED_OUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FINGERPRINT_LOCKED_OUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FACE_INPUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NONE
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.bouncer.data.factory.BouncerMessageFactory
+import com.android.systemui.bouncer.shared.model.BouncerMessageModel
+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.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.TrustRepository
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+
+/** Provide different sources of messages that needs to be shown on the bouncer. */
+interface BouncerMessageRepository {
+    /**
+     * Messages that are shown in response to the incorrect security attempts on the bouncer and
+     * primary authentication method being locked out, along with countdown messages before primary
+     * auth is active again.
+     */
+    val primaryAuthMessage: Flow<BouncerMessageModel?>
+
+    /**
+     * Help messages that are shown to the user on how to successfully perform authentication using
+     * face.
+     */
+    val faceAcquisitionMessage: Flow<BouncerMessageModel?>
+
+    /**
+     * Help messages that are shown to the user on how to successfully perform authentication using
+     * fingerprint.
+     */
+    val fingerprintAcquisitionMessage: Flow<BouncerMessageModel?>
+
+    /** Custom message that is displayed when the bouncer is being shown to launch an app. */
+    val customMessage: Flow<BouncerMessageModel?>
+
+    /**
+     * Messages that are shown in response to biometric authentication attempts through face or
+     * fingerprint.
+     */
+    val biometricAuthMessage: Flow<BouncerMessageModel?>
+
+    /** Messages that are shown when certain auth flags are set. */
+    val authFlagsMessage: Flow<BouncerMessageModel?>
+
+    /** Messages that are show after biometrics are locked out temporarily or permanently */
+    val biometricLockedOutMessage: Flow<BouncerMessageModel?>
+
+    /** Set the value for [primaryAuthMessage] */
+    fun setPrimaryAuthMessage(value: BouncerMessageModel?)
+
+    /** Set the value for [faceAcquisitionMessage] */
+    fun setFaceAcquisitionMessage(value: BouncerMessageModel?)
+    /** Set the value for [fingerprintAcquisitionMessage] */
+    fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?)
+
+    /** Set the value for [customMessage] */
+    fun setCustomMessage(value: BouncerMessageModel?)
+
+    /**
+     * Clear any previously set messages for [primaryAuthMessage], [faceAcquisitionMessage],
+     * [fingerprintAcquisitionMessage] & [customMessage]
+     */
+    fun clearMessage()
+}
+
+@SysUISingleton
+class BouncerMessageRepositoryImpl
+@Inject
+constructor(
+    trustRepository: TrustRepository,
+    biometricSettingsRepository: BiometricSettingsRepository,
+    updateMonitor: KeyguardUpdateMonitor,
+    private val bouncerMessageFactory: BouncerMessageFactory,
+    private val userRepository: UserRepository,
+    fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+) : BouncerMessageRepository {
+
+    private val isFaceEnrolledAndEnabled =
+        and(
+            biometricSettingsRepository.isFaceAuthenticationEnabled,
+            biometricSettingsRepository.isFaceEnrolled
+        )
+
+    private val isFingerprintEnrolledAndEnabled =
+        and(
+            biometricSettingsRepository.isFingerprintEnabledByDevicePolicy,
+            biometricSettingsRepository.isFingerprintEnrolled
+        )
+
+    private val isAnyBiometricsEnabledAndEnrolled =
+        or(isFaceEnrolledAndEnabled, isFingerprintEnrolledAndEnabled)
+
+    private val authFlagsBasedPromptReason: Flow<Int> =
+        combine(
+                biometricSettingsRepository.authenticationFlags,
+                trustRepository.isCurrentUserTrustManaged,
+                isAnyBiometricsEnabledAndEnrolled,
+                ::Triple
+            )
+            .map { (flags, isTrustManaged, biometricsEnrolledAndEnabled) ->
+                val trustOrBiometricsAvailable = (isTrustManaged || biometricsEnrolledAndEnabled)
+                return@map if (
+                    trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterReboot
+                ) {
+                    PROMPT_REASON_RESTART
+                } else if (trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterTimeout) {
+                    PROMPT_REASON_TIMEOUT
+                } else if (flags.isPrimaryAuthRequiredAfterDpmLockdown) {
+                    PROMPT_REASON_DEVICE_ADMIN
+                } else if (isTrustManaged && flags.someAuthRequiredAfterUserRequest) {
+                    PROMPT_REASON_TRUSTAGENT_EXPIRED
+                } else if (isTrustManaged && flags.someAuthRequiredAfterTrustAgentExpired) {
+                    PROMPT_REASON_TRUSTAGENT_EXPIRED
+                } else if (trustOrBiometricsAvailable && flags.isInUserLockdown) {
+                    PROMPT_REASON_USER_REQUEST
+                } else if (
+                    trustOrBiometricsAvailable && flags.primaryAuthRequiredForUnattendedUpdate
+                ) {
+                    PROMPT_REASON_PREPARE_FOR_UPDATE
+                } else if (
+                    trustOrBiometricsAvailable &&
+                        flags.strongerAuthRequiredAfterNonStrongBiometricsTimeout
+                ) {
+                    PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT
+                } else {
+                    PROMPT_REASON_NONE
+                }
+            }
+
+    private val biometricAuthReason: Flow<Int> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : KeyguardUpdateMonitorCallback() {
+                        override fun onBiometricAuthFailed(
+                            biometricSourceType: BiometricSourceType?
+                        ) {
+                            val promptReason =
+                                if (biometricSourceType == FINGERPRINT)
+                                    PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT
+                                else if (
+                                    biometricSourceType == FACE && !updateMonitor.isFaceLockedOut
+                                ) {
+                                    PROMPT_REASON_INCORRECT_FACE_INPUT
+                                } else PROMPT_REASON_NONE
+                            trySendWithFailureLogging(promptReason, TAG, "onBiometricAuthFailed")
+                        }
+
+                        override fun onBiometricsCleared() {
+                            trySendWithFailureLogging(
+                                PROMPT_REASON_NONE,
+                                TAG,
+                                "onBiometricsCleared"
+                            )
+                        }
+
+                        override fun onBiometricAcquired(
+                            biometricSourceType: BiometricSourceType?,
+                            acquireInfo: Int
+                        ) {
+                            trySendWithFailureLogging(
+                                PROMPT_REASON_NONE,
+                                TAG,
+                                "clearBiometricPrompt for new auth session."
+                            )
+                        }
+
+                        override fun onBiometricAuthenticated(
+                            userId: Int,
+                            biometricSourceType: BiometricSourceType?,
+                            isStrongBiometric: Boolean
+                        ) {
+                            trySendWithFailureLogging(
+                                PROMPT_REASON_NONE,
+                                TAG,
+                                "onBiometricAuthenticated"
+                            )
+                        }
+                    }
+                updateMonitor.registerCallback(callback)
+                awaitClose { updateMonitor.removeCallback(callback) }
+            }
+            .distinctUntilChanged()
+
+    private val _primaryAuthMessage = MutableStateFlow<BouncerMessageModel?>(null)
+    override val primaryAuthMessage: Flow<BouncerMessageModel?> = _primaryAuthMessage
+
+    private val _faceAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
+    override val faceAcquisitionMessage: Flow<BouncerMessageModel?> = _faceAcquisitionMessage
+
+    private val _fingerprintAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
+    override val fingerprintAcquisitionMessage: Flow<BouncerMessageModel?> =
+        _fingerprintAcquisitionMessage
+
+    private val _customMessage = MutableStateFlow<BouncerMessageModel?>(null)
+    override val customMessage: Flow<BouncerMessageModel?> = _customMessage
+
+    override val biometricAuthMessage: Flow<BouncerMessageModel?> =
+        biometricAuthReason
+            .map {
+                if (it == PROMPT_REASON_NONE) null
+                else
+                    bouncerMessageFactory.createFromPromptReason(
+                        it,
+                        userRepository.getSelectedUserInfo().id
+                    )
+            }
+            .onStart { emit(null) }
+            .distinctUntilChanged()
+
+    override val authFlagsMessage: Flow<BouncerMessageModel?> =
+        authFlagsBasedPromptReason
+            .map {
+                if (it == PROMPT_REASON_NONE) null
+                else
+                    bouncerMessageFactory.createFromPromptReason(
+                        it,
+                        userRepository.getSelectedUserInfo().id
+                    )
+            }
+            .onStart { emit(null) }
+            .distinctUntilChanged()
+
+    // TODO (b/262838215): Replace with DeviceEntryFaceAuthRepository when the new face auth system
+    // has been launched.
+    private val faceLockedOut: Flow<Boolean> = conflatedCallbackFlow {
+        val callback =
+            object : KeyguardUpdateMonitorCallback() {
+                override fun onLockedOutStateChanged(biometricSourceType: BiometricSourceType?) {
+                    if (biometricSourceType == FACE) {
+                        trySendWithFailureLogging(
+                            updateMonitor.isFaceLockedOut,
+                            TAG,
+                            "face lock out state changed."
+                        )
+                    }
+                }
+            }
+        updateMonitor.registerCallback(callback)
+        trySendWithFailureLogging(updateMonitor.isFaceLockedOut, TAG, "face lockout initial value")
+        awaitClose { updateMonitor.removeCallback(callback) }
+    }
+
+    override val biometricLockedOutMessage: Flow<BouncerMessageModel?> =
+        combine(fingerprintAuthRepository.isLockedOut, faceLockedOut) { fp, face ->
+            return@combine if (fp) {
+                bouncerMessageFactory.createFromPromptReason(
+                    PROMPT_REASON_FINGERPRINT_LOCKED_OUT,
+                    userRepository.getSelectedUserInfo().id
+                )
+            } else if (face) {
+                bouncerMessageFactory.createFromPromptReason(
+                    PROMPT_REASON_FACE_LOCKED_OUT,
+                    userRepository.getSelectedUserInfo().id
+                )
+            } else null
+        }
+
+    override fun setPrimaryAuthMessage(value: BouncerMessageModel?) {
+        _primaryAuthMessage.value = value
+    }
+
+    override fun setFaceAcquisitionMessage(value: BouncerMessageModel?) {
+        _faceAcquisitionMessage.value = value
+    }
+
+    override fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?) {
+        _fingerprintAcquisitionMessage.value = value
+    }
+
+    override fun setCustomMessage(value: BouncerMessageModel?) {
+        _customMessage.value = value
+    }
+
+    override fun clearMessage() {
+        _fingerprintAcquisitionMessage.value = null
+        _faceAcquisitionMessage.value = null
+        _primaryAuthMessage.value = null
+        _customMessage.value = null
+    }
+
+    companion object {
+        const val TAG = "BouncerDetailedMessageRepository"
+    }
+}
+
+private fun and(flow: Flow<Boolean>, anotherFlow: Flow<Boolean>) =
+    flow.combine(anotherFlow) { a, b -> a && b }
+
+private fun or(flow: Flow<Boolean>, anotherFlow: Flow<Boolean>) =
+    flow.combine(anotherFlow) { a, b -> a || b }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
new file mode 100644
index 0000000..ff896fa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
@@ -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.bouncer.data.repository
+
+import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Provides access to bouncer-related application state. */
+@SysUISingleton
+class BouncerRepository @Inject constructor() {
+    private val _message = MutableStateFlow<String?>(null)
+    /** The user-facing message to show in the bouncer. */
+    val message: StateFlow<String?> = _message.asStateFlow()
+
+    private val _throttling = MutableStateFlow<AuthenticationThrottledModel?>(null)
+    /** The current authentication throttling state. If `null`, there's no throttling. */
+    val throttling: StateFlow<AuthenticationThrottledModel?> = _throttling.asStateFlow()
+
+    fun setMessage(message: String?) {
+        _message.value = message
+    }
+
+    fun setThrottling(throttling: AuthenticationThrottledModel?) {
+        _throttling.value = throttling
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
new file mode 100644
index 0000000..918e168
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
@@ -0,0 +1,273 @@
+/*
+ * 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.bouncer.data.repository
+
+import android.os.Build
+import android.util.Log
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.dagger.BouncerTableLog
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
+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
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+
+/**
+ * Encapsulates app state for the lock screen primary and alternate bouncer.
+ *
+ * Make sure to add newly added flows to the logger.
+ */
+interface KeyguardBouncerRepository {
+    /** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
+    val primaryBouncerShow: StateFlow<Boolean>
+    val primaryBouncerShowingSoon: StateFlow<Boolean>
+    val primaryBouncerStartingToHide: StateFlow<Boolean>
+    val primaryBouncerStartingDisappearAnimation: StateFlow<Runnable?>
+    /** Determines if we want to instantaneously show the primary bouncer instead of translating. */
+    val primaryBouncerScrimmed: StateFlow<Boolean>
+    /**
+     * 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
+     * ```
+     */
+    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 alternateBouncerVisible: StateFlow<Boolean>
+    val alternateBouncerUIAvailable: StateFlow<Boolean>
+    val sideFpsShowing: StateFlow<Boolean>
+
+    var lastAlternateBouncerVisibleTime: Long
+
+    fun setPrimaryScrimmed(isScrimmed: Boolean)
+
+    fun setPrimaryShow(isShowing: Boolean)
+
+    fun setPrimaryShowingSoon(showingSoon: 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 clock: SystemClock,
+    @Application private val applicationScope: CoroutineScope,
+    @BouncerTableLog private val buffer: TableLogBuffer,
+) : KeyguardBouncerRepository {
+    /** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
+    private val _primaryBouncerShow = MutableStateFlow(false)
+    override val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
+    private val _primaryBouncerShowingSoon = MutableStateFlow(false)
+    override val primaryBouncerShowingSoon = _primaryBouncerShowingSoon.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)
+    override val keyguardPosition = _keyguardPosition.asStateFlow()
+    private val _isBackButtonEnabled = MutableStateFlow<Boolean?>(null)
+    override val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
+    private val _keyguardAuthenticated = MutableStateFlow<Boolean?>(null)
+    /** Determines if user is already unlocked */
+    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()
+    /** 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()
+    }
+
+    override fun setPrimaryScrimmed(isScrimmed: Boolean) {
+        _primaryBouncerScrimmed.value = isScrimmed
+    }
+
+    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(isShowing: Boolean) {
+        _primaryBouncerShow.value = isShowing
+    }
+
+    override fun setPrimaryShowingSoon(showingSoon: Boolean) {
+        _primaryBouncerShowingSoon.value = showingSoon
+    }
+
+    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
+    }
+
+    /** Sets up logs for state flows. */
+    private fun setUpLogging() {
+        if (!Build.IS_DEBUGGABLE) {
+            return
+        }
+
+        primaryBouncerShow
+            .logDiffsForTable(buffer, "", "PrimaryBouncerShow", false)
+            .onEach { Log.d(TAG, "Keyguard Bouncer is ${if (it) "showing" else "hiding."}") }
+            .launchIn(applicationScope)
+        primaryBouncerShowingSoon
+            .logDiffsForTable(buffer, "", "PrimaryBouncerShowingSoon", false)
+            .launchIn(applicationScope)
+        primaryBouncerStartingToHide
+            .logDiffsForTable(buffer, "", "PrimaryBouncerStartingToHide", false)
+            .launchIn(applicationScope)
+        primaryBouncerStartingDisappearAnimation
+            .map { it != null }
+            .logDiffsForTable(buffer, "", "PrimaryBouncerStartingDisappearAnimation", false)
+            .launchIn(applicationScope)
+        primaryBouncerScrimmed
+            .logDiffsForTable(buffer, "", "PrimaryBouncerScrimmed", false)
+            .launchIn(applicationScope)
+        panelExpansionAmount
+            .map { (it * 1000).toInt() }
+            .logDiffsForTable(buffer, "", "PanelExpansionAmountMillis", -1)
+            .launchIn(applicationScope)
+        keyguardPosition
+            .map { it.toInt() }
+            .logDiffsForTable(buffer, "", "KeyguardPosition", -1)
+            .launchIn(applicationScope)
+        isBackButtonEnabled
+            .filterNotNull()
+            .logDiffsForTable(buffer, "", "IsBackButtonEnabled", false)
+            .launchIn(applicationScope)
+        showMessage
+            .map { it?.message }
+            .logDiffsForTable(buffer, "", "ShowMessage", null)
+            .launchIn(applicationScope)
+        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
+        private const val TAG = "KeyguardBouncerRepositoryImpl"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
new file mode 100644
index 0000000..2abdb84
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.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.bouncer.domain.interactor
+
+import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+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 statusBarStateController: StatusBarStateController,
+    private val keyguardStateController: KeyguardStateController,
+    private val bouncerRepository: KeyguardBouncerRepository,
+    private val biometricSettingsRepository: BiometricSettingsRepository,
+    private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+    private val systemClock: SystemClock,
+) {
+    var receivedDownTouch = false
+    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 {
+        bouncerRepository.setAlternateVisible(canShowAlternateBouncerForFingerprint())
+        return isVisibleState()
+    }
+
+    /**
+     * 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 {
+        receivedDownTouch = false
+        val wasAlternateBouncerVisible = isVisibleState()
+        bouncerRepository.setAlternateVisible(false)
+        return wasAlternateBouncerVisible && !isVisibleState()
+    }
+
+    fun isVisibleState(): Boolean {
+        return bouncerRepository.alternateBouncerVisible.value
+    }
+
+    fun setAlternateBouncerUIAvailable(isAvailable: Boolean) {
+        bouncerRepository.setAlternateBouncerUIAvailable(isAvailable)
+    }
+
+    fun canShowAlternateBouncerForFingerprint(): Boolean {
+        return bouncerRepository.alternateBouncerUIAvailable.value &&
+            biometricSettingsRepository.isFingerprintEnrolled.value &&
+            biometricSettingsRepository.isStrongBiometricAllowed.value &&
+            biometricSettingsRepository.isFingerprintEnabledByDevicePolicy.value &&
+            !deviceEntryFingerprintAuthRepository.isLockedOut.value &&
+            !keyguardStateController.isUnlocked &&
+            !statusBarStateController.isDozing
+    }
+
+    /**
+     * 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 (systemClock.uptimeMillis() - bouncerRepository.lastAlternateBouncerVisibleTime) >
+            MIN_VISIBILITY_DURATION_UNTIL_TOUCHES_DISMISS_ALTERNATE_BOUNCER_MS
+    }
+    /**
+     * Should only be called through StatusBarKeyguardViewManager which propagates the source of
+     * truth to other concerned controllers. Will hide the alternate bouncer if it's no longer
+     * allowed to show.
+     *
+     * @return true if the alternate bouncer was newly hidden, else false.
+     */
+    fun maybeHide(): Boolean {
+        if (isVisibleState() && !canShowAlternateBouncerForFingerprint()) {
+            return hide()
+        }
+        return false
+    }
+
+    companion object {
+        private const val MIN_VISIBILITY_DURATION_UNTIL_TOUCHES_DISMISS_ALTERNATE_BOUNCER_MS = 200L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index 1d2fce7..5dd24b2 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.R
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.bouncer.data.repository.BouncerRepository
 import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.scene.domain.interactor.SceneInteractor
@@ -35,6 +35,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.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
@@ -68,44 +71,44 @@
                     )
             )
 
-    /**
-     * The currently-configured authentication method. This determines how the authentication
-     * challenge is completed in order to unlock an otherwise locked device.
-     */
-    val authenticationMethod: StateFlow<AuthenticationMethodModel> =
-        authenticationInteractor.authenticationMethod
-
     /** The current authentication throttling state. If `null`, there's no throttling. */
     val throttling: StateFlow<AuthenticationThrottledModel?> = repository.throttling
 
     init {
+        // UNLOCKING SHOWS Gone.
+        //
+        // Move to the gone scene if the device becomes unlocked while on the bouncer scene.
         applicationScope.launch {
-            combine(
-                    sceneInteractor.currentScene(containerName),
-                    authenticationInteractor.authenticationMethod,
-                    ::Pair,
-                )
-                .collect { (currentScene, authMethod) ->
+            sceneInteractor
+                .currentScene(containerName)
+                .flatMapLatest { currentScene ->
                     if (currentScene.key == SceneKey.Bouncer) {
-                        when (authMethod) {
-                            is AuthenticationMethodModel.None ->
-                                sceneInteractor.setCurrentScene(
-                                    containerName,
-                                    SceneModel(SceneKey.Gone),
-                                )
-                            is AuthenticationMethodModel.Swipe ->
-                                sceneInteractor.setCurrentScene(
-                                    containerName,
-                                    SceneModel(SceneKey.Lockscreen),
-                                )
-                            else -> Unit
-                        }
+                        authenticationInteractor.isUnlocked
+                    } else {
+                        flowOf(false)
+                    }
+                }
+                .distinctUntilChanged()
+                .collect { isUnlocked ->
+                    if (isUnlocked) {
+                        sceneInteractor.setCurrentScene(
+                            containerName = containerName,
+                            scene = SceneModel(SceneKey.Gone),
+                        )
                     }
                 }
         }
     }
 
     /**
+     * Returns the currently-configured authentication method. This determines how the
+     * authentication challenge is completed in order to unlock an otherwise locked device.
+     */
+    suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
+        return authenticationInteractor.getAuthenticationMethod()
+    }
+
+    /**
      * Either shows the bouncer or unlocks the device, if the bouncer doesn't need to be shown.
      *
      * @param containerName The name of the scene container to show the bouncer in.
@@ -115,18 +118,19 @@
         containerName: String,
         message: String? = null,
     ) {
-        if (authenticationInteractor.isAuthenticationRequired()) {
-            repository.setMessage(message ?: promptMessage(authenticationMethod.value))
-            sceneInteractor.setCurrentScene(
-                containerName = containerName,
-                scene = SceneModel(SceneKey.Bouncer),
-            )
-        } else {
-            authenticationInteractor.unlockDevice()
-            sceneInteractor.setCurrentScene(
-                containerName = containerName,
-                scene = SceneModel(SceneKey.Gone),
-            )
+        applicationScope.launch {
+            if (authenticationInteractor.isAuthenticationRequired()) {
+                repository.setMessage(message ?: promptMessage(getAuthenticationMethod()))
+                sceneInteractor.setCurrentScene(
+                    containerName = containerName,
+                    scene = SceneModel(SceneKey.Bouncer),
+                )
+            } else {
+                sceneInteractor.setCurrentScene(
+                    containerName = containerName,
+                    scene = SceneModel(SceneKey.Gone),
+                )
+            }
         }
     }
 
@@ -135,7 +139,7 @@
      * method.
      */
     fun resetMessage() {
-        repository.setMessage(promptMessage(authenticationMethod.value))
+        applicationScope.launch { repository.setMessage(promptMessage(getAuthenticationMethod())) }
     }
 
     /** Removes the user-facing message. */
@@ -149,19 +153,28 @@
      * If the input is correct, the device will be unlocked and the lock screen and bouncer will be
      * dismissed and hidden.
      *
+     * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method
+     * supports auto-confirming, and the input's length is at least the code's length. Otherwise,
+     * `null` is returned.
+     *
      * @param input The input from the user to try to authenticate with. This can be a list of
      *   different things, based on the current authentication method.
-     * @return `true` if the authentication succeeded and the device is now unlocked; `false`
-     *   otherwise.
+     * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit
+     *   request to validate.
+     * @return `true` if the authentication succeeded and the device is now unlocked; `false` when
+     *   authentication failed, `null` if the check was not performed.
      */
-    fun authenticate(
+    suspend fun authenticate(
         input: List<Any>,
-    ): Boolean {
+        tryAutoConfirm: Boolean = false,
+    ): Boolean? {
         if (repository.throttling.value != null) {
             return false
         }
 
-        val isAuthenticated = authenticationInteractor.authenticate(input)
+        val isAuthenticated =
+            authenticationInteractor.authenticate(input, tryAutoConfirm) ?: return null
+
         val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value
         when {
             isAuthenticated -> {
@@ -189,7 +202,7 @@
                     repository.setThrottling(null)
                     clearMessage()
                 }
-            else -> repository.setMessage(errorMessage(authenticationMethod.value))
+            else -> repository.setMessage(errorMessage(getAuthenticationMethod()))
         }
 
         return isAuthenticated
@@ -197,7 +210,7 @@
 
     private fun promptMessage(authMethod: AuthenticationMethodModel): String {
         return when (authMethod) {
-            is AuthenticationMethodModel.PIN ->
+            is AuthenticationMethodModel.Pin ->
                 applicationContext.getString(R.string.keyguard_enter_your_pin)
             is AuthenticationMethodModel.Password ->
                 applicationContext.getString(R.string.keyguard_enter_your_password)
@@ -209,7 +222,7 @@
 
     private fun errorMessage(authMethod: AuthenticationMethodModel): String {
         return when (authMethod) {
-            is AuthenticationMethodModel.PIN -> applicationContext.getString(R.string.kg_wrong_pin)
+            is AuthenticationMethodModel.Pin -> applicationContext.getString(R.string.kg_wrong_pin)
             is AuthenticationMethodModel.Password ->
                 applicationContext.getString(R.string.kg_wrong_password)
             is AuthenticationMethodModel.Pattern ->
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageAuditLogger.kt
new file mode 100644
index 0000000..497747f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageAuditLogger.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.bouncer.domain.interactor
+
+import android.os.Build
+import android.util.Log
+import com.android.systemui.CoreStartable
+import com.android.systemui.bouncer.data.repository.BouncerMessageRepository
+import com.android.systemui.bouncer.shared.model.BouncerMessageModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+
+private val TAG = BouncerMessageAuditLogger::class.simpleName!!
+
+/** Logger that echoes bouncer messages state to logcat in debuggable builds. */
+@SysUISingleton
+class BouncerMessageAuditLogger
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val repository: BouncerMessageRepository,
+    private val interactor: BouncerMessageInteractor,
+) : CoreStartable {
+    override fun start() {
+        if (Build.isDebuggable()) {
+            collectAndLog(repository.biometricAuthMessage, "biometricMessage: ")
+            collectAndLog(repository.primaryAuthMessage, "primaryAuthMessage: ")
+            collectAndLog(repository.customMessage, "customMessage: ")
+            collectAndLog(repository.faceAcquisitionMessage, "faceAcquisitionMessage: ")
+            collectAndLog(
+                repository.fingerprintAcquisitionMessage,
+                "fingerprintAcquisitionMessage: "
+            )
+            collectAndLog(repository.authFlagsMessage, "authFlagsMessage: ")
+            collectAndLog(interactor.bouncerMessage, "interactor.bouncerMessage: ")
+        }
+    }
+
+    private fun collectAndLog(flow: Flow<BouncerMessageModel?>, context: String) {
+        scope.launch { flow.collect { Log.d(TAG, context + it) } }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
new file mode 100644
index 0000000..d06dd8e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.domain.interactor
+
+import android.os.CountDownTimer
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT
+import com.android.systemui.bouncer.data.factory.BouncerMessageFactory
+import com.android.systemui.bouncer.data.repository.BouncerMessageRepository
+import com.android.systemui.bouncer.shared.model.BouncerMessageModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlin.math.roundToInt
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+@SysUISingleton
+class BouncerMessageInteractor
+@Inject
+constructor(
+    private val repository: BouncerMessageRepository,
+    private val factory: BouncerMessageFactory,
+    private val userRepository: UserRepository,
+    private val countDownTimerUtil: CountDownTimerUtil,
+    private val featureFlags: FeatureFlags,
+) {
+    fun onPrimaryAuthLockedOut(secondsBeforeLockoutReset: Long) {
+        if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
+
+        val callback =
+            object : CountDownTimerCallback {
+                override fun onFinish() {
+                    repository.clearMessage()
+                }
+
+                override fun onTick(millisUntilFinished: Long) {
+                    val secondsRemaining = (millisUntilFinished / 1000.0).roundToInt()
+                    val message =
+                        factory.createFromPromptReason(
+                            reason = PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT,
+                            userId = userRepository.getSelectedUserInfo().id
+                        )
+                    message?.message?.animate = false
+                    message?.message?.formatterArgs =
+                        mutableMapOf<String, Any>(Pair("count", secondsRemaining))
+                    repository.setPrimaryAuthMessage(message)
+                }
+            }
+        countDownTimerUtil.startNewTimer(secondsBeforeLockoutReset * 1000, 1000, callback)
+    }
+
+    fun onPrimaryAuthIncorrectAttempt() {
+        if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
+
+        repository.setPrimaryAuthMessage(
+            factory.createFromPromptReason(
+                PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
+                userRepository.getSelectedUserInfo().id
+            )
+        )
+    }
+
+    fun setFingerprintAcquisitionMessage(value: String?) {
+        if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
+
+        repository.setFingerprintAcquisitionMessage(
+            if (value != null) {
+                factory.createFromString(secondaryMsg = value)
+            } else {
+                null
+            }
+        )
+    }
+
+    fun setFaceAcquisitionMessage(value: String?) {
+        if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
+
+        repository.setFaceAcquisitionMessage(
+            if (value != null) {
+                factory.createFromString(secondaryMsg = value)
+            } else {
+                null
+            }
+        )
+    }
+
+    fun setCustomMessage(value: String?) {
+        if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
+
+        repository.setCustomMessage(
+            if (value != null) {
+                factory.createFromString(secondaryMsg = value)
+            } else {
+                null
+            }
+        )
+    }
+
+    fun onPrimaryBouncerUserInput() {
+        if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
+
+        repository.clearMessage()
+    }
+
+    fun onBouncerBeingHidden() {
+        if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
+
+        repository.clearMessage()
+    }
+
+    private fun firstNonNullMessage(
+        oneMessageModel: Flow<BouncerMessageModel?>,
+        anotherMessageModel: Flow<BouncerMessageModel?>
+    ): Flow<BouncerMessageModel?> {
+        return oneMessageModel.combine(anotherMessageModel) { a, b -> a ?: b }
+    }
+
+    // Null if feature flag is enabled which gets ignored always or empty bouncer message model that
+    // always maps to an empty string.
+    private fun nullOrEmptyMessage() =
+        flowOf(
+            if (featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) null
+            else factory.createFromString("", "")
+        )
+
+    val bouncerMessage =
+        listOf(
+                nullOrEmptyMessage(),
+                repository.primaryAuthMessage,
+                repository.biometricAuthMessage,
+                repository.fingerprintAcquisitionMessage,
+                repository.faceAcquisitionMessage,
+                repository.customMessage,
+                repository.authFlagsMessage,
+                repository.biometricLockedOutMessage,
+                userRepository.selectedUserInfo.map {
+                    factory.createFromPromptReason(PROMPT_REASON_DEFAULT, it.id)
+                },
+            )
+            .reduce(::firstNonNullMessage)
+            .distinctUntilChanged()
+}
+
+interface CountDownTimerCallback {
+    fun onFinish()
+    fun onTick(millisUntilFinished: Long)
+}
+
+@SysUISingleton
+open class CountDownTimerUtil @Inject constructor() {
+
+    /**
+     * Start a new count down timer that runs for [millisInFuture] with a tick every
+     * [millisInterval]
+     */
+    fun startNewTimer(
+        millisInFuture: Long,
+        millisInterval: Long,
+        callback: CountDownTimerCallback,
+    ): CountDownTimer {
+        return object : CountDownTimer(millisInFuture, millisInterval) {
+                override fun onFinish() = callback.onFinish()
+
+                override fun onTick(millisUntilFinished: Long) =
+                    callback.onTick(millisUntilFinished)
+            }
+            .start()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractor.kt
new file mode 100644
index 0000000..5fbe99a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractor.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.bouncer.domain.interactor
+
+import android.view.View
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE
+import com.android.systemui.dagger.SysUISingleton
+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<KeyguardResetCallback>()
+    private var expansionCallbacks = ArrayList<PrimaryBouncerExpansionCallback>()
+
+    /** Add a KeyguardResetCallback. */
+    fun addKeyguardResetCallback(callback: KeyguardResetCallback) {
+        resetCallbacks.addIfAbsent(callback)
+    }
+
+    /** Remove a KeyguardResetCallback. */
+    fun removeKeyguardResetCallback(callback: KeyguardResetCallback) {
+        resetCallbacks.remove(callback)
+    }
+
+    /** Adds a callback to listen to bouncer expansion updates. */
+    fun addBouncerExpansionCallback(callback: PrimaryBouncerExpansionCallback) {
+        if (!expansionCallbacks.contains(callback)) {
+            expansionCallbacks.add(callback)
+        }
+    }
+
+    /**
+     * Removes a previously added callback. If the callback was never added, this method does
+     * nothing.
+     */
+    fun removeBouncerExpansionCallback(callback: PrimaryBouncerExpansionCallback) {
+        expansionCallbacks.remove(callback)
+    }
+
+    /** Propagate fully shown to bouncer expansion callbacks. */
+    fun dispatchFullyShown() {
+        for (callback in expansionCallbacks) {
+            callback.onFullyShown()
+        }
+    }
+
+    /** Propagate starting to hide to bouncer expansion callbacks. */
+    fun dispatchStartingToHide() {
+        for (callback in expansionCallbacks) {
+            callback.onStartingToHide()
+        }
+    }
+
+    /** Propagate starting to show to bouncer expansion callbacks. */
+    fun dispatchStartingToShow() {
+        for (callback in expansionCallbacks) {
+            callback.onStartingToShow()
+        }
+    }
+
+    /** Propagate fully hidden to bouncer expansion callbacks. */
+    fun dispatchFullyHidden() {
+        for (callback in expansionCallbacks) {
+            callback.onFullyHidden()
+        }
+    }
+
+    /** Propagate expansion changes to bouncer expansion callbacks. */
+    fun dispatchExpansionChanged(expansion: Float) {
+        for (callback in expansionCallbacks) {
+            callback.onExpansionChanged(expansion)
+        }
+    }
+    /** Propagate visibility changes to bouncer expansion callbacks. */
+    fun dispatchVisibilityChanged(visibility: Int) {
+        for (callback in expansionCallbacks) {
+            callback.onVisibilityChanged(visibility == View.VISIBLE)
+        }
+    }
+
+    /** Propagate keyguard reset. */
+    fun dispatchReset() {
+        for (callback in resetCallbacks) {
+            callback.onKeyguardReset()
+        }
+    }
+
+    /** 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/bouncer/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
new file mode 100644
index 0000000..c486603
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
@@ -0,0 +1,410 @@
+/*
+ * 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.bouncer.domain.interactor
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.hardware.biometrics.BiometricSourceType
+import android.os.Handler
+import android.os.Trace
+import android.util.Log
+import android.view.View
+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.bouncer.data.repository.KeyguardBouncerRepository
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
+import com.android.systemui.bouncer.ui.BouncerView
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.repository.TrustRepository
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.shared.system.SysUiStatsLog
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/**
+ * Encapsulates business logic for interacting with the lock-screen primary (pin/pattern/password)
+ * bouncer.
+ */
+@SysUISingleton
+class PrimaryBouncerInteractor
+@Inject
+constructor(
+    private val repository: KeyguardBouncerRepository,
+    private val primaryBouncerView: BouncerView,
+    @Main private val mainHandler: Handler,
+    private val keyguardStateController: KeyguardStateController,
+    private val keyguardSecurityModel: KeyguardSecurityModel,
+    private val primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor,
+    private val falsingCollector: FalsingCollector,
+    private val dismissCallbackRegistry: DismissCallbackRegistry,
+    private val context: Context,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val trustRepository: TrustRepository,
+    private val featureFlags: FeatureFlags,
+    @Application private val applicationScope: CoroutineScope,
+) {
+    private val passiveAuthBouncerDelay =
+        context.resources.getInteger(R.integer.primary_bouncer_passive_auth_delay).toLong()
+    /** Runnable to show the primary bouncer. */
+    val showRunnable = Runnable {
+        repository.setPrimaryShow(true)
+        repository.setPrimaryShowingSoon(false)
+        primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.VISIBLE)
+    }
+
+    val keyguardAuthenticated: Flow<Boolean> = repository.keyguardAuthenticated.filterNotNull()
+    val isShowing: StateFlow<Boolean> = repository.primaryBouncerShow
+    val startingToHide: Flow<Unit> = repository.primaryBouncerStartingToHide.filter { it }.map {}
+    val isBackButtonEnabled: Flow<Boolean> = repository.isBackButtonEnabled.filterNotNull()
+    val showMessage: Flow<BouncerShowMessageModel> = repository.showMessage.filterNotNull()
+    val startingDisappearAnimation: Flow<Runnable> =
+        repository.primaryBouncerStartingDisappearAnimation.filterNotNull()
+    val resourceUpdateRequests: Flow<Boolean> = repository.resourceUpdateRequests.filter { it }
+    val keyguardPosition: Flow<Float> = repository.keyguardPosition
+    val panelExpansionAmount: Flow<Float> = repository.panelExpansionAmount
+    /** 0f = bouncer fully hidden. 1f = bouncer fully visible. */
+    val bouncerExpansion: Flow<Float> =
+        combine(repository.panelExpansionAmount, repository.primaryBouncerShow) {
+            panelExpansion,
+            primaryBouncerIsShowing ->
+            if (primaryBouncerIsShowing) {
+                1f - panelExpansion
+            } else {
+                0f
+            }
+        }
+    /** Allow for interaction when just about fully visible */
+    val isInteractable: Flow<Boolean> = bouncerExpansion.map { it > 0.9 }
+    val sideFpsShowing: Flow<Boolean> = repository.sideFpsShowing
+    private var currentUserActiveUnlockRunning = false
+
+    /** This callback needs to be a class field so it does not get garbage collected. */
+    val keyguardUpdateMonitorCallback =
+        object : KeyguardUpdateMonitorCallback() {
+            override fun onBiometricRunningStateChanged(
+                running: Boolean,
+                biometricSourceType: BiometricSourceType?
+            ) {
+                updateSideFpsVisibility()
+            }
+
+            override fun onStrongAuthStateChanged(userId: Int) {
+                updateSideFpsVisibility()
+            }
+        }
+
+    init {
+        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
+        if (featureFlags.isEnabled(Flags.DELAY_BOUNCER)) {
+            applicationScope.launch {
+                trustRepository.isCurrentUserActiveUnlockRunning.collect {
+                    currentUserActiveUnlockRunning = it
+                }
+            }
+        }
+    }
+
+    // TODO(b/243685699): Move isScrimmed logic to data layer.
+    // TODO(b/243695312): Encapsulate all of the show logic for the bouncer.
+    /** Show the bouncer if necessary and set the relevant states. */
+    @JvmOverloads
+    fun show(isScrimmed: Boolean) {
+        // Reset some states as we show the bouncer.
+        repository.setKeyguardAuthenticated(null)
+        repository.setPrimaryStartingToHide(false)
+
+        val resumeBouncer =
+            (isBouncerShowing() || repository.primaryBouncerShowingSoon.value) &&
+                needsFullscreenBouncer()
+
+        Trace.beginSection("KeyguardBouncer#show")
+        repository.setPrimaryScrimmed(isScrimmed)
+        if (isScrimmed) {
+            setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE)
+        }
+
+        // In this special case, we want to hide the bouncer and show it again. We want to emit
+        // show(true) again so that we can reinflate the new view.
+        if (resumeBouncer) {
+            repository.setPrimaryShow(false)
+        }
+
+        if (primaryBouncerView.delegate?.showNextSecurityScreenOrFinish() == true) {
+            // Keyguard is done.
+            return
+        }
+
+        repository.setPrimaryShowingSoon(true)
+        if (usePrimaryBouncerPassiveAuthDelay()) {
+            Log.d(TAG, "delay bouncer, passive auth may succeed")
+            mainHandler.postDelayed(showRunnable, passiveAuthBouncerDelay)
+        } else {
+            DejankUtils.postAfterTraversal(showRunnable)
+        }
+        keyguardStateController.notifyPrimaryBouncerShowing(true)
+        primaryBouncerCallbackInteractor.dispatchStartingToShow()
+        Trace.endSection()
+    }
+
+    /** Sets the correct bouncer states to hide the bouncer. */
+    fun hide() {
+        Trace.beginSection("KeyguardBouncer#hide")
+        if (isFullyShowing()) {
+            SysUiStatsLog.write(
+                SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED,
+                SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__HIDDEN
+            )
+            dismissCallbackRegistry.notifyDismissCancelled()
+        }
+
+        repository.setPrimaryStartDisappearAnimation(null)
+        falsingCollector.onBouncerHidden()
+        keyguardStateController.notifyPrimaryBouncerShowing(false /* showing */)
+        cancelShowRunnable()
+        repository.setPrimaryShowingSoon(false)
+        repository.setPrimaryShow(false)
+        repository.setPanelExpansion(EXPANSION_HIDDEN)
+        primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.INVISIBLE)
+        Trace.endSection()
+    }
+
+    /**
+     * Sets the panel expansion which is calculated further upstream. Panel expansion is from 0f
+     * (panel fully hidden) to 1f (panel fully showing). As the panel shows (from 0f => 1f), the
+     * bouncer hides and as the panel becomes hidden (1f => 0f), the bouncer starts to show.
+     * Therefore, a panel expansion of 1f represents the bouncer fully hidden and a panel expansion
+     * of 0f represents the bouncer fully showing.
+     */
+    fun setPanelExpansion(expansion: Float) {
+        val oldExpansion = repository.panelExpansionAmount.value
+        val expansionChanged = oldExpansion != expansion
+        if (repository.primaryBouncerStartingDisappearAnimation.value == null) {
+            repository.setPanelExpansion(expansion)
+        }
+
+        if (
+            expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE &&
+                oldExpansion != KeyguardBouncerConstants.EXPANSION_VISIBLE
+        ) {
+            falsingCollector.onBouncerShown()
+            primaryBouncerCallbackInteractor.dispatchFullyShown()
+        } else if (
+            expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN &&
+                oldExpansion != KeyguardBouncerConstants.EXPANSION_HIDDEN
+        ) {
+            /*
+             * 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()
+            DejankUtils.postAfterTraversal { primaryBouncerCallbackInteractor.dispatchReset() }
+            primaryBouncerCallbackInteractor.dispatchFullyHidden()
+        } else if (
+            expansion != KeyguardBouncerConstants.EXPANSION_VISIBLE &&
+                oldExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE
+        ) {
+            primaryBouncerCallbackInteractor.dispatchStartingToHide()
+            repository.setPrimaryStartingToHide(true)
+        }
+        if (expansionChanged) {
+            primaryBouncerCallbackInteractor.dispatchExpansionChanged(expansion)
+        }
+    }
+
+    /** Set the initial keyguard message to show when bouncer is shown. */
+    fun showMessage(message: String?, colorStateList: ColorStateList?) {
+        repository.setShowMessage(BouncerShowMessageModel(message, colorStateList))
+    }
+
+    /**
+     * Sets actions to the bouncer based on how the bouncer is dismissed. If the bouncer is
+     * unlocked, we will run the onDismissAction. If the bouncer is existed before unlocking, we
+     * call cancelAction.
+     */
+    fun setDismissAction(
+        onDismissAction: ActivityStarter.OnDismissAction?,
+        cancelAction: Runnable?
+    ) {
+        primaryBouncerView.delegate?.setDismissAction(onDismissAction, cancelAction)
+    }
+
+    /** Update the resources of the views. */
+    fun updateResources() {
+        repository.setResourceUpdateRequests(true)
+    }
+
+    /** Tell the bouncer that keyguard is authenticated. */
+    fun notifyKeyguardAuthenticated(strongAuth: Boolean) {
+        repository.setKeyguardAuthenticated(strongAuth)
+    }
+
+    /** Update the position of the bouncer when showing. */
+    fun setKeyguardPosition(position: Float) {
+        repository.setKeyguardPosition(position)
+    }
+
+    /** Notifies that the state change was handled. */
+    fun notifyKeyguardAuthenticatedHandled() {
+        repository.setKeyguardAuthenticated(null)
+    }
+
+    /** Notifies that the message was shown. */
+    fun onMessageShown() {
+        repository.setShowMessage(null)
+    }
+
+    /** Notify that the resources have been updated */
+    fun notifyUpdatedResources() {
+        repository.setResourceUpdateRequests(false)
+    }
+
+    /** Set whether back button is enabled when on the bouncer screen. */
+    fun setBackButtonEnabled(enabled: Boolean) {
+        repository.setIsBackButtonEnabled(enabled)
+    }
+
+    /** Tell the bouncer to start the pre hide animation. */
+    fun startDisappearAnimation(runnable: Runnable) {
+        if (willRunDismissFromKeyguard()) {
+            runnable.run()
+            return
+        }
+
+        repository.setPrimaryStartDisappearAnimation(runnable)
+    }
+
+    /** 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 toShow =
+            (isBouncerShowing() &&
+                sfpsEnabled &&
+                fpsDetectionRunning &&
+                isUnlockingWithFpAllowed &&
+                !isAnimatingAway())
+
+        if (KeyguardConstants.DEBUG) {
+            Log.d(
+                TAG,
+                ("sideFpsToShow=$toShow\n" +
+                    "isBouncerShowing=${isBouncerShowing()}\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 || isBouncerShowing()) &&
+            repository.panelExpansionAmount.value == KeyguardBouncerConstants.EXPANSION_VISIBLE &&
+            repository.primaryBouncerStartingDisappearAnimation.value == null
+    }
+
+    /** Returns whether bouncer is scrimmed. */
+    fun isScrimmed(): Boolean {
+        return repository.primaryBouncerScrimmed.value
+    }
+
+    /** If bouncer expansion is between 0f and 1f non-inclusive. */
+    fun isInTransit(): Boolean {
+        return repository.primaryBouncerShowingSoon.value ||
+            repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_HIDDEN &&
+                repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_VISIBLE
+    }
+
+    /** Return whether bouncer is animating away. */
+    fun isAnimatingAway(): Boolean {
+        return repository.primaryBouncerStartingDisappearAnimation.value != null
+    }
+
+    /** Return whether bouncer will dismiss with actions */
+    fun willDismissWithAction(): Boolean {
+        return primaryBouncerView.delegate?.willDismissWithActions() == true
+    }
+
+    /** Will the dismissal run from the keyguard layout (instead of from bouncer) */
+    fun willRunDismissFromKeyguard(): Boolean {
+        return primaryBouncerView.delegate?.willRunDismissFromKeyguard() == true
+    }
+
+    /** Returns whether the bouncer should be full screen. */
+    private fun needsFullscreenBouncer(): Boolean {
+        val mode: KeyguardSecurityModel.SecurityMode =
+            keyguardSecurityModel.getSecurityMode(KeyguardUpdateMonitor.getCurrentUser())
+        return mode == KeyguardSecurityModel.SecurityMode.SimPin ||
+            mode == KeyguardSecurityModel.SecurityMode.SimPuk
+    }
+
+    /** Remove the show runnable from the main handler queue to improve performance. */
+    private fun cancelShowRunnable() {
+        DejankUtils.removeCallbacks(showRunnable)
+        mainHandler.removeCallbacks(showRunnable)
+    }
+
+    /** Returns whether the primary bouncer is currently showing. */
+    fun isBouncerShowing(): Boolean {
+        return isShowing.value
+    }
+
+    /** Whether we want to wait to show the bouncer in case passive auth succeeds. */
+    private fun usePrimaryBouncerPassiveAuthDelay(): Boolean {
+        val canRunFaceAuth =
+            keyguardStateController.isFaceAuthEnabled &&
+                keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE) &&
+                keyguardUpdateMonitor.doesCurrentPostureAllowFaceAuth()
+        val canRunActiveUnlock =
+            currentUserActiveUnlockRunning &&
+                keyguardUpdateMonitor.canTriggerActiveUnlockBasedOnDeviceState()
+
+        return featureFlags.isEnabled(Flags.DELAY_BOUNCER) &&
+            !needsFullscreenBouncer() &&
+            (canRunFaceAuth || canRunActiveUnlock)
+    }
+
+    companion object {
+        private const val TAG = "PrimaryBouncerInteractor"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt
new file mode 100644
index 0000000..9f17811
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.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.bouncer.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
+
+    /**
+     * This value is used for denoting the PIN length at which we want to layout the view in which
+     * PIN hinting is enabled
+     */
+    const val DEFAULT_PIN_LENGTH = 6
+
+    object ColorId {
+        const val TITLE = com.android.internal.R.attr.materialColorOnSurface
+        const val PIN_SHAPES = com.android.internal.R.attr.materialColorOnSurfaceVariant
+        const val NUM_PAD_BACKGROUND = com.android.internal.R.attr.materialColorSurfaceContainerHigh
+        const val NUM_PAD_BACKGROUND_PRESSED = com.android.internal.R.attr.materialColorPrimaryFixed
+        const val NUM_PAD_PRESSED = com.android.internal.R.attr.materialColorOnPrimaryFixed
+        const val NUM_PAD_KEY = com.android.internal.R.attr.materialColorOnSurface
+        const val NUM_PAD_BUTTON = com.android.internal.R.attr.materialColorOnSecondaryFixed
+        const val EMERGENCY_BUTTON = com.android.internal.R.attr.materialColorTertiaryFixed
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerCallbackActionsModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerCallbackActionsModel.kt
new file mode 100644
index 0000000..afddd39
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerCallbackActionsModel.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.bouncer.shared.model
+
+import com.android.systemui.plugins.ActivityStarter
+
+/** Encapsulates callbacks to be invoked by the bouncer logic. */
+// TODO(b/243683121): Move dismiss logic from view controllers
+data class BouncerCallbackActionsModel(
+    val onDismissAction: ActivityStarter.OnDismissAction?,
+    val cancelAction: Runnable?
+)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerMessageModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerMessageModel.kt
new file mode 100644
index 0000000..0e9e962
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerMessageModel.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.bouncer.shared.model
+
+import android.content.res.ColorStateList
+
+/**
+ * Represents the message displayed on the bouncer. It has two parts, primary and a secondary
+ * message
+ */
+data class BouncerMessageModel(val message: Message? = null, val secondaryMessage: Message? = null)
+
+/**
+ * Representation of a single message on the bouncer. It can be either a string or a string resource
+ * ID
+ */
+data class Message(
+    val message: String? = null,
+    val messageResId: Int? = null,
+    val colorState: ColorStateList? = null,
+    /** Any plural formatter arguments that can used to format the [messageResId] */
+    var formatterArgs: Map<String, Any>? = null,
+    /** Specifies whether this text should be animated when it is shown. */
+    var animate: Boolean = true,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerShowMessageModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerShowMessageModel.kt
new file mode 100644
index 0000000..1878572
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerShowMessageModel.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.bouncer.shared.model
+
+import android.content.res.ColorStateList
+
+/** Show a keyguard message to the bouncer. */
+data class BouncerShowMessageModel(val message: String?, val colorStateList: ColorStateList?)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt
new file mode 100644
index 0000000..47fac2b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.LinearLayout
+import com.android.keyguard.BouncerKeyguardMessageArea
+import com.android.keyguard.KeyguardMessageArea
+import com.android.keyguard.KeyguardMessageAreaController
+import com.android.systemui.R
+
+class BouncerMessageView : LinearLayout {
+    constructor(context: Context?) : super(context)
+
+    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+
+    init {
+        inflate(context, R.layout.bouncer_message_view, this)
+    }
+
+    var primaryMessageView: BouncerKeyguardMessageArea? = null
+    var secondaryMessageView: BouncerKeyguardMessageArea? = null
+    var primaryMessage: KeyguardMessageAreaController<KeyguardMessageArea>? = null
+    var secondaryMessage: KeyguardMessageAreaController<KeyguardMessageArea>? = null
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        primaryMessageView = findViewById(R.id.bouncer_primary_message_area)
+        secondaryMessageView = findViewById(R.id.bouncer_secondary_message_area)
+        primaryMessageView?.disable()
+        secondaryMessageView?.disable()
+    }
+
+    fun init(factory: KeyguardMessageAreaController.Factory) {
+        primaryMessage = factory.create(primaryMessageView)
+        primaryMessage?.init()
+        secondaryMessage = factory.create(secondaryMessageView)
+        secondaryMessage?.init()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerView.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerView.kt
new file mode 100644
index 0000000..dc00321
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerView.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.bouncer.ui
+
+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
+import javax.inject.Inject
+
+/** An abstraction to interface with the ui layer, without changing state. */
+interface BouncerView {
+    var delegate: BouncerViewDelegate?
+}
+
+/** A lightweight class to hold reference to the ui delegate. */
+@SysUISingleton
+class BouncerViewImpl @Inject constructor() : BouncerView {
+    private var _delegate: WeakReference<BouncerViewDelegate?> = WeakReference(null)
+    override var delegate: BouncerViewDelegate?
+        get() = _delegate.get()
+        set(value) {
+            _delegate = WeakReference(value)
+        }
+}
+
+/** An abstraction that implements view logic. */
+interface BouncerViewDelegate {
+    fun isFullScreenBouncer(): Boolean
+    fun shouldDismissOnMenuPressed(): Boolean
+    fun interceptMediaKey(event: KeyEvent?): Boolean
+    fun dispatchBackKeyEventPreIme(): Boolean
+    fun showNextSecurityScreenOrFinish(): Boolean
+    fun resume()
+    fun setDismissAction(
+        onDismissAction: ActivityStarter.OnDismissAction?,
+        cancelAction: Runnable?,
+    )
+    fun willDismissWithActions(): Boolean
+    fun willRunDismissFromKeyguard(): Boolean
+    /** @return the {@link OnBackAnimationCallback} to animate Bouncer during a back gesture. */
+    fun getBackCallback(): OnBackAnimationCallback
+    fun showPromptReason(reason: Int)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
new file mode 100644
index 0000000..0cbfb68
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui
+
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface BouncerViewModule {
+    /** Binds BouncerView to BouncerViewImpl and makes it injectable. */
+    @Binds fun bindBouncerView(bouncerViewImpl: BouncerViewImpl): BouncerView
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerMessageViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerMessageViewBinder.kt
new file mode 100644
index 0000000..5a59012
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerMessageViewBinder.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.bouncer.ui.binder
+
+import android.text.TextUtils
+import android.util.PluralsMessageFormatter
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.keyguard.BouncerKeyguardMessageArea
+import com.android.keyguard.KeyguardMessageArea
+import com.android.keyguard.KeyguardMessageAreaController
+import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
+import com.android.systemui.bouncer.shared.model.Message
+import com.android.systemui.bouncer.ui.BouncerMessageView
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.log.BouncerLogger
+import kotlinx.coroutines.launch
+
+object BouncerMessageViewBinder {
+    @JvmStatic
+    fun bind(
+        view: BouncerMessageView,
+        interactor: BouncerMessageInteractor,
+        factory: KeyguardMessageAreaController.Factory,
+        bouncerLogger: BouncerLogger,
+        featureFlags: FeatureFlags
+    ) {
+        view.repeatWhenAttached {
+            if (!featureFlags.isEnabled(Flags.REVAMPED_BOUNCER_MESSAGES)) {
+                view.primaryMessageView?.disable()
+                view.secondaryMessageView?.disable()
+                return@repeatWhenAttached
+            }
+            view.init(factory)
+            view.primaryMessage?.setIsVisible(true)
+            view.secondaryMessage?.setIsVisible(true)
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                bouncerLogger.startBouncerMessageInteractor()
+                launch {
+                    interactor.bouncerMessage.collect {
+                        bouncerLogger.bouncerMessageUpdated(it)
+                        updateView(
+                            view.primaryMessage,
+                            view.primaryMessageView,
+                            message = it?.message,
+                            allowTruncation = true,
+                        )
+                        updateView(
+                            view.secondaryMessage,
+                            view.secondaryMessageView,
+                            message = it?.secondaryMessage,
+                            allowTruncation = false,
+                        )
+                        view.requestLayout()
+                    }
+                }
+            }
+        }
+    }
+
+    private fun updateView(
+        controller: KeyguardMessageAreaController<KeyguardMessageArea>?,
+        view: BouncerKeyguardMessageArea?,
+        message: Message?,
+        allowTruncation: Boolean = false,
+    ) {
+        if (view == null || controller == null) return
+        if (message?.message != null || message?.messageResId != null) {
+            controller.setIsVisible(true)
+            var newMessage = message.message ?: ""
+            if (message.messageResId != null && message.messageResId != 0) {
+                newMessage = view.resources.getString(message.messageResId)
+                if (message.formatterArgs != null) {
+                    newMessage =
+                        PluralsMessageFormatter.format(
+                            view.resources,
+                            message.formatterArgs,
+                            message.messageResId
+                        )
+                }
+            }
+            controller.setMessage(newMessage, message.animate)
+        } else {
+            controller.setIsVisible(false)
+            controller.setMessage(0)
+        }
+        message?.colorState?.let { controller.setNextMessageColor(it) }
+        view.ellipsize =
+            if (allowTruncation) TextUtils.TruncateAt.END else TextUtils.TruncateAt.MARQUEE
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
new file mode 100644
index 0000000..34e934b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
@@ -0,0 +1,253 @@
+/*
+ * 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.bouncer.ui.binder
+
+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.keyguard.KeyguardMessageAreaController
+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.systemui.bouncer.domain.interactor.BouncerMessageInteractor
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE
+import com.android.systemui.bouncer.ui.BouncerViewDelegate
+import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.log.BouncerLogger
+import com.android.systemui.plugins.ActivityStarter
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
+
+/** Binds the bouncer container to its view model. */
+object KeyguardBouncerViewBinder {
+    @JvmStatic
+    fun bind(
+        view: ViewGroup,
+        viewModel: KeyguardBouncerViewModel,
+        primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
+        componentFactory: KeyguardBouncerComponent.Factory,
+        messageAreaControllerFactory: KeyguardMessageAreaController.Factory,
+        bouncerMessageInteractor: BouncerMessageInteractor,
+        bouncerLogger: BouncerLogger,
+        featureFlags: FeatureFlags,
+    ) {
+        // 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 = securityContainerController.currentSecurityMode
+                    return mode == KeyguardSecurityModel.SecurityMode.SimPin ||
+                        mode == KeyguardSecurityModel.SecurityMode.SimPuk
+                }
+
+                override fun getBackCallback(): OnBackAnimationCallback {
+                    return securityContainerController.backCallback
+                }
+
+                override fun shouldDismissOnMenuPressed(): Boolean {
+                    return securityContainerController.shouldEnableMenuKey()
+                }
+
+                override fun interceptMediaKey(event: KeyEvent?): Boolean {
+                    return securityContainerController.interceptMediaKey(event)
+                }
+
+                override fun dispatchBackKeyEventPreIme(): Boolean {
+                    return securityContainerController.dispatchBackKeyEventPreIme()
+                }
+
+                override fun showNextSecurityScreenOrFinish(): Boolean {
+                    return securityContainerController.dismiss(
+                        KeyguardUpdateMonitor.getCurrentUser()
+                    )
+                }
+
+                override fun resume() {
+                    securityContainerController.showPrimarySecurityScreen(/* isTurningOff= */ false)
+                    securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON)
+                }
+
+                override fun setDismissAction(
+                    onDismissAction: ActivityStarter.OnDismissAction?,
+                    cancelAction: Runnable?
+                ) {
+                    securityContainerController.setOnDismissAction(onDismissAction, cancelAction)
+                }
+
+                override fun willDismissWithActions(): Boolean {
+                    return securityContainerController.hasDismissActions()
+                }
+
+                override fun willRunDismissFromKeyguard(): Boolean {
+                    return securityContainerController.willRunDismissFromKeyguard()
+                }
+
+                override fun showPromptReason(reason: Int) {
+                    securityContainerController.showPromptReason(reason)
+                }
+            }
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                try {
+                    viewModel.setBouncerViewDelegate(delegate)
+                    launch {
+                        viewModel.isShowing.collect { isShowing ->
+                            view.visibility = if (isShowing) View.VISIBLE else View.INVISIBLE
+                            if (isShowing) {
+                                securityContainerController.reinflateViewFlipper {
+                                    // Reset Security Container entirely.
+                                    securityContainerController.onBouncerVisibilityChanged(
+                                        /* isVisible= */ true
+                                    )
+                                    securityContainerController.showPrimarySecurityScreen(
+                                        /* turningOff= */ false
+                                    )
+                                    securityContainerController.setInitialMessage()
+                                    securityContainerController.appear()
+                                    securityContainerController.onResume(
+                                        KeyguardSecurityView.SCREEN_ON
+                                    )
+                                    bouncerLogger.bindingBouncerMessageView()
+                                    it.bindMessageView(
+                                        bouncerMessageInteractor,
+                                        messageAreaControllerFactory,
+                                        bouncerLogger,
+                                        featureFlags
+                                    )
+                                }
+                            } else {
+                                bouncerMessageInteractor.onBouncerBeingHidden()
+                                securityContainerController.onBouncerVisibilityChanged(
+                                    /* isVisible= */ false
+                                )
+                                securityContainerController.cancelDismissAction()
+                                securityContainerController.reset()
+                                securityContainerController.onPause()
+                            }
+                        }
+                    }
+
+                    launch {
+                        viewModel.startingToHide.collect {
+                            securityContainerController.onStartingToHide()
+                        }
+                    }
+
+                    launch {
+                        viewModel.startDisappearAnimation.collect {
+                            securityContainerController.startDisappearAnimation(it)
+                        }
+                    }
+
+                    launch {
+                        viewModel.bouncerExpansionAmount.collect { expansion ->
+                            securityContainerController.setExpansion(expansion)
+                        }
+                    }
+
+                    launch {
+                        primaryBouncerToGoneTransitionViewModel.bouncerAlpha.collect { alpha ->
+                            securityContainerController.setAlpha(alpha)
+                        }
+                    }
+
+                    launch {
+                        viewModel.bouncerExpansionAmount
+                            .filter { it == EXPANSION_VISIBLE }
+                            .collect {
+                                securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON)
+                                view.announceForAccessibility(securityContainerController.title)
+                            }
+                    }
+
+                    launch {
+                        viewModel.isInteractable.collect { isInteractable ->
+                            securityContainerController.setInteractable(isInteractable)
+                        }
+                    }
+
+                    launch {
+                        viewModel.keyguardPosition.collect { position ->
+                            securityContainerController.updateKeyguardPosition(position)
+                        }
+                    }
+
+                    launch {
+                        viewModel.updateResources.collect {
+                            securityContainerController.updateResources()
+                            viewModel.notifyUpdateResources()
+                        }
+                    }
+
+                    launch {
+                        viewModel.bouncerShowMessage.collect {
+                            securityContainerController.showMessage(
+                                it.message,
+                                it.colorStateList,
+                                /* animated= */ true
+                            )
+                            viewModel.onMessageShown()
+                        }
+                    }
+
+                    launch {
+                        viewModel.keyguardAuthenticated.collect {
+                            securityContainerController.finish(
+                                it,
+                                KeyguardUpdateMonitor.getCurrentUser()
+                            )
+                            viewModel.notifyKeyguardAuthenticated()
+                        }
+                    }
+
+                    launch {
+                        viewModel
+                            .observeOnIsBackButtonEnabled { view.systemUiVisibility }
+                            .collect { view.systemUiVisibility = it }
+                    }
+
+                    launch {
+                        viewModel.shouldUpdateSideFps.collect {
+                            viewModel.updateSideFpsVisibility()
+                        }
+                    }
+
+                    launch {
+                        viewModel.sideFpsShowing.collect {
+                            securityContainerController.updateSideFpsVisibility(it)
+                        }
+                    }
+                    awaitCancellation()
+                } finally {
+                    viewModel.setBouncerViewDelegate(null)
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index 984d9ab..db6ca0b 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -22,10 +22,13 @@
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.util.kotlin.pairwise
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -58,6 +61,7 @@
 
     private val pin: PinBouncerViewModel by lazy {
         PinBouncerViewModel(
+            applicationContext = applicationContext,
             applicationScope = applicationScope,
             interactor = interactor,
             isInputEnabled = isInputEnabled,
@@ -66,6 +70,7 @@
 
     private val password: PasswordBouncerViewModel by lazy {
         PasswordBouncerViewModel(
+            applicationScope = applicationScope,
             interactor = interactor,
             isInputEnabled = isInputEnabled,
         )
@@ -81,14 +86,30 @@
     }
 
     /** View-model for the current UI, based on the current authentication method. */
+    private val _authMethod =
+        MutableSharedFlow<AuthMethodBouncerViewModel?>(
+            replay = 1,
+            onBufferOverflow = BufferOverflow.DROP_OLDEST,
+        )
     val authMethod: StateFlow<AuthMethodBouncerViewModel?> =
-        interactor.authenticationMethod
-            .map { authMethod -> toViewModel(authMethod) }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = toViewModel(interactor.authenticationMethod.value),
-            )
+        _authMethod.stateIn(
+            scope = applicationScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = null,
+        )
+
+    init {
+        applicationScope.launch {
+            _authMethod.subscriptionCount
+                .pairwise()
+                .map { (previousCount, currentCount) -> currentCount > previousCount }
+                .collect { subscriberAdded ->
+                    if (subscriberAdded) {
+                        reloadAuthMethod()
+                    }
+                }
+        }
+    }
 
     /** The user-facing message to show in the bouncer. */
     val message: StateFlow<MessageViewModel> =
@@ -125,8 +146,8 @@
             interactor.throttling
                 .map { model ->
                     model?.let {
-                        when (interactor.authenticationMethod.value) {
-                            is AuthenticationMethodModel.PIN ->
+                        when (interactor.getAuthenticationMethod()) {
+                            is AuthenticationMethodModel.Pin ->
                                 R.string.kg_too_many_failed_pin_attempts_dialog_message
                             is AuthenticationMethodModel.Password ->
                                 R.string.kg_too_many_failed_password_attempts_dialog_message
@@ -161,17 +182,6 @@
         _throttlingDialogMessage.value = null
     }
 
-    private fun toViewModel(
-        authMethod: AuthenticationMethodModel,
-    ): AuthMethodBouncerViewModel? {
-        return when (authMethod) {
-            is AuthenticationMethodModel.PIN -> pin
-            is AuthenticationMethodModel.Password -> password
-            is AuthenticationMethodModel.Pattern -> pattern
-            else -> null
-        }
-    }
-
     private fun toMessageViewModel(
         message: String?,
         throttling: AuthenticationThrottledModel?,
@@ -182,6 +192,17 @@
         )
     }
 
+    private suspend fun reloadAuthMethod() {
+        _authMethod.tryEmit(
+            when (interactor.getAuthenticationMethod()) {
+                is AuthenticationMethodModel.Pin -> pin
+                is AuthenticationMethodModel.Password -> password
+                is AuthenticationMethodModel.Pattern -> pattern
+                else -> null
+            }
+        )
+    }
+
     data class MessageViewModel(
         val text: String,
 
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModel.kt
new file mode 100644
index 0000000..6ba8439
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModel.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.bouncer.ui.viewmodel
+
+import android.view.View
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
+import com.android.systemui.bouncer.ui.BouncerView
+import com.android.systemui.bouncer.ui.BouncerViewDelegate
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+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
+@Inject
+constructor(
+    private val view: BouncerView,
+    private val interactor: PrimaryBouncerInteractor,
+) {
+    /** Observe on bouncer expansion amount. */
+    val bouncerExpansionAmount: Flow<Float> = interactor.panelExpansionAmount
+
+    /** Can the user interact with the view? */
+    val isInteractable: Flow<Boolean> = interactor.isInteractable
+
+    /** Observe whether bouncer is showing or not. */
+    val isShowing: Flow<Boolean> = interactor.isShowing
+
+    /** Observe whether bouncer is starting to hide. */
+    val startingToHide: Flow<Unit> = interactor.startingToHide
+
+    /** Observe whether we want to start the disappear animation. */
+    val startDisappearAnimation: Flow<Runnable> = interactor.startingDisappearAnimation
+
+    /** Observe whether we want to update keyguard position. */
+    val keyguardPosition: Flow<Float> = interactor.keyguardPosition
+
+    /** Observe whether we want to update resources. */
+    val updateResources: Flow<Boolean> = interactor.resourceUpdateRequests
+
+    /** Observe whether we want to set a keyguard message when the bouncer shows. */
+    val bouncerShowMessage: Flow<BouncerShowMessageModel> = interactor.showMessage
+
+    /** Observe whether keyguard is authenticated already. */
+    val keyguardAuthenticated: Flow<Boolean> = interactor.keyguardAuthenticated
+
+    /** Observe whether 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.isShowing.map {},
+            interactor.startingToHide,
+            interactor.startingDisappearAnimation.filterNotNull().map {}
+        )
+
+    /** Observe whether we want to update resources. */
+    fun notifyUpdateResources() {
+        interactor.notifyUpdatedResources()
+    }
+
+    /** Notify that keyguard authenticated was handled */
+    fun notifyKeyguardAuthenticated() {
+        interactor.notifyKeyguardAuthenticatedHandled()
+    }
+
+    /** Notifies that the message was shown. */
+    fun onMessageShown() {
+        interactor.onMessageShown()
+    }
+
+    fun updateSideFpsVisibility() {
+        interactor.updateSideFpsVisibility()
+    }
+
+    /** Observe whether back button is enabled. */
+    fun observeOnIsBackButtonEnabled(systemUiVisibility: () -> Int): Flow<Int> {
+        return interactor.isBackButtonEnabled.map { enabled ->
+            var vis: Int = systemUiVisibility()
+            vis =
+                if (enabled) {
+                    vis and View.STATUS_BAR_DISABLE_BACK.inv()
+                } else {
+                    vis or View.STATUS_BAR_DISABLE_BACK
+                }
+            vis
+        }
+    }
+
+    /** Set an abstraction that will hold reference to the ui delegate for the bouncer view. */
+    fun setBouncerViewDelegate(delegate: BouncerViewDelegate?) {
+        view.delegate = delegate
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index 55929b5..ca15f4e 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -17,12 +17,15 @@
 package com.android.systemui.bouncer.ui.viewmodel
 
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the password bouncer UI. */
 class PasswordBouncerViewModel(
+    private val applicationScope: CoroutineScope,
     private val interactor: BouncerInteractor,
     isInputEnabled: StateFlow<Boolean>,
 ) :
@@ -50,10 +53,13 @@
 
     /** Notifies that the user has pressed the key for attempting to authenticate the password. */
     fun onAuthenticateKeyPressed() {
-        if (!interactor.authenticate(password.value.toCharArray().toList())) {
-            showFailureAnimation()
-        }
-
+        val password = _password.value.toCharArray().toList()
         _password.value = ""
+
+        applicationScope.launch {
+            if (interactor.authenticate(password) != true) {
+                showFailureAnimation()
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index d9ef75d..5efa6f0 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -29,13 +29,15 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the pattern bouncer UI. */
 class PatternBouncerViewModel(
     private val applicationContext: Context,
-    applicationScope: CoroutineScope,
+    private val applicationScope: CoroutineScope,
     private val interactor: BouncerInteractor,
     isInputEnabled: StateFlow<Boolean>,
 ) :
@@ -69,12 +71,17 @@
 
     /** Whether the pattern itself should be rendered visibly. */
     val isPatternVisible: StateFlow<Boolean> =
-        interactor.authenticationMethod
-            .map { authMethod -> isPatternVisible(authMethod) }
+        flow {
+                emit(null)
+                emit(interactor.getAuthenticationMethod())
+            }
+            .map { authMethod ->
+                (authMethod as? AuthenticationMethodModel.Pattern)?.isPatternVisible ?: false
+            }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.Eagerly,
-                initialValue = isPatternVisible(interactor.authenticationMethod.value),
+                initialValue = false,
             )
 
     /** Notifies that the UI has been shown to the user. */
@@ -153,19 +160,16 @@
 
     /** Notifies that the user has ended the drag gesture across the dot grid. */
     fun onDragEnd() {
-        val isSuccessfullyAuthenticated =
-            interactor.authenticate(_selectedDots.value.map { it.toCoordinate() })
-        if (!isSuccessfullyAuthenticated) {
-            showFailureAnimation()
-        }
-
+        val pattern = _selectedDots.value.map { it.toCoordinate() }
         _dots.value = defaultDots()
         _currentDot.value = null
         _selectedDots.value = linkedSetOf()
-    }
 
-    private fun isPatternVisible(authMethodModel: AuthenticationMethodModel): Boolean {
-        return (authMethodModel as? AuthenticationMethodModel.Pattern)?.isPatternVisible ?: false
+        applicationScope.launch {
+            if (interactor.authenticate(pattern) != true) {
+                showFailureAnimation()
+            }
+        }
     }
 
     private fun defaultDots(): List<PatternDotViewModel> {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index 5c0fd92..641e863 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -16,21 +16,22 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
-import androidx.annotation.VisibleForTesting
+import android.content.Context
+import com.android.keyguard.PinShapeAdapter
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
-import com.android.systemui.util.kotlin.pairwise
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the PIN code bouncer UI. */
 class PinBouncerViewModel(
+    applicationContext: Context,
     private val applicationScope: CoroutineScope,
     private val interactor: BouncerInteractor,
     isInputEnabled: StateFlow<Boolean>,
@@ -39,21 +40,51 @@
         isInputEnabled = isInputEnabled,
     ) {
 
-    private val entered = MutableStateFlow<List<Int>>(emptyList())
-    /**
-     * The length of the PIN digits that were input so far, two values are supplied the previous and
-     * the current.
-     */
-    val pinLengths: StateFlow<Pair<Int, Int>> =
-        entered
-            .pairwise()
-            .map { it.previousValue.size to it.newValue.size }
+    val pinShapes = PinShapeAdapter(applicationContext)
+
+    private val mutablePinEntries = MutableStateFlow<List<EnteredKey>>(emptyList())
+    val pinEntries: StateFlow<List<EnteredKey>> = mutablePinEntries
+
+    /** The length of the hinted PIN, or `null` if pin length hint should not be shown. */
+    val hintedPinLength: StateFlow<Int?> =
+        flow { emit(interactor.getAuthenticationMethod()) }
+            .map { authMethod ->
+                // Hinting is enabled for 6-digit codes only
+                autoConfirmPinLength(authMethod).takeIf { it == HINTING_PASSCODE_LENGTH }
+            }
             .stateIn(
                 scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = 0 to 0,
+                started = SharingStarted.Eagerly,
+                initialValue = null,
             )
-    private var resetPinJob: Job? = null
+
+    /** Appearance of the backspace button. */
+    val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> =
+        mutablePinEntries
+            .map { mutablePinEntries ->
+                computeBackspaceButtonAppearance(
+                    interactor.getAuthenticationMethod(),
+                    mutablePinEntries
+                )
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = ActionButtonAppearance.Hidden,
+            )
+
+    /** Appearance of the confirm button. */
+    val confirmButtonAppearance: StateFlow<ActionButtonAppearance> =
+        flow {
+                emit(null)
+                emit(interactor.getAuthenticationMethod())
+            }
+            .map { authMethod -> computeConfirmButtonAppearance(authMethod) }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = ActionButtonAppearance.Hidden,
+            )
 
     /** Notifies that the UI has been shown to the user. */
     fun onShown() {
@@ -62,47 +93,106 @@
 
     /** Notifies that the user clicked on a PIN button with the given digit value. */
     fun onPinButtonClicked(input: Int) {
-        resetPinJob?.cancel()
-        resetPinJob = null
-
-        if (entered.value.isEmpty()) {
+        if (mutablePinEntries.value.isEmpty()) {
             interactor.clearMessage()
         }
 
-        entered.value += input
+        mutablePinEntries.value += EnteredKey(input)
+
+        tryAuthenticate(useAutoConfirm = true)
     }
 
     /** Notifies that the user clicked the backspace button. */
     fun onBackspaceButtonClicked() {
-        if (entered.value.isEmpty()) {
+        if (mutablePinEntries.value.isEmpty()) {
             return
         }
-
-        entered.value = entered.value.toMutableList().apply { removeLast() }
+        mutablePinEntries.value = mutablePinEntries.value.toMutableList().apply { removeLast() }
     }
 
     /** Notifies that the user long-pressed the backspace button. */
     fun onBackspaceButtonLongPressed() {
-        resetPinJob?.cancel()
-        resetPinJob =
-            applicationScope.launch {
-                while (entered.value.isNotEmpty()) {
-                    onBackspaceButtonClicked()
-                    delay(BACKSPACE_LONG_PRESS_DELAY_MS)
-                }
-            }
+        mutablePinEntries.value = emptyList()
     }
 
     /** Notifies that the user clicked the "enter" button. */
     fun onAuthenticateButtonClicked() {
-        if (!interactor.authenticate(entered.value)) {
-            showFailureAnimation()
+        tryAuthenticate(useAutoConfirm = false)
+    }
+
+    private fun tryAuthenticate(useAutoConfirm: Boolean) {
+        val pinCode = mutablePinEntries.value.map { it.input }
+
+        applicationScope.launch {
+            val isSuccess = interactor.authenticate(pinCode, useAutoConfirm) ?: return@launch
+
+            if (!isSuccess) {
+                showFailureAnimation()
+            }
+
+            mutablePinEntries.value = emptyList()
         }
-
-        entered.value = emptyList()
     }
 
-    companion object {
-        @VisibleForTesting const val BACKSPACE_LONG_PRESS_DELAY_MS = 80L
+    private fun isAutoConfirmEnabled(authMethodModel: AuthenticationMethodModel?): Boolean {
+        return (authMethodModel as? AuthenticationMethodModel.Pin)?.autoConfirm == true
     }
+
+    private fun autoConfirmPinLength(authMethodModel: AuthenticationMethodModel?): Int? {
+        if (!isAutoConfirmEnabled(authMethodModel)) return null
+
+        return (authMethodModel as? AuthenticationMethodModel.Pin)?.code?.size
+    }
+
+    private fun computeBackspaceButtonAppearance(
+        authMethodModel: AuthenticationMethodModel,
+        enteredPin: List<EnteredKey>
+    ): ActionButtonAppearance {
+        val isAutoConfirmEnabled = isAutoConfirmEnabled(authMethodModel)
+        val isEmpty = enteredPin.isEmpty()
+
+        return when {
+            isAutoConfirmEnabled && isEmpty -> ActionButtonAppearance.Hidden
+            isAutoConfirmEnabled -> ActionButtonAppearance.Subtle
+            else -> ActionButtonAppearance.Shown
+        }
+    }
+    private fun computeConfirmButtonAppearance(
+        authMethodModel: AuthenticationMethodModel?
+    ): ActionButtonAppearance {
+        return if (isAutoConfirmEnabled(authMethodModel)) {
+            ActionButtonAppearance.Hidden
+        } else {
+            ActionButtonAppearance.Shown
+        }
+    }
+}
+
+/** Appearance of pin-pad action buttons. */
+enum class ActionButtonAppearance {
+    /** Button must not be shown. */
+    Hidden,
+    /** Button is shown, but with no background to make it less prominent. */
+    Subtle,
+    /** Button is shown. */
+    Shown,
+}
+
+/** Auto-confirm passcodes of exactly 6 digits show a length hint, see http://shortn/_IXlmSNbDh6 */
+private const val HINTING_PASSCODE_LENGTH = 6
+
+private var nextSequenceNumber = 1
+
+/**
+ * The pin bouncer [input] as digits 0-9, together with a [sequenceNumber] to indicate the ordering.
+ *
+ * Since the model only allows appending/removing [EnteredKey]s from the end, the [sequenceNumber]
+ * is strictly increasing in input order of the pin, but not guaranteed to be monotonic or start at
+ * a specific number.
+ */
+data class EnteredKey
+internal constructor(val input: Int, val sequenceNumber: Int = nextSequenceNumber++) :
+    Comparable<EnteredKey> {
+    override fun compareTo(other: EnteredKey): Int =
+        compareValuesBy(this, other, EnteredKey::sequenceNumber)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
index 5b3a982..068f329 100644
--- a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
@@ -21,10 +21,10 @@
 import android.content.Intent
 import android.content.IntentFilter
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogMessage
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.INFO
+import com.android.systemui.log.core.LogMessage
 import com.android.systemui.log.dagger.BroadcastDispatcherLog
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
index b2bcb05..25ccc16 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
@@ -396,6 +396,7 @@
                 || mDataProvider.isDocked()
                 || mAccessibilityManager.isTouchExplorationEnabled()
                 || mDataProvider.isA11yAction()
+                || mDataProvider.isFromTrackpad()
                 || (mFeatureFlags.isEnabled(Flags.FALSING_OFF_FOR_UNFOLDED)
                     && mDataProvider.isUnfolded());
     }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
index d6c85fb..809d5b2 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
@@ -261,6 +261,16 @@
         return mLastMotionEvent.getY() < mFirstRecentMotionEvent.getY();
     }
 
+    public boolean isFromTrackpad() {
+        if (mRecentMotionEvents.isEmpty()) {
+            return false;
+        }
+
+        int classification = mRecentMotionEvents.get(0).getClassification();
+        return classification == MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE
+                || classification == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE;
+    }
+
     private void recalculateData() {
         if (!mDirty) {
             return;
@@ -343,7 +353,9 @@
                     motionEvent.getDeviceId(),
                     motionEvent.getEdgeFlags(),
                     motionEvent.getSource(),
-                    motionEvent.getFlags()
+                    motionEvent.getDisplayId(),
+                    motionEvent.getFlags(),
+                    motionEvent.getClassification()
             ));
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index 757ebf4..ffd836b 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -18,6 +18,7 @@
 
 import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
 
+
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_ACTIONS;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_SHOWN;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_TAPPED;
@@ -33,6 +34,7 @@
 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_IMAGE_TIMEOUT;
+import static com.android.systemui.flags.Flags.CLIPBOARD_SHARED_TRANSITIONS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -47,6 +49,7 @@
 import android.net.Uri;
 import android.os.Looper;
 import android.provider.DeviceConfig;
+import android.util.Log;
 import android.view.InputEvent;
 import android.view.InputEventReceiver;
 import android.view.InputMonitor;
@@ -54,6 +57,7 @@
 import android.view.WindowInsets;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.UiEventLogger;
@@ -73,7 +77,8 @@
 /**
  * Controls state and UI for the overlay that appears when something is added to the clipboard
  */
-public class ClipboardOverlayController implements ClipboardListener.ClipboardOverlay {
+public class ClipboardOverlayController implements ClipboardListener.ClipboardOverlay,
+        ClipboardOverlayView.ClipboardOverlayCallbacks {
     private static final String TAG = "ClipboardOverlayCtrlr";
 
     /** Constants for screenshot/copy deconflicting */
@@ -92,6 +97,7 @@
     private final FeatureFlags mFeatureFlags;
     private final Executor mBgExecutor;
     private final ClipboardImageLoader mClipboardImageLoader;
+    private final ClipboardTransitionExecutor mTransitionExecutor;
 
     private final ClipboardOverlayView mView;
 
@@ -179,10 +185,12 @@
             ClipboardOverlayUtils clipboardUtils,
             @Background Executor bgExecutor,
             ClipboardImageLoader clipboardImageLoader,
+            ClipboardTransitionExecutor transitionExecutor,
             UiEventLogger uiEventLogger) {
         mContext = context;
         mBroadcastDispatcher = broadcastDispatcher;
         mClipboardImageLoader = clipboardImageLoader;
+        mTransitionExecutor = transitionExecutor;
 
         mClipboardLogger = new ClipboardLogger(uiEventLogger);
 
@@ -200,7 +208,11 @@
         mClipboardUtils = clipboardUtils;
         mBgExecutor = bgExecutor;
 
-        mView.setCallbacks(mClipboardCallbacks);
+        if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+            mView.setCallbacks(this);
+        } else {
+            mView.setCallbacks(mClipboardCallbacks);
+        }
 
         mWindow.withWindowAttached(() -> {
             mWindow.setContentView(mView);
@@ -209,16 +221,24 @@
         });
 
         mTimeoutHandler.setOnTimeoutRunnable(() -> {
-            mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TIMED_OUT);
-            animateOut();
+            if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+                finish(CLIPBOARD_OVERLAY_TIMED_OUT);
+            } else {
+                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();
+                    if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+                        finish(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
+                    } else {
+                        mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
+                        animateOut();
+                    }
                 }
             }
         };
@@ -229,8 +249,12 @@
             @Override
             public void onReceive(Context context, Intent intent) {
                 if (SCREENSHOT_ACTION.equals(intent.getAction())) {
-                    mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
-                    animateOut();
+                    if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+                        finish(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
+                    } else {
+                        mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
+                        animateOut();
+                    }
                 }
             }
         };
@@ -457,8 +481,12 @@
                 remoteAction.ifPresent(action -> {
                     mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_ACTION_SHOWN);
                     mView.post(() -> mView.setActionChip(action, () -> {
-                        mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
-                        animateOut();
+                        if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+                            finish(CLIPBOARD_OVERLAY_ACTION_TAPPED);
+                        } else {
+                            mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
+                            animateOut();
+                        }
                     }));
                 });
             }
@@ -500,8 +528,12 @@
                     if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
                         if (!mView.isInTouchRegion(
                                 (int) motionEvent.getRawX(), (int) motionEvent.getRawY())) {
-                            mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
-                            animateOut();
+                            if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+                                finish(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
+                            } else {
+                                mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
+                                animateOut();
+                            }
                         }
                     }
                 }
@@ -551,12 +583,16 @@
         mEnterAnimator.start();
     }
 
+    private void finish(ClipboardOverlayEvent event) {
+        finish(event, null);
+    }
+
     private void animateOut() {
         if (mExitAnimator != null && mExitAnimator.isRunning()) {
             return;
         }
-        Animator anim = mView.getExitAnimation();
-        anim.addListener(new AnimatorListenerAdapter() {
+        mExitAnimator = mView.getExitAnimation();
+        mExitAnimator.addListener(new AnimatorListenerAdapter() {
             private boolean mCancelled;
 
             @Override
@@ -573,8 +609,47 @@
                 }
             }
         });
-        mExitAnimator = anim;
-        anim.start();
+        mExitAnimator.start();
+    }
+
+    private void finish(ClipboardOverlayEvent event, @Nullable Intent intent) {
+        if (mExitAnimator != null && mExitAnimator.isRunning()) {
+            return;
+        }
+        mExitAnimator = mView.getExitAnimation();
+        mExitAnimator.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) {
+                    mClipboardLogger.logSessionComplete(event);
+                    if (intent != null) {
+                        mContext.startActivity(intent);
+                    }
+                    hideImmediate();
+                }
+            }
+        });
+        mExitAnimator.start();
+    }
+
+    private void finishWithSharedTransition(ClipboardOverlayEvent event, Intent intent) {
+        if (mExitAnimator != null && mExitAnimator.isRunning()) {
+            return;
+        }
+        mClipboardLogger.logSessionComplete(event);
+        mExitAnimator = mView.getFadeOutAnimation();
+        mExitAnimator.start();
+        mTransitionExecutor.startSharedTransition(
+                mWindow, mView.getPreview(), intent, this::hideImmediate);
     }
 
     void hideImmediate() {
@@ -613,6 +688,76 @@
         mClipboardLogger.reset();
     }
 
+    @Override
+    public void onDismissButtonTapped() {
+        if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+            finish(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
+        }
+    }
+
+    @Override
+    public void onRemoteCopyButtonTapped() {
+        if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+            finish(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED,
+                    IntentCreator.getRemoteCopyIntent(mClipboardModel.getClipData(), mContext));
+        }
+    }
+
+    @Override
+    public void onShareButtonTapped() {
+        if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+            if (mClipboardModel.getType() != ClipboardModel.Type.OTHER) {
+                finishWithSharedTransition(CLIPBOARD_OVERLAY_SHARE_TAPPED,
+                        IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext));
+            }
+        }
+    }
+
+    @Override
+    public void onPreviewTapped() {
+        if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
+            switch (mClipboardModel.getType()) {
+                case TEXT:
+                    finish(CLIPBOARD_OVERLAY_EDIT_TAPPED,
+                            IntentCreator.getTextEditorIntent(mContext));
+                    break;
+                case IMAGE:
+                    finishWithSharedTransition(CLIPBOARD_OVERLAY_EDIT_TAPPED,
+                            IntentCreator.getImageEditIntent(mClipboardModel.getUri(), mContext));
+                    break;
+                default:
+                    Log.w(TAG, "Got preview tapped callback for non-editable type "
+                            + mClipboardModel.getType());
+            }
+        }
+    }
+
+    @Override
+    public void onMinimizedViewTapped() {
+        animateFromMinimized();
+    }
+
+    @Override
+    public void onInteraction() {
+        if (!mClipboardModel.isRemote()) {
+            mTimeoutHandler.resetTimeout();
+        }
+    }
+
+    @Override
+    public void onSwipeDismissInitiated(Animator animator) {
+        if (mExitAnimator != null && mExitAnimator.isRunning()) {
+            mExitAnimator.cancel();
+        }
+        mExitAnimator = animator;
+        mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
+    }
+
+    @Override
+    public void onDismissComplete() {
+        hideImmediate();
+    }
+
     static class ClipboardLogger {
         private final UiEventLogger mUiEventLogger;
         private String mClipSource;
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
index 28c57d3..a76d2ea 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
@@ -254,6 +254,10 @@
                 });
     }
 
+    View getPreview() {
+        return mClipboardPreview;
+    }
+
     void showImagePreview(@Nullable Bitmap thumbnail) {
         if (thumbnail == null) {
             mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden));
@@ -368,6 +372,19 @@
         return enterAnim;
     }
 
+    Animator getFadeOutAnimation() {
+        ValueAnimator alphaAnim = ValueAnimator.ofFloat(1, 0);
+        alphaAnim.addUpdateListener(animation -> {
+            float alpha = (float) animation.getAnimatedValue();
+            mActionContainer.setAlpha(alpha);
+            mActionContainerBackground.setAlpha(alpha);
+            mPreviewBorder.setAlpha(alpha);
+            mDismissButton.setAlpha(alpha);
+        });
+        alphaAnim.setDuration(300);
+        return alphaAnim;
+    }
+
     Animator getExitAnimation() {
         TimeInterpolator linearInterpolator = new LinearInterpolator();
         TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f);
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardTransitionExecutor.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardTransitionExecutor.kt
new file mode 100644
index 0000000..0b8e83e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardTransitionExecutor.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.clipboardoverlay
+
+import android.app.ActivityOptions
+import android.app.ExitTransitionCoordinator
+import android.content.Context
+import android.content.Intent
+import android.os.RemoteException
+import android.util.Log
+import android.util.Pair
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.IRemoteAnimationRunner
+import android.view.RemoteAnimationAdapter
+import android.view.RemoteAnimationTarget
+import android.view.View
+import android.view.Window
+import android.view.WindowManagerGlobal
+import com.android.internal.app.ChooserActivity
+import com.android.systemui.settings.DisplayTracker
+import javax.inject.Inject
+
+class ClipboardTransitionExecutor
+@Inject
+constructor(val context: Context, val displayTracker: DisplayTracker) {
+    fun startSharedTransition(window: Window, view: View, intent: Intent, onReady: Runnable) {
+        val transition: Pair<ActivityOptions, ExitTransitionCoordinator> =
+            ActivityOptions.startSharedElementAnimation(
+                window,
+                object : ExitTransitionCoordinator.ExitTransitionCallbacks {
+                    override fun isReturnTransitionAllowed(): Boolean {
+                        return false
+                    }
+
+                    override fun hideSharedElements() {
+                        onReady.run()
+                    }
+
+                    override fun onFinish() {}
+                },
+                null,
+                Pair.create(view, ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)
+            )
+        transition.second.startExit()
+        context.startActivity(intent, transition.first.toBundle())
+        val runner = RemoteAnimationAdapter(NULL_ACTIVITY_TRANSITION, 0, 0)
+        try {
+            WindowManagerGlobal.getWindowManagerService()
+                .overridePendingAppTransitionRemote(runner, displayTracker.defaultDisplayId)
+        } catch (e: Exception) {
+            Log.e(TAG, "Error overriding clipboard app transition", e)
+        }
+    }
+
+    private val TAG: String = "ClipboardTransitionExec"
+
+    /**
+     * This is effectively a no-op, but we need something non-null to pass in, in order to
+     * successfully override the pending activity entrance animation.
+     */
+    private val NULL_ACTIVITY_TRANSITION: IRemoteAnimationRunner.Stub =
+        object : IRemoteAnimationRunner.Stub() {
+            override fun onAnimationStart(
+                transit: Int,
+                apps: Array<RemoteAnimationTarget>,
+                wallpapers: Array<RemoteAnimationTarget>,
+                nonApps: Array<RemoteAnimationTarget>,
+                finishedCallback: IRemoteAnimationFinishedCallback
+            ) {
+                try {
+                    finishedCallback.onAnimationFinished()
+                } catch (e: RemoteException) {
+                    Log.e(TAG, "Error finishing screenshot remote animation", e)
+                }
+            }
+
+            override fun onAnimationCancelled() {}
+        }
+}
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
index de3a990..277b427 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.common.ui.view;
 
+import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.TypedArray;
@@ -27,8 +28,12 @@
 import android.widget.LinearLayout;
 import android.widget.SeekBar;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.R;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * The layout contains a seekbar whose progress could be modified
  * through the icons on two ends of the seekbar.
@@ -37,12 +42,16 @@
 
     private static final int DEFAULT_SEEKBAR_MAX = 6;
     private static final int DEFAULT_SEEKBAR_PROGRESS = 0;
+    private static final int DEFAULT_SEEKBAR_TICK_MARK = 0;
 
     private ViewGroup mIconStartFrame;
     private ViewGroup mIconEndFrame;
     private ImageView mIconStart;
     private ImageView mIconEnd;
     private SeekBar mSeekbar;
+    private int mSeekBarChangeMagnitude = 1;
+
+    private boolean mSetProgressFromButtonFlag = false;
 
     private SeekBarChangeListener mSeekBarListener = new SeekBarChangeListener();
     private String[] mStateLabels = null;
@@ -102,6 +111,15 @@
                         context.getString(iconEndFrameContentDescriptionId);
                 mIconEndFrame.setContentDescription(contentDescription);
             }
+            int tickMarkId = typedArray.getResourceId(
+                    R.styleable.SeekBarWithIconButtonsView_Layout_tickMark,
+                    DEFAULT_SEEKBAR_TICK_MARK);
+            if (tickMarkId != DEFAULT_SEEKBAR_TICK_MARK) {
+                mSeekbar.setTickMark(getResources().getDrawable(tickMarkId));
+            }
+            mSeekBarChangeMagnitude = typedArray.getInt(
+                    R.styleable.SeekBarWithIconButtonsView_Layout_seekBarChangeMagnitude,
+                    /* defValue= */ 1);
         } else {
             mSeekbar.setMax(DEFAULT_SEEKBAR_MAX);
             setProgress(DEFAULT_SEEKBAR_PROGRESS);
@@ -109,21 +127,8 @@
 
         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());
-            }
-        });
+        mIconStartFrame.setOnClickListener((view) -> onIconStartClicked());
+        mIconEndFrame.setOnClickListener((view) -> onIconEndClicked());
     }
 
     private static void setIconViewAndFrameEnabled(View iconView, boolean enabled) {
@@ -160,9 +165,9 @@
      * 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);
+    public void setOnSeekBarWithIconButtonsChangeListener(
+            @Nullable OnSeekBarWithIconButtonsChangeListener onSeekBarChangeListener) {
+        mSeekBarListener.setOnSeekBarWithIconButtonsChangeListener(onSeekBarChangeListener);
     }
 
     /**
@@ -183,6 +188,20 @@
     }
 
     /**
+     * Gets max to the seekbar in the layout.
+     */
+    public int getMax() {
+        return mSeekbar.getMax();
+    }
+
+    /**
+     * @return the magnitude by which seekbar progress changes when start and end icons are clicked.
+     */
+    public int getChangeMagnitude() {
+        return mSeekBarChangeMagnitude;
+    }
+
+    /**
      * 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
@@ -190,11 +209,72 @@
      */
     public void setProgress(int progress) {
         mSeekbar.setProgress(progress);
-        updateIconViewIfNeeded(progress);
+        updateIconViewIfNeeded(mSeekbar.getProgress());
+    }
+
+    private void setProgressFromButton(int progress) {
+        mSetProgressFromButtonFlag = true;
+        mSeekbar.setProgress(progress);
+        updateIconViewIfNeeded(mSeekbar.getProgress());
+    }
+
+    private void onIconStartClicked() {
+        final int progress = mSeekbar.getProgress();
+        if (progress > 0) {
+            setProgressFromButton(progress - mSeekBarChangeMagnitude);
+        }
+    }
+
+    private void onIconEndClicked() {
+        final int progress = mSeekbar.getProgress();
+        if (progress < mSeekbar.getMax()) {
+            setProgressFromButton(progress + mSeekBarChangeMagnitude);
+        }
+    }
+
+    /**
+     * Get current seekbar progress
+     *
+     * @return
+     */
+    @VisibleForTesting
+    public int getProgress() {
+        return mSeekbar.getProgress();
+    }
+
+    /**
+     * Extended from {@link SeekBar.OnSeekBarChangeListener} to add callback to notify the listeners
+     * the user interaction with the SeekBarWithIconButtonsView is finalized.
+     */
+    public interface OnSeekBarWithIconButtonsChangeListener
+            extends SeekBar.OnSeekBarChangeListener {
+
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef({
+                ControlUnitType.SLIDER,
+                ControlUnitType.BUTTON
+        })
+        /** Denotes the Last user interacted control unit type. */
+        @interface ControlUnitType {
+            int SLIDER = 0;
+            int BUTTON = 1;
+        }
+
+        /**
+         * Notification that the user interaction with SeekBarWithIconButtonsView is finalized. This
+         * would be triggered after user ends dragging on the slider or clicks icon buttons.
+         *
+         * @param seekBar The SeekBar in which the user ends interaction with
+         * @param control The last user interacted control unit. It would be
+         *                {@link ControlUnitType#SLIDER} if the user was changing the seekbar
+         *                progress through dragging the slider, or {@link ControlUnitType#BUTTON}
+         *                is the user was clicking button to change the progress.
+         */
+        void onUserInteractionFinalized(SeekBar seekBar, @ControlUnitType int control);
     }
 
     private class SeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
-        private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener = null;
+        private OnSeekBarWithIconButtonsChangeListener mOnSeekBarChangeListener = null;
 
         @Override
         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
@@ -202,7 +282,17 @@
                 setSeekbarStateDescription();
             }
             if (mOnSeekBarChangeListener != null) {
-                mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser);
+                if (mSetProgressFromButtonFlag) {
+                    mSetProgressFromButtonFlag = false;
+                    mOnSeekBarChangeListener.onProgressChanged(
+                            seekBar, progress, /* fromUser= */ true);
+                    // Directly trigger onUserInteractionFinalized since the interaction
+                    // (click button) is ended.
+                    mOnSeekBarChangeListener.onUserInteractionFinalized(
+                            seekBar, OnSeekBarWithIconButtonsChangeListener.ControlUnitType.BUTTON);
+                } else {
+                    mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser);
+                }
             }
             updateIconViewIfNeeded(progress);
         }
@@ -218,10 +308,13 @@
         public void onStopTrackingTouch(SeekBar seekBar) {
             if (mOnSeekBarChangeListener != null) {
                 mOnSeekBarChangeListener.onStopTrackingTouch(seekBar);
+                mOnSeekBarChangeListener.onUserInteractionFinalized(
+                        seekBar, OnSeekBarWithIconButtonsChangeListener.ControlUnitType.SLIDER);
             }
         }
 
-        void setOnSeekBarChangeListener(SeekBar.OnSeekBarChangeListener listener) {
+        void setOnSeekBarWithIconButtonsChangeListener(
+                OnSeekBarWithIconButtonsChangeListener 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 4173bdc..b15c60e 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -24,6 +24,9 @@
 import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
+import com.android.systemui.scene.shared.model.Scene
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
 import com.android.systemui.util.time.SystemClock
 
 /**
@@ -66,4 +69,11 @@
         viewModel: MultiShadeViewModel,
         clock: SystemClock,
     ): View
+
+    /** Create a [View] to represent [viewModel] on screen. */
+    fun createSceneContainerView(
+        context: Context,
+        viewModel: SceneContainerViewModel,
+        sceneByKey: Map<SceneKey, Scene>,
+    ): View
 }
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 18bd467..ccbde01 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
@@ -312,6 +312,7 @@
                 Log.d(TAG, "Canceling loadSubscribtion")
                 it.invoke()
             }
+            callback.error("Load cancelled")
         }
 
         override fun onSubscribe(token: IBinder, subs: IControlsSubscription) {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
index d629e3e..8e41974 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
@@ -154,7 +154,7 @@
     private fun bindViews() {
         setContentView(R.layout.controls_management)
 
-        getLifecycle().addObserver(
+        lifecycle.addObserver(
             ControlsAnimations.observerForAnimations(
                 requireViewById<ViewGroup>(R.id.controls_management_root),
                 window,
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
index d3ffc95..d3aa449 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
@@ -268,7 +268,7 @@
     private fun bindViews() {
         setContentView(R.layout.controls_management)
 
-        getLifecycle().addObserver(
+        lifecycle.addObserver(
             ControlsAnimations.observerForAnimations(
                 requireViewById<ViewGroup>(R.id.controls_management_root),
                 window,
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 fb19ac9..d5b8693 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
@@ -91,7 +91,7 @@
 
         setContentView(R.layout.controls_management)
 
-        getLifecycle().addObserver(
+        lifecycle.addObserver(
             ControlsAnimations.observerForAnimations(
                 requireViewById<ViewGroup>(R.id.controls_management_root),
                 window,
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 4a22e4e..557dcf4 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
@@ -72,7 +72,7 @@
 
         setContentView(R.layout.controls_fullscreen)
 
-        getLifecycle().addObserver(
+        lifecycle.addObserver(
             ControlsAnimations.observerForAnimations(
                 requireViewById(R.id.control_detail_root),
                 window,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 2262d8a..f68bd49 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -40,7 +40,6 @@
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsImplementation;
 import com.android.systemui.rotationlock.RotationLockModule;
-import com.android.systemui.scene.SceneContainerFrameworkModule;
 import com.android.systemui.screenshot.ReferenceScreenshotModule;
 import com.android.systemui.settings.dagger.MultiUserUtilsModule;
 import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
@@ -104,7 +103,6 @@
         QSModule.class,
         ReferenceScreenshotModule.class,
         RotationLockModule.class,
-        SceneContainerFrameworkModule.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 52355f3..d82bf58 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.dagger;
 
-import com.android.keyguard.clock.ClockOptionsProvider;
 import com.android.systemui.BootCompleteCacheImpl;
 import com.android.systemui.CoreStartable;
 import com.android.systemui.Dependency;
@@ -252,10 +251,5 @@
     /**
      * Member injection into the supplied argument.
      */
-    void inject(ClockOptionsProvider clockOptionsProvider);
-
-    /**
-     * Member injection into the supplied argument.
-     */
     void inject(PeopleProvider peopleProvider);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 25634f0..b1f513d 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.keyboard.KeyboardUI
 import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable
 import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.KeyguardViewConfigurator
 import com.android.systemui.keyguard.data.quickaffordance.MuteQuickAffordanceCoreStartable
 import com.android.systemui.log.SessionTracker
 import com.android.systemui.media.RingtonePlayer
@@ -41,6 +42,7 @@
 import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper
 import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver
 import com.android.systemui.media.taptotransfer.sender.MediaTttSenderCoordinator
+import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherCoreStartable
 import com.android.systemui.power.PowerUI
 import com.android.systemui.reardisplay.RearDisplayDialogController
 import com.android.systemui.recents.Recents
@@ -48,6 +50,7 @@
 import com.android.systemui.shortcut.ShortcutKeyDispatcher
 import com.android.systemui.statusbar.notification.InstantAppNotifier
 import com.android.systemui.statusbar.phone.KeyguardLiftController
+import com.android.systemui.statusbar.phone.LockscreenWallpaper
 import com.android.systemui.stylus.StylusUsiPowerStartable
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 import com.android.systemui.theme.ThemeOverlayController
@@ -109,6 +112,14 @@
     @ClassKey(KeyboardUI::class)
     abstract fun bindKeyboardUI(sysui: KeyboardUI): CoreStartable
 
+    /** Inject into MediaProjectionTaskSwitcherCoreStartable. */
+    @Binds
+    @IntoMap
+    @ClassKey(MediaProjectionTaskSwitcherCoreStartable::class)
+    abstract fun bindProjectedTaskListener(
+            sysui: MediaProjectionTaskSwitcherCoreStartable
+    ): CoreStartable
+
     /** Inject into KeyguardBiometricLockoutLogger */
     @Binds
     @IntoMap
@@ -295,4 +306,14 @@
     @IntoMap
     @ClassKey(AssistantAttentionMonitor::class)
     abstract fun bindAssistantAttentionMonitor(sysui: AssistantAttentionMonitor): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(KeyguardViewConfigurator::class)
+    abstract fun bindKeyguardViewConfigurator(impl: KeyguardViewConfigurator): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(LockscreenWallpaper::class)
+    abstract fun bindLockscreenWallpaper(impl: LockscreenWallpaper): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 5bcf32a..3b89739 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -23,7 +23,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.keyguard.clock.ClockInfoModule;
 import com.android.keyguard.dagger.ClockRegistryModule;
 import com.android.keyguard.dagger.KeyguardBouncerComponent;
 import com.android.systemui.BootCompleteCache;
@@ -39,6 +38,7 @@
 import com.android.systemui.biometrics.UdfpsDisplayModeProvider;
 import com.android.systemui.biometrics.dagger.BiometricsModule;
 import com.android.systemui.biometrics.dagger.UdfpsModule;
+import com.android.systemui.bouncer.ui.BouncerViewModule;
 import com.android.systemui.classifier.FalsingModule;
 import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
 import com.android.systemui.common.ui.data.repository.CommonRepositoryModule;
@@ -47,17 +47,19 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.SystemUser;
 import com.android.systemui.demomode.dagger.DemoModeModule;
+import com.android.systemui.display.DisplayModule;
 import com.android.systemui.doze.dagger.DozeComponent;
 import com.android.systemui.dreams.dagger.DreamModule;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.FlagsModule;
 import com.android.systemui.keyboard.KeyboardModule;
-import com.android.systemui.keyguard.data.BouncerViewModule;
+import com.android.systemui.keyguard.ui.view.layout.LockscreenLayoutModule;
 import com.android.systemui.log.dagger.LogModule;
 import com.android.systemui.log.dagger.MonitorLog;
 import com.android.systemui.log.table.TableLogBuffer;
 import com.android.systemui.mediaprojection.appselector.MediaProjectionModule;
+import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherModule;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.motiontool.MotionToolModule;
 import com.android.systemui.navigationbar.NavigationBarComponent;
@@ -74,6 +76,7 @@
 import com.android.systemui.qs.footer.dagger.FooterActionsModule;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.retail.dagger.RetailModeModule;
+import com.android.systemui.scene.SceneContainerFrameworkModule;
 import com.android.systemui.screenrecord.ScreenRecordModule;
 import com.android.systemui.screenshot.dagger.ScreenshotModule;
 import com.android.systemui.security.data.repository.SecurityRepositoryModule;
@@ -88,6 +91,7 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.connectivity.ConnectivityModule;
+import com.android.systemui.statusbar.disableflags.dagger.DisableFlagsModule;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -160,14 +164,15 @@
             BiometricsModule.class,
             BouncerViewModule.class,
             ClipboardOverlayModule.class,
-            ClockInfoModule.class,
             ClockRegistryModule.class,
             CommonRepositoryModule.class,
+            DisplayModule.class,
             ConnectivityModule.class,
             CoroutinesModule.class,
             DreamModule.class,
             ControlsModule.class,
             DemoModeModule.class,
+            DisableFlagsModule.class,
             FalsingModule.class,
             FlagsModule.class,
             SystemPropertiesFlagsModule.class,
@@ -175,8 +180,10 @@
             GarbageMonitorModule.class,
             KeyboardModule.class,
             LetterboxModule.class,
+            LockscreenLayoutModule.class,
             LogModule.class,
             MediaProjectionModule.class,
+            MediaProjectionTaskSwitcherModule.class,
             MotionToolModule.class,
             PeopleHubModule.class,
             PeopleModule.class,
@@ -186,6 +193,7 @@
             QRCodeScannerModule.class,
             QSFragmentStartableModule.class,
             RetailModeModule.class,
+            SceneContainerFrameworkModule.class,
             ScreenshotModule.class,
             SensorModule.class,
             SecurityRepositoryModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
index b71871e..d70c57f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
@@ -16,12 +16,11 @@
 
 package com.android.systemui.dagger;
 
-import android.content.Context;
 import android.os.HandlerThread;
 
 import androidx.annotation.Nullable;
 
-import com.android.systemui.SystemUIInitializerFactory;
+import com.android.systemui.SystemUIInitializer;
 import com.android.systemui.tv.TvWMComponent;
 import com.android.wm.shell.back.BackAnimation;
 import com.android.wm.shell.bubbles.Bubbles;
@@ -49,7 +48,7 @@
 /**
  * Dagger Subcomponent for WindowManager.  This class explicitly describes the interfaces exported
  * from the WM component into the SysUI component (in
- * {@link SystemUIInitializerFactory#init(Context, boolean)}), and references the specific dependencies
+ * {@link SystemUIInitializer#init(boolean)}), and references the specific dependencies
  * provided by its particular device/form-factor SystemUI implementation.
  *
  * ie. {@link WMComponent} includes {@link WMShellModule}
diff --git a/packages/SystemUI/src/com/android/systemui/decor/DebugRoundedCornerDelegate.kt b/packages/SystemUI/src/com/android/systemui/decor/DebugRoundedCornerDelegate.kt
new file mode 100644
index 0000000..5571687
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/decor/DebugRoundedCornerDelegate.kt
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.decor
+
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorFilter
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.PixelFormat
+import android.graphics.drawable.Drawable
+import android.util.Size
+import java.io.PrintWriter
+
+/**
+ * Rounded corner delegate that handles incoming debug commands and can convert them to path
+ * drawables to be shown instead of the system-defined rounded corners.
+ *
+ * These debug corners are expected to supersede the system-defined corners
+ */
+class DebugRoundedCornerDelegate : RoundedCornerResDelegate {
+    override var hasTop: Boolean = false
+        private set
+    override var topRoundedDrawable: Drawable? = null
+        private set
+    override var topRoundedSize: Size = Size(0, 0)
+        private set
+
+    override var hasBottom: Boolean = false
+        private set
+    override var bottomRoundedDrawable: Drawable? = null
+        private set
+    override var bottomRoundedSize: Size = Size(0, 0)
+        private set
+
+    override var physicalPixelDisplaySizeRatio: Float = 1f
+        set(value) {
+            if (field == value) {
+                return
+            }
+            field = value
+            reloadMeasures()
+        }
+
+    var color: Int = Color.RED
+        set(value) {
+            if (field == value) {
+                return
+            }
+
+            field = value
+            paint.color = field
+        }
+
+    var paint =
+        Paint().apply {
+            color = Color.RED
+            style = Paint.Style.FILL
+        }
+
+    override fun updateDisplayUniqueId(newDisplayUniqueId: String?, newReloadToken: Int?) {
+        // nop -- debug corners draw the same on every display
+    }
+
+    fun applyNewDebugCorners(
+        topCorner: DebugRoundedCornerModel?,
+        bottomCorner: DebugRoundedCornerModel?,
+    ) {
+        topCorner?.let {
+            hasTop = true
+            topRoundedDrawable = it.toPathDrawable(paint)
+            topRoundedSize = it.size()
+        }
+            ?: {
+                hasTop = false
+                topRoundedDrawable = null
+                topRoundedSize = Size(0, 0)
+            }
+
+        bottomCorner?.let {
+            hasBottom = true
+            bottomRoundedDrawable = it.toPathDrawable(paint)
+            bottomRoundedSize = it.size()
+        }
+            ?: {
+                hasBottom = false
+                bottomRoundedDrawable = null
+                bottomRoundedSize = Size(0, 0)
+            }
+    }
+
+    /**
+     * Remove accumulated debug state by clearing out the drawables and setting [hasTop] and
+     * [hasBottom] to false.
+     */
+    fun removeDebugState() {
+        hasTop = false
+        topRoundedDrawable = null
+        topRoundedSize = Size(0, 0)
+
+        hasBottom = false
+        bottomRoundedDrawable = null
+        bottomRoundedSize = Size(0, 0)
+    }
+
+    /**
+     * Scaling here happens when the display resolution is changed. This logic is exactly the same
+     * as in [RoundedCornerResDelegateImpl]
+     */
+    private fun reloadMeasures() {
+        topRoundedDrawable?.let { topRoundedSize = Size(it.intrinsicWidth, it.intrinsicHeight) }
+        bottomRoundedDrawable?.let {
+            bottomRoundedSize = Size(it.intrinsicWidth, it.intrinsicHeight)
+        }
+
+        if (physicalPixelDisplaySizeRatio != 1f) {
+            if (topRoundedSize.width != 0) {
+                topRoundedSize =
+                    Size(
+                        (physicalPixelDisplaySizeRatio * topRoundedSize.width + 0.5f).toInt(),
+                        (physicalPixelDisplaySizeRatio * topRoundedSize.height + 0.5f).toInt()
+                    )
+            }
+            if (bottomRoundedSize.width != 0) {
+                bottomRoundedSize =
+                    Size(
+                        (physicalPixelDisplaySizeRatio * bottomRoundedSize.width + 0.5f).toInt(),
+                        (physicalPixelDisplaySizeRatio * bottomRoundedSize.height + 0.5f).toInt()
+                    )
+            }
+        }
+    }
+
+    fun dump(pw: PrintWriter) {
+        pw.println("DebugRoundedCornerDelegate state:")
+        pw.println("  hasTop=$hasTop")
+        pw.println("  hasBottom=$hasBottom")
+        pw.println("  topRoundedSize(w,h)=(${topRoundedSize.width},${topRoundedSize.height})")
+        pw.println(
+            "  bottomRoundedSize(w,h)=(${bottomRoundedSize.width},${bottomRoundedSize.height})"
+        )
+        pw.println("  physicalPixelDisplaySizeRatio=$physicalPixelDisplaySizeRatio")
+    }
+}
+
+/** Encapsulates the data coming in from the command line args and turns into a [PathDrawable] */
+data class DebugRoundedCornerModel(
+    val path: Path,
+    val width: Int,
+    val height: Int,
+    val scaleX: Float,
+    val scaleY: Float,
+) {
+    fun size() = Size(width, height)
+
+    fun toPathDrawable(paint: Paint) =
+        PathDrawable(
+            path,
+            width,
+            height,
+            scaleX,
+            scaleY,
+            paint,
+        )
+}
+
+/**
+ * PathDrawable accepts paths from the command line via [DebugRoundedCornerModel], and renders them
+ * in the canvas provided by the screen decor rounded corner provider
+ */
+class PathDrawable(
+    val path: Path,
+    val width: Int,
+    val height: Int,
+    val scaleX: Float = 1f,
+    val scaleY: Float = 1f,
+    val paint: Paint,
+) : Drawable() {
+    private var cf: ColorFilter? = null
+
+    override fun draw(canvas: Canvas) {
+        if (scaleX != 1f || scaleY != 1f) {
+            canvas.scale(scaleX, scaleY)
+        }
+        canvas.drawPath(path, paint)
+    }
+
+    override fun getIntrinsicHeight(): Int = height
+    override fun getIntrinsicWidth(): Int = width
+
+    override fun getOpacity(): Int = PixelFormat.OPAQUE
+
+    override fun setAlpha(alpha: Int) {}
+
+    override fun setColorFilter(colorFilter: ColorFilter?) {
+        cf = colorFilter
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt
index 8b4aeef..c64766a 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt
@@ -27,44 +27,50 @@
 import com.android.systemui.R
 import java.io.PrintWriter
 
-class RoundedCornerResDelegate(
+interface RoundedCornerResDelegate {
+    val hasTop: Boolean
+    val topRoundedDrawable: Drawable?
+    val topRoundedSize: Size
+
+    val hasBottom: Boolean
+    val bottomRoundedDrawable: Drawable?
+    val bottomRoundedSize: Size
+
+    var physicalPixelDisplaySizeRatio: Float
+
+    fun updateDisplayUniqueId(newDisplayUniqueId: String?, newReloadToken: Int?)
+}
+
+/**
+ * Delegate for the device-default rounded corners. These will always be loaded from the config
+ * values `R.array.config_roundedCornerTopDrawableArray` and `R.drawable.rounded_corner_top`
+ */
+class RoundedCornerResDelegateImpl(
     private val res: Resources,
     private var displayUniqueId: String?
-) : Dumpable {
-
-    private val density: Float
-        get() = res.displayMetrics.density
+) : RoundedCornerResDelegate, Dumpable {
 
     private var reloadToken: Int = 0
 
-    var hasTop: Boolean = false
+    override var hasTop: Boolean = false
         private set
 
-    var hasBottom: Boolean = false
+    override var hasBottom: Boolean = false
         private set
 
-    var topRoundedDrawable: Drawable? = null
+    override var topRoundedDrawable: Drawable? = null
         private set
 
-    var bottomRoundedDrawable: Drawable? = null
+    override var bottomRoundedDrawable: Drawable? = null
         private set
 
-    var topRoundedSize = Size(0, 0)
+    override var topRoundedSize = Size(0, 0)
         private set
 
-    var bottomRoundedSize = Size(0, 0)
+    override var bottomRoundedSize = Size(0, 0)
         private set
 
-    var tuningSizeFactor: Int? = null
-        set(value) {
-            if (field == value) {
-                return
-            }
-            field = value
-            reloadMeasures()
-        }
-
-    var physicalPixelDisplaySizeRatio: Float = 1f
+    override var physicalPixelDisplaySizeRatio: Float = 1f
         set(value) {
             if (field == value) {
                 return
@@ -78,7 +84,7 @@
         reloadMeasures()
     }
 
-    fun updateDisplayUniqueId(newDisplayUniqueId: String?, newReloadToken: Int?) {
+    override fun updateDisplayUniqueId(newDisplayUniqueId: String?, newReloadToken: Int?) {
         if (displayUniqueId != newDisplayUniqueId) {
             displayUniqueId = newDisplayUniqueId
             newReloadToken ?.let { reloadToken = it }
@@ -122,19 +128,6 @@
             bottomRoundedSize = Size(it.intrinsicWidth, it.intrinsicHeight)
         }
 
-        tuningSizeFactor?.let {
-            if (it <= 0) {
-                return
-            }
-            val length: Int = (it * density).toInt()
-            if (topRoundedSize.width > 0) {
-                topRoundedSize = Size(length, length)
-            }
-            if (bottomRoundedSize.width > 0) {
-                bottomRoundedSize = Size(length, length)
-            }
-        }
-
         if (physicalPixelDisplaySizeRatio != 1f) {
             if (topRoundedSize.width != 0) {
                 topRoundedSize = Size(
diff --git a/packages/SystemUI/src/com/android/systemui/decor/ScreenDecorCommand.kt b/packages/SystemUI/src/com/android/systemui/decor/ScreenDecorCommand.kt
new file mode 100644
index 0000000..fa1d898
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/decor/ScreenDecorCommand.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.decor
+
+import android.graphics.Color
+import android.graphics.Path
+import android.util.PathParser
+import com.android.systemui.statusbar.commandline.ParseableCommand
+import com.android.systemui.statusbar.commandline.Type
+import com.android.systemui.statusbar.commandline.map
+import java.io.PrintWriter
+
+/** Debug screen-decor command to be handled by the SystemUI command line interface */
+class ScreenDecorCommand(
+    private val callback: Callback,
+) : ParseableCommand(SCREEN_DECOR_CMD_NAME) {
+    val debug: Boolean? by
+        param(
+            longName = "debug",
+            description =
+                "Enter or exits debug mode. Effectively makes the corners visible and allows " +
+                    "for overriding the path data for the anti-aliasing corner paths and display " +
+                    "cutout.",
+            valueParser = Type.Boolean,
+        )
+
+    val color: Int? by
+        param(
+            longName = "color",
+            shortName = "c",
+            description =
+                "Set a specific color for the debug assets. See Color#parseString() for " +
+                    "accepted inputs.",
+            valueParser = Type.String.map { it.toColorIntOrNull() }
+        )
+
+    val roundedTop: RoundedCornerSubCommand? by subCommand(RoundedCornerSubCommand("rounded-top"))
+
+    val roundedBottom: RoundedCornerSubCommand? by
+        subCommand(RoundedCornerSubCommand("rounded-bottom"))
+
+    override fun execute(pw: PrintWriter) {
+        callback.onExecute(this, pw)
+    }
+
+    override fun toString(): String {
+        return "ScreenDecorCommand(" +
+            "debug=$debug, " +
+            "color=$color, " +
+            "roundedTop=$roundedTop, " +
+            "roundedBottom=$roundedBottom)"
+    }
+
+    /** For use in ScreenDecorations.java, define a Callback */
+    interface Callback {
+        fun onExecute(cmd: ScreenDecorCommand, pw: PrintWriter)
+    }
+
+    companion object {
+        const val SCREEN_DECOR_CMD_NAME = "screen-decor"
+    }
+}
+
+/**
+ * Defines a subcommand suitable for `rounded-top` and `rounded-bottom`. They both have the same
+ * API.
+ */
+class RoundedCornerSubCommand(name: String) : ParseableCommand(name) {
+    val height by
+        param(
+                longName = "height",
+                description = "The height of a corner, in pixels.",
+                valueParser = Type.Int,
+            )
+            .required()
+
+    val width by
+        param(
+                longName = "width",
+                description =
+                    "The width of the corner, in pixels. Likely should be equal to the height.",
+                valueParser = Type.Int,
+            )
+            .required()
+
+    val pathData by
+        param(
+                longName = "path-data",
+                shortName = "d",
+                description =
+                    "PathParser-compatible path string to be rendered as the corner drawable. " +
+                        "This path should be a closed arc oriented as the top-left corner " +
+                        "of the device",
+                valueParser = Type.String.map { it.toPathOrNull() }
+            )
+            .required()
+
+    val viewportHeight: Float? by
+        param(
+            longName = "viewport-height",
+            description =
+                "The height of the viewport for the given path string. " +
+                    "If null, the corner height will be used.",
+            valueParser = Type.Float,
+        )
+
+    val scaleY: Float
+        get() = viewportHeight?.let { height.toFloat() / it } ?: 1.0f
+
+    val viewportWidth: Float? by
+        param(
+            longName = "viewport-width",
+            description =
+                "The width of the viewport for the given path string. " +
+                    "If null, the corner width will be used.",
+            valueParser = Type.Float,
+        )
+
+    val scaleX: Float
+        get() = viewportWidth?.let { width.toFloat() / it } ?: 1.0f
+
+    override fun execute(pw: PrintWriter) {
+        // Not needed for a subcommand
+    }
+
+    override fun toString(): String {
+        return "RoundedCornerSubCommand(" +
+            "height=$height," +
+            " width=$width," +
+            " pathData='$pathData'," +
+            " viewportHeight=$viewportHeight," +
+            " viewportWidth=$viewportWidth)"
+    }
+
+    fun toRoundedCornerDebugModel(): DebugRoundedCornerModel =
+        DebugRoundedCornerModel(
+            path = pathData,
+            width = width,
+            height = height,
+            scaleX = scaleX,
+            scaleY = scaleY,
+        )
+}
+
+fun String.toPathOrNull(): Path? =
+    try {
+        PathParser.createPathFromPathData(this)
+    } catch (e: Exception) {
+        null
+    }
+
+fun String.toColorIntOrNull(): Int? =
+    try {
+        Color.parseColor(this)
+    } catch (e: Exception) {
+        null
+    }
diff --git a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
new file mode 100644
index 0000000..65cd84b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.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.display
+
+import com.android.systemui.display.data.repository.DisplayRepository
+import com.android.systemui.display.data.repository.DisplayRepositoryImpl
+import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
+import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractorImpl
+import dagger.Binds
+import dagger.Module
+
+/** Module binding display related classes. */
+@Module
+interface DisplayModule {
+    @Binds
+    fun bindConnectedDisplayInteractor(
+        provider: ConnectedDisplayInteractorImpl
+    ): ConnectedDisplayInteractor
+
+    @Binds fun bindsDisplayRepository(displayRepository: DisplayRepositoryImpl): DisplayRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt
new file mode 100644
index 0000000..bcfeeb9e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt
@@ -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.display.data.repository
+
+import android.content.Context
+import android.content.res.Configuration
+import android.util.DisplayMetrics
+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.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.DisplayMetricsRepoLog
+import com.android.systemui.statusbar.policy.ConfigurationController
+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.onEach
+import kotlinx.coroutines.flow.stateIn
+
+/** Repository tracking display-related metrics like display height and width. */
+@SysUISingleton
+class DisplayMetricsRepository
+@Inject
+constructor(
+    @Application scope: CoroutineScope,
+    configurationController: ConfigurationController,
+    displayMetricsHolder: DisplayMetrics,
+    context: Context,
+    @DisplayMetricsRepoLog logBuffer: LogBuffer,
+) {
+
+    private val displayMetrics: StateFlow<DisplayMetrics> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : ConfigurationController.ConfigurationListener {
+                        override fun onConfigChanged(newConfig: Configuration?) {
+                            context.display.getMetrics(displayMetricsHolder)
+                            trySend(displayMetricsHolder)
+                        }
+                    }
+                configurationController.addCallback(callback)
+                awaitClose { configurationController.removeCallback(callback) }
+            }
+            .onEach {
+                logBuffer.log(
+                    "DisplayMetrics",
+                    LogLevel.INFO,
+                    { str1 = it.toString() },
+                    { "New metrics: $str1" },
+                )
+            }
+            .stateIn(scope, SharingStarted.Eagerly, displayMetricsHolder)
+
+    /** Returns the current display height in pixels. */
+    val heightPixels: Int
+        get() = displayMetrics.value.heightPixels
+}
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt
new file mode 100644
index 0000000..b18f7bf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.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.display.data.repository
+
+import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManager.DisplayListener
+import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_ADDED
+import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
+import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_REMOVED
+import android.os.Handler
+import android.view.Display
+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.util.traceSection
+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.flowOn
+import kotlinx.coroutines.flow.stateIn
+
+/** Provides a [Flow] of [Display] as returned by [DisplayManager]. */
+interface DisplayRepository {
+    /** Provides a nullable set of displays. */
+    val displays: Flow<Set<Display>>
+}
+
+@SysUISingleton
+class DisplayRepositoryImpl
+@Inject
+constructor(
+    private val displayManager: DisplayManager,
+    @Background backgroundHandler: Handler,
+    @Application applicationScope: CoroutineScope,
+    @Background backgroundCoroutineDispatcher: CoroutineDispatcher
+) : DisplayRepository {
+
+    override val displays: Flow<Set<Display>> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : DisplayListener {
+                        override fun onDisplayAdded(displayId: Int) {
+                            trySend(getDisplays())
+                        }
+
+                        override fun onDisplayRemoved(displayId: Int) {
+                            trySend(getDisplays())
+                        }
+
+                        override fun onDisplayChanged(displayId: Int) {
+                            trySend(getDisplays())
+                        }
+                    }
+                displayManager.registerDisplayListener(
+                    callback,
+                    backgroundHandler,
+                    EVENT_FLAG_DISPLAY_ADDED or
+                        EVENT_FLAG_DISPLAY_CHANGED or
+                        EVENT_FLAG_DISPLAY_REMOVED,
+                )
+                awaitClose { displayManager.unregisterDisplayListener(callback) }
+            }
+            .flowOn(backgroundCoroutineDispatcher)
+            .stateIn(
+                applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = getDisplays()
+            )
+
+    fun getDisplays(): Set<Display> =
+        traceSection("DisplayRepository#getDisplays()") {
+            displayManager.displays?.toSet() ?: emptySet()
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt
new file mode 100644
index 0000000..4b957c7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.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.display.domain.interactor
+
+import android.view.Display
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.display.data.repository.DisplayRepository
+import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+/** Provides information about an external connected display. */
+interface ConnectedDisplayInteractor {
+    /**
+     * Provides the current external display state.
+     *
+     * The state is:
+     * - [State.CONNECTED] when there is at least one display with [TYPE_EXTERNAL].
+     * - [State.CONNECTED_SECURE] when is at least one display with both [TYPE_EXTERNAL] AND
+     *   [Display.FLAG_SECURE] set
+     */
+    val connectedDisplayState: Flow<State>
+
+    /** Possible connected display state. */
+    enum class State {
+        DISCONNECTED,
+        CONNECTED,
+        CONNECTED_SECURE,
+    }
+}
+
+@SysUISingleton
+class ConnectedDisplayInteractorImpl
+@Inject
+constructor(
+    displayRepository: DisplayRepository,
+) : ConnectedDisplayInteractor {
+
+    override val connectedDisplayState: Flow<State> =
+        displayRepository.displays
+            .map { displays ->
+                val externalDisplays =
+                    displays.filter { display -> display.type == Display.TYPE_EXTERNAL }
+
+                val secureExternalDisplays =
+                    externalDisplays.filter { it.flags and Display.FLAG_SECURE != 0 }
+
+                if (externalDisplays.isEmpty()) {
+                    State.DISCONNECTED
+                } else if (!secureExternalDisplays.isEmpty()) {
+                    State.CONNECTED_SECURE
+                } else {
+                    State.CONNECTED
+                }
+            }
+            .distinctUntilChanged()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
index 5369780..75b8e51 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
@@ -20,9 +20,9 @@
 import com.android.systemui.doze.DozeLog.Reason
 import com.android.systemui.doze.DozeLog.reasonToString
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.ERROR
+import com.android.systemui.log.core.LogLevel.INFO
 import com.android.systemui.log.dagger.DozeLog
 import com.android.systemui.statusbar.policy.DevicePostureController
 import com.google.errorprone.annotations.CompileTimeConstant
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamLogger.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamLogger.kt
index fdb7651..0e22406 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamLogger.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.dreams
 
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.dagger.DreamLog
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index ff07bb6..78ac453 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -39,8 +39,8 @@
 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.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.statusbar.BlurUtils;
 import com.android.systemui.util.ViewController;
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayLifecycleOwner.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayLifecycleOwner.kt
index 8325356..003d2c7 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayLifecycleOwner.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayLifecycleOwner.kt
@@ -28,7 +28,8 @@
 class DreamOverlayLifecycleOwner @Inject constructor() : LifecycleOwner {
     val registry: LifecycleRegistry = LifecycleRegistry(this)
 
-    override fun getLifecycle(): Lifecycle {
-        return registry
-    }
+    override val lifecycle: Lifecycle
+        get() {
+            return registry
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index d509015..553405f 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -294,6 +294,7 @@
 
         mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
         mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+        mWindow.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS);
         mWindow.requestFeature(Window.FEATURE_NO_TITLE);
         // Hide all insets when the dream is showing
         mWindow.getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars());
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
index b141db1..c2421dc 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
@@ -19,7 +19,6 @@
 import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_ENABLED;
 
 import android.service.dreams.DreamService;
-import android.util.Log;
 
 import androidx.annotation.NonNull;
 
@@ -52,7 +51,6 @@
 public class DreamOverlayStateController implements
         CallbackController<DreamOverlayStateController.Callback> {
     private static final String TAG = "DreamOverlayStateCtlr";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     public static final int STATE_DREAM_OVERLAY_ACTIVE = 1 << 0;
     public static final int STATE_LOW_LIGHT_ACTIVE = 1 << 1;
@@ -110,13 +108,17 @@
 
     private final int mSupportedTypes;
 
+    private final DreamLogger mLogger;
+
     @VisibleForTesting
     @Inject
     public DreamOverlayStateController(@Main Executor executor,
             @Named(DREAM_OVERLAY_ENABLED) boolean overlayEnabled,
-            FeatureFlags featureFlags) {
+            FeatureFlags featureFlags,
+            DreamLogger dreamLogger) {
         mExecutor = executor;
         mOverlayEnabled = overlayEnabled;
+        mLogger = dreamLogger;
         mFeatureFlags = featureFlags;
         if (mFeatureFlags.isEnabled(Flags.ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS)) {
             mSupportedTypes = Complication.COMPLICATION_TYPE_NONE
@@ -124,9 +126,7 @@
         } else {
             mSupportedTypes = Complication.COMPLICATION_TYPE_NONE;
         }
-        if (DEBUG) {
-            Log.d(TAG, "Dream overlay enabled:" + mOverlayEnabled);
-        }
+        mLogger.d(TAG, "Dream overlay enabled: " + mOverlayEnabled);
     }
 
     /**
@@ -134,18 +134,14 @@
      */
     public void addComplication(Complication complication) {
         if (!mOverlayEnabled) {
-            if (DEBUG) {
-                Log.d(TAG,
-                        "Ignoring adding complication due to overlay disabled:" + complication);
-            }
+            mLogger.d(TAG,
+                    "Ignoring adding complication due to overlay disabled: " + complication);
             return;
         }
 
         mExecutor.execute(() -> {
             if (mComplications.add(complication)) {
-                if (DEBUG) {
-                    Log.d(TAG, "addComplication: added " + complication);
-                }
+                mLogger.d(TAG, "Added dream complication: " + complication);
                 mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged());
             }
         });
@@ -156,18 +152,14 @@
      */
     public void removeComplication(Complication complication) {
         if (!mOverlayEnabled) {
-            if (DEBUG) {
-                Log.d(TAG,
-                        "Ignoring removing complication due to overlay disabled:" + complication);
-            }
+            mLogger.d(TAG,
+                    "Ignoring removing complication due to overlay disabled: " + complication);
             return;
         }
 
         mExecutor.execute(() -> {
             if (mComplications.remove(complication)) {
-                if (DEBUG) {
-                    Log.d(TAG, "removeComplication: removed " + complication);
-                }
+                mLogger.d(TAG, "Removed dream complication: " + complication);
                 mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged());
             }
         });
@@ -313,6 +305,7 @@
      * @param active {@code true} if overlay is active, {@code false} otherwise.
      */
     public void setOverlayActive(boolean active) {
+        mLogger.d(TAG, "Dream overlay active: " + active);
         modifyState(active ? OP_SET_STATE : OP_CLEAR_STATE, STATE_DREAM_OVERLAY_ACTIVE);
     }
 
@@ -321,6 +314,8 @@
      * @param active {@code true} if low light mode is active, {@code false} otherwise.
      */
     public void setLowLightActive(boolean active) {
+        mLogger.d(TAG, "Low light mode active: " + active);
+
         if (isLowLightActive() && !active) {
             // Notify that we're exiting low light only on the transition from active to not active.
             mCallbacks.forEach(Callback::onExitLowLight);
@@ -351,6 +346,7 @@
      * @param hasAttention {@code true} if has the user's attention, {@code false} otherwise.
      */
     public void setHasAssistantAttention(boolean hasAttention) {
+        mLogger.d(TAG, "Dream overlay has Assistant attention: " + hasAttention);
         modifyState(hasAttention ? OP_SET_STATE : OP_CLEAR_STATE, STATE_HAS_ASSISTANT_ATTENTION);
     }
 
@@ -359,6 +355,7 @@
      * @param visible {@code true} if the status bar is visible, {@code false} otherwise.
      */
     public void setDreamOverlayStatusBarVisible(boolean visible) {
+        mLogger.d(TAG, "Dream overlay status bar visible: " + visible);
         modifyState(
                 visible ? OP_SET_STATE : OP_CLEAR_STATE, STATE_DREAM_OVERLAY_STATUS_BAR_VISIBLE);
     }
@@ -376,6 +373,7 @@
      */
     public void setAvailableComplicationTypes(@Complication.ComplicationType int types) {
         mExecutor.execute(() -> {
+            mLogger.d(TAG, "Available complication types: " + types);
             mAvailableComplicationTypes = types;
             mCallbacks.forEach(Callback::onAvailableComplicationTypesChanged);
         });
@@ -393,6 +391,7 @@
      */
     public void setShouldShowComplications(boolean shouldShowComplications) {
         mExecutor.execute(() -> {
+            mLogger.d(TAG, "Should show complications: " + shouldShowComplications);
             mShouldShowComplications = shouldShowComplications;
             mCallbacks.forEach(Callback::onAvailableComplicationTypesChanged);
         });
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
index 7c1bfed..1865c38 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
@@ -141,6 +141,20 @@
         mExtraSystemStatusViewGroup = findViewById(R.id.dream_overlay_extra_items);
     }
 
+    protected static String getLoggableStatusIconType(@StatusIconType int type) {
+        return switch (type) {
+            case STATUS_ICON_NOTIFICATIONS -> "notifications";
+            case STATUS_ICON_WIFI_UNAVAILABLE -> "wifi_unavailable";
+            case STATUS_ICON_ALARM_SET -> "alarm_set";
+            case STATUS_ICON_CAMERA_DISABLED -> "camera_disabled";
+            case STATUS_ICON_MIC_DISABLED -> "mic_disabled";
+            case STATUS_ICON_MIC_CAMERA_DISABLED -> "mic_camera_disabled";
+            case STATUS_ICON_PRIORITY_MODE_ON -> "priority_mode_on";
+            case STATUS_ICON_ASSISTANT_ATTENTION_ACTIVE -> "assistant_attention_active";
+            default -> type + "(unknown)";
+        };
+    }
+
     void showIcon(@StatusIconType int iconType, boolean show, @Nullable String contentDescription) {
         View icon = mStatusIcons.get(iconType);
         if (icon == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
index c954f98..3a28408 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
@@ -61,6 +61,8 @@
  */
 @DreamOverlayComponent.DreamOverlayScope
 public class DreamOverlayStatusBarViewController extends ViewController<DreamOverlayStatusBarView> {
+    private static final String TAG = "DreamStatusBarCtrl";
+
     private final ConnectivityManager mConnectivityManager;
     private final TouchInsetManager.TouchInsetSession mTouchInsetSession;
     private final NextAlarmController mNextAlarmController;
@@ -78,6 +80,7 @@
     private final Executor mMainExecutor;
     private final List<DreamOverlayStatusBarItemsProvider.StatusBarItem> mExtraStatusBarItems =
             new ArrayList<>();
+    private final DreamLogger mLogger;
 
     private boolean mIsAttached;
 
@@ -157,7 +160,8 @@
             StatusBarWindowStateController statusBarWindowStateController,
             DreamOverlayStatusBarItemsProvider statusBarItemsProvider,
             DreamOverlayStateController dreamOverlayStateController,
-            UserTracker userTracker) {
+            UserTracker userTracker,
+            DreamLogger dreamLogger) {
         super(view);
         mResources = resources;
         mMainExecutor = mainExecutor;
@@ -173,6 +177,7 @@
         mZenModeController = zenModeController;
         mDreamOverlayStateController = dreamOverlayStateController;
         mUserTracker = userTracker;
+        mLogger = dreamLogger;
 
         // 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.
@@ -341,6 +346,8 @@
             @Nullable String contentDescription) {
         mMainExecutor.execute(() -> {
             if (mIsAttached) {
+                mLogger.d(TAG, (show ? "Showing" : "Hiding") + " dream status bar item: "
+                        + DreamOverlayStatusBarView.getLoggableStatusIconType(iconType));
                 mView.showIcon(iconType, show, contentDescription);
             }
         });
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 570132e1..1cd3774 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
@@ -35,9 +35,11 @@
 
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
 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.settings.UserTracker;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -76,6 +78,8 @@
 
     private static final String TAG = "BouncerSwipeTouchHandler";
     private final NotificationShadeWindowController mNotificationShadeWindowController;
+    private final LockPatternUtils mLockPatternUtils;
+    private final UserTracker mUserTracker;
     private final float mBouncerZoneScreenPercentage;
 
     private final ScrimManager mScrimManager;
@@ -151,6 +155,11 @@
                         return true;
                     }
 
+                    // Don't set expansion if the user doesn't have a pin/password set.
+                    if (!mLockPatternUtils.isSecure(mUserTracker.getUserId())) {
+                        return true;
+                    }
+
                     // For consistency, we adopt the expansion definition found in the
                     // PanelViewController. In this case, expansion refers to the view above the
                     // bouncer. As that view's expansion shrinks, the bouncer appears. The bouncer
@@ -204,6 +213,8 @@
             NotificationShadeWindowController notificationShadeWindowController,
             ValueAnimatorCreator valueAnimatorCreator,
             VelocityTrackerFactory velocityTrackerFactory,
+            LockPatternUtils lockPatternUtils,
+            UserTracker userTracker,
             @Named(SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING)
                     FlingAnimationUtils flingAnimationUtils,
             @Named(SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_CLOSING)
@@ -213,6 +224,8 @@
         mCentralSurfaces = centralSurfaces;
         mScrimManager = scrimManager;
         mNotificationShadeWindowController = notificationShadeWindowController;
+        mLockPatternUtils = lockPatternUtils;
+        mUserTracker = userTracker;
         mBouncerZoneScreenPercentage = swipeRegionPercentage;
         mFlingAnimationUtils = flingAnimationUtils;
         mFlingAnimationUtilsClosing = flingAnimationUtilsClosing;
@@ -347,6 +360,11 @@
             return;
         }
 
+        // Don't set expansion if the user doesn't have a pin/password set.
+        if (!mLockPatternUtils.isSecure(mUserTracker.getUserId())) {
+            return;
+        }
+
         // The animation utils deal in pixel units, rather than expansion height.
         final float viewHeight = mTouchSession.getBounds().height();
         final float currentHeight = viewHeight * mCurrentExpansion;
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
index 4b03fd3..75284fc 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
@@ -20,12 +20,16 @@
 import android.os.SystemClock
 import android.os.Trace
 import com.android.systemui.CoreStartable
+import com.android.systemui.ProtoDumpable
 import com.android.systemui.R
 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL
 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL
+import com.android.systemui.dump.DumpsysEntry.DumpableEntry
+import com.android.systemui.dump.DumpsysEntry.LogBufferEntry
+import com.android.systemui.dump.DumpsysEntry.TableLogBufferEntry
 import com.android.systemui.dump.nano.SystemUIProtoDump
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
+import com.android.systemui.log.table.TableLogBuffer
 import com.google.protobuf.nano.MessageNano
 import java.io.BufferedOutputStream
 import java.io.FileDescriptor
@@ -39,11 +43,11 @@
  *
  * Dump output is split into two sections, CRITICAL and NORMAL. In general, the CRITICAL section
  * contains all dumpables that were registered to the [DumpManager], while the NORMAL sections
- * contains all [LogBuffer]s (due to their length).
+ * contains all [LogBuffer]s and [TableLogBuffer]s (due to their length).
  *
- * The CRITICAL and NORMAL sections can be found within a bug report by searching for
- * "SERVICE com.android.systemui/.SystemUIService" and
- * "SERVICE com.android.systemui/.dump.SystemUIAuxiliaryDumpService", respectively.
+ * The CRITICAL and NORMAL sections can be found within a bug report by searching for "SERVICE
+ * com.android.systemui/.SystemUIService" and "SERVICE
+ * com.android.systemui/.dump.SystemUIAuxiliaryDumpService", respectively.
  *
  * Finally, some or all of the dump can be triggered on-demand via adb (see below).
  *
@@ -72,6 +76,7 @@
  * # To dump all dumpables or all buffers:
  * $ <invocation> dumpables
  * $ <invocation> buffers
+ * $ <invocation> tables
  *
  * # Finally, the following will simulate what we dump during the CRITICAL and NORMAL sections of a
  * # bug report:
@@ -83,37 +88,26 @@
  * $ <invocation> --help
  * ```
  */
-class DumpHandler @Inject constructor(
+class DumpHandler
+@Inject
+constructor(
     private val context: Context,
     private val dumpManager: DumpManager,
     private val logBufferEulogizer: LogBufferEulogizer,
     private val startables: MutableMap<Class<*>, Provider<CoreStartable>>,
-    private val uncaughtExceptionPreHandlerManager: UncaughtExceptionPreHandlerManager
 ) {
-    /**
-     * Registers an uncaught exception handler
-     */
-    fun init() {
-        uncaughtExceptionPreHandlerManager.registerHandler { _, e ->
-            if (e is Exception) {
-                logBufferEulogizer.record(e)
-            }
-        }
-    }
-
-    /**
-     * Dump the diagnostics! Behavior can be controlled via [args].
-     */
+    /** Dump the diagnostics! Behavior can be controlled via [args]. */
     fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) {
         Trace.beginSection("DumpManager#dump()")
         val start = SystemClock.uptimeMillis()
 
-        val parsedArgs = try {
-            parseArgs(args)
-        } catch (e: ArgParseException) {
-            pw.println(e.message)
-            return
-        }
+        val parsedArgs =
+            try {
+                parseArgs(args)
+            } catch (e: ArgParseException) {
+                pw.println(e.message)
+                return
+            }
 
         when {
             parsedArgs.dumpPriority == PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs)
@@ -134,6 +128,7 @@
             "bugreport-normal" -> dumpNormal(pw, args)
             "dumpables" -> dumpDumpables(pw, args)
             "buffers" -> dumpBuffers(pw, args)
+            "tables" -> dumpTables(pw, args)
             "config" -> dumpConfig(pw)
             "help" -> dumpHelp(pw)
             else -> {
@@ -147,44 +142,65 @@
     }
 
     private fun dumpCritical(pw: PrintWriter, args: ParsedArgs) {
-        dumpManager.dumpCritical(pw, args.rawArgs)
+        val targets = dumpManager.getDumpables()
+        for (target in targets) {
+            if (target.priority == DumpPriority.CRITICAL) {
+                dumpDumpable(target, pw, args.rawArgs)
+            }
+        }
         dumpConfig(pw)
     }
 
     private fun dumpNormal(pw: PrintWriter, args: ParsedArgs) {
-        dumpManager.dumpNormal(pw, args.rawArgs, args.tailLength)
+        val targets = dumpManager.getDumpables()
+        for (target in targets) {
+            if (target.priority == DumpPriority.NORMAL) {
+                dumpDumpable(target, pw, args.rawArgs)
+            }
+        }
+
+        val buffers = dumpManager.getLogBuffers()
+        for (buffer in buffers) {
+            dumpBuffer(buffer, pw, args.tailLength)
+        }
+
+        val tableBuffers = dumpManager.getTableLogBuffers()
+        for (table in tableBuffers) {
+            dumpTableBuffer(table, pw, args.rawArgs)
+        }
+
         logBufferEulogizer.readEulogyIfPresent(pw)
     }
 
-    private fun dumpDumpables(pw: PrintWriter, args: ParsedArgs) {
-        if (args.listOnly) {
-            dumpManager.listDumpables(pw)
-        } else {
-            dumpManager.dumpDumpables(pw, args.rawArgs)
+    private fun dumpDumpables(pw: PrintWriter, args: ParsedArgs) =
+        dumpManager.getDumpables().listOrDumpEntries(pw, args)
+
+    private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) =
+        dumpManager.getLogBuffers().listOrDumpEntries(pw, args)
+
+    private fun dumpTables(pw: PrintWriter, args: ParsedArgs) =
+        dumpManager.getTableLogBuffers().listOrDumpEntries(pw, args)
+
+    private fun listTargetNames(targets: Collection<DumpsysEntry>, pw: PrintWriter) {
+        for (target in targets) {
+            pw.println(target.name)
         }
     }
 
-    private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) {
-        if (args.listOnly) {
-            dumpManager.listBuffers(pw)
-        } else {
-            dumpManager.dumpBuffers(pw, args.tailLength)
-        }
-    }
-
-    private fun dumpProtoTargets(
-            targets: List<String>,
-            fd: FileDescriptor,
-            args: ParsedArgs
-    ) {
+    private fun dumpProtoTargets(targets: List<String>, fd: FileDescriptor, args: ParsedArgs) {
         val systemUIProto = SystemUIProtoDump()
+        val dumpables = dumpManager.getDumpables()
         if (targets.isNotEmpty()) {
             for (target in targets) {
-                dumpManager.dumpProtoTarget(target, systemUIProto, args.rawArgs)
+                findBestProtoTargetMatch(dumpables, target)?.dumpProto(systemUIProto, args.rawArgs)
             }
         } else {
-            dumpManager.dumpProtoDumpables(systemUIProto, args.rawArgs)
+            // Dump all protos
+            for (dumpable in dumpables) {
+                (dumpable.dumpable as? ProtoDumpable)?.dumpProto(systemUIProto, args.rawArgs)
+            }
         }
+
         val buffer = BufferedOutputStream(FileOutputStream(fd))
         buffer.use {
             it.write(MessageNano.toByteArray(systemUIProto))
@@ -192,36 +208,70 @@
         }
     }
 
-    private fun dumpTargets(
-        targets: List<String>,
-        pw: PrintWriter,
-        args: ParsedArgs
-    ) {
+    // Attempts to dump the target list to the given PrintWriter. Since the arguments come in as
+    // a list of strings, we use the [findBestTargetMatch] method to determine the most-correct
+    // target with the given search string.
+    private fun dumpTargets(targets: List<String>, pw: PrintWriter, args: ParsedArgs) {
         if (targets.isNotEmpty()) {
-            for (target in targets) {
-                dumpManager.dumpTarget(target, pw, args.rawArgs, args.tailLength)
+            val dumpables = dumpManager.getDumpables()
+            val buffers = dumpManager.getLogBuffers()
+            val tableBuffers = dumpManager.getTableLogBuffers()
+
+            targets.forEach { target ->
+                findTargetInCollection(target, dumpables, buffers, tableBuffers)?.dump(pw, args)
             }
         } else {
             if (args.listOnly) {
+                val dumpables = dumpManager.getDumpables()
+                val buffers = dumpManager.getLogBuffers()
+
                 pw.println("Dumpables:")
-                dumpManager.listDumpables(pw)
+                listTargetNames(dumpables, pw)
                 pw.println()
 
                 pw.println("Buffers:")
-                dumpManager.listBuffers(pw)
+                listTargetNames(buffers, pw)
             } else {
                 pw.println("Nothing to dump :(")
             }
         }
     }
 
+    private fun findTargetInCollection(
+        target: String,
+        dumpables: Collection<DumpableEntry>,
+        logBuffers: Collection<LogBufferEntry>,
+        tableBuffers: Collection<TableLogBufferEntry>,
+    ) =
+        sequence {
+                findBestTargetMatch(dumpables, target)?.let { yield(it) }
+                findBestTargetMatch(logBuffers, target)?.let { yield(it) }
+                findBestTargetMatch(tableBuffers, target)?.let { yield(it) }
+            }
+            .sortedBy { it.name }
+            .minByOrNull { it.name.length }
+
+    private fun dumpDumpable(entry: DumpableEntry, pw: PrintWriter, args: Array<String>) {
+        pw.preamble(entry)
+        entry.dumpable.dump(pw, args)
+    }
+
+    private fun dumpBuffer(entry: LogBufferEntry, pw: PrintWriter, tailLength: Int) {
+        pw.preamble(entry)
+        entry.buffer.dump(pw, tailLength)
+    }
+
+    private fun dumpTableBuffer(buffer: TableLogBufferEntry, pw: PrintWriter, args: Array<String>) {
+        pw.preamble(buffer)
+        buffer.table.dump(pw, args)
+    }
+
     private fun dumpConfig(pw: PrintWriter) {
         pw.println("SystemUiServiceComponents configuration:")
         pw.print("vendor component: ")
         pw.println(context.resources.getString(R.string.config_systemUIVendorServiceComponent))
-        val services: MutableList<String> = startables.keys
-                .map({ cls: Class<*> -> cls.simpleName })
-                .toMutableList()
+        val services: MutableList<String> =
+            startables.keys.map({ cls: Class<*> -> cls.simpleName }).toMutableList()
 
         services.add(context.resources.getString(R.string.config_systemUIVendorServiceComponent))
         dumpServiceList(pw, "global", services.toTypedArray())
@@ -265,6 +315,7 @@
         pw.println("Special commands:")
         pw.println("$ <invocation> dumpables")
         pw.println("$ <invocation> buffers")
+        pw.println("$ <invocation> tables")
         pw.println("$ <invocation> bugreport-critical")
         pw.println("$ <invocation> bugreport-normal")
         pw.println("$ <invocation> config")
@@ -274,6 +325,7 @@
         pw.println("$ <invocation> --list")
         pw.println("$ <invocation> dumpables --list")
         pw.println("$ <invocation> buffers --list")
+        pw.println("$ <invocation> tables --list")
         pw.println()
 
         pw.println("Show only the most recent N lines of buffers")
@@ -291,24 +343,26 @@
                 iterator.remove()
                 when (arg) {
                     PRIORITY_ARG -> {
-                        pArgs.dumpPriority = readArgument(iterator, PRIORITY_ARG) {
-                            if (PRIORITY_OPTIONS.contains(it)) {
-                                it
-                            } else {
-                                throw IllegalArgumentException()
+                        pArgs.dumpPriority =
+                            readArgument(iterator, PRIORITY_ARG) {
+                                if (PRIORITY_OPTIONS.contains(it)) {
+                                    it
+                                } else {
+                                    throw IllegalArgumentException()
+                                }
                             }
-                        }
                     }
                     PROTO -> pArgs.proto = true
-                    "-t", "--tail" -> {
-                        pArgs.tailLength = readArgument(iterator, arg) {
-                            it.toInt()
-                        }
+                    "-t",
+                    "--tail" -> {
+                        pArgs.tailLength = readArgument(iterator, arg) { it.toInt() }
                     }
-                    "-l", "--list" -> {
+                    "-l",
+                    "--list" -> {
                         pArgs.listOnly = true
                     }
-                    "-h", "--help" -> {
+                    "-h",
+                    "--help" -> {
                         pArgs.command = "help"
                     }
                     // This flag is passed as part of the proto dump in Bug reports, we can ignore
@@ -345,29 +399,131 @@
         }
     }
 
+    private fun DumpsysEntry.dump(pw: PrintWriter, args: ParsedArgs) =
+        when (this) {
+            is DumpableEntry -> dumpDumpable(this, pw, args.rawArgs)
+            is LogBufferEntry -> dumpBuffer(this, pw, args.tailLength)
+            is TableLogBufferEntry -> dumpTableBuffer(this, pw, args.rawArgs)
+        }
+
+    private fun Collection<DumpsysEntry>.listOrDumpEntries(pw: PrintWriter, args: ParsedArgs) =
+        if (args.listOnly) {
+            listTargetNames(this, pw)
+        } else {
+            forEach { it.dump(pw, args) }
+        }
+
     companion object {
         const val PRIORITY_ARG = "--dump-priority"
         const val PRIORITY_ARG_CRITICAL = "CRITICAL"
         const val PRIORITY_ARG_NORMAL = "NORMAL"
         const val PROTO = "--proto"
+
+        /**
+         * Important: do not change this divider without updating any bug report processing tools
+         * (e.g. ABT), since this divider is used to determine boundaries for bug report views
+         */
+        const val DUMPSYS_DUMPABLE_DIVIDER =
+            "----------------------------------------------------------------------------"
+
+        /**
+         * Important: do not change this divider without updating any bug report processing tools
+         * (e.g. ABT), since this divider is used to determine boundaries for bug report views
+         */
+        const val DUMPSYS_BUFFER_DIVIDER =
+            "============================================================================"
+
+        private fun findBestTargetMatch(c: Collection<DumpsysEntry>, target: String) =
+            c.asSequence().filter { it.name.endsWith(target) }.minByOrNull { it.name.length }
+
+        private fun findBestProtoTargetMatch(
+            c: Collection<DumpableEntry>,
+            target: String
+        ): ProtoDumpable? =
+            c.asSequence()
+                .filter { it.name.endsWith(target) }
+                .filter { it.dumpable is ProtoDumpable }
+                .minByOrNull { it.name.length }
+                ?.dumpable as? ProtoDumpable
+
+        private fun PrintWriter.preamble(entry: DumpsysEntry) =
+            when (entry) {
+                // Historically TableLogBuffer was not separate from dumpables, so they have the
+                // same header
+                is DumpableEntry,
+                is TableLogBufferEntry -> {
+                    println()
+                    println(entry.name)
+                    println(DUMPSYS_DUMPABLE_DIVIDER)
+                }
+                is LogBufferEntry -> {
+                    println()
+                    println()
+                    println("BUFFER ${entry.name}:")
+                    println(DUMPSYS_BUFFER_DIVIDER)
+                }
+            }
+
+        /**
+         * Zero-arg utility to write a [DumpableEntry] to the given [PrintWriter] in a
+         * dumpsys-appropriate format.
+         */
+        private fun dumpDumpable(entry: DumpableEntry, pw: PrintWriter) {
+            pw.preamble(entry)
+            entry.dumpable.dump(pw, arrayOf())
+        }
+
+        /**
+         * Zero-arg utility to write a [LogBufferEntry] to the given [PrintWriter] in a
+         * dumpsys-appropriate format.
+         */
+        private fun dumpBuffer(entry: LogBufferEntry, pw: PrintWriter) {
+            pw.preamble(entry)
+            entry.buffer.dump(pw, 0)
+        }
+
+        /**
+         * Zero-arg utility to write a [TableLogBufferEntry] to the given [PrintWriter] in a
+         * dumpsys-appropriate format.
+         */
+        private fun dumpTableBuffer(entry: TableLogBufferEntry, pw: PrintWriter) {
+            pw.preamble(entry)
+            entry.table.dump(pw, arrayOf())
+        }
+
+        /**
+         * Zero-arg utility to write a [DumpsysEntry] to the given [PrintWriter] in a
+         * dumpsys-appropriate format.
+         */
+        fun DumpsysEntry.dump(pw: PrintWriter) {
+            when (this) {
+                is DumpableEntry -> dumpDumpable(this, pw)
+                is LogBufferEntry -> dumpBuffer(this, pw)
+                is TableLogBufferEntry -> dumpTableBuffer(this, pw)
+            }
+        }
+
+        /** Format [entries] in a dumpsys-appropriate way, using [pw] */
+        fun dumpEntries(entries: Collection<DumpsysEntry>, pw: PrintWriter) {
+            entries.forEach { it.dump(pw) }
+        }
     }
 }
 
 private val PRIORITY_OPTIONS = arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_NORMAL)
 
-private val COMMANDS = arrayOf(
+private val COMMANDS =
+    arrayOf(
         "bugreport-critical",
         "bugreport-normal",
         "buffers",
         "dumpables",
+        "tables",
         "config",
         "help"
-)
+    )
 
-private class ParsedArgs(
-    val rawArgs: Array<String>,
-    val nonFlagArgs: List<String>
-) {
+private class ParsedArgs(val rawArgs: Array<String>, val nonFlagArgs: List<String>) {
     var dumpPriority: String? = null
     var tailLength: Int = 0
     var command: String? = null
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
index 2d57633..c924df6 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
@@ -18,9 +18,11 @@
 
 import com.android.systemui.Dumpable
 import com.android.systemui.ProtoDumpable
-import com.android.systemui.dump.nano.SystemUIProtoDump
+import com.android.systemui.dump.DumpsysEntry.DumpableEntry
+import com.android.systemui.dump.DumpsysEntry.LogBufferEntry
+import com.android.systemui.dump.DumpsysEntry.TableLogBufferEntry
 import com.android.systemui.log.LogBuffer
-import java.io.PrintWriter
+import com.android.systemui.log.table.TableLogBuffer
 import java.util.TreeMap
 import javax.inject.Inject
 import javax.inject.Singleton
@@ -37,8 +39,9 @@
 @Singleton
 open class DumpManager @Inject constructor() {
     // NOTE: Using TreeMap ensures that iteration is in a predictable & alphabetical order.
-    private val dumpables: MutableMap<String, RegisteredDumpable<Dumpable>> = TreeMap()
-    private val buffers: MutableMap<String, RegisteredDumpable<LogBuffer>> = TreeMap()
+    private val dumpables: MutableMap<String, DumpableEntry> = TreeMap()
+    private val buffers: MutableMap<String, LogBufferEntry> = TreeMap()
+    private val tableLogBuffers: MutableMap<String, TableLogBufferEntry> = TreeMap()
 
     /** See [registerCriticalDumpable]. */
     fun registerCriticalDumpable(module: Dumpable) {
@@ -77,14 +80,14 @@
      * Register a dumpable to be called during a bug report.
      *
      * @param name The name to register the dumpable under. This is typically the qualified class
-     * name of the thing being dumped (getClass().getName()), but can be anything as long as it
-     * doesn't clash with an existing registration.
+     *   name of the thing being dumped (getClass().getName()), but can be anything as long as it
+     *   doesn't clash with an existing registration.
      * @param priority the priority level of this dumpable, which affects at what point in the bug
-     * report this gets dump. By default, the dumpable will be called during the CRITICAL section of
-     * the bug report, so don't dump an excessive amount of stuff here.
+     *   report this gets dump. By default, the dumpable will be called during the CRITICAL section
+     *   of the bug report, so don't dump an excessive amount of stuff here.
      *
      * TODO(b/259973758): Replace all calls to this method with calls to [registerCriticalDumpable]
-     * or [registerNormalDumpable] instead.
+     *   or [registerNormalDumpable] instead.
      */
     @Synchronized
     @JvmOverloads
@@ -98,7 +101,7 @@
             throw IllegalArgumentException("'$name' is already registered")
         }
 
-        dumpables[name] = RegisteredDumpable(name, module, priority)
+        dumpables[name] = DumpableEntry(module, name, priority)
     }
 
     /**
@@ -110,217 +113,62 @@
         registerDumpable(module::class.java.simpleName, module)
     }
 
-    /**
-     * Unregisters a previously-registered dumpable.
-     */
+    /** Unregisters a previously-registered dumpable. */
     @Synchronized
     fun unregisterDumpable(name: String) {
         dumpables.remove(name)
     }
 
-    /**
-     * Register a [LogBuffer] to be dumped during a bug report.
-     */
+    /** Register a [LogBuffer] to be dumped during a bug report. */
     @Synchronized
     fun registerBuffer(name: String, buffer: LogBuffer) {
         if (!canAssignToNameLocked(name, buffer)) {
             throw IllegalArgumentException("'$name' is already registered")
         }
 
+        buffers[name] = LogBufferEntry(buffer, name)
+    }
+
+    /** Register a [TableLogBuffer] to be dumped during a bugreport */
+    @Synchronized
+    fun registerTableLogBuffer(name: String, buffer: TableLogBuffer) {
+        if (!canAssignToNameLocked(name, buffer)) {
+            throw IllegalArgumentException("'$name' is already registered")
+        }
+
         // All buffers must be priority NORMAL, not CRITICAL, because they often contain a lot of
         // data.
-        buffers[name] = RegisteredDumpable(name, buffer, DumpPriority.NORMAL)
+        tableLogBuffers[name] = TableLogBufferEntry(buffer, name)
     }
 
-    /**
-     * Dumps the alphabetically first, shortest-named dumpable or buffer whose registered name ends
-     * with [target].
-     */
-    @Synchronized
-    fun dumpTarget(
-        target: String,
-        pw: PrintWriter,
-        args: Array<String>,
-        tailLength: Int,
-    ) {
-        sequence {
-            findBestTargetMatch(dumpables, target)?.let {
-                yield(it.name to { dumpDumpable(it, pw, args) })
-            }
-            findBestTargetMatch(buffers, target)?.let {
-                yield(it.name to { dumpBuffer(it, pw, tailLength) })
-            }
-        }.sortedBy { it.first }.minByOrNull { it.first.length }?.second?.invoke()
-    }
+    @Synchronized fun getDumpables(): Collection<DumpableEntry> = dumpables.values.toList()
+
+    @Synchronized fun getLogBuffers(): Collection<LogBufferEntry> = buffers.values.toList()
 
     @Synchronized
-    fun dumpProtoTarget(
-        target: String,
-        protoDump: SystemUIProtoDump,
-        args: Array<String>
-    ) {
-        findBestProtoTargetMatch(dumpables, target)?.let {
-            dumpProtoDumpable(it, protoDump, args)
-        }
-    }
-
-    @Synchronized
-    fun dumpProtoDumpables(
-        systemUIProtoDump: SystemUIProtoDump,
-        args: Array<String>
-    ) {
-        for (dumpable in dumpables.values) {
-            if (dumpable.dumpable is ProtoDumpable) {
-                dumpProtoDumpable(
-                    dumpable.dumpable,
-                    systemUIProtoDump,
-                    args
-                )
-            }
-        }
-    }
-
-    /**
-     * Dumps all registered dumpables with critical priority to [pw]
-     */
-    @Synchronized
-    fun dumpCritical(pw: PrintWriter, args: Array<String>) {
-        for (dumpable in dumpables.values) {
-            if (dumpable.priority == DumpPriority.CRITICAL) {
-                dumpDumpable(dumpable, pw, args)
-            }
-        }
-    }
-
-    /**
-     * To [pw], dumps (1) all registered dumpables with normal priority; and (2) all [LogBuffer]s.
-     */
-    @Synchronized
-    fun dumpNormal(pw: PrintWriter, args: Array<String>, tailLength: Int = 0) {
-        for (dumpable in dumpables.values) {
-            if (dumpable.priority == DumpPriority.NORMAL) {
-                dumpDumpable(dumpable, pw, args)
-            }
-        }
-
-        for (buffer in buffers.values) {
-            dumpBuffer(buffer, pw, tailLength)
-        }
-    }
-
-    /**
-     * Dump all the instances of [Dumpable].
-     */
-    @Synchronized
-    fun dumpDumpables(pw: PrintWriter, args: Array<String>) {
-        for (module in dumpables.values) {
-            dumpDumpable(module, pw, args)
-        }
-    }
-
-    /**
-     * Dumps the names of all registered dumpables (one per line)
-     */
-    @Synchronized
-    fun listDumpables(pw: PrintWriter) {
-        for (module in dumpables.values) {
-            pw.println(module.name)
-        }
-    }
-
-    /**
-     * Dumps all registered [LogBuffer]s to [pw]
-     */
-    @Synchronized
-    fun dumpBuffers(pw: PrintWriter, tailLength: Int) {
-        for (buffer in buffers.values) {
-            dumpBuffer(buffer, pw, tailLength)
-        }
-    }
-
-    /**
-     * Dumps the names of all registered buffers (one per line)
-     */
-    @Synchronized
-    fun listBuffers(pw: PrintWriter) {
-        for (buffer in buffers.values) {
-            pw.println(buffer.name)
-        }
-    }
+    fun getTableLogBuffers(): Collection<TableLogBufferEntry> = tableLogBuffers.values.toList()
 
     @Synchronized
     fun freezeBuffers() {
         for (buffer in buffers.values) {
-            buffer.dumpable.freeze()
+            buffer.buffer.freeze()
         }
     }
 
     @Synchronized
     fun unfreezeBuffers() {
         for (buffer in buffers.values) {
-            buffer.dumpable.unfreeze()
+            buffer.buffer.unfreeze()
         }
     }
 
-    private fun dumpDumpable(
-        dumpable: RegisteredDumpable<Dumpable>,
-        pw: PrintWriter,
-        args: Array<String>
-    ) {
-        pw.println()
-        pw.println("${dumpable.name}:")
-        pw.println("----------------------------------------------------------------------------")
-        dumpable.dumpable.dump(pw, args)
-    }
-
-    private fun dumpBuffer(
-        buffer: RegisteredDumpable<LogBuffer>,
-        pw: PrintWriter,
-        tailLength: Int
-    ) {
-        pw.println()
-        pw.println()
-        pw.println("BUFFER ${buffer.name}:")
-        pw.println("============================================================================")
-        buffer.dumpable.dump(pw, tailLength)
-    }
-
-    private fun dumpProtoDumpable(
-        protoDumpable: ProtoDumpable,
-        systemUIProtoDump: SystemUIProtoDump,
-        args: Array<String>
-    ) {
-        protoDumpable.dumpProto(systemUIProtoDump, args)
-    }
-
     private fun canAssignToNameLocked(name: String, newDumpable: Any): Boolean {
-        val existingDumpable = dumpables[name]?.dumpable ?: buffers[name]?.dumpable
+        val existingDumpable =
+            dumpables[name]?.dumpable ?: buffers[name]?.buffer ?: tableLogBuffers[name]?.table
         return existingDumpable == null || newDumpable == existingDumpable
     }
-
-    private fun <V : Any> findBestTargetMatch(map: Map<String, V>, target: String): V? = map
-        .asSequence()
-        .filter { it.key.endsWith(target) }
-        .minByOrNull { it.key.length }
-        ?.value
-
-    private fun findBestProtoTargetMatch(
-        map: Map<String, RegisteredDumpable<Dumpable>>,
-        target: String
-    ): ProtoDumpable? = map
-        .asSequence()
-        .filter { it.key.endsWith(target) }
-        .filter { it.value.dumpable is ProtoDumpable }
-        .minByOrNull { it.key.length }
-        ?.value?.dumpable as? ProtoDumpable
 }
 
-private data class RegisteredDumpable<T>(
-    val name: String,
-    val dumpable: T,
-    val priority: DumpPriority,
-)
-
 /**
  * The priority level for a given dumpable, which affects at what point in the bug report this gets
  * dumped.
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpsysEntry.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpsysEntry.kt
new file mode 100644
index 0000000..cd3e1bb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpsysEntry.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.dump
+
+import com.android.systemui.Dumpable
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.table.TableLogBuffer
+
+/**
+ * A DumpsysEntry is a named, registered entry tracked by [DumpManager] which can be addressed and
+ * used both in a bugreport / dumpsys invocation or in an individual CLI implementation.
+ *
+ * The idea here is that we define every type that [DumpManager] knows about and defines the minimum
+ * shared interface between each type. So far, just [name] and [priority]. This way, [DumpManager]
+ * can just store them in separate maps and do the minimal amount of work to discriminate between
+ * them.
+ *
+ * Individual consumers can request these participants in a list via the relevant get* methods on
+ * [DumpManager]
+ */
+sealed interface DumpsysEntry {
+    val name: String
+    val priority: DumpPriority
+
+    data class DumpableEntry(
+        val dumpable: Dumpable,
+        override val name: String,
+        override val priority: DumpPriority,
+    ) : DumpsysEntry
+
+    data class LogBufferEntry(
+        val buffer: LogBuffer,
+        override val name: String,
+    ) : DumpsysEntry {
+        // All buffers must be priority NORMAL, not CRITICAL, because they often contain a lot of
+        // data.
+        override val priority: DumpPriority = DumpPriority.NORMAL
+    }
+
+    data class TableLogBufferEntry(
+        val table: TableLogBuffer,
+        override val name: String,
+    ) : DumpsysEntry {
+        // All buffers must be priority NORMAL, not CRITICAL, because they often contain a lot of
+        // data.
+        override val priority: DumpPriority = DumpPriority.NORMAL
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
index 2d5c9ae..bd43302 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
@@ -20,6 +20,7 @@
 import android.icu.text.SimpleDateFormat
 import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpHandler.Companion.dumpEntries
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.util.io.Files
 import com.android.systemui.util.time.SystemClock
@@ -48,20 +49,21 @@
     private val files: Files,
     private val logPath: Path,
     private val minWriteGap: Long,
-    private val maxLogAgeToDump: Long
+    private val maxLogAgeToDump: Long,
 ) {
-    @Inject constructor(
+    @Inject
+    constructor(
         context: Context,
         dumpManager: DumpManager,
         systemClock: SystemClock,
-        files: Files
+        files: Files,
     ) : this(
         dumpManager,
         systemClock,
         files,
         Paths.get(context.filesDir.toPath().toString(), "log_buffers.txt"),
         MIN_WRITE_GAP,
-        MAX_AGE_TO_DUMP
+        MAX_AGE_TO_DUMP,
     )
 
     /**
@@ -70,7 +72,7 @@
      * The file will be prefaced by the [reason], which will then be returned (presumably so it can
      * be thrown).
      */
-    fun <T : Exception> record(reason: T): T {
+    fun <T : Throwable> record(reason: T): T {
         val start = systemClock.uptimeMillis()
         var duration = 0L
 
@@ -91,7 +93,8 @@
                 pw.println()
                 pw.println("Dump triggered by exception:")
                 reason.printStackTrace(pw)
-                dumpManager.dumpBuffers(pw, 0)
+                val buffers = dumpManager.getLogBuffers()
+                dumpEntries(buffers, pw)
                 duration = systemClock.uptimeMillis() - start
                 pw.println()
                 pw.println("Buffer eulogy took ${duration}ms")
@@ -105,16 +108,17 @@
         return reason
     }
 
-    /**
-     * If a eulogy file is present, writes its contents to [pw].
-     */
+    /** If a eulogy file is present, writes its contents to [pw]. */
     fun readEulogyIfPresent(pw: PrintWriter) {
         try {
             val millisSinceLastWrite = getMillisSinceLastWrite(logPath)
             if (millisSinceLastWrite > maxLogAgeToDump) {
-                Log.i(TAG, "Not eulogizing buffers; they are " +
+                Log.i(
+                    TAG,
+                    "Not eulogizing buffers; they are " +
                         TimeUnit.HOURS.convert(millisSinceLastWrite, TimeUnit.MILLISECONDS) +
-                        " hours old")
+                        " hours old"
+                )
                 return
             }
 
@@ -122,9 +126,7 @@
                 pw.println()
                 pw.println()
                 pw.println("=============== BUFFERS FROM MOST RECENT CRASH ===============")
-                s.forEach { line ->
-                    pw.println(line)
-                }
+                s.forEach { line -> pw.println(line) }
             }
         } catch (e: IOException) {
             // File doesn't exist, okay
@@ -134,12 +136,13 @@
     }
 
     private fun getMillisSinceLastWrite(path: Path): Long {
-        val stats = try {
-            files.readAttributes(path, BasicFileAttributes::class.java)
-        } catch (e: IOException) {
-            // File doesn't exist
-            null
-        }
+        val stats =
+            try {
+                files.readAttributes(path, BasicFileAttributes::class.java)
+            } catch (e: IOException) {
+                // File doesn't exist
+                null
+            }
         return systemClock.currentTimeMillis() - (stats?.lastModifiedTime()?.toMillis() ?: 0)
     }
 }
@@ -147,4 +150,4 @@
 private const val TAG = "BufferEulogizer"
 private val MIN_WRITE_GAP = TimeUnit.MINUTES.toMillis(5)
 private val MAX_AGE_TO_DUMP = TimeUnit.HOURS.toMillis(48)
-private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
\ No newline at end of file
+private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 31f7e88..6abe951 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -72,10 +72,6 @@
     val NOTIFICATION_MEMORY_LOGGING_ENABLED =
         unreleasedFlag(119, "notification_memory_logging_enabled")
 
-    @JvmField
-    val SIMPLIFIED_APPEAR_FRACTION =
-        releasedFlag(259395680, "simplified_appear_fraction")
-
     // TODO(b/257315550): Tracking Bug
     val NO_HUN_FOR_OLD_WHEN = releasedFlag(118, "no_hun_for_old_when")
 
@@ -95,6 +91,15 @@
     val NOTIFICATION_SHELF_REFACTOR =
         unreleasedFlag(271161129, "notification_shelf_refactor")
 
+    // TODO(b/288326013): Tracking Bug
+    @JvmField
+    val NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION =
+            unreleasedFlag(
+                    288326013,
+                    "notification_async_hybrid_view_inflation",
+                    teamfood = false
+            )
+
     @JvmField
     val ANIMATED_NOTIFICATION_SHADE_INSETS =
         releasedFlag(270682168, "animated_notification_shade_insets")
@@ -155,12 +160,6 @@
     // TODO(b/255607168): Tracking Bug
     @JvmField val DOZING_MIGRATION_1 = unreleasedFlag(213, "dozing_migration_1")
 
-    // TODO(b/252897742): Tracking Bug
-    @JvmField val NEW_ELLIPSE_DETECTION = unreleasedFlag(214, "new_ellipse_detection")
-
-    // TODO(b/252897742): Tracking Bug
-    @JvmField val NEW_UDFPS_OVERLAY = unreleasedFlag(215, "new_udfps_overlay")
-
     /**
      * Whether to enable the code powering customizable lock screen quick affordances.
      *
@@ -251,12 +250,15 @@
     /** Whether to delay showing bouncer UI when face auth or active unlock are enrolled. */
     // TODO(b/279794160): Tracking bug.
     @JvmField
-    val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer")
+    val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer", teamfood = true)
+
+
+    /** Keyguard Migration */
 
     /** Migrate the indication area to the new keyguard root view. */
     // TODO(b/280067944): Tracking bug.
     @JvmField
-    val MIGRATE_INDICATION_AREA = unreleasedFlag(236, "migrate_indication_area")
+    val MIGRATE_INDICATION_AREA = unreleasedFlag(236, "migrate_indication_area", teamfood = true)
 
     /** Whether to listen for fingerprint authentication over keyguard occluding activities. */
     // TODO(b/283260512): Tracking bug.
@@ -268,6 +270,15 @@
     @JvmField
     val KEYGUARD_TALKBACK_FIX = releasedFlag(238, "keyguard_talkback_fix")
 
+    // TODO(b/287268101): Tracking bug.
+    @JvmField
+    val TRANSIT_CLOCK = unreleasedFlag(239, "lockscreen_custom_transit_clock")
+
+    /** Migrate the lock icon view to the new keyguard root view. */
+    // TODO(b/286552209): Tracking bug.
+    @JvmField
+    val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon")
+
     // 300 - power menu
     // TODO(b/254512600): Tracking Bug
     @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
@@ -364,7 +375,7 @@
 
     // TODO(b/280426085): Tracking Bug
     @JvmField
-    val NEW_BLUETOOTH_REPOSITORY = unreleasedFlag(612, "new_bluetooth_repository")
+    val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository")
 
     // 700 - dialer/calls
     // TODO(b/254512734): Tracking Bug
@@ -446,9 +457,6 @@
     // TODO(b/254512758): Tracking Bug
     @JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple")
 
-    // TODO(b/265045965): Tracking Bug
-    val SHOW_LOWLIGHT_ON_DIRECT_BOOT = releasedFlag(1003, "show_lowlight_on_direct_boot")
-
     // TODO(b/273509374): Tracking Bug
     @JvmField
     val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS = releasedFlag(1006,
@@ -485,7 +493,7 @@
     @Keep
     @JvmField
     val WM_CAPTION_ON_SHELL =
-        sysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", default = false)
+        sysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", default = true)
 
     @Keep
     @JvmField
@@ -543,7 +551,7 @@
     // TODO(b/273443374): Tracking Bug
     @Keep
     @JvmField val LOCKSCREEN_LIVE_WALLPAPER =
-        sysPropBooleanFlag(1117, "persist.wm.debug.lockscreen_live_wallpaper", default = false)
+        sysPropBooleanFlag(1117, "persist.wm.debug.lockscreen_live_wallpaper", default = true)
 
     // TODO(b/281648899): Tracking bug
     @Keep
@@ -578,7 +586,7 @@
 
     // TODO(b/270987164): Tracking Bug
     @JvmField
-    val TRACKPAD_GESTURE_FEATURES = unreleasedFlag(1205, "trackpad_gesture_features", teamfood = true)
+    val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features")
 
     // TODO(b/263826204): Tracking Bug
     @JvmField
@@ -637,6 +645,9 @@
     // TODO(b/265944639): Tracking Bug
     @JvmField val DUAL_SHADE = unreleasedFlag(1801, "dual_shade")
 
+    // TODO(b/283300105): Tracking Bug
+    @JvmField val SCENE_CONTAINER = unreleasedFlag(1802, "scene_container")
+
     // 1900
     @JvmField val NOTE_TASKS = releasedFlag(1900, "keycode_flag")
 
@@ -671,8 +682,7 @@
     @JvmField
     val ENABLE_USI_BATTERY_NOTIFICATIONS =
         releasedFlag(2302, "enable_usi_battery_notifications")
-    @JvmField val ENABLE_STYLUS_EDUCATION =
-        unreleasedFlag(2303, "enable_stylus_education", teamfood = true)
+    @JvmField val ENABLE_STYLUS_EDUCATION = releasedFlag(2303, "enable_stylus_education")
 
     // 2400 - performance tools and debugging info
     // TODO(b/238923086): Tracking Bug
@@ -711,23 +721,22 @@
 
     // TODO(b/259428678): Tracking Bug
     @JvmField
-    val KEYBOARD_BACKLIGHT_INDICATOR =
-            unreleasedFlag(2601, "keyboard_backlight_indicator", teamfood = true)
+    val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator")
 
     // TODO(b/277192623): Tracking Bug
     @JvmField
     val KEYBOARD_EDUCATION =
         unreleasedFlag(2603, "keyboard_education", teamfood = false)
 
-    // TODO(b/272036292): Tracking Bug
-    @JvmField
-    val LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION =
-            releasedFlag(2602, "large_shade_granular_alpha_interpolation")
-
     // TODO(b/277201412): Tracking Bug
     @JvmField
     val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION =
-            unreleasedFlag(2805, "split_shade_subpixel_optimization", teamfood = true)
+            releasedFlag(2805, "split_shade_subpixel_optimization")
+
+    // TODO(b/288868056): Tracking Bug
+    @JvmField
+    val PARTIAL_SCREEN_SHARING_TASK_SWITCHER =
+            unreleasedFlag(288868056, "pss_task_switcher")
 
     // TODO(b/278761837): Tracking Bug
     @JvmField
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index 76322ad..040ee79 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -125,6 +125,7 @@
 import com.android.systemui.plugins.GlobalActionsPanelPlugin;
 import com.android.systemui.scrim.ScrimDrawable;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -251,6 +252,7 @@
     private int mSmallestScreenWidthDp;
     private int mOrientation;
     private final Optional<CentralSurfaces> mCentralSurfacesOptional;
+    private final ShadeController mShadeController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private final DialogLaunchAnimator mDialogLaunchAnimator;
 
@@ -361,6 +363,7 @@
             @Main Handler handler,
             PackageManager packageManager,
             Optional<CentralSurfaces> centralSurfacesOptional,
+            ShadeController shadeController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             DialogLaunchAnimator dialogLaunchAnimator) {
         mContext = context;
@@ -394,6 +397,7 @@
         mSmallestScreenWidthDp = resources.getConfiguration().smallestScreenWidthDp;
         mOrientation = resources.getConfiguration().orientation;
         mCentralSurfacesOptional = centralSurfacesOptional;
+        mShadeController = shadeController;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mDialogLaunchAnimator = dialogLaunchAnimator;
 
@@ -702,7 +706,9 @@
                 mAdapter, mOverflowAdapter, mSysuiColorExtractor, mStatusBarService,
                 mLightBarController,
                 mNotificationShadeWindowController, this::onRefresh, mKeyguardShowing,
-                mPowerAdapter, mUiEventLogger, mCentralSurfacesOptional, mKeyguardUpdateMonitor,
+                mPowerAdapter, mUiEventLogger, mCentralSurfacesOptional,
+                mShadeController,
+                mKeyguardUpdateMonitor,
                 mLockPatternUtils);
 
         dialog.setOnDismissListener(this);
@@ -912,7 +918,7 @@
             mUiEventLogger.log(GlobalActionsEvent.GA_EMERGENCY_DIALER_PRESS);
             if (mTelecomManager != null) {
                 // Close shade so user sees the activity
-                mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade);
+                mShadeController.cancelExpansionAndCollapseShade();
                 Intent intent = mTelecomManager.createLaunchEmergencyDialerIntent(
                         null /* number */);
                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
@@ -2209,6 +2215,7 @@
         private UiEventLogger mUiEventLogger;
         private GestureDetector mGestureDetector;
         private Optional<CentralSurfaces> mCentralSurfacesOptional;
+        private final ShadeController mShadeController;
         private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
         private LockPatternUtils mLockPatternUtils;
         private float mWindowDimAmount;
@@ -2282,6 +2289,7 @@
                 Runnable onRefreshCallback, boolean keyguardShowing,
                 MyPowerOptionsAdapter powerAdapter, UiEventLogger uiEventLogger,
                 Optional<CentralSurfaces> centralSurfacesOptional,
+                ShadeController shadeController,
                 KeyguardUpdateMonitor keyguardUpdateMonitor,
                 LockPatternUtils lockPatternUtils) {
             // We set dismissOnDeviceLock to false because we have a custom broadcast receiver to
@@ -2299,6 +2307,7 @@
             mKeyguardShowing = keyguardShowing;
             mUiEventLogger = uiEventLogger;
             mCentralSurfacesOptional = centralSurfacesOptional;
+            mShadeController = shadeController;
             mKeyguardUpdateMonitor = keyguardUpdateMonitor;
             mLockPatternUtils = lockPatternUtils;
             mGestureDetector = new GestureDetector(mContext, mGestureListener);
@@ -2348,12 +2357,10 @@
             mUiEventLogger.log(GlobalActionsEvent.GA_CLOSE_TAP_OUTSIDE);
             if (mCentralSurfacesOptional.map(CentralSurfaces::isKeyguardShowing).orElse(false)) {
                 // match existing lockscreen behavior to open QS when swiping from status bar
-                mCentralSurfacesOptional.ifPresent(
-                        centralSurfaces -> centralSurfaces.animateExpandSettingsPanel(null));
+                mShadeController.animateExpandQs();
             } else {
                 // otherwise, swiping down should expand notification shade
-                mCentralSurfacesOptional.ifPresent(
-                        centralSurfaces -> centralSurfaces.animateExpandNotificationsPanel());
+                mShadeController.animateExpandShade();
             }
             dismiss();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardLifecyclesDispatcher.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardLifecyclesDispatcher.java
index 757afb6..67c85bd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardLifecyclesDispatcher.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardLifecyclesDispatcher.java
@@ -16,11 +16,19 @@
 
 package com.android.systemui.keyguard;
 
+import android.annotation.IntDef;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.Message;
-import android.os.Trace;
+import android.os.TraceNameSupplier;
+
+import androidx.annotation.NonNull;
 
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 import javax.inject.Inject;
 
@@ -29,7 +37,6 @@
  */
 @SysUISingleton
 public class KeyguardLifecyclesDispatcher {
-
     static final int SCREEN_TURNING_ON = 0;
     static final int SCREEN_TURNED_ON = 1;
     static final int SCREEN_TURNING_OFF = 2;
@@ -39,19 +46,46 @@
     static final int FINISHED_WAKING_UP = 5;
     static final int STARTED_GOING_TO_SLEEP = 6;
     static final int FINISHED_GOING_TO_SLEEP = 7;
-    private static final String TAG = "KeyguardLifecyclesDispatcher";
 
-    private final ScreenLifecycle mScreenLifecycle;
-    private final WakefulnessLifecycle mWakefulnessLifecycle;
-
-    @Inject
-    public KeyguardLifecyclesDispatcher(ScreenLifecycle screenLifecycle,
-            WakefulnessLifecycle wakefulnessLifecycle) {
-        mScreenLifecycle = screenLifecycle;
-        mWakefulnessLifecycle = wakefulnessLifecycle;
+    @IntDef({
+            SCREEN_TURNING_ON,
+            SCREEN_TURNED_ON,
+            SCREEN_TURNING_OFF,
+            SCREEN_TURNED_OFF,
+            STARTED_WAKING_UP,
+            FINISHED_WAKING_UP,
+            STARTED_GOING_TO_SLEEP,
+            FINISHED_GOING_TO_SLEEP,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface KeyguardLifecycleMessageType {
     }
 
-    void dispatch(int what) {
+    private static String getNameOfMessage(@KeyguardLifecycleMessageType int what) {
+        return switch (what) {
+            case SCREEN_TURNING_ON -> "SCREEN_TURNING_ON";
+            case SCREEN_TURNED_ON -> "SCREEN_TURNED_ON";
+            case SCREEN_TURNING_OFF -> "SCREEN_TURNING_OFF";
+            case SCREEN_TURNED_OFF -> "SCREEN_TURNED_OFF";
+            case STARTED_WAKING_UP -> "STARTED_WAKING_UP";
+            case FINISHED_WAKING_UP -> "FINISHED_WAKING_UP";
+            case STARTED_GOING_TO_SLEEP -> "STARTED_GOING_TO_SLEEP";
+            case FINISHED_GOING_TO_SLEEP -> "FINISHED_GOING_TO_SLEEP";
+            default -> "UNKNOWN";
+        };
+    }
+
+    private final Handler mHandler;
+
+    @Inject
+    public KeyguardLifecyclesDispatcher(
+            @Main Looper mainLooper,
+            ScreenLifecycle screenLifecycle,
+            WakefulnessLifecycle wakefulnessLifecycle) {
+        mHandler = new KeyguardLifecycleHandler(mainLooper, screenLifecycle, wakefulnessLifecycle);
+    }
+
+    void dispatch(@KeyguardLifecycleMessageType int what) {
         mHandler.obtainMessage(what).sendToTarget();
     }
 
@@ -60,7 +94,7 @@
      * @param pmReason Reason this message was triggered - this should be a value from either
      * {@link PowerManager.WakeReason} or {@link PowerManager.GoToSleepReason}.
      */
-    void dispatch(int what, int pmReason) {
+    void dispatch(@KeyguardLifecycleMessageType int what, int pmReason) {
         final Message message = mHandler.obtainMessage(what);
         message.arg1 = pmReason;
         message.sendToTarget();
@@ -70,44 +104,48 @@
      * @param what Message to send.
      * @param object Object to send with the message
      */
-    void dispatch(int what, Object object) {
+    void dispatch(@KeyguardLifecycleMessageType int what, Object object) {
         mHandler.obtainMessage(what, object).sendToTarget();
     }
 
-    private Handler mHandler = new Handler() {
+    private static class KeyguardLifecycleHandler extends Handler {
+        private static final String TAG = "KeyguardLifecycleHandler";
+        private final ScreenLifecycle mScreenLifecycle;
+        private final WakefulnessLifecycle mWakefulnessLifecycle;
+
+        public KeyguardLifecycleHandler(Looper looper,
+                                         ScreenLifecycle screenLifecycle,
+                                         WakefulnessLifecycle wakefulnessLifecycle) {
+            super(looper);
+            mScreenLifecycle = screenLifecycle;
+            mWakefulnessLifecycle = wakefulnessLifecycle;
+        }
+
+        @NonNull
         @Override
-        public void handleMessage(Message msg) {
+        public String getTraceName(@NonNull Message msg) {
+            if (msg.getCallback() instanceof TraceNameSupplier || msg.getCallback() != null) {
+                return super.getTraceName(msg);
+            }
+            return TAG + "#" + getNameOfMessage(msg.what);
+        }
+
+        @Override
+        public void handleMessage(@NonNull Message msg) {
             switch (msg.what) {
-                case SCREEN_TURNING_ON:
-                    Trace.beginSection("KeyguardLifecyclesDispatcher#SCREEN_TURNING_ON");
-                    mScreenLifecycle.dispatchScreenTurningOn();
-                    Trace.endSection();
-                    break;
-                case SCREEN_TURNED_ON:
-                    mScreenLifecycle.dispatchScreenTurnedOn();
-                    break;
-                case SCREEN_TURNING_OFF:
-                    mScreenLifecycle.dispatchScreenTurningOff();
-                    break;
-                case SCREEN_TURNED_OFF:
-                    mScreenLifecycle.dispatchScreenTurnedOff();
-                    break;
-                case STARTED_WAKING_UP:
-                    mWakefulnessLifecycle.dispatchStartedWakingUp(msg.arg1 /* pmReason */);
-                    break;
-                case FINISHED_WAKING_UP:
-                    mWakefulnessLifecycle.dispatchFinishedWakingUp();
-                    break;
-                case STARTED_GOING_TO_SLEEP:
-                    mWakefulnessLifecycle.dispatchStartedGoingToSleep(msg.arg1 /* pmReason */);
-                    break;
-                case FINISHED_GOING_TO_SLEEP:
-                    mWakefulnessLifecycle.dispatchFinishedGoingToSleep();
-                    break;
-                default:
-                    throw new IllegalArgumentException("Unknown message: " + msg);
+                case SCREEN_TURNING_ON -> mScreenLifecycle.dispatchScreenTurningOn();
+                case SCREEN_TURNED_ON -> mScreenLifecycle.dispatchScreenTurnedOn();
+                case SCREEN_TURNING_OFF -> mScreenLifecycle.dispatchScreenTurningOff();
+                case SCREEN_TURNED_OFF -> mScreenLifecycle.dispatchScreenTurnedOff();
+                case STARTED_WAKING_UP ->
+                        mWakefulnessLifecycle.dispatchStartedWakingUp(msg.arg1 /* pmReason */);
+                case FINISHED_WAKING_UP -> mWakefulnessLifecycle.dispatchFinishedWakingUp();
+                case STARTED_GOING_TO_SLEEP ->
+                        mWakefulnessLifecycle.dispatchStartedGoingToSleep(msg.arg1 /* pmReason */);
+                case FINISHED_GOING_TO_SLEEP ->
+                        mWakefulnessLifecycle.dispatchFinishedGoingToSleep();
+                default -> throw new IllegalArgumentException("Unknown message: " + msg);
             }
         }
-    };
-
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
index 573de97..732102e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
@@ -49,7 +49,8 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.systemui.R;
-import com.android.systemui.SystemUIAppComponentFactory;
+import com.android.systemui.SystemUIAppComponentFactoryBase;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.NotificationMediaManager;
@@ -72,13 +73,13 @@
 /**
  * Simple Slice provider that shows the current date.
  *
- * Injection is handled by {@link SystemUIAppComponentFactory} +
+ * Injection is handled by {@link SystemUIAppComponentFactoryBase} +
  * {@link com.android.systemui.dagger.GlobalRootComponent#inject(KeyguardSliceProvider)}.
  */
 public class KeyguardSliceProvider extends SliceProvider implements
         NextAlarmController.NextAlarmChangeCallback, ZenModeController.Callback,
         NotificationMediaManager.MediaListener, StatusBarStateController.StateListener,
-        SystemUIAppComponentFactory.ContextInitializer {
+        SystemUIAppComponentFactoryBase.ContextInitializer {
 
     private static final String TAG = "KgdSliceProvider";
 
@@ -148,9 +149,12 @@
     protected boolean mDozing;
     private int mStatusBarState;
     private boolean mMediaIsVisible;
-    private SystemUIAppComponentFactory.ContextAvailableCallback mContextAvailableCallback;
+    private SystemUIAppComponentFactoryBase.ContextAvailableCallback mContextAvailableCallback;
     @Inject
     WakeLockLogger mWakeLockLogger;
+    @Inject
+    @Background
+    Handler mBgHandler;
 
     /**
      * Receiver responsible for time ticking and updating the date format.
@@ -502,7 +506,7 @@
     }
 
     protected void notifyChange() {
-        mContentResolver.notifyChange(mSliceUri, null /* observer */);
+        mBgHandler.post(() -> mContentResolver.notifyChange(mSliceUri, null /* observer */));
     }
 
     @Override
@@ -533,7 +537,7 @@
 
     @Override
     public void setContextAvailableCallback(
-            SystemUIAppComponentFactory.ContextAvailableCallback callback) {
+            SystemUIAppComponentFactoryBase.ContextAvailableCallback callback) {
         mContextAvailableCallback = callback;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index 68e72c5..3646144 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -906,10 +906,7 @@
     }
 
     fun setWallpaperAppearAmount(amount: Float) {
-        val animationAlpha = when {
-            !powerManager.isInteractive -> 0f
-            else -> amount
-        }
+        val animationAlpha = amount
 
         wallpaperTargets?.forEach { wallpaper ->
             // SyncRtSurfaceTransactionApplier cannot apply transaction when the target view is
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
new file mode 100644
index 0000000..f59ad90
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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
+
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.CoreStartable
+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.keyguard.ui.binder.KeyguardIndicationAreaBinder
+import com.android.systemui.keyguard.ui.view.KeyguardRootView
+import com.android.systemui.keyguard.ui.view.layout.KeyguardLayoutManager
+import com.android.systemui.keyguard.ui.view.layout.KeyguardLayoutManagerCommandListener
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
+import com.android.systemui.shade.NotificationShadeWindowView
+import com.android.systemui.statusbar.KeyguardIndicationController
+import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
+
+/** Binds keyguard views on startup, and also exposes methods to allow rebinding if views change */
+@SysUISingleton
+class KeyguardViewConfigurator
+@Inject
+constructor(
+    private val keyguardRootView: KeyguardRootView,
+    private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel,
+    private val notificationShadeWindowView: NotificationShadeWindowView,
+    private val featureFlags: FeatureFlags,
+    private val indicationController: KeyguardIndicationController,
+    private val keyguardLayoutManager: KeyguardLayoutManager,
+    private val keyguardLayoutManagerCommandListener: KeyguardLayoutManagerCommandListener,
+) : CoreStartable {
+
+    private var indicationAreaHandle: DisposableHandle? = null
+
+    override fun start() {
+        val notificationPanel =
+            notificationShadeWindowView.requireViewById(R.id.notification_panel) as ViewGroup
+        bindIndicationArea(notificationPanel)
+        bindLockIconView(notificationPanel)
+        keyguardLayoutManager.layoutViews()
+        keyguardLayoutManagerCommandListener.start()
+    }
+
+    fun bindIndicationArea(legacyParent: ViewGroup) {
+        indicationAreaHandle?.dispose()
+
+        // At startup, 2 views with the ID `R.id.keyguard_indication_area` will be available.
+        // Disable one of them
+        if (featureFlags.isEnabled(Flags.MIGRATE_INDICATION_AREA)) {
+            legacyParent.findViewById<View>(R.id.keyguard_indication_area)?.let {
+                legacyParent.removeView(it)
+            }
+        } else {
+            keyguardRootView.findViewById<View?>(R.id.keyguard_indication_area)?.let {
+                keyguardRootView.removeView(it)
+            }
+        }
+
+        indicationAreaHandle =
+            KeyguardIndicationAreaBinder.bind(
+                notificationShadeWindowView,
+                keyguardIndicationAreaViewModel,
+                indicationController
+            )
+    }
+
+    private fun bindLockIconView(legacyParent: ViewGroup) {
+        if (featureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON)) {
+            legacyParent.requireViewById<View>(R.id.lock_icon_view).let {
+                legacyParent.removeView(it)
+            }
+        } else {
+            keyguardRootView.findViewById<View?>(R.id.lock_icon_view)?.let {
+                keyguardRootView.removeView(it)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 2d82c50..468d760 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -17,6 +17,8 @@
 package com.android.systemui.keyguard;
 
 import static android.app.StatusBarManager.SESSION_KEYGUARD;
+import static android.provider.Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT;
+import static android.provider.Settings.System.LOCKSCREEN_SOUNDS_ENABLED;
 import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_NO_WINDOW_ANIMATIONS;
@@ -73,7 +75,6 @@
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -162,6 +163,9 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.util.DeviceConfigProxy;
+import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.settings.SystemSettings;
+import com.android.systemui.util.time.SystemClock;
 import com.android.wm.shell.keyguard.KeyguardTransitions;
 
 import dagger.Lazy;
@@ -226,7 +230,7 @@
 
     private final static String TAG = "KeyguardViewMediator";
 
-    private static final String DELAYED_KEYGUARD_ACTION =
+    public static final String DELAYED_KEYGUARD_ACTION =
         "com.android.internal.policy.impl.PhoneWindowManager.DELAYED_KEYGUARD";
     private static final String DELAYED_LOCK_PROFILE_ACTION =
             "com.android.internal.policy.impl.PhoneWindowManager.DELAYED_LOCK";
@@ -261,7 +265,7 @@
      * turning on the keyguard (i.e, the user has this much time to turn
      * the screen back on without having to face the keyguard).
      */
-    private static final int KEYGUARD_LOCK_AFTER_DELAY_DEFAULT = 5000;
+    public static final int KEYGUARD_LOCK_AFTER_DELAY_DEFAULT = 5000;
 
     /**
      * How long we'll wait for the {@link ViewMediatorCallback#keyguardDoneDrawing()}
@@ -320,6 +324,9 @@
 
     /** UserSwitcherController for creating guest user on boot complete */
     private final UserSwitcherController mUserSwitcherController;
+    private final SecureSettings mSecureSettings;
+    private final SystemSettings mSystemSettings;
+    private final SystemClock mSystemClock;
     private SystemPropertiesHelper mSystemPropertiesHelper;
 
     /**
@@ -578,17 +585,9 @@
         @Override
         public void onUserSwitching(int userId) {
             if (DEBUG) Log.d(TAG, String.format("onUserSwitching %d", userId));
-            // Note that the mLockPatternUtils user has already been updated from setCurrentUser.
-            // We need to force a reset of the views, since lockNow (called by
-            // ActivityManagerService) will not reconstruct the keyguard if it is already showing.
             synchronized (KeyguardViewMediator.this) {
                 resetKeyguardDonePendingLocked();
-                if (mLockPatternUtils.isLockScreenDisabled(userId)) {
-                    // If we are switching to a user that has keyguard disabled, dismiss keyguard.
-                    dismiss(null /* callback */, null /* message */);
-                } else {
-                    resetStateLocked();
-                }
+                dismiss(null /* callback */, null /* message */);
                 adjustStatusBarLocked();
             }
         }
@@ -596,16 +595,9 @@
         @Override
         public void onUserSwitchComplete(int userId) {
             if (DEBUG) Log.d(TAG, String.format("onUserSwitchComplete %d", userId));
-            if (userId != UserHandle.USER_SYSTEM) {
-                UserInfo info = UserManager.get(mContext).getUserInfo(userId);
-                // Don't try to dismiss if the user has Pin/Pattern/Password set
-                if (info == null || mLockPatternUtils.isSecure(userId)) {
-                    return;
-                } else if (info.isGuest() || info.isDemo()) {
-                    // If we just switched to a guest, try to dismiss keyguard.
-                    dismiss(null /* callback */, null /* message */);
-                }
-            }
+            // We are calling dismiss again and with a delay as there are race conditions
+            // in some scenarios caused by async layout listeners
+            mHandler.postDelayed(() -> dismiss(null /* callback */, null /* message */), 500);
         }
 
         @Override
@@ -830,6 +822,11 @@
         }
 
         @Override
+        public void onBouncerSwipeDown() {
+            mKeyguardViewControllerLazy.get().reset(/* hideBouncerWhenShowing= */ true);
+        }
+
+        @Override
         public void playTrustedSound() {
             KeyguardViewMediator.this.playTrustedSound();
         }
@@ -1330,6 +1327,9 @@
             Lazy<ActivityLaunchAnimator> activityLaunchAnimator,
             Lazy<ScrimController> scrimControllerLazy,
             FeatureFlags featureFlags,
+            SecureSettings secureSettings,
+            SystemSettings systemSettings,
+            SystemClock systemClock,
             @Main CoroutineDispatcher mainDispatcher,
             Lazy<DreamingToLockscreenTransitionViewModel> dreamingToLockscreenTransitionViewModel,
             SystemPropertiesHelper systemPropertiesHelper) {
@@ -1346,6 +1346,9 @@
         mPM = powerManager;
         mTrustManager = trustManager;
         mUserSwitcherController = userSwitcherController;
+        mSecureSettings = secureSettings;
+        mSystemSettings = systemSettings;
+        mSystemClock = systemClock;
         mSystemPropertiesHelper = systemPropertiesHelper;
         mStatusBarService = IStatusBarService.Stub.asInterface(
                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
@@ -1398,7 +1401,7 @@
     }
 
     public void userActivity() {
-        mPM.userActivity(SystemClock.uptimeMillis(), false);
+        mPM.userActivity(mSystemClock.uptimeMillis(), false);
     }
 
     private void setupLocked() {
@@ -1603,7 +1606,7 @@
 
             if (cameraGestureTriggered) {
                 // Just to make sure, make sure the device is awake.
-                mContext.getSystemService(PowerManager.class).wakeUp(SystemClock.uptimeMillis(),
+                mContext.getSystemService(PowerManager.class).wakeUp(mSystemClock.uptimeMillis(),
                         PowerManager.WAKE_REASON_CAMERA_LAUNCH,
                         "com.android.systemui:CAMERA_GESTURE_PREVENT_LOCK");
                 setPendingLock(false);
@@ -1700,12 +1703,11 @@
         // to enable it a bit later (i.e, give the user a chance
         // to turn the screen back on within a certain window without
         // having to unlock the screen)
-        final ContentResolver cr = mContext.getContentResolver();
 
         // From SecuritySettings
-        final long lockAfterTimeout = Settings.Secure.getIntForUser(cr,
-                Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
-                KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, userId);
+        final long lockAfterTimeout = mSecureSettings.getIntForUser(LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+                KEYGUARD_LOCK_AFTER_DELAY_DEFAULT,
+                userId);
 
         // From DevicePolicyAdmin
         final long policyTimeout = mLockPatternUtils.getDevicePolicyManager()
@@ -1717,8 +1719,9 @@
             timeout = lockAfterTimeout;
         } else {
             // From DisplaySettings
-            long displayTimeout = Settings.System.getIntForUser(cr, SCREEN_OFF_TIMEOUT,
-                    KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT, userId);
+            long displayTimeout = mSystemSettings.getIntForUser(SCREEN_OFF_TIMEOUT,
+                    KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT,
+                    userId);
 
             // policy in effect. Make sure we don't go beyond policy limit.
             displayTimeout = Math.max(displayTimeout, 0); // ignore negative values
@@ -1739,7 +1742,7 @@
 
     private void doKeyguardLaterLocked(long timeout) {
         // Lock in the future
-        long when = SystemClock.elapsedRealtime() + timeout;
+        long when = mSystemClock.elapsedRealtime() + timeout;
         Intent intent = new Intent(DELAYED_KEYGUARD_ACTION);
         intent.setPackage(mContext.getPackageName());
         intent.putExtra("seq", mDelayedShowingSequence);
@@ -1761,7 +1764,7 @@
                 if (userTimeout == 0) {
                     doKeyguardForChildProfilesLocked();
                 } else {
-                    long userWhen = SystemClock.elapsedRealtime() + userTimeout;
+                    long userWhen = mSystemClock.elapsedRealtime() + userTimeout;
                     Intent lockIntent = new Intent(DELAYED_LOCK_PROFILE_ACTION);
                     lockIntent.setPackage(mContext.getPackageName());
                     lockIntent.putExtra("seq", mDelayedProfileShowingSequence);
@@ -2384,58 +2387,72 @@
     private Handler mHandler = new Handler(Looper.myLooper(), null, true /*async*/) {
         @Override
         public void handleMessage(Message msg) {
+            String message = "";
             switch (msg.what) {
                 case SHOW:
+                    message = "SHOW";
                     handleShow((Bundle) msg.obj);
                     break;
                 case HIDE:
+                    message = "HIDE";
                     handleHide();
                     break;
                 case RESET:
+                    message = "RESET";
                     handleReset(msg.arg1 != 0);
                     break;
                 case VERIFY_UNLOCK:
+                    message = "VERIFY_UNLOCK";
                     Trace.beginSection("KeyguardViewMediator#handleMessage VERIFY_UNLOCK");
                     handleVerifyUnlock();
                     Trace.endSection();
                     break;
                 case NOTIFY_STARTED_GOING_TO_SLEEP:
+                    message = "NOTIFY_STARTED_GOING_TO_SLEEP";
                     handleNotifyStartedGoingToSleep();
                     break;
                 case NOTIFY_FINISHED_GOING_TO_SLEEP:
+                    message = "NOTIFY_FINISHED_GOING_TO_SLEEP";
                     handleNotifyFinishedGoingToSleep();
                     break;
                 case NOTIFY_STARTED_WAKING_UP:
+                    message = "NOTIFY_STARTED_WAKING_UP";
                     Trace.beginSection(
                             "KeyguardViewMediator#handleMessage NOTIFY_STARTED_WAKING_UP");
                     handleNotifyStartedWakingUp();
                     Trace.endSection();
                     break;
                 case KEYGUARD_DONE:
+                    message = "KEYGUARD_DONE";
                     Trace.beginSection("KeyguardViewMediator#handleMessage KEYGUARD_DONE");
                     handleKeyguardDone();
                     Trace.endSection();
                     break;
                 case KEYGUARD_DONE_DRAWING:
+                    message = "KEYGUARD_DONE_DRAWING";
                     Trace.beginSection("KeyguardViewMediator#handleMessage KEYGUARD_DONE_DRAWING");
                     handleKeyguardDoneDrawing();
                     Trace.endSection();
                     break;
                 case SET_OCCLUDED:
+                    message = "SET_OCCLUDED";
                     Trace.beginSection("KeyguardViewMediator#handleMessage SET_OCCLUDED");
                     handleSetOccluded(msg.arg1 != 0, msg.arg2 != 0);
                     Trace.endSection();
                     break;
                 case KEYGUARD_TIMEOUT:
+                    message = "KEYGUARD_TIMEOUT";
                     synchronized (KeyguardViewMediator.this) {
                         doKeyguardLocked((Bundle) msg.obj);
                     }
                     break;
                 case DISMISS:
-                    final DismissMessage message = (DismissMessage) msg.obj;
-                    handleDismiss(message.getCallback(), message.getMessage());
+                    message = "DISMISS";
+                    final DismissMessage dismissMsg = (DismissMessage) msg.obj;
+                    handleDismiss(dismissMsg.getCallback(), dismissMsg.getMessage());
                     break;
                 case START_KEYGUARD_EXIT_ANIM:
+                    message = "START_KEYGUARD_EXIT_ANIM";
                     Trace.beginSection(
                             "KeyguardViewMediator#handleMessage START_KEYGUARD_EXIT_ANIM");
                     synchronized (KeyguardViewMediator.this) {
@@ -2453,21 +2470,25 @@
                     Trace.endSection();
                     break;
                 case CANCEL_KEYGUARD_EXIT_ANIM:
+                    message = "CANCEL_KEYGUARD_EXIT_ANIM";
                     Trace.beginSection(
                             "KeyguardViewMediator#handleMessage CANCEL_KEYGUARD_EXIT_ANIM");
                     handleCancelKeyguardExitAnimation();
                     Trace.endSection();
                     break;
                 case KEYGUARD_DONE_PENDING_TIMEOUT:
+                    message = "KEYGUARD_DONE_PENDING_TIMEOUT";
                     Trace.beginSection("KeyguardViewMediator#handleMessage"
                             + " KEYGUARD_DONE_PENDING_TIMEOUT");
                     Log.w(TAG, "Timeout while waiting for activity drawn!");
                     Trace.endSection();
                     break;
                 case SYSTEM_READY:
+                    message = "SYSTEM_READY";
                     handleSystemReady();
                     break;
             }
+            Log.d(TAG, "KeyguardViewMediator queue processing message: " + message);
         }
     };
 
@@ -2570,9 +2591,7 @@
 
     private void playSound(int soundId) {
         if (soundId == 0) return;
-        final ContentResolver cr = mContext.getContentResolver();
-        int lockscreenSoundsEnabled = Settings.System.getIntForUser(cr,
-                Settings.System.LOCKSCREEN_SOUNDS_ENABLED, 1,
+        int lockscreenSoundsEnabled = mSystemSettings.getIntForUser(LOCKSCREEN_SOUNDS_ENABLED, 1,
                 KeyguardUpdateMonitor.getCurrentUser());
         if (lockscreenSoundsEnabled == 1) {
 
@@ -2749,7 +2768,7 @@
         // It's possible that the device was unlocked (via BOUNCER) while dozing. It's time to
         // wake up.
         if (mAodShowing) {
-            mPM.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+            mPM.wakeUp(mSystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
                     "com.android.systemui:BOUNCER_DOZING");
         }
 
@@ -2764,7 +2783,7 @@
                 // TODO(bc-unlock): Fill parameters
                 mNotificationShadeWindowControllerLazy.get().batchApplyWindowLayoutParams(() -> {
                     handleStartKeyguardExitAnimation(
-                            SystemClock.uptimeMillis() + mHideAnimation.getStartOffset(),
+                            mSystemClock.uptimeMillis() + mHideAnimation.getStartOffset(),
                             mHideAnimation.getDuration(), null /* apps */, null /* wallpapers */,
                             null /* nonApps */, null /* finishedCallback */);
                 });
@@ -2772,8 +2791,8 @@
 
             // It's possible that the device was unlocked (via BOUNCER or Fingerprint) while
             // dreaming. It's time to wake up.
-            if (mDreamOverlayShowing) {
-                mPM.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+            if (mDreamOverlayShowing || mUpdateMonitor.isDreaming()) {
+                mPM.wakeUp(mSystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
                         "com.android.systemui:UNLOCK_DREAMING");
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/factory/BouncerMessageFactory.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/factory/BouncerMessageFactory.kt
deleted file mode 100644
index 11bc88a..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/factory/BouncerMessageFactory.kt
+++ /dev/null
@@ -1,322 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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.bouncer.data.factory
-
-import android.annotation.IntDef
-import com.android.keyguard.KeyguardSecurityModel
-import com.android.keyguard.KeyguardSecurityModel.SecurityMode
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_AFTER_LOCKOUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEVICE_ADMIN
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FACE_LOCKED_OUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FINGERPRINT_LOCKED_OUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FACE_INPUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NONE
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.R.string.keyguard_enter_password
-import com.android.systemui.R.string.keyguard_enter_pattern
-import com.android.systemui.R.string.keyguard_enter_pin
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
-import com.android.systemui.keyguard.bouncer.shared.model.Message
-import javax.inject.Inject
-
-@SysUISingleton
-class BouncerMessageFactory
-@Inject
-constructor(
-    private val updateMonitor: KeyguardUpdateMonitor,
-    private val securityModel: KeyguardSecurityModel,
-) {
-
-    fun createFromPromptReason(
-        @BouncerPromptReason reason: Int,
-        userId: Int,
-    ): BouncerMessageModel? {
-        val pair =
-            getBouncerMessage(
-                reason,
-                securityModel.getSecurityMode(userId),
-                updateMonitor.isFingerprintAllowedInBouncer
-            )
-        return pair?.let {
-            BouncerMessageModel(
-                message = Message(messageResId = pair.first),
-                secondaryMessage = Message(messageResId = pair.second)
-            )
-        }
-    }
-
-    fun createFromString(
-        primaryMsg: String? = null,
-        secondaryMsg: String? = null
-    ): BouncerMessageModel =
-        BouncerMessageModel(
-            message = primaryMsg?.let { Message(message = it) },
-            secondaryMessage = secondaryMsg?.let { Message(message = it) },
-        )
-
-    /**
-     * Helper method that provides the relevant bouncer message that should be shown for different
-     * scenarios indicated by [reason]. [securityMode] & [fpAllowedInBouncer] parameters are used to
-     * provide a more specific message.
-     */
-    private fun getBouncerMessage(
-        @BouncerPromptReason reason: Int,
-        securityMode: SecurityMode,
-        fpAllowedInBouncer: Boolean = false
-    ): Pair<Int, Int>? {
-        return when (reason) {
-            PROMPT_REASON_RESTART -> authRequiredAfterReboot(securityMode)
-            PROMPT_REASON_TIMEOUT -> authRequiredAfterPrimaryAuthTimeout(securityMode)
-            PROMPT_REASON_DEVICE_ADMIN -> authRequiredAfterAdminLockdown(securityMode)
-            PROMPT_REASON_USER_REQUEST -> authRequiredAfterUserLockdown(securityMode)
-            PROMPT_REASON_AFTER_LOCKOUT -> biometricLockout(securityMode)
-            PROMPT_REASON_PREPARE_FOR_UPDATE -> authRequiredForUnattendedUpdate(securityMode)
-            PROMPT_REASON_FINGERPRINT_LOCKED_OUT -> fingerprintUnlockUnavailable(securityMode)
-            PROMPT_REASON_FACE_LOCKED_OUT -> faceUnlockUnavailable(securityMode)
-            PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT ->
-                if (fpAllowedInBouncer) incorrectSecurityInputWithFingerprint(securityMode)
-                else incorrectSecurityInput(securityMode)
-            PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT ->
-                if (fpAllowedInBouncer) nonStrongAuthTimeoutWithFingerprintAllowed(securityMode)
-                else nonStrongAuthTimeout(securityMode)
-            PROMPT_REASON_TRUSTAGENT_EXPIRED ->
-                if (fpAllowedInBouncer) trustAgentDisabledWithFingerprintAllowed(securityMode)
-                else trustAgentDisabled(securityMode)
-            PROMPT_REASON_INCORRECT_FACE_INPUT ->
-                if (fpAllowedInBouncer) incorrectFaceInputWithFingerprintAllowed(securityMode)
-                else incorrectFaceInput(securityMode)
-            PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT -> incorrectFingerprintInput(securityMode)
-            PROMPT_REASON_DEFAULT ->
-                if (fpAllowedInBouncer) defaultMessageWithFingerprint(securityMode)
-                else defaultMessage(securityMode)
-            PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT -> primaryAuthLockedOut(securityMode)
-            else -> null
-        }
-    }
-}
-
-@Retention(AnnotationRetention.SOURCE)
-@IntDef(
-    PROMPT_REASON_TIMEOUT,
-    PROMPT_REASON_DEVICE_ADMIN,
-    PROMPT_REASON_USER_REQUEST,
-    PROMPT_REASON_AFTER_LOCKOUT,
-    PROMPT_REASON_PREPARE_FOR_UPDATE,
-    PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT,
-    PROMPT_REASON_TRUSTAGENT_EXPIRED,
-    PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
-    PROMPT_REASON_INCORRECT_FACE_INPUT,
-    PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT,
-    PROMPT_REASON_FACE_LOCKED_OUT,
-    PROMPT_REASON_FINGERPRINT_LOCKED_OUT,
-    PROMPT_REASON_DEFAULT,
-    PROMPT_REASON_NONE,
-    PROMPT_REASON_RESTART,
-    PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT,
-)
-annotation class BouncerPromptReason
-
-private fun defaultMessage(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun defaultMessageWithFingerprint(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun incorrectSecurityInput(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun incorrectSecurityInputWithFingerprint(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun incorrectFingerprintInput(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun incorrectFaceInput(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun incorrectFaceInputWithFingerprintAllowed(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun biometricLockout(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun authRequiredAfterReboot(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun authRequiredAfterAdminLockdown(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun authRequiredAfterUserLockdown(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun authRequiredForUnattendedUpdate(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun authRequiredAfterPrimaryAuthTimeout(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun nonStrongAuthTimeout(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun nonStrongAuthTimeoutWithFingerprintAllowed(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun faceUnlockUnavailable(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun fingerprintUnlockUnavailable(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun trustAgentDisabled(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun trustAgentDisabledWithFingerprintAllowed(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
-
-private fun primaryAuthLockedOut(securityMode: SecurityMode): Pair<Int, Int> {
-    return when (securityMode) {
-        SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0)
-        SecurityMode.Password -> Pair(keyguard_enter_password, 0)
-        SecurityMode.PIN -> Pair(keyguard_enter_pin, 0)
-        else -> Pair(0, 0)
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepository.kt
deleted file mode 100644
index c4400bc..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepository.kt
+++ /dev/null
@@ -1,332 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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.bouncer.data.repository
-
-import android.hardware.biometrics.BiometricSourceType
-import android.hardware.biometrics.BiometricSourceType.FACE
-import android.hardware.biometrics.BiometricSourceType.FINGERPRINT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEVICE_ADMIN
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FACE_LOCKED_OUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FINGERPRINT_LOCKED_OUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FACE_INPUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NONE
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.keyguard.KeyguardUpdateMonitorCallback
-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.bouncer.data.factory.BouncerMessageFactory
-import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
-import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.TrustRepository
-import com.android.systemui.user.data.repository.UserRepository
-import javax.inject.Inject
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
-
-/** Provide different sources of messages that needs to be shown on the bouncer. */
-interface BouncerMessageRepository {
-    /**
-     * Messages that are shown in response to the incorrect security attempts on the bouncer and
-     * primary authentication method being locked out, along with countdown messages before primary
-     * auth is active again.
-     */
-    val primaryAuthMessage: Flow<BouncerMessageModel?>
-
-    /**
-     * Help messages that are shown to the user on how to successfully perform authentication using
-     * face.
-     */
-    val faceAcquisitionMessage: Flow<BouncerMessageModel?>
-
-    /**
-     * Help messages that are shown to the user on how to successfully perform authentication using
-     * fingerprint.
-     */
-    val fingerprintAcquisitionMessage: Flow<BouncerMessageModel?>
-
-    /** Custom message that is displayed when the bouncer is being shown to launch an app. */
-    val customMessage: Flow<BouncerMessageModel?>
-
-    /**
-     * Messages that are shown in response to biometric authentication attempts through face or
-     * fingerprint.
-     */
-    val biometricAuthMessage: Flow<BouncerMessageModel?>
-
-    /** Messages that are shown when certain auth flags are set. */
-    val authFlagsMessage: Flow<BouncerMessageModel?>
-
-    /** Messages that are show after biometrics are locked out temporarily or permanently */
-    val biometricLockedOutMessage: Flow<BouncerMessageModel?>
-
-    /** Set the value for [primaryAuthMessage] */
-    fun setPrimaryAuthMessage(value: BouncerMessageModel?)
-
-    /** Set the value for [faceAcquisitionMessage] */
-    fun setFaceAcquisitionMessage(value: BouncerMessageModel?)
-    /** Set the value for [fingerprintAcquisitionMessage] */
-    fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?)
-
-    /** Set the value for [customMessage] */
-    fun setCustomMessage(value: BouncerMessageModel?)
-
-    /**
-     * Clear any previously set messages for [primaryAuthMessage], [faceAcquisitionMessage],
-     * [fingerprintAcquisitionMessage] & [customMessage]
-     */
-    fun clearMessage()
-}
-
-@SysUISingleton
-class BouncerMessageRepositoryImpl
-@Inject
-constructor(
-    trustRepository: TrustRepository,
-    biometricSettingsRepository: BiometricSettingsRepository,
-    updateMonitor: KeyguardUpdateMonitor,
-    private val bouncerMessageFactory: BouncerMessageFactory,
-    private val userRepository: UserRepository,
-    fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
-) : BouncerMessageRepository {
-
-    private val isFaceEnrolledAndEnabled =
-        and(
-            biometricSettingsRepository.isFaceAuthenticationEnabled,
-            biometricSettingsRepository.isFaceEnrolled
-        )
-
-    private val isFingerprintEnrolledAndEnabled =
-        and(
-            biometricSettingsRepository.isFingerprintEnabledByDevicePolicy,
-            biometricSettingsRepository.isFingerprintEnrolled
-        )
-
-    private val isAnyBiometricsEnabledAndEnrolled =
-        or(isFaceEnrolledAndEnabled, isFingerprintEnrolledAndEnabled)
-
-    private val authFlagsBasedPromptReason: Flow<Int> =
-        combine(
-                biometricSettingsRepository.authenticationFlags,
-                trustRepository.isCurrentUserTrustManaged,
-                isAnyBiometricsEnabledAndEnrolled,
-                ::Triple
-            )
-            .map { (flags, isTrustManaged, biometricsEnrolledAndEnabled) ->
-                val trustOrBiometricsAvailable = (isTrustManaged || biometricsEnrolledAndEnabled)
-                return@map if (
-                    trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterReboot
-                ) {
-                    PROMPT_REASON_RESTART
-                } else if (trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterTimeout) {
-                    PROMPT_REASON_TIMEOUT
-                } else if (flags.isPrimaryAuthRequiredAfterDpmLockdown) {
-                    PROMPT_REASON_DEVICE_ADMIN
-                } else if (isTrustManaged && flags.someAuthRequiredAfterUserRequest) {
-                    PROMPT_REASON_TRUSTAGENT_EXPIRED
-                } else if (isTrustManaged && flags.someAuthRequiredAfterTrustAgentExpired) {
-                    PROMPT_REASON_TRUSTAGENT_EXPIRED
-                } else if (trustOrBiometricsAvailable && flags.isInUserLockdown) {
-                    PROMPT_REASON_USER_REQUEST
-                } else if (
-                    trustOrBiometricsAvailable && flags.primaryAuthRequiredForUnattendedUpdate
-                ) {
-                    PROMPT_REASON_PREPARE_FOR_UPDATE
-                } else if (
-                    trustOrBiometricsAvailable &&
-                        flags.strongerAuthRequiredAfterNonStrongBiometricsTimeout
-                ) {
-                    PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT
-                } else {
-                    PROMPT_REASON_NONE
-                }
-            }
-
-    private val biometricAuthReason: Flow<Int> =
-        conflatedCallbackFlow {
-                val callback =
-                    object : KeyguardUpdateMonitorCallback() {
-                        override fun onBiometricAuthFailed(
-                            biometricSourceType: BiometricSourceType?
-                        ) {
-                            val promptReason =
-                                if (biometricSourceType == FINGERPRINT)
-                                    PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT
-                                else if (
-                                    biometricSourceType == FACE && !updateMonitor.isFaceLockedOut
-                                ) {
-                                    PROMPT_REASON_INCORRECT_FACE_INPUT
-                                } else PROMPT_REASON_NONE
-                            trySendWithFailureLogging(promptReason, TAG, "onBiometricAuthFailed")
-                        }
-
-                        override fun onBiometricsCleared() {
-                            trySendWithFailureLogging(
-                                PROMPT_REASON_NONE,
-                                TAG,
-                                "onBiometricsCleared"
-                            )
-                        }
-
-                        override fun onBiometricAcquired(
-                            biometricSourceType: BiometricSourceType?,
-                            acquireInfo: Int
-                        ) {
-                            trySendWithFailureLogging(
-                                PROMPT_REASON_NONE,
-                                TAG,
-                                "clearBiometricPrompt for new auth session."
-                            )
-                        }
-
-                        override fun onBiometricAuthenticated(
-                            userId: Int,
-                            biometricSourceType: BiometricSourceType?,
-                            isStrongBiometric: Boolean
-                        ) {
-                            trySendWithFailureLogging(
-                                PROMPT_REASON_NONE,
-                                TAG,
-                                "onBiometricAuthenticated"
-                            )
-                        }
-                    }
-                updateMonitor.registerCallback(callback)
-                awaitClose { updateMonitor.removeCallback(callback) }
-            }
-            .distinctUntilChanged()
-
-    private val _primaryAuthMessage = MutableStateFlow<BouncerMessageModel?>(null)
-    override val primaryAuthMessage: Flow<BouncerMessageModel?> = _primaryAuthMessage
-
-    private val _faceAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
-    override val faceAcquisitionMessage: Flow<BouncerMessageModel?> = _faceAcquisitionMessage
-
-    private val _fingerprintAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
-    override val fingerprintAcquisitionMessage: Flow<BouncerMessageModel?> =
-        _fingerprintAcquisitionMessage
-
-    private val _customMessage = MutableStateFlow<BouncerMessageModel?>(null)
-    override val customMessage: Flow<BouncerMessageModel?> = _customMessage
-
-    override val biometricAuthMessage: Flow<BouncerMessageModel?> =
-        biometricAuthReason
-            .map {
-                if (it == PROMPT_REASON_NONE) null
-                else
-                    bouncerMessageFactory.createFromPromptReason(
-                        it,
-                        userRepository.getSelectedUserInfo().id
-                    )
-            }
-            .onStart { emit(null) }
-            .distinctUntilChanged()
-
-    override val authFlagsMessage: Flow<BouncerMessageModel?> =
-        authFlagsBasedPromptReason
-            .map {
-                if (it == PROMPT_REASON_NONE) null
-                else
-                    bouncerMessageFactory.createFromPromptReason(
-                        it,
-                        userRepository.getSelectedUserInfo().id
-                    )
-            }
-            .onStart { emit(null) }
-            .distinctUntilChanged()
-
-    // TODO (b/262838215): Replace with DeviceEntryFaceAuthRepository when the new face auth system
-    // has been launched.
-    private val faceLockedOut: Flow<Boolean> = conflatedCallbackFlow {
-        val callback =
-            object : KeyguardUpdateMonitorCallback() {
-                override fun onLockedOutStateChanged(biometricSourceType: BiometricSourceType?) {
-                    if (biometricSourceType == FACE) {
-                        trySendWithFailureLogging(
-                            updateMonitor.isFaceLockedOut,
-                            TAG,
-                            "face lock out state changed."
-                        )
-                    }
-                }
-            }
-        updateMonitor.registerCallback(callback)
-        trySendWithFailureLogging(updateMonitor.isFaceLockedOut, TAG, "face lockout initial value")
-        awaitClose { updateMonitor.removeCallback(callback) }
-    }
-
-    override val biometricLockedOutMessage: Flow<BouncerMessageModel?> =
-        combine(fingerprintAuthRepository.isLockedOut, faceLockedOut) { fp, face ->
-            return@combine if (fp) {
-                bouncerMessageFactory.createFromPromptReason(
-                    PROMPT_REASON_FINGERPRINT_LOCKED_OUT,
-                    userRepository.getSelectedUserInfo().id
-                )
-            } else if (face) {
-                bouncerMessageFactory.createFromPromptReason(
-                    PROMPT_REASON_FACE_LOCKED_OUT,
-                    userRepository.getSelectedUserInfo().id
-                )
-            } else null
-        }
-
-    override fun setPrimaryAuthMessage(value: BouncerMessageModel?) {
-        _primaryAuthMessage.value = value
-    }
-
-    override fun setFaceAcquisitionMessage(value: BouncerMessageModel?) {
-        _faceAcquisitionMessage.value = value
-    }
-
-    override fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?) {
-        _fingerprintAcquisitionMessage.value = value
-    }
-
-    override fun setCustomMessage(value: BouncerMessageModel?) {
-        _customMessage.value = value
-    }
-
-    override fun clearMessage() {
-        _fingerprintAcquisitionMessage.value = null
-        _faceAcquisitionMessage.value = null
-        _primaryAuthMessage.value = null
-        _customMessage.value = null
-    }
-
-    companion object {
-        const val TAG = "BouncerDetailedMessageRepository"
-    }
-}
-
-private fun and(flow: Flow<Boolean>, anotherFlow: Flow<Boolean>) =
-    flow.combine(anotherFlow) { a, b -> a && b }
-
-private fun or(flow: Flow<Boolean>, anotherFlow: Flow<Boolean>) =
-    flow.combine(anotherFlow) { a, b -> a || b }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageAuditLogger.kt
deleted file mode 100644
index 56f81fc..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageAuditLogger.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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.bouncer.domain.interactor
-
-import android.os.Build
-import android.util.Log
-import com.android.systemui.CoreStartable
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepository
-import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.launch
-
-private val TAG = BouncerMessageAuditLogger::class.simpleName!!
-
-/** Logger that echoes bouncer messages state to logcat in debuggable builds. */
-@SysUISingleton
-class BouncerMessageAuditLogger
-@Inject
-constructor(
-    @Application private val scope: CoroutineScope,
-    private val repository: BouncerMessageRepository,
-    private val interactor: BouncerMessageInteractor,
-) : CoreStartable {
-    override fun start() {
-        if (Build.isDebuggable()) {
-            collectAndLog(repository.biometricAuthMessage, "biometricMessage: ")
-            collectAndLog(repository.primaryAuthMessage, "primaryAuthMessage: ")
-            collectAndLog(repository.customMessage, "customMessage: ")
-            collectAndLog(repository.faceAcquisitionMessage, "faceAcquisitionMessage: ")
-            collectAndLog(
-                repository.fingerprintAcquisitionMessage,
-                "fingerprintAcquisitionMessage: "
-            )
-            collectAndLog(repository.authFlagsMessage, "authFlagsMessage: ")
-            collectAndLog(interactor.bouncerMessage, "interactor.bouncerMessage: ")
-        }
-    }
-
-    private fun collectAndLog(flow: Flow<BouncerMessageModel?>, context: String) {
-        scope.launch { flow.collect { Log.d(TAG, context + it) } }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractor.kt
deleted file mode 100644
index 1754d93..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractor.kt
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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.bouncer.domain.interactor
-
-import android.os.CountDownTimer
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES
-import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory
-import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepository
-import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
-import com.android.systemui.user.data.repository.UserRepository
-import javax.inject.Inject
-import kotlin.math.roundToInt
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-
-@SysUISingleton
-class BouncerMessageInteractor
-@Inject
-constructor(
-    private val repository: BouncerMessageRepository,
-    private val factory: BouncerMessageFactory,
-    private val userRepository: UserRepository,
-    private val countDownTimerUtil: CountDownTimerUtil,
-    private val featureFlags: FeatureFlags,
-) {
-    fun onPrimaryAuthLockedOut(secondsBeforeLockoutReset: Long) {
-        if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
-
-        val callback =
-            object : CountDownTimerCallback {
-                override fun onFinish() {
-                    repository.clearMessage()
-                }
-
-                override fun onTick(millisUntilFinished: Long) {
-                    val secondsRemaining = (millisUntilFinished / 1000.0).roundToInt()
-                    val message =
-                        factory.createFromPromptReason(
-                            reason = PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT,
-                            userId = userRepository.getSelectedUserInfo().id
-                        )
-                    message?.message?.animate = false
-                    message?.message?.formatterArgs =
-                        mutableMapOf<String, Any>(Pair("count", secondsRemaining))
-                    repository.setPrimaryAuthMessage(message)
-                }
-            }
-        countDownTimerUtil.startNewTimer(secondsBeforeLockoutReset * 1000, 1000, callback)
-    }
-
-    fun onPrimaryAuthIncorrectAttempt() {
-        if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
-
-        repository.setPrimaryAuthMessage(
-            factory.createFromPromptReason(
-                PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
-                userRepository.getSelectedUserInfo().id
-            )
-        )
-    }
-
-    fun setFingerprintAcquisitionMessage(value: String?) {
-        if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
-
-        repository.setFingerprintAcquisitionMessage(
-            if (value != null) {
-                factory.createFromString(secondaryMsg = value)
-            } else {
-                null
-            }
-        )
-    }
-
-    fun setFaceAcquisitionMessage(value: String?) {
-        if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
-
-        repository.setFaceAcquisitionMessage(
-            if (value != null) {
-                factory.createFromString(secondaryMsg = value)
-            } else {
-                null
-            }
-        )
-    }
-
-    fun setCustomMessage(value: String?) {
-        if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
-
-        repository.setCustomMessage(
-            if (value != null) {
-                factory.createFromString(secondaryMsg = value)
-            } else {
-                null
-            }
-        )
-    }
-
-    fun onPrimaryBouncerUserInput() {
-        if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
-
-        repository.clearMessage()
-    }
-
-    fun onBouncerBeingHidden() {
-        if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
-
-        repository.clearMessage()
-    }
-
-    private fun firstNonNullMessage(
-        oneMessageModel: Flow<BouncerMessageModel?>,
-        anotherMessageModel: Flow<BouncerMessageModel?>
-    ): Flow<BouncerMessageModel?> {
-        return oneMessageModel.combine(anotherMessageModel) { a, b -> a ?: b }
-    }
-
-    // Null if feature flag is enabled which gets ignored always or empty bouncer message model that
-    // always maps to an empty string.
-    private fun nullOrEmptyMessage() =
-        flowOf(
-            if (featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) null
-            else factory.createFromString("", "")
-        )
-
-    val bouncerMessage =
-        listOf(
-                nullOrEmptyMessage(),
-                repository.primaryAuthMessage,
-                repository.biometricAuthMessage,
-                repository.fingerprintAcquisitionMessage,
-                repository.faceAcquisitionMessage,
-                repository.customMessage,
-                repository.authFlagsMessage,
-                repository.biometricLockedOutMessage,
-                userRepository.selectedUserInfo.map {
-                    factory.createFromPromptReason(PROMPT_REASON_DEFAULT, it.id)
-                },
-            )
-            .reduce(::firstNonNullMessage)
-            .distinctUntilChanged()
-}
-
-interface CountDownTimerCallback {
-    fun onFinish()
-    fun onTick(millisUntilFinished: Long)
-}
-
-@SysUISingleton
-open class CountDownTimerUtil @Inject constructor() {
-
-    /**
-     * Start a new count down timer that runs for [millisInFuture] with a tick every
-     * [millisInterval]
-     */
-    fun startNewTimer(
-        millisInFuture: Long,
-        millisInterval: Long,
-        callback: CountDownTimerCallback,
-    ): CountDownTimer {
-        return object : CountDownTimer(millisInFuture, millisInterval) {
-                override fun onFinish() = callback.onFinish()
-
-                override fun onTick(millisUntilFinished: Long) =
-                    callback.onTick(millisUntilFinished)
-            }
-            .start()
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/shared/model/BouncerMessageModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/shared/model/BouncerMessageModel.kt
deleted file mode 100644
index 46e8873..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/shared/model/BouncerMessageModel.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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.bouncer.shared.model
-
-import android.content.res.ColorStateList
-
-/**
- * Represents the message displayed on the bouncer. It has two parts, primary and a secondary
- * message
- */
-data class BouncerMessageModel(val message: Message? = null, val secondaryMessage: Message? = null)
-
-/**
- * Representation of a single message on the bouncer. It can be either a string or a string resource
- * ID
- */
-data class Message(
-    val message: String? = null,
-    val messageResId: Int? = null,
-    val colorState: ColorStateList? = null,
-    /** Any plural formatter arguments that can used to format the [messageResId] */
-    var formatterArgs: Map<String, Any>? = null,
-    /** Specifies whether this text should be animated when it is shown. */
-    var animate: Boolean = true,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/ui/BouncerMessageView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/ui/BouncerMessageView.kt
deleted file mode 100644
index 4dc52ff..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/ui/BouncerMessageView.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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.bouncer.ui
-
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.LinearLayout
-import com.android.keyguard.BouncerKeyguardMessageArea
-import com.android.keyguard.KeyguardMessageArea
-import com.android.keyguard.KeyguardMessageAreaController
-import com.android.systemui.R
-
-class BouncerMessageView : LinearLayout {
-    constructor(context: Context?) : super(context)
-
-    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
-
-    init {
-        inflate(context, R.layout.bouncer_message_view, this)
-    }
-
-    var primaryMessageView: BouncerKeyguardMessageArea? = null
-    var secondaryMessageView: BouncerKeyguardMessageArea? = null
-    var primaryMessage: KeyguardMessageAreaController<KeyguardMessageArea>? = null
-    var secondaryMessage: KeyguardMessageAreaController<KeyguardMessageArea>? = null
-    override fun onFinishInflate() {
-        super.onFinishInflate()
-        primaryMessageView = findViewById(R.id.bouncer_primary_message_area)
-        secondaryMessageView = findViewById(R.id.bouncer_secondary_message_area)
-        primaryMessageView?.disable()
-        secondaryMessageView?.disable()
-    }
-
-    fun init(factory: KeyguardMessageAreaController.Factory) {
-        primaryMessage = factory.create(primaryMessageView)
-        primaryMessage?.init()
-        secondaryMessage = factory.create(secondaryMessageView)
-        secondaryMessage?.init()
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardFaceAuthNotSupportedModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardFaceAuthNotSupportedModule.kt
index a44df7e..d8eb81c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardFaceAuthNotSupportedModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardFaceAuthNotSupportedModule.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.keyguard.dagger
 
+import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.NoopDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
 import com.android.systemui.keyguard.domain.interactor.NoopKeyguardFaceAuthInteractor
 import dagger.Binds
@@ -32,4 +34,9 @@
 interface KeyguardFaceAuthNotSupportedModule {
     @Binds
     fun keyguardFaceAuthInteractor(impl: NoopKeyguardFaceAuthInteractor): KeyguardFaceAuthInteractor
+
+    @Binds
+    fun deviceEntryFaceAuthRepository(
+        impl: NoopDeviceEntryFaceAuthRepository
+    ): DeviceEntryFaceAuthRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 1f121e9..61bacbda 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -67,6 +67,9 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.util.DeviceConfigProxy;
+import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.settings.SystemSettings;
+import com.android.systemui.util.time.SystemClock;
 import com.android.wm.shell.keyguard.KeyguardTransitions;
 
 import dagger.Lazy;
@@ -134,6 +137,9 @@
             Lazy<ActivityLaunchAnimator> activityLaunchAnimator,
             Lazy<ScrimController> scrimControllerLazy,
             FeatureFlags featureFlags,
+            SecureSettings secureSettings,
+            SystemSettings systemSettings,
+            SystemClock systemClock,
             @Main CoroutineDispatcher mainDispatcher,
             Lazy<DreamingToLockscreenTransitionViewModel> dreamingToLockscreenTransitionViewModel,
             SystemPropertiesHelper systemPropertiesHelper) {
@@ -171,6 +177,9 @@
                 activityLaunchAnimator,
                 scrimControllerLazy,
                 featureFlags,
+                secureSettings,
+                systemSettings,
+                systemClock,
                 mainDispatcher,
                 dreamingToLockscreenTransitionViewModel,
                 systemPropertiesHelper);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
deleted file mode 100644
index 871a3ff..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
+++ /dev/null
@@ -1,59 +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.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
-import javax.inject.Inject
-
-/** An abstraction to interface with the ui layer, without changing state. */
-interface BouncerView {
-    var delegate: BouncerViewDelegate?
-}
-
-/** A lightweight class to hold reference to the ui delegate. */
-@SysUISingleton
-class BouncerViewImpl @Inject constructor() : BouncerView {
-    private var _delegate: WeakReference<BouncerViewDelegate?> = WeakReference(null)
-    override var delegate: BouncerViewDelegate?
-        get() = _delegate.get()
-        set(value) {
-            _delegate = WeakReference(value)
-        }
-}
-
-/** An abstraction that implements view logic. */
-interface BouncerViewDelegate {
-    fun isFullScreenBouncer(): Boolean
-    fun shouldDismissOnMenuPressed(): Boolean
-    fun interceptMediaKey(event: KeyEvent?): Boolean
-    fun dispatchBackKeyEventPreIme(): Boolean
-    fun showNextSecurityScreenOrFinish(): Boolean
-    fun resume()
-    fun setDismissAction(
-        onDismissAction: ActivityStarter.OnDismissAction?,
-        cancelAction: Runnable?,
-    )
-    fun willDismissWithActions(): Boolean
-    fun willRunDismissFromKeyguard(): Boolean
-    /** @return the {@link OnBackAnimationCallback} to animate Bouncer during a back gesture. */
-    fun getBackCallback(): OnBackAnimationCallback
-    fun showPromptReason(reason: Int)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerViewModule.kt
deleted file mode 100644
index 390c54e..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerViewModule.kt
+++ /dev/null
@@ -1,26 +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.data
-
-import dagger.Binds
-import dagger.Module
-
-@Module
-interface BouncerViewModule {
-    /** Binds BouncerView to BouncerViewImpl and makes it injectable. */
-    @Binds fun bindBouncerView(bouncerViewImpl: BouncerViewImpl): BouncerView
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
index 9621f03..3d8f6fd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -26,6 +26,7 @@
 import com.android.keyguard.FaceAuthUiEvent
 import com.android.systemui.Dumpable
 import com.android.systemui.R
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
@@ -34,7 +35,6 @@
 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.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.AcquiredAuthenticationStatus
@@ -254,13 +254,17 @@
 
     private fun observeFaceDetectGatingChecks() {
         // Face detection can run only when lockscreen bypass is enabled
-        // & detection is supported & biometric unlock is not allowed.
+        // & detection is supported
+        //   & biometric unlock is not allowed
+        //     or user is trusted by trust manager & we want to run face detect to dismiss keyguard
         listOf(
                 canFaceAuthOrDetectRun(faceDetectLog),
                 logAndObserve(isBypassEnabled, "isBypassEnabled", faceDetectLog),
                 logAndObserve(
-                    biometricSettingsRepository.isNonStrongBiometricAllowed.isFalse(),
-                    "nonStrongBiometricIsNotAllowed",
+                    biometricSettingsRepository.isNonStrongBiometricAllowed
+                        .isFalse()
+                        .or(trustRepository.isCurrentUserTrusted),
+                    "nonStrongBiometricIsNotAllowedOrCurrentUserIsTrusted",
                     faceDetectLog
                 ),
                 // We don't want to run face detect if fingerprint can be used to unlock the device
@@ -312,18 +316,19 @@
                     tableLogBuffer
                 ),
                 logAndObserve(
-                    keyguardRepository.wakefulness.map { it.isStartingToSleepOrAsleep() }.isFalse(),
-                    "deviceNotSleepingOrNotStartingToSleep",
+                    keyguardRepository.wakefulness.map { it.isStartingToSleep() }.isFalse(),
+                    "deviceNotStartingToSleep",
                     tableLogBuffer
                 ),
                 logAndObserve(
-                    combine(
-                        keyguardInteractor.isSecureCameraActive,
-                        alternateBouncerInteractor.isVisible
-                    ) { a, b ->
-                        !a || b
-                    },
-                    "secureCameraNotActiveOrAltBouncerIsShowing",
+                    keyguardInteractor.isSecureCameraActive
+                        .isFalse()
+                        .or(
+                            alternateBouncerInteractor.isVisible.or(
+                                keyguardInteractor.primaryBouncerShowing
+                            )
+                        ),
+                    "secureCameraNotActiveOrAnyBouncerIsShowing",
                     tableLogBuffer
                 ),
                 logAndObserve(
@@ -335,6 +340,11 @@
                     biometricSettingsRepository.isCurrentUserInLockdown.isFalse(),
                     "userHasNotLockedDownDevice",
                     tableLogBuffer
+                ),
+                logAndObserve(
+                    keyguardRepository.isKeyguardShowing,
+                    "isKeyguardShowing",
+                    tableLogBuffer
                 )
             )
             .reduce(::and)
@@ -360,6 +370,7 @@
                     "nonStrongBiometricIsAllowed",
                     faceAuthLog
                 ),
+                logAndObserve(isAuthenticated.isFalse(), "faceNotAuthenticated", faceAuthLog),
             )
             .reduce(::and)
             .distinctUntilChanged()
@@ -598,7 +609,7 @@
         const val DEFAULT_CANCEL_SIGNAL_TIMEOUT = 3000L
 
         /** Number of allowed retries whenever there is a face hardware error */
-        const val HAL_ERROR_RETRY_MAX = 20
+        const val HAL_ERROR_RETRY_MAX = 5
 
         /** Timeout before retries whenever there is a HAL error. */
         const val HAL_ERROR_RETRY_TIMEOUT = 500L // ms
@@ -634,6 +645,10 @@
 private fun and(flow: Flow<Boolean>, anotherFlow: Flow<Boolean>) =
     flow.combine(anotherFlow) { a, b -> a && b }
 
+/** Combine two boolean flows by or-ing both of them */
+private fun Flow<Boolean>.or(anotherFlow: Flow<Boolean>) =
+    this.combine(anotherFlow) { a, b -> a || b }
+
 /** "Not" the given flow. The return [Flow] will be true when [this] flow is false. */
 private fun Flow<Boolean>.isFalse(): Flow<Boolean> {
     return this.map { !it }
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
deleted file mode 100644
index 5d15e69..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
+++ /dev/null
@@ -1,273 +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.data.repository
-
-import android.os.Build
-import android.util.Log
-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.log.dagger.BouncerTableLog
-import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.log.table.logDiffsForTable
-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
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-
-/**
- * Encapsulates app state for the lock screen primary and alternate bouncer.
- *
- * Make sure to add newly added flows to the logger.
- */
-interface KeyguardBouncerRepository {
-    /** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
-    val primaryBouncerShow: StateFlow<Boolean>
-    val primaryBouncerShowingSoon: StateFlow<Boolean>
-    val primaryBouncerStartingToHide: StateFlow<Boolean>
-    val primaryBouncerStartingDisappearAnimation: StateFlow<Runnable?>
-    /** Determines if we want to instantaneously show the primary bouncer instead of translating. */
-    val primaryBouncerScrimmed: StateFlow<Boolean>
-    /**
-     * 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
-     * ```
-     */
-    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 alternateBouncerVisible: StateFlow<Boolean>
-    val alternateBouncerUIAvailable: StateFlow<Boolean>
-    val sideFpsShowing: StateFlow<Boolean>
-
-    var lastAlternateBouncerVisibleTime: Long
-
-    fun setPrimaryScrimmed(isScrimmed: Boolean)
-
-    fun setPrimaryShow(isShowing: Boolean)
-
-    fun setPrimaryShowingSoon(showingSoon: 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 clock: SystemClock,
-    @Application private val applicationScope: CoroutineScope,
-    @BouncerTableLog private val buffer: TableLogBuffer,
-) : KeyguardBouncerRepository {
-    /** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
-    private val _primaryBouncerShow = MutableStateFlow(false)
-    override val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
-    private val _primaryBouncerShowingSoon = MutableStateFlow(false)
-    override val primaryBouncerShowingSoon = _primaryBouncerShowingSoon.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)
-    override val keyguardPosition = _keyguardPosition.asStateFlow()
-    private val _isBackButtonEnabled = MutableStateFlow<Boolean?>(null)
-    override val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
-    private val _keyguardAuthenticated = MutableStateFlow<Boolean?>(null)
-    /** Determines if user is already unlocked */
-    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()
-    /** 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()
-    }
-
-    override fun setPrimaryScrimmed(isScrimmed: Boolean) {
-        _primaryBouncerScrimmed.value = isScrimmed
-    }
-
-    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(isShowing: Boolean) {
-        _primaryBouncerShow.value = isShowing
-    }
-
-    override fun setPrimaryShowingSoon(showingSoon: Boolean) {
-        _primaryBouncerShowingSoon.value = showingSoon
-    }
-
-    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
-    }
-
-    /** Sets up logs for state flows. */
-    private fun setUpLogging() {
-        if (!Build.IS_DEBUGGABLE) {
-            return
-        }
-
-        primaryBouncerShow
-            .logDiffsForTable(buffer, "", "PrimaryBouncerShow", false)
-            .onEach { Log.d(TAG, "Keyguard Bouncer is ${if (it) "showing" else "hiding."}") }
-            .launchIn(applicationScope)
-        primaryBouncerShowingSoon
-            .logDiffsForTable(buffer, "", "PrimaryBouncerShowingSoon", false)
-            .launchIn(applicationScope)
-        primaryBouncerStartingToHide
-            .logDiffsForTable(buffer, "", "PrimaryBouncerStartingToHide", false)
-            .launchIn(applicationScope)
-        primaryBouncerStartingDisappearAnimation
-            .map { it != null }
-            .logDiffsForTable(buffer, "", "PrimaryBouncerStartingDisappearAnimation", false)
-            .launchIn(applicationScope)
-        primaryBouncerScrimmed
-            .logDiffsForTable(buffer, "", "PrimaryBouncerScrimmed", false)
-            .launchIn(applicationScope)
-        panelExpansionAmount
-            .map { (it * 1000).toInt() }
-            .logDiffsForTable(buffer, "", "PanelExpansionAmountMillis", -1)
-            .launchIn(applicationScope)
-        keyguardPosition
-            .map { it.toInt() }
-            .logDiffsForTable(buffer, "", "KeyguardPosition", -1)
-            .launchIn(applicationScope)
-        isBackButtonEnabled
-            .filterNotNull()
-            .logDiffsForTable(buffer, "", "IsBackButtonEnabled", false)
-            .launchIn(applicationScope)
-        showMessage
-            .map { it?.message }
-            .logDiffsForTable(buffer, "", "ShowMessage", null)
-            .launchIn(applicationScope)
-        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
-        private const val TAG = "KeyguardBouncerRepositoryImpl"
-    }
-}
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 742e535..edc0b45 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
@@ -25,6 +25,7 @@
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 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.dagger.qualifiers.Main
 import com.android.systemui.doze.DozeMachine
 import com.android.systemui.doze.DozeTransitionCallback
@@ -44,13 +45,16 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController
 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.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.stateIn
 
 /** Defines interface for classes that encapsulate application state for the keyguard. */
 interface KeyguardRepository {
@@ -138,7 +142,7 @@
     val statusBarState: Flow<StatusBarState>
 
     /** Observable for device wake/sleep state */
-    val wakefulness: Flow<WakefulnessModel>
+    val wakefulness: StateFlow<WakefulnessModel>
 
     /** Observable for biometric unlock modes */
     val biometricUnlockState: Flow<BiometricUnlockModel>
@@ -202,7 +206,8 @@
     private val dozeParameters: DozeParameters,
     private val authController: AuthController,
     private val dreamOverlayCallbackController: DreamOverlayCallbackController,
-    @Main private val mainDispatcher: CoroutineDispatcher
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    @Application private val scope: CoroutineScope,
 ) : KeyguardRepository {
     private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
     override val animateBottomAreaDozingTransitions =
@@ -297,7 +302,15 @@
                             trySendWithFailureLogging(
                                 keyguardStateController.isUnlocked,
                                 TAG,
-                                "updated isKeyguardUnlocked"
+                                "updated isKeyguardUnlocked due to onUnlockedChanged"
+                            )
+                        }
+
+                        override fun onKeyguardShowingChanged() {
+                            trySendWithFailureLogging(
+                                keyguardStateController.isUnlocked,
+                                TAG,
+                                "updated isKeyguardUnlocked due to onKeyguardShowingChanged"
                             )
                         }
                     }
@@ -486,47 +499,48 @@
         awaitClose { biometricUnlockController.removeListener(callback) }
     }
 
-    override val wakefulness: Flow<WakefulnessModel> = conflatedCallbackFlow {
-        val observer =
-            object : WakefulnessLifecycle.Observer {
-                override fun onStartedWakingUp() {
-                    dispatchNewState()
-                }
+    override val wakefulness: StateFlow<WakefulnessModel> =
+        conflatedCallbackFlow {
+                val observer =
+                    object : WakefulnessLifecycle.Observer {
+                        override fun onStartedWakingUp() {
+                            dispatchNewState()
+                        }
 
-                override fun onFinishedWakingUp() {
-                    dispatchNewState()
-                }
+                        override fun onFinishedWakingUp() {
+                            dispatchNewState()
+                        }
 
-                override fun onPostFinishedWakingUp() {
-                    dispatchNewState()
-                }
+                        override fun onPostFinishedWakingUp() {
+                            dispatchNewState()
+                        }
 
-                override fun onStartedGoingToSleep() {
-                    dispatchNewState()
-                }
+                        override fun onStartedGoingToSleep() {
+                            dispatchNewState()
+                        }
 
-                override fun onFinishedGoingToSleep() {
-                    dispatchNewState()
-                }
+                        override fun onFinishedGoingToSleep() {
+                            dispatchNewState()
+                        }
 
-                private fun dispatchNewState() {
-                    trySendWithFailureLogging(
-                        WakefulnessModel.fromWakefulnessLifecycle(wakefulnessLifecycle),
-                        TAG,
-                        "updated wakefulness state"
-                    )
-                }
+                        private fun dispatchNewState() {
+                            trySendWithFailureLogging(
+                                WakefulnessModel.fromWakefulnessLifecycle(wakefulnessLifecycle),
+                                TAG,
+                                "updated wakefulness state",
+                            )
+                        }
+                    }
+
+                wakefulnessLifecycle.addObserver(observer)
+                awaitClose { wakefulnessLifecycle.removeObserver(observer) }
             }
-
-        wakefulnessLifecycle.addObserver(observer)
-        trySendWithFailureLogging(
-            WakefulnessModel.fromWakefulnessLifecycle(wakefulnessLifecycle),
-            TAG,
-            "initial wakefulness state"
-        )
-
-        awaitClose { wakefulnessLifecycle.removeObserver(observer) }
-    }
+            .stateIn(
+                scope,
+                // Use Eagerly so that we're always listening and never miss an event.
+                SharingStarted.Eagerly,
+                initialValue = WakefulnessModel.fromWakefulnessLifecycle(wakefulnessLifecycle),
+            )
 
     override val fingerprintSensorLocation: Flow<Point?> = conflatedCallbackFlow {
         fun sendFpLocation() {
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 4055fd0..246ee33 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
@@ -17,9 +17,11 @@
 package com.android.systemui.keyguard.data.repository
 
 import com.android.systemui.CoreStartable
-import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepository
-import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepositoryImpl
-import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageAuditLogger
+import com.android.systemui.bouncer.data.repository.BouncerMessageRepository
+import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl
+import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
+import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepositoryImpl
+import com.android.systemui.bouncer.domain.interactor.BouncerMessageAuditLogger
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.ClassKey
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 358ab01..84cd3ef 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
@@ -47,6 +47,11 @@
  * To create or modify logic that controls when and how transitions get created, look at
  * [TransitionInteractor]. These interactors will call [startTransition] and [updateTransition] on
  * this repository.
+ *
+ * To print all transitions to logcat to help with debugging, run this command:
+ * adb shell settings put global systemui/buffer/KeyguardLog VERBOSE
+ *
+ * This will print all keyguard transitions to logcat with the KeyguardTransitionAuditLogger tag.
  */
 interface KeyguardTransitionRepository {
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
new file mode 100644
index 0000000..abe59b7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.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.keyguard.data.repository
+
+import com.android.keyguard.FaceAuthUiEvent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.AuthenticationStatus
+import com.android.systemui.keyguard.shared.model.DetectionStatus
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.emptyFlow
+
+/**
+ * Implementation of the repository that noops all face auth operations.
+ *
+ * This is required for SystemUI variants that do not support face authentication but still inject
+ * other SysUI components that depend on [DeviceEntryFaceAuthRepository].
+ */
+@SysUISingleton
+class NoopDeviceEntryFaceAuthRepository @Inject constructor() : DeviceEntryFaceAuthRepository {
+    override val isAuthenticated: Flow<Boolean>
+        get() = emptyFlow()
+
+    private val _canRunFaceAuth = MutableStateFlow(false)
+    override val canRunFaceAuth: StateFlow<Boolean> = _canRunFaceAuth
+
+    override val authenticationStatus: Flow<AuthenticationStatus>
+        get() = emptyFlow()
+
+    override val detectionStatus: Flow<DetectionStatus>
+        get() = emptyFlow()
+
+    private val _isLockedOut = MutableStateFlow(false)
+    override val isLockedOut: StateFlow<Boolean> = _isLockedOut
+
+    private val _isAuthRunning = MutableStateFlow(false)
+    override val isAuthRunning: StateFlow<Boolean> = _isAuthRunning
+
+    override val isBypassEnabled: Flow<Boolean>
+        get() = emptyFlow()
+
+    /**
+     * 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]
+     *
+     * Run only face detection when [fallbackToDetection] is true and [canRunFaceAuth] is false.
+     */
+    override suspend fun authenticate(uiEvent: FaceAuthUiEvent, fallbackToDetection: Boolean) {}
+
+    /** Stop currently running face authentication or detection. */
+    override fun cancel() {}
+}
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
deleted file mode 100644
index 148d425..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.domain.interactor
-
-import com.android.systemui.dagger.SysUISingleton
-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.plugins.statusbar.StatusBarStateController
-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 statusBarStateController: StatusBarStateController,
-    private val keyguardStateController: KeyguardStateController,
-    private val bouncerRepository: KeyguardBouncerRepository,
-    private val biometricSettingsRepository: BiometricSettingsRepository,
-    private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
-    private val systemClock: SystemClock,
-) {
-    var receivedDownTouch = false
-    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 {
-        bouncerRepository.setAlternateVisible(canShowAlternateBouncerForFingerprint())
-        return isVisibleState()
-    }
-
-    /**
-     * 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 {
-        receivedDownTouch = false
-        val wasAlternateBouncerVisible = isVisibleState()
-        bouncerRepository.setAlternateVisible(false)
-        return wasAlternateBouncerVisible && !isVisibleState()
-    }
-
-    fun isVisibleState(): Boolean {
-        return bouncerRepository.alternateBouncerVisible.value
-    }
-
-    fun setAlternateBouncerUIAvailable(isAvailable: Boolean) {
-        bouncerRepository.setAlternateBouncerUIAvailable(isAvailable)
-    }
-
-    fun canShowAlternateBouncerForFingerprint(): Boolean {
-        return bouncerRepository.alternateBouncerUIAvailable.value &&
-            biometricSettingsRepository.isFingerprintEnrolled.value &&
-            biometricSettingsRepository.isStrongBiometricAllowed.value &&
-            biometricSettingsRepository.isFingerprintEnabledByDevicePolicy.value &&
-            !deviceEntryFingerprintAuthRepository.isLockedOut.value &&
-            !keyguardStateController.isUnlocked &&
-            !statusBarStateController.isDozing
-    }
-
-    /**
-     * 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 (systemClock.uptimeMillis() - bouncerRepository.lastAlternateBouncerVisibleTime) >
-            MIN_VISIBILITY_DURATION_UNTIL_TOUCHES_DISMISS_ALTERNATE_BOUNCER_MS
-    }
-    /**
-     * Should only be called through StatusBarKeyguardViewManager which propagates the source of
-     * truth to other concerned controllers. Will hide the alternate bouncer if it's no longer
-     * allowed to show.
-     *
-     * @return true if the alternate bouncer was newly hidden, else false.
-     */
-    fun maybeHide(): Boolean {
-        if (isVisibleState() && !canShowAlternateBouncerForFingerprint()) {
-            return hide()
-        }
-        return false
-    }
-
-    companion object {
-        private const val MIN_VISIBILITY_DURATION_UNTIL_TOUCHES_DISMISS_ALTERNATE_BOUNCER_MS = 200L
-    }
-}
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
index 38eacce..8f0b91b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
@@ -22,7 +22,6 @@
 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.Utils.Companion.toQuad
 import com.android.systemui.util.kotlin.Utils.Companion.toQuint
@@ -38,11 +37,14 @@
 class FromAlternateBouncerTransitionInteractor
 @Inject
 constructor(
+    override val transitionRepository: KeyguardTransitionRepository,
+    override val transitionInteractor: KeyguardTransitionInteractor,
     @Application private val scope: CoroutineScope,
     private val keyguardInteractor: KeyguardInteractor,
-    private val keyguardTransitionRepository: KeyguardTransitionRepository,
-    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(FromAlternateBouncerTransitionInteractor::class.simpleName!!) {
+) :
+    TransitionInteractor(
+        fromState = KeyguardState.ALTERNATE_BOUNCER,
+    ) {
 
     override fun start() {
         listenForAlternateBouncerToGone()
@@ -60,7 +62,7 @@
                 .sample(
                     combine(
                         keyguardInteractor.primaryBouncerShowing,
-                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        transitionInteractor.startedKeyguardTransitionStep,
                         keyguardInteractor.wakefulnessModel,
                         keyguardInteractor.isAodAvailable,
                         ::toQuad
@@ -92,14 +94,7 @@
                             } else {
                                 KeyguardState.LOCKSCREEN
                             }
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                ownerName = name,
-                                from = KeyguardState.ALTERNATE_BOUNCER,
-                                to = to,
-                                animator = getAnimator(),
-                            )
-                        )
+                        startTransitionTo(to)
                     }
                 }
         }
@@ -108,17 +103,10 @@
     private fun listenForAlternateBouncerToGone() {
         scope.launch {
             keyguardInteractor.isKeyguardGoingAway
-                .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
+                .sample(transitionInteractor.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(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.GONE)
                     }
                 }
         }
@@ -127,26 +115,19 @@
     private fun listenForAlternateBouncerToPrimaryBouncer() {
         scope.launch {
             keyguardInteractor.primaryBouncerShowing
-                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(transitionInteractor.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(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.PRIMARY_BOUNCER)
                     }
                 }
         }
     }
 
-    private fun getAnimator(): ValueAnimator {
+    override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
         return ValueAnimator().apply {
             interpolator = Interpolators.LINEAR
             duration = TRANSITION_DURATION_MS
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index 7e9cbc1..2085c87 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -24,7 +24,6 @@
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionInfo
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -34,11 +33,14 @@
 class FromAodTransitionInteractor
 @Inject
 constructor(
+    override val transitionRepository: KeyguardTransitionRepository,
+    override val transitionInteractor: KeyguardTransitionInteractor,
     @Application private val scope: CoroutineScope,
     private val keyguardInteractor: KeyguardInteractor,
-    private val keyguardTransitionRepository: KeyguardTransitionRepository,
-    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(FromAodTransitionInteractor::class.simpleName!!) {
+) :
+    TransitionInteractor(
+        fromState = KeyguardState.AOD,
+    ) {
 
     override fun start() {
         listenForAodToLockscreen()
@@ -49,18 +51,11 @@
         scope.launch {
             keyguardInteractor
                 .dozeTransitionTo(DozeStateModel.FINISH)
-                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
                 .collect { pair ->
                     val (dozeToAod, lastStartedStep) = pair
                     if (lastStartedStep.to == KeyguardState.AOD) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.AOD,
-                                KeyguardState.LOCKSCREEN,
-                                getAnimator(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.LOCKSCREEN)
                     }
                 }
         }
@@ -69,29 +64,22 @@
     private fun listenForAodToGone() {
         scope.launch {
             keyguardInteractor.biometricUnlockState
-                .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
+                .sample(transitionInteractor.finishedKeyguardState, ::Pair)
                 .collect { pair ->
                     val (biometricUnlockState, keyguardState) = pair
                     if (
                         keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState)
                     ) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.AOD,
-                                KeyguardState.GONE,
-                                getAnimator(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.GONE)
                     }
                 }
         }
     }
 
-    private fun getAnimator(): ValueAnimator {
+    override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
         return ValueAnimator().apply {
-            setInterpolator(Interpolators.LINEAR)
-            setDuration(TRANSITION_DURATION_MS)
+            interpolator = Interpolators.LINEAR
+            duration = TRANSITION_DURATION_MS
         }
     }
 
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 ee2c2df..c867c43 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
@@ -23,10 +23,8 @@
 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.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionInfo
 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.launch
@@ -35,11 +33,14 @@
 class FromDozingTransitionInteractor
 @Inject
 constructor(
+    override val transitionRepository: KeyguardTransitionRepository,
+    override val transitionInteractor: KeyguardTransitionInteractor,
     @Application private val scope: CoroutineScope,
     private val keyguardInteractor: KeyguardInteractor,
-    private val keyguardTransitionRepository: KeyguardTransitionRepository,
-    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(FromDozingTransitionInteractor::class.simpleName!!) {
+) :
+    TransitionInteractor(
+        fromState = KeyguardState.DOZING,
+    ) {
 
     override fun start() {
         listenForDozingToLockscreen()
@@ -49,20 +50,13 @@
     private fun listenForDozingToLockscreen() {
         scope.launch {
             keyguardInteractor.wakefulnessModel
-                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
                 .collect { (wakefulnessModel, lastStartedTransition) ->
                     if (
                         wakefulnessModel.isStartingToWakeOrAwake() &&
                             lastStartedTransition.to == KeyguardState.DOZING
                     ) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.DOZING,
-                                KeyguardState.LOCKSCREEN,
-                                getAnimator(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.LOCKSCREEN)
                     }
                 }
         }
@@ -71,29 +65,22 @@
     private fun listenForDozingToGone() {
         scope.launch {
             keyguardInteractor.biometricUnlockState
-                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
                 .collect { (biometricUnlockState, lastStartedTransition) ->
                     if (
                         lastStartedTransition.to == KeyguardState.DOZING &&
                             isWakeAndUnlock(biometricUnlockState)
                     ) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.DOZING,
-                                KeyguardState.GONE,
-                                getAnimator(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.GONE)
                     }
                 }
         }
     }
 
-    private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
+    override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
         return ValueAnimator().apply {
-            setInterpolator(Interpolators.LINEAR)
-            setDuration(duration.inWholeMilliseconds)
+            interpolator = Interpolators.LINEAR
+            duration = DEFAULT_DURATION.inWholeMilliseconds
         }
     }
 
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 ccf4bc1..98d7434 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
@@ -24,11 +24,9 @@
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionInfo
 import com.android.systemui.util.kotlin.Utils.Companion.toTriple
 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.delay
@@ -40,11 +38,14 @@
 class FromDreamingTransitionInteractor
 @Inject
 constructor(
+    override val transitionRepository: KeyguardTransitionRepository,
+    override val transitionInteractor: KeyguardTransitionInteractor,
     @Application private val scope: CoroutineScope,
     private val keyguardInteractor: KeyguardInteractor,
-    private val keyguardTransitionRepository: KeyguardTransitionRepository,
-    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(FromDreamingTransitionInteractor::class.simpleName!!) {
+) :
+    TransitionInteractor(
+        fromState = KeyguardState.DREAMING,
+    ) {
 
     override fun start() {
         listenForDreamingToOccluded()
@@ -54,15 +55,8 @@
 
     fun startToLockscreenTransition() {
         scope.launch {
-            if (keyguardTransitionInteractor.startedKeyguardState.value == KeyguardState.DREAMING) {
-                keyguardTransitionRepository.startTransition(
-                    TransitionInfo(
-                        name,
-                        KeyguardState.DREAMING,
-                        KeyguardState.LOCKSCREEN,
-                        getAnimator(TO_LOCKSCREEN_DURATION),
-                    )
-                )
+            if (transitionInteractor.startedKeyguardState.value == KeyguardState.DREAMING) {
+                startTransitionTo(KeyguardState.LOCKSCREEN)
             }
         }
     }
@@ -76,7 +70,7 @@
                 .sample(
                     combine(
                         keyguardInteractor.isKeyguardOccluded,
-                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        transitionInteractor.startedKeyguardTransitionStep,
                         ::Pair,
                     ),
                     ::toTriple
@@ -92,14 +86,7 @@
                         // action. There's no great signal to determine when the dream is ending
                         // and a transition to OCCLUDED is beginning directly. For now, the solution
                         // is DREAMING->LOCKSCREEN->OCCLUDED
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                lastStartedTransition.to,
-                                KeyguardState.OCCLUDED,
-                                getAnimator(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.OCCLUDED)
                     }
                 }
         }
@@ -109,14 +96,7 @@
         scope.launch {
             keyguardInteractor.biometricUnlockState.collect { biometricUnlockState ->
                 if (biometricUnlockState == BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM) {
-                    keyguardTransitionRepository.startTransition(
-                        TransitionInfo(
-                            name,
-                            KeyguardState.DREAMING,
-                            KeyguardState.GONE,
-                            getAnimator(),
-                        )
-                    )
+                    startTransitionTo(KeyguardState.GONE)
                 }
             }
         }
@@ -126,7 +106,7 @@
         scope.launch {
             combine(
                     keyguardInteractor.dozeTransitionModel,
-                    keyguardTransitionInteractor.finishedKeyguardState,
+                    transitionInteractor.finishedKeyguardState,
                     ::Pair
                 )
                 .collect { (dozeTransitionModel, keyguardState) ->
@@ -134,23 +114,18 @@
                         dozeTransitionModel.to == DozeStateModel.DOZE &&
                             keyguardState == KeyguardState.DREAMING
                     ) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.DREAMING,
-                                KeyguardState.DOZING,
-                                getAnimator(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.DOZING)
                     }
                 }
         }
     }
 
-    private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
+    override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
         return ValueAnimator().apply {
-            setInterpolator(Interpolators.LINEAR)
-            setDuration(duration.inWholeMilliseconds)
+            interpolator = Interpolators.LINEAR
+            duration =
+                if (toState == KeyguardState.LOCKSCREEN) TO_LOCKSCREEN_DURATION.inWholeMilliseconds
+                else DEFAULT_DURATION.inWholeMilliseconds
         }
     }
 
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 cfcb654..f82633f 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
@@ -22,12 +22,10 @@
 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.Utils.Companion.toTriple
 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
@@ -37,11 +35,14 @@
 class FromGoneTransitionInteractor
 @Inject
 constructor(
+    override val transitionRepository: KeyguardTransitionRepository,
+    override val transitionInteractor: KeyguardTransitionInteractor,
     @Application private val scope: CoroutineScope,
     private val keyguardInteractor: KeyguardInteractor,
-    private val keyguardTransitionRepository: KeyguardTransitionRepository,
-    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(FromGoneTransitionInteractor::class.simpleName!!) {
+) :
+    TransitionInteractor(
+        fromState = KeyguardState.GONE,
+    ) {
 
     override fun start() {
         listenForGoneToAodOrDozing()
@@ -53,17 +54,10 @@
     private fun listenForGoneToLockscreen() {
         scope.launch {
             keyguardInteractor.isKeyguardShowing
-                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
                 .collect { (isKeyguardShowing, lastStartedStep) ->
                     if (isKeyguardShowing && lastStartedStep.to == KeyguardState.GONE) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.GONE,
-                                KeyguardState.LOCKSCREEN,
-                                getAnimator(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.LOCKSCREEN)
                     }
                 }
         }
@@ -72,17 +66,10 @@
     private fun listenForGoneToDreaming() {
         scope.launch {
             keyguardInteractor.isAbleToDream
-                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
                 .collect { (isAbleToDream, lastStartedStep) ->
                     if (isAbleToDream && lastStartedStep.to == KeyguardState.GONE) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.GONE,
-                                KeyguardState.DREAMING,
-                                getAnimator(TO_DREAMING_DURATION),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.DREAMING)
                     }
                 }
         }
@@ -93,7 +80,7 @@
             keyguardInteractor.wakefulnessModel
                 .sample(
                     combine(
-                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        transitionInteractor.startedKeyguardTransitionStep,
                         keyguardInteractor.isAodAvailable,
                         ::Pair
                     ),
@@ -104,30 +91,24 @@
                         lastStartedStep.to == KeyguardState.GONE &&
                             wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP
                     ) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.GONE,
-                                if (isAodAvailable) {
-                                    KeyguardState.AOD
-                                } else {
-                                    KeyguardState.DOZING
-                                },
-                                getAnimator(),
-                            )
+                        startTransitionTo(
+                            if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING
                         )
                     }
                 }
         }
     }
 
-    private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
+    override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
         return ValueAnimator().apply {
-            setInterpolator(Interpolators.LINEAR)
-            setDuration(duration.inWholeMilliseconds)
+            interpolator = Interpolators.LINEAR
+            duration =
+                when (toState) {
+                    KeyguardState.DREAMING -> TO_DREAMING_DURATION
+                    else -> DEFAULT_DURATION
+                }.inWholeMilliseconds
         }
     }
-
     companion object {
         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 b5e289f..ed1bf3e 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
@@ -32,7 +32,6 @@
 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.combine
@@ -42,12 +41,15 @@
 class FromLockscreenTransitionInteractor
 @Inject
 constructor(
+    override val transitionRepository: KeyguardTransitionRepository,
+    override val transitionInteractor: KeyguardTransitionInteractor,
     @Application private val scope: CoroutineScope,
     private val keyguardInteractor: KeyguardInteractor,
     private val shadeRepository: ShadeRepository,
-    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-    private val keyguardTransitionRepository: KeyguardTransitionRepository,
-) : TransitionInteractor(FromLockscreenTransitionInteractor::class.simpleName!!) {
+) :
+    TransitionInteractor(
+        fromState = KeyguardState.LOCKSCREEN,
+    ) {
 
     override fun start() {
         listenForLockscreenToGone()
@@ -66,8 +68,8 @@
             keyguardInteractor.isAbleToDream
                 .sample(
                     combine(
-                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
-                        keyguardTransitionInteractor.finishedKeyguardState,
+                        transitionInteractor.startedKeyguardTransitionStep,
+                        transitionInteractor.finishedKeyguardState,
                         ::Pair
                     ),
                     ::toTriple
@@ -78,14 +80,7 @@
                         lastStartedTransition.to == KeyguardState.LOCKSCREEN &&
                             !invalidFromStates.contains(lastStartedTransition.from)
                     if (isAbleToDream && (isOnLockscreen || isTransitionInterruptible)) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.LOCKSCREEN,
-                                KeyguardState.DREAMING,
-                                getAnimator(TO_DREAMING_DURATION),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.DREAMING)
                     }
                 }
         }
@@ -94,20 +89,13 @@
     private fun listenForLockscreenToPrimaryBouncer() {
         scope.launch {
             keyguardInteractor.primaryBouncerShowing
-                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
                 .collect { pair ->
                     val (isBouncerShowing, lastStartedTransitionStep) = pair
                     if (
                         isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.LOCKSCREEN
                     ) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                ownerName = name,
-                                from = KeyguardState.LOCKSCREEN,
-                                to = KeyguardState.PRIMARY_BOUNCER,
-                                animator = getAnimator(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.PRIMARY_BOUNCER)
                     }
                 }
         }
@@ -116,21 +104,14 @@
     private fun listenForLockscreenToAlternateBouncer() {
         scope.launch {
             keyguardInteractor.alternateBouncerShowing
-                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(transitionInteractor.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(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.ALTERNATE_BOUNCER)
                     }
                 }
         }
@@ -143,7 +124,7 @@
             shadeRepository.shadeModel
                 .sample(
                     combine(
-                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        transitionInteractor.startedKeyguardTransitionStep,
                         keyguardInteractor.statusBarState,
                         keyguardInteractor.isKeyguardUnlocked,
                         ::Triple
@@ -164,7 +145,7 @@
                                 } else {
                                     TransitionState.RUNNING
                                 }
-                            keyguardTransitionRepository.updateTransition(
+                            transitionRepository.updateTransition(
                                 id,
                                 1f - shadeModel.expansionAmount,
                                 nextState,
@@ -178,13 +159,18 @@
                             }
 
                             // If canceled, just put the state back
+                            // TODO: This logic should happen in FromPrimaryBouncerInteractor.
                             if (nextState == TransitionState.CANCELED) {
-                                keyguardTransitionRepository.startTransition(
+                                transitionRepository.startTransition(
                                     TransitionInfo(
                                         ownerName = name,
                                         from = KeyguardState.PRIMARY_BOUNCER,
                                         to = KeyguardState.LOCKSCREEN,
-                                        animator = getAnimator(0.milliseconds)
+                                        animator =
+                                            getDefaultAnimatorForTransitionsToState(
+                                                    KeyguardState.LOCKSCREEN
+                                                )
+                                                .apply { duration = 0 }
                                     )
                                 )
                             }
@@ -198,15 +184,7 @@
                                 !isKeyguardUnlocked &&
                                 statusBarState == KEYGUARD
                         ) {
-                            transitionId =
-                                keyguardTransitionRepository.startTransition(
-                                    TransitionInfo(
-                                        ownerName = name,
-                                        from = KeyguardState.LOCKSCREEN,
-                                        to = KeyguardState.PRIMARY_BOUNCER,
-                                        animator = null,
-                                    )
-                                )
+                            transitionId = startTransitionTo(KeyguardState.PRIMARY_BOUNCER)
                         }
                     }
                 }
@@ -216,18 +194,11 @@
     private fun listenForLockscreenToGone() {
         scope.launch {
             keyguardInteractor.isKeyguardGoingAway
-                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
                 .collect { pair ->
                     val (isKeyguardGoingAway, lastStartedStep) = pair
                     if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.LOCKSCREEN,
-                                KeyguardState.GONE,
-                                getAnimator(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.GONE)
                     }
                 }
         }
@@ -238,7 +209,7 @@
             keyguardInteractor.isKeyguardOccluded
                 .sample(
                     combine(
-                        keyguardTransitionInteractor.finishedKeyguardState,
+                        transitionInteractor.finishedKeyguardState,
                         keyguardInteractor.isDreaming,
                         ::Pair
                     ),
@@ -246,14 +217,7 @@
                 )
                 .collect { (isOccluded, keyguardState, isDreaming) ->
                     if (isOccluded && !isDreaming && keyguardState == KeyguardState.LOCKSCREEN) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                keyguardState,
-                                KeyguardState.OCCLUDED,
-                                getAnimator(TO_OCCLUDED_DURATION),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.OCCLUDED)
                     }
                 }
         }
@@ -263,7 +227,7 @@
     private fun listenForLockscreenToCamera() {
         scope.launch {
             keyguardInteractor.onCameraLaunchDetected
-                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
                 .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
@@ -274,14 +238,7 @@
                             lastStartedStep.to == KeyguardState.AOD ||
                             lastStartedStep.to == KeyguardState.OFF
                     ) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.LOCKSCREEN,
-                                KeyguardState.OCCLUDED,
-                                getAnimator(TO_OCCLUDED_DURATION),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.OCCLUDED)
                     }
                 }
         }
@@ -292,7 +249,7 @@
             keyguardInteractor.wakefulnessModel
                 .sample(
                     combine(
-                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        transitionInteractor.startedKeyguardTransitionStep,
                         keyguardInteractor.isAodAvailable,
                         ::Pair
                     ),
@@ -303,27 +260,23 @@
                         lastStartedStep.to == KeyguardState.LOCKSCREEN &&
                             wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP
                     ) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.LOCKSCREEN,
-                                if (isAodAvailable) {
-                                    KeyguardState.AOD
-                                } else {
-                                    KeyguardState.DOZING
-                                },
-                                getAnimator(),
-                            )
+                        startTransitionTo(
+                            if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING
                         )
                     }
                 }
         }
     }
 
-    private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
+    override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
         return ValueAnimator().apply {
-            setInterpolator(Interpolators.LINEAR)
-            setDuration(duration.inWholeMilliseconds)
+            interpolator = Interpolators.LINEAR
+            duration =
+                when (toState) {
+                    KeyguardState.DREAMING -> TO_DREAMING_DURATION
+                    KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION
+                    else -> DEFAULT_DURATION
+                }.inWholeMilliseconds
         }
     }
 
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 b0dbc59..ff0db34 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
@@ -22,12 +22,10 @@
 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.Utils.Companion.toTriple
 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
@@ -37,11 +35,14 @@
 class FromOccludedTransitionInteractor
 @Inject
 constructor(
+    override val transitionRepository: KeyguardTransitionRepository,
+    override val transitionInteractor: KeyguardTransitionInteractor,
     @Application private val scope: CoroutineScope,
     private val keyguardInteractor: KeyguardInteractor,
-    private val keyguardTransitionRepository: KeyguardTransitionRepository,
-    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(FromOccludedTransitionInteractor::class.simpleName!!) {
+) :
+    TransitionInteractor(
+        fromState = KeyguardState.OCCLUDED,
+    ) {
 
     override fun start() {
         listenForOccludedToLockscreen()
@@ -54,18 +55,11 @@
     private fun listenForOccludedToDreaming() {
         scope.launch {
             keyguardInteractor.isAbleToDream
-                .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
+                .sample(transitionInteractor.finishedKeyguardState, ::Pair)
                 .collect { pair ->
                     val (isAbleToDream, keyguardState) = pair
                     if (isAbleToDream && keyguardState == KeyguardState.OCCLUDED) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.OCCLUDED,
-                                KeyguardState.DREAMING,
-                                getAnimator(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.DREAMING)
                     }
                 }
         }
@@ -77,7 +71,7 @@
                 .sample(
                     combine(
                         keyguardInteractor.isKeyguardShowing,
-                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        transitionInteractor.startedKeyguardTransitionStep,
                         ::Pair
                     ),
                     ::toTriple
@@ -90,14 +84,7 @@
                             isShowing &&
                             lastStartedKeyguardState.to == KeyguardState.OCCLUDED
                     ) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.OCCLUDED,
-                                KeyguardState.LOCKSCREEN,
-                                getAnimator(TO_LOCKSCREEN_DURATION),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.LOCKSCREEN)
                     }
                 }
         }
@@ -109,7 +96,7 @@
                 .sample(
                     combine(
                         keyguardInteractor.isKeyguardShowing,
-                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        transitionInteractor.startedKeyguardTransitionStep,
                         ::Pair
                     ),
                     ::toTriple
@@ -122,14 +109,7 @@
                             !isShowing &&
                             lastStartedKeyguardState.to == KeyguardState.OCCLUDED
                     ) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.OCCLUDED,
-                                KeyguardState.GONE,
-                                getAnimator(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.GONE)
                     }
                 }
         }
@@ -140,7 +120,7 @@
             keyguardInteractor.wakefulnessModel
                 .sample(
                     combine(
-                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        transitionInteractor.startedKeyguardTransitionStep,
                         keyguardInteractor.isAodAvailable,
                         ::Pair
                     ),
@@ -151,17 +131,8 @@
                         lastStartedStep.to == KeyguardState.OCCLUDED &&
                             wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP
                     ) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                name,
-                                KeyguardState.OCCLUDED,
-                                if (isAodAvailable) {
-                                    KeyguardState.AOD
-                                } else {
-                                    KeyguardState.DOZING
-                                },
-                                getAnimator(),
-                            )
+                        startTransitionTo(
+                            if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING
                         )
                     }
                 }
@@ -171,29 +142,31 @@
     private fun listenForOccludedToAlternateBouncer() {
         scope.launch {
             keyguardInteractor.alternateBouncerShowing
-                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
                 .collect { (isAlternateBouncerShowing, lastStartedTransitionStep) ->
                     if (
                         isAlternateBouncerShowing &&
                             lastStartedTransitionStep.to == KeyguardState.OCCLUDED
                     ) {
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                ownerName = name,
-                                from = KeyguardState.OCCLUDED,
-                                to = KeyguardState.ALTERNATE_BOUNCER,
-                                animator = getAnimator(),
-                            )
-                        )
+                        startTransitionTo(KeyguardState.ALTERNATE_BOUNCER)
                     }
                 }
         }
     }
 
-    private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
+    override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
         return ValueAnimator().apply {
-            setInterpolator(Interpolators.LINEAR)
-            setDuration(duration.inWholeMilliseconds)
+            interpolator =
+                when (toState) {
+                    KeyguardState.ALTERNATE_BOUNCER -> Interpolators.FAST_OUT_SLOW_IN
+                    else -> Interpolators.LINEAR
+                }
+
+            duration =
+                when (toState) {
+                    KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
+                    else -> DEFAULT_DURATION
+                }.inWholeMilliseconds
         }
     }
 
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
index da09e1f..e1754f5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -25,12 +25,10 @@
 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.Utils.Companion.toQuad
 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
@@ -40,12 +38,15 @@
 class FromPrimaryBouncerTransitionInteractor
 @Inject
 constructor(
+    override val transitionRepository: KeyguardTransitionRepository,
+    override val transitionInteractor: KeyguardTransitionInteractor,
     @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!!) {
+) :
+    TransitionInteractor(
+        fromState = KeyguardState.PRIMARY_BOUNCER,
+    ) {
 
     override fun start() {
         listenForPrimaryBouncerToGone()
@@ -59,7 +60,7 @@
                 .sample(
                     combine(
                         keyguardInteractor.wakefulnessModel,
-                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        transitionInteractor.startedKeyguardTransitionStep,
                         keyguardInteractor.isKeyguardOccluded,
                         ::Triple
                     ),
@@ -73,20 +74,8 @@
                             (wakefulnessState.state == WakefulnessState.AWAKE ||
                                 wakefulnessState.state == WakefulnessState.STARTING_TO_WAKE)
                     ) {
-                        val to =
-                            if (occluded) {
-                                KeyguardState.OCCLUDED
-                            } else {
-                                KeyguardState.LOCKSCREEN
-                            }
-
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                ownerName = name,
-                                from = KeyguardState.PRIMARY_BOUNCER,
-                                to = to,
-                                animator = getAnimator(),
-                            )
+                        startTransitionTo(
+                            if (occluded) KeyguardState.OCCLUDED else KeyguardState.LOCKSCREEN
                         )
                     }
                 }
@@ -99,7 +88,7 @@
                 .sample(
                     combine(
                         keyguardInteractor.wakefulnessModel,
-                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                        transitionInteractor.startedKeyguardTransitionStep,
                         keyguardInteractor.isAodAvailable,
                         ::Triple
                     ),
@@ -114,20 +103,8 @@
                             (wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP ||
                                 wakefulnessState.state == WakefulnessState.ASLEEP)
                     ) {
-                        val to =
-                            if (isAodAvailable) {
-                                KeyguardState.AOD
-                            } else {
-                                KeyguardState.DOZING
-                            }
-
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                ownerName = name,
-                                from = KeyguardState.PRIMARY_BOUNCER,
-                                to = to,
-                                animator = getAnimator(),
-                            )
+                        startTransitionTo(
+                            if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING
                         )
                     }
                 }
@@ -137,7 +114,7 @@
     private fun listenForPrimaryBouncerToGone() {
         scope.launch {
             keyguardInteractor.isKeyguardGoingAway
-                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
                 .collect { (isKeyguardGoingAway, lastStartedTransitionStep) ->
                     if (
                         isKeyguardGoingAway &&
@@ -154,24 +131,24 @@
                             } else {
                                 TO_GONE_DURATION
                             }
-                        keyguardTransitionRepository.startTransition(
-                            TransitionInfo(
-                                ownerName = name,
-                                from = KeyguardState.PRIMARY_BOUNCER,
-                                to = KeyguardState.GONE,
-                                animator = getAnimator(duration),
-                            ),
-                            resetIfCanceled = true,
+
+                        startTransitionTo(
+                            toState = KeyguardState.GONE,
+                            animator =
+                                getDefaultAnimatorForTransitionsToState(KeyguardState.GONE).apply {
+                                    this.duration = duration.inWholeMilliseconds
+                                },
+                            resetIfCancelled = true
                         )
                     }
                 }
         }
     }
 
-    private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
+    override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
         return ValueAnimator().apply {
-            setInterpolator(Interpolators.LINEAR)
-            setDuration(duration.inWholeMilliseconds)
+            interpolator = Interpolators.LINEAR
+            duration = DEFAULT_DURATION.inWholeMilliseconds
         }
     }
 
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 c44435e..228290a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -19,12 +19,13 @@
 
 import android.app.StatusBarManager
 import android.graphics.Point
+import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
 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
@@ -39,6 +40,7 @@
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
@@ -59,6 +61,7 @@
     private val commandQueue: CommandQueue,
     featureFlags: FeatureFlags,
     bouncerRepository: KeyguardBouncerRepository,
+    configurationRepository: ConfigurationRepository,
 ) {
     /**
      * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at
@@ -97,7 +100,7 @@
     }
 
     /** The device wake/sleep state */
-    val wakefulnessModel: Flow<WakefulnessModel> = repository.wakefulness
+    val wakefulnessModel: StateFlow<WakefulnessModel> = repository.wakefulness
 
     /**
      * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means
@@ -172,6 +175,9 @@
     /** The approximate location on the screen of the face unlock sensor, if one is available. */
     val faceSensorLocation: Flow<Point?> = repository.faceSensorLocation
 
+    /** Notifies when a new configuration is set */
+    val configurationChange: Flow<Unit> = configurationRepository.onAnyConfigurationChange
+
     fun dozeTransitionTo(vararg states: DozeStateModel): Flow<DozeTransitionModel> {
         return dozeTransitionModel.filter { states.contains(it.to) }
     }
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 ea9c2b2..f692a39 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
@@ -49,6 +49,7 @@
 import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.TraceUtils.Companion.traceAsync
 import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
@@ -433,13 +434,19 @@
             KeyguardPickerFlag(
                 name = Contract.FlagsTable.FLAG_NAME_WALLPAPER_PICKER_UI_FOR_AIWP,
                 value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_UI_FOR_AIWP)
+            ),
+            KeyguardPickerFlag(
+                name = Contract.FlagsTable.FLAG_NAME_TRANSIT_CLOCK,
+                value = featureFlags.isEnabled(Flags.TRANSIT_CLOCK)
             )
         )
     }
 
     private suspend fun isFeatureDisabledByDevicePolicy(): Boolean =
-        withContext(backgroundDispatcher) {
-            devicePolicyManager.areKeyguardShortcutsDisabled(userId = userTracker.userId)
+        traceAsync("isFeatureDisabledByDevicePolicy", TAG) {
+            withContext(backgroundDispatcher) {
+                devicePolicyManager.areKeyguardShortcutsDisabled(userId = userTracker.userId)
+            }
         }
 
     companion object {
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 7c5641f..4f7abd4 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,7 +19,7 @@
 import com.android.keyguard.logging.KeyguardLogger
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.log.LogLevel.VERBOSE
+import com.android.systemui.log.core.LogLevel.VERBOSE
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
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 41a81a8..45bf20d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -21,6 +21,7 @@
 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.KeyguardState.ALTERNATE_BOUNCER
 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
@@ -47,6 +48,8 @@
     private val repository: KeyguardTransitionRepository,
     @Application val scope: CoroutineScope,
 ) {
+    private val TAG = this::class.simpleName
+
     /** (any)->GONE transition information */
     val anyStateToGoneTransition: Flow<TransitionStep> =
         repository.transitions.filter { step -> step.to == GONE }
@@ -59,6 +62,26 @@
     val fromDreamingTransition: Flow<TransitionStep> =
         repository.transitions.filter { step -> step.from == DREAMING }
 
+    /** (any)->Lockscreen transition information */
+    val anyStateToLockscreenTransition: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.to == LOCKSCREEN }
+
+    /** (any)->Occluded transition information */
+    val anyStateToOccludedTransition: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.to == OCCLUDED }
+
+    /** (any)->PrimaryBouncer transition information */
+    val anyStateToPrimaryBouncerTransition: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.to == PRIMARY_BOUNCER }
+
+    /** (any)->Dreaming transition information */
+    val anyStateToDreamingTransition: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.to == DREAMING }
+
+    /** (any)->AlternateBouncer transition information */
+    val anyStateToAlternateBouncerTransition: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.to == ALTERNATE_BOUNCER }
+
     /** AOD->LOCKSCREEN transition information. */
     val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN)
 
@@ -66,6 +89,9 @@
     val dreamingToLockscreenTransition: Flow<TransitionStep> =
         repository.transition(DREAMING, LOCKSCREEN)
 
+    /** GONE->AOD transition information. */
+    val goneToAodTransition: Flow<TransitionStep> = repository.transition(GONE, AOD)
+
     /** GONE->DREAMING transition information. */
     val goneToDreamingTransition: Flow<TransitionStep> = repository.transition(GONE, DREAMING)
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractor.kt
index d0bc25f..c8f7efb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractor.kt
@@ -23,7 +23,6 @@
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
-import com.android.systemui.util.kotlin.pairwise
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
@@ -61,23 +60,16 @@
 
     /** Whether it's currently possible to swipe up to dismiss the lockscreen. */
     val isSwipeToDismissEnabled: StateFlow<Boolean> =
-        combine(
-                authenticationInteractor.isUnlocked,
-                authenticationInteractor.authenticationMethod,
-            ) { isUnlocked, authMethod ->
-                isSwipeToUnlockEnabled(
-                    isUnlocked = isUnlocked,
-                    authMethod = authMethod,
-                )
+        authenticationInteractor.isUnlocked
+            .map { isUnlocked ->
+                !isUnlocked &&
+                    authenticationInteractor.getAuthenticationMethod() is
+                        AuthenticationMethodModel.Swipe
             }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue =
-                    isSwipeToUnlockEnabled(
-                        isUnlocked = authenticationInteractor.isUnlocked.value,
-                        authMethod = authenticationInteractor.authenticationMethod.value,
-                    ),
+                initialValue = false,
             )
 
     init {
@@ -118,51 +110,6 @@
                     }
                 }
         }
-
-        // SWIPE TO DISMISS Lockscreen.
-        //
-        // If switched from the lockscreen to the gone scene and the auth method was a swipe,
-        // unlocks the device.
-        applicationScope.launch {
-            combine(
-                    authenticationInteractor.authenticationMethod,
-                    sceneInteractor.currentScene(containerName).pairwise(),
-                    ::Pair,
-                )
-                .collect { (authMethod, scenes) ->
-                    val (previousScene, currentScene) = scenes
-                    if (
-                        authMethod is AuthenticationMethodModel.Swipe &&
-                            previousScene.key == SceneKey.Lockscreen &&
-                            currentScene.key == SceneKey.Gone
-                    ) {
-                        authenticationInteractor.unlockDevice()
-                    }
-                }
-        }
-
-        // DISMISS Lockscreen IF AUTH METHOD IS REMOVED.
-        //
-        // If the auth method becomes None while on the lockscreen scene, dismisses the lock
-        // screen.
-        applicationScope.launch {
-            combine(
-                    authenticationInteractor.authenticationMethod,
-                    sceneInteractor.currentScene(containerName),
-                    ::Pair,
-                )
-                .collect { (authMethod, scene) ->
-                    if (
-                        scene.key == SceneKey.Lockscreen &&
-                            authMethod == AuthenticationMethodModel.None
-                    ) {
-                        sceneInteractor.setCurrentScene(
-                            containerName = containerName,
-                            scene = SceneModel(SceneKey.Gone),
-                        )
-                    }
-                }
-        }
     }
 
     /** Attempts to dismiss the lockscreen. This will cause the bouncer to show, if needed. */
@@ -170,13 +117,6 @@
         bouncerInteractor.showOrUnlockDevice(containerName = containerName)
     }
 
-    private fun isSwipeToUnlockEnabled(
-        isUnlocked: Boolean,
-        authMethod: AuthenticationMethodModel,
-    ): Boolean {
-        return !isUnlocked && authMethod is AuthenticationMethodModel.Swipe
-    }
-
     @AssistedFactory
     interface Factory {
         fun create(
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
deleted file mode 100644
index 3099a49..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.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.keyguard.domain.interactor
-
-import android.view.View
-import com.android.systemui.dagger.SysUISingleton
-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<KeyguardResetCallback>()
-    private var expansionCallbacks = ArrayList<PrimaryBouncerExpansionCallback>()
-
-    /** Add a KeyguardResetCallback. */
-    fun addKeyguardResetCallback(callback: KeyguardResetCallback) {
-        resetCallbacks.addIfAbsent(callback)
-    }
-
-    /** Remove a KeyguardResetCallback. */
-    fun removeKeyguardResetCallback(callback: KeyguardResetCallback) {
-        resetCallbacks.remove(callback)
-    }
-
-    /** Adds a callback to listen to bouncer expansion updates. */
-    fun addBouncerExpansionCallback(callback: PrimaryBouncerExpansionCallback) {
-        if (!expansionCallbacks.contains(callback)) {
-            expansionCallbacks.add(callback)
-        }
-    }
-
-    /**
-     * Removes a previously added callback. If the callback was never added, this method does
-     * nothing.
-     */
-    fun removeBouncerExpansionCallback(callback: PrimaryBouncerExpansionCallback) {
-        expansionCallbacks.remove(callback)
-    }
-
-    /** Propagate fully shown to bouncer expansion callbacks. */
-    fun dispatchFullyShown() {
-        for (callback in expansionCallbacks) {
-            callback.onFullyShown()
-        }
-    }
-
-    /** Propagate starting to hide to bouncer expansion callbacks. */
-    fun dispatchStartingToHide() {
-        for (callback in expansionCallbacks) {
-            callback.onStartingToHide()
-        }
-    }
-
-    /** Propagate starting to show to bouncer expansion callbacks. */
-    fun dispatchStartingToShow() {
-        for (callback in expansionCallbacks) {
-            callback.onStartingToShow()
-        }
-    }
-
-    /** Propagate fully hidden to bouncer expansion callbacks. */
-    fun dispatchFullyHidden() {
-        for (callback in expansionCallbacks) {
-            callback.onFullyHidden()
-        }
-    }
-
-    /** Propagate expansion changes to bouncer expansion callbacks. */
-    fun dispatchExpansionChanged(expansion: Float) {
-        for (callback in expansionCallbacks) {
-            callback.onExpansionChanged(expansion)
-        }
-    }
-    /** Propagate visibility changes to bouncer expansion callbacks. */
-    fun dispatchVisibilityChanged(visibility: Int) {
-        for (callback in expansionCallbacks) {
-            callback.onVisibilityChanged(visibility == View.VISIBLE)
-        }
-    }
-
-    /** Propagate keyguard reset. */
-    fun dispatchReset() {
-        for (callback in resetCallbacks) {
-            callback.onKeyguardReset()
-        }
-    }
-
-    /** 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
deleted file mode 100644
index 54bc349..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
+++ /dev/null
@@ -1,406 +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.content.Context
-import android.content.res.ColorStateList
-import android.hardware.biometrics.BiometricSourceType
-import android.os.Handler
-import android.os.Trace
-import android.util.Log
-import android.view.View
-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.Application
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-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.data.repository.TrustRepository
-import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants
-import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
-import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.shared.system.SysUiStatsLog
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-/**
- * Encapsulates business logic for interacting with the lock-screen primary (pin/pattern/password)
- * bouncer.
- */
-@SysUISingleton
-class PrimaryBouncerInteractor
-@Inject
-constructor(
-    private val repository: KeyguardBouncerRepository,
-    private val primaryBouncerView: BouncerView,
-    @Main private val mainHandler: Handler,
-    private val keyguardStateController: KeyguardStateController,
-    private val keyguardSecurityModel: KeyguardSecurityModel,
-    private val primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor,
-    private val falsingCollector: FalsingCollector,
-    private val dismissCallbackRegistry: DismissCallbackRegistry,
-    private val context: Context,
-    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-    private val trustRepository: TrustRepository,
-    private val featureFlags: FeatureFlags,
-    @Application private val applicationScope: CoroutineScope,
-) {
-    private val passiveAuthBouncerDelay = context.resources.getInteger(
-            R.integer.primary_bouncer_passive_auth_delay).toLong()
-    /** Runnable to show the primary bouncer. */
-    val showRunnable = Runnable {
-        repository.setPrimaryShow(true)
-        repository.setPrimaryShowingSoon(false)
-        primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.VISIBLE)
-    }
-
-    val keyguardAuthenticated: Flow<Boolean> = repository.keyguardAuthenticated.filterNotNull()
-    val isShowing: Flow<Boolean> = repository.primaryBouncerShow
-    val startingToHide: Flow<Unit> = repository.primaryBouncerStartingToHide.filter { it }.map {}
-    val isBackButtonEnabled: Flow<Boolean> = repository.isBackButtonEnabled.filterNotNull()
-    val showMessage: Flow<BouncerShowMessageModel> = repository.showMessage.filterNotNull()
-    val startingDisappearAnimation: Flow<Runnable> =
-        repository.primaryBouncerStartingDisappearAnimation.filterNotNull()
-    val resourceUpdateRequests: Flow<Boolean> = repository.resourceUpdateRequests.filter { it }
-    val keyguardPosition: Flow<Float> = repository.keyguardPosition
-    val panelExpansionAmount: Flow<Float> = repository.panelExpansionAmount
-    /** 0f = bouncer fully hidden. 1f = bouncer fully visible. */
-    val bouncerExpansion: Flow<Float> =
-        combine(repository.panelExpansionAmount, repository.primaryBouncerShow) {
-            panelExpansion,
-            primaryBouncerIsShowing ->
-            if (primaryBouncerIsShowing) {
-                1f - panelExpansion
-            } else {
-                0f
-            }
-        }
-    /** Allow for interaction when just about fully visible */
-    val isInteractable: Flow<Boolean> = bouncerExpansion.map { it > 0.9 }
-    val sideFpsShowing: Flow<Boolean> = repository.sideFpsShowing
-    private var currentUserActiveUnlockRunning = false
-
-    /** This callback needs to be a class field so it does not get garbage collected. */
-    val keyguardUpdateMonitorCallback =
-        object : KeyguardUpdateMonitorCallback() {
-            override fun onBiometricRunningStateChanged(
-                running: Boolean,
-                biometricSourceType: BiometricSourceType?
-            ) {
-                updateSideFpsVisibility()
-            }
-
-            override fun onStrongAuthStateChanged(userId: Int) {
-                updateSideFpsVisibility()
-            }
-        }
-
-    init {
-        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
-        if (featureFlags.isEnabled(Flags.DELAY_BOUNCER)) {
-            applicationScope.launch {
-                trustRepository.isCurrentUserActiveUnlockRunning.collect {
-                    currentUserActiveUnlockRunning = it
-                }
-            }
-        }
-    }
-
-    // TODO(b/243685699): Move isScrimmed logic to data layer.
-    // TODO(b/243695312): Encapsulate all of the show logic for the bouncer.
-    /** Show the bouncer if necessary and set the relevant states. */
-    @JvmOverloads
-    fun show(isScrimmed: Boolean) {
-        // Reset some states as we show the bouncer.
-        repository.setKeyguardAuthenticated(null)
-        repository.setPrimaryStartingToHide(false)
-
-        val resumeBouncer =
-            (isBouncerShowing() || repository.primaryBouncerShowingSoon.value) &&
-                needsFullscreenBouncer()
-
-        Trace.beginSection("KeyguardBouncer#show")
-        repository.setPrimaryScrimmed(isScrimmed)
-        if (isScrimmed) {
-            setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE)
-        }
-
-        // In this special case, we want to hide the bouncer and show it again. We want to emit
-        // show(true) again so that we can reinflate the new view.
-        if (resumeBouncer) {
-            repository.setPrimaryShow(false)
-        }
-
-        if (primaryBouncerView.delegate?.showNextSecurityScreenOrFinish() == true) {
-            // Keyguard is done.
-            return
-        }
-
-        repository.setPrimaryShowingSoon(true)
-        if (usePrimaryBouncerPassiveAuthDelay()) {
-            Log.d(TAG, "delay bouncer, passive auth may succeed")
-            mainHandler.postDelayed(showRunnable, passiveAuthBouncerDelay)
-        } else {
-            DejankUtils.postAfterTraversal(showRunnable)
-        }
-        keyguardStateController.notifyPrimaryBouncerShowing(true)
-        primaryBouncerCallbackInteractor.dispatchStartingToShow()
-        Trace.endSection()
-    }
-
-    /** Sets the correct bouncer states to hide the bouncer. */
-    fun hide() {
-        Trace.beginSection("KeyguardBouncer#hide")
-        if (isFullyShowing()) {
-            SysUiStatsLog.write(
-                SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED,
-                SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__HIDDEN
-            )
-            dismissCallbackRegistry.notifyDismissCancelled()
-        }
-
-        repository.setPrimaryStartDisappearAnimation(null)
-        falsingCollector.onBouncerHidden()
-        keyguardStateController.notifyPrimaryBouncerShowing(false /* showing */)
-        cancelShowRunnable()
-        repository.setPrimaryShowingSoon(false)
-        repository.setPrimaryShow(false)
-        repository.setPanelExpansion(EXPANSION_HIDDEN)
-        primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.INVISIBLE)
-        Trace.endSection()
-    }
-
-    /**
-     * Sets the panel expansion which is calculated further upstream. Panel expansion is from 0f
-     * (panel fully hidden) to 1f (panel fully showing). As the panel shows (from 0f => 1f), the
-     * bouncer hides and as the panel becomes hidden (1f => 0f), the bouncer starts to show.
-     * Therefore, a panel expansion of 1f represents the bouncer fully hidden and a panel expansion
-     * of 0f represents the bouncer fully showing.
-     */
-    fun setPanelExpansion(expansion: Float) {
-        val oldExpansion = repository.panelExpansionAmount.value
-        val expansionChanged = oldExpansion != expansion
-        if (repository.primaryBouncerStartingDisappearAnimation.value == null) {
-            repository.setPanelExpansion(expansion)
-        }
-
-        if (
-            expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE &&
-                oldExpansion != KeyguardBouncerConstants.EXPANSION_VISIBLE
-        ) {
-            falsingCollector.onBouncerShown()
-            primaryBouncerCallbackInteractor.dispatchFullyShown()
-        } else if (
-            expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN &&
-                oldExpansion != KeyguardBouncerConstants.EXPANSION_HIDDEN
-        ) {
-            /*
-             * 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()
-            DejankUtils.postAfterTraversal { primaryBouncerCallbackInteractor.dispatchReset() }
-            primaryBouncerCallbackInteractor.dispatchFullyHidden()
-        } else if (
-            expansion != KeyguardBouncerConstants.EXPANSION_VISIBLE &&
-                oldExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE
-        ) {
-            primaryBouncerCallbackInteractor.dispatchStartingToHide()
-            repository.setPrimaryStartingToHide(true)
-        }
-        if (expansionChanged) {
-            primaryBouncerCallbackInteractor.dispatchExpansionChanged(expansion)
-        }
-    }
-
-    /** Set the initial keyguard message to show when bouncer is shown. */
-    fun showMessage(message: String?, colorStateList: ColorStateList?) {
-        repository.setShowMessage(BouncerShowMessageModel(message, colorStateList))
-    }
-
-    /**
-     * Sets actions to the bouncer based on how the bouncer is dismissed. If the bouncer is
-     * unlocked, we will run the onDismissAction. If the bouncer is existed before unlocking, we
-     * call cancelAction.
-     */
-    fun setDismissAction(
-        onDismissAction: ActivityStarter.OnDismissAction?,
-        cancelAction: Runnable?
-    ) {
-        primaryBouncerView.delegate?.setDismissAction(onDismissAction, cancelAction)
-    }
-
-    /** Update the resources of the views. */
-    fun updateResources() {
-        repository.setResourceUpdateRequests(true)
-    }
-
-    /** Tell the bouncer that keyguard is authenticated. */
-    fun notifyKeyguardAuthenticated(strongAuth: Boolean) {
-        repository.setKeyguardAuthenticated(strongAuth)
-    }
-
-    /** Update the position of the bouncer when showing. */
-    fun setKeyguardPosition(position: Float) {
-        repository.setKeyguardPosition(position)
-    }
-
-    /** Notifies that the state change was handled. */
-    fun notifyKeyguardAuthenticatedHandled() {
-        repository.setKeyguardAuthenticated(null)
-    }
-
-    /** Notifies that the message was shown. */
-    fun onMessageShown() {
-        repository.setShowMessage(null)
-    }
-
-    /** Notify that the resources have been updated */
-    fun notifyUpdatedResources() {
-        repository.setResourceUpdateRequests(false)
-    }
-
-    /** Set whether back button is enabled when on the bouncer screen. */
-    fun setBackButtonEnabled(enabled: Boolean) {
-        repository.setIsBackButtonEnabled(enabled)
-    }
-
-    /** Tell the bouncer to start the pre hide animation. */
-    fun startDisappearAnimation(runnable: Runnable) {
-        if (willRunDismissFromKeyguard()) {
-            runnable.run()
-            return
-        }
-
-        repository.setPrimaryStartDisappearAnimation(runnable)
-    }
-
-    /** 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 toShow =
-            (isBouncerShowing() &&
-                sfpsEnabled &&
-                fpsDetectionRunning &&
-                isUnlockingWithFpAllowed &&
-                !isAnimatingAway())
-
-        if (KeyguardConstants.DEBUG) {
-            Log.d(
-                TAG,
-                ("sideFpsToShow=$toShow\n" +
-                    "isBouncerShowing=${isBouncerShowing()}\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 || isBouncerShowing()) &&
-            repository.panelExpansionAmount.value == KeyguardBouncerConstants.EXPANSION_VISIBLE &&
-            repository.primaryBouncerStartingDisappearAnimation.value == null
-    }
-
-    /** Returns whether bouncer is scrimmed. */
-    fun isScrimmed(): Boolean {
-        return repository.primaryBouncerScrimmed.value
-    }
-
-    /** If bouncer expansion is between 0f and 1f non-inclusive. */
-    fun isInTransit(): Boolean {
-        return repository.primaryBouncerShowingSoon.value ||
-            repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_HIDDEN &&
-                repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_VISIBLE
-    }
-
-    /** Return whether bouncer is animating away. */
-    fun isAnimatingAway(): Boolean {
-        return repository.primaryBouncerStartingDisappearAnimation.value != null
-    }
-
-    /** Return whether bouncer will dismiss with actions */
-    fun willDismissWithAction(): Boolean {
-        return primaryBouncerView.delegate?.willDismissWithActions() == true
-    }
-
-    /** Will the dismissal run from the keyguard layout (instead of from bouncer) */
-    fun willRunDismissFromKeyguard(): Boolean {
-        return primaryBouncerView.delegate?.willRunDismissFromKeyguard() == true
-    }
-
-    /** Returns whether the bouncer should be full screen. */
-    private fun needsFullscreenBouncer(): Boolean {
-        val mode: KeyguardSecurityModel.SecurityMode =
-            keyguardSecurityModel.getSecurityMode(KeyguardUpdateMonitor.getCurrentUser())
-        return mode == KeyguardSecurityModel.SecurityMode.SimPin ||
-            mode == KeyguardSecurityModel.SecurityMode.SimPuk
-    }
-
-    /** Remove the show runnable from the main handler queue to improve performance. */
-    private fun cancelShowRunnable() {
-        DejankUtils.removeCallbacks(showRunnable)
-        mainHandler.removeCallbacks(showRunnable)
-    }
-
-    private fun isBouncerShowing(): Boolean {
-        return repository.primaryBouncerShow.value
-    }
-
-    /** Whether we want to wait to show the bouncer in case passive auth succeeds. */
-    private fun usePrimaryBouncerPassiveAuthDelay(): Boolean {
-        val canRunFaceAuth = keyguardStateController.isFaceAuthEnabled &&
-                keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE)
-        val canRunActiveUnlock = currentUserActiveUnlockRunning &&
-                keyguardUpdateMonitor.canTriggerActiveUnlockBasedOnDeviceState()
-
-        return featureFlags.isEnabled(Flags.DELAY_BOUNCER) &&
-                !needsFullscreenBouncer() &&
-                (canRunFaceAuth || canRunActiveUnlock)
-    }
-
-    companion object {
-        private const val TAG = "PrimaryBouncerInteractor"
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
index 6b515da..d467225 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
@@ -16,15 +16,22 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.content.Context
+import android.hardware.biometrics.BiometricFaceConstants
 import com.android.keyguard.FaceAuthUiEvent
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.CoreStartable
+import com.android.systemui.R
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.shared.model.AuthenticationStatus
+import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.log.FaceAuthenticationLogger
 import com.android.systemui.util.kotlin.pairwise
@@ -32,7 +39,9 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
@@ -48,6 +57,7 @@
 class SystemUIKeyguardFaceAuthInteractor
 @Inject
 constructor(
+    private val context: Context,
     @Application private val applicationScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
     private val repository: DeviceEntryFaceAuthRepository,
@@ -155,17 +165,28 @@
         repository.cancel()
     }
 
+    private val _authenticationStatusOverride = MutableStateFlow<AuthenticationStatus?>(null)
     /** Provide the status of face authentication */
-    override val authenticationStatus = repository.authenticationStatus
+    override val authenticationStatus =
+        merge(_authenticationStatusOverride.filterNotNull(), repository.authenticationStatus)
 
     /** Provide the status of face detection */
     override val detectionStatus = repository.detectionStatus
 
     private fun runFaceAuth(uiEvent: FaceAuthUiEvent, fallbackToDetect: Boolean) {
         if (featureFlags.isEnabled(Flags.FACE_AUTH_REFACTOR)) {
-            applicationScope.launch {
-                faceAuthenticationLogger.authRequested(uiEvent)
-                repository.authenticate(uiEvent, fallbackToDetection = fallbackToDetect)
+            if (repository.isLockedOut.value) {
+                _authenticationStatusOverride.value =
+                    ErrorAuthenticationStatus(
+                        BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT,
+                        context.resources.getString(R.string.keyguard_face_unlock_unavailable)
+                    )
+            } else {
+                _authenticationStatusOverride.value = null
+                applicationScope.launch {
+                    faceAuthenticationLogger.authRequested(uiEvent)
+                    repository.authenticate(uiEvent, fallbackToDetection = fallbackToDetect)
+                }
             }
         } else {
             faceAuthenticationLogger.ignoredFaceAuthTrigger(
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 b7dd1a5..0dda625 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
@@ -15,6 +15,14 @@
  */
 
 package com.android.systemui.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import android.util.Log
+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 java.util.UUID
+
 /**
  * Each TransitionInteractor is responsible for determining under which conditions to notify
  * [KeyguardTransitionRepository] to signal a transition. When (and if) the transition occurs is
@@ -26,6 +34,50 @@
  * MUST list implementing classes in dagger module [StartKeyguardTransitionModule] and also in the
  * 'when' clause of [KeyguardTransitionCoreStartable]
  */
-sealed class TransitionInteractor(val name: String) {
+sealed class TransitionInteractor(
+    val fromState: KeyguardState,
+) {
+    val name = this::class.simpleName ?: "UnknownTransitionInteractor"
+
+    abstract val transitionRepository: KeyguardTransitionRepository
+    abstract val transitionInteractor: KeyguardTransitionInteractor
     abstract fun start()
+
+    fun startTransitionTo(
+        toState: KeyguardState,
+        animator: ValueAnimator? = getDefaultAnimatorForTransitionsToState(toState),
+        resetIfCancelled: Boolean = false
+    ): UUID? {
+        if (
+            fromState != transitionInteractor.startedKeyguardState.value &&
+                fromState != transitionInteractor.finishedKeyguardState.value
+        ) {
+            Log.e(
+                name,
+                "startTransition: We were asked to transition from " +
+                    "$fromState to $toState, however we last finished a transition to " +
+                    "${transitionInteractor.finishedKeyguardState.value}, " +
+                    "and last started a transition to " +
+                    "${transitionInteractor.startedKeyguardState.value}. " +
+                    "Ignoring startTransition, but this should never happen."
+            )
+            return null
+        }
+
+        return transitionRepository.startTransition(
+            TransitionInfo(
+                name,
+                fromState,
+                toState,
+                animator,
+            ),
+            resetIfCancelled
+        )
+    }
+
+    /**
+     * Returns a ValueAnimator to be used for transitions to [toState], if one is not explicitly
+     * passed to [startTransitionTo].
+     */
+    abstract fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator?
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt
new file mode 100644
index 0000000..bba0e37
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt
@@ -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.keyguard.domain.interactor
+
+import android.animation.FloatEvaluator
+import android.animation.IntEvaluator
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+
+/** Encapsulates business logic for transitions between UDFPS states on the keyguard. */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class UdfpsKeyguardInteractor
+@Inject
+constructor(
+    configRepo: ConfigurationRepository,
+    burnInInteractor: BurnInInteractor,
+    keyguardInteractor: KeyguardInteractor,
+) {
+    private val intEvaluator = IntEvaluator()
+    private val floatEvaluator = FloatEvaluator()
+
+    val dozeAmount = keyguardInteractor.dozeAmount
+    val scaleForResolution = configRepo.scaleForResolution
+
+    /** Burn-in offsets for the UDFPS view to mitigate burn-in on AOD. */
+    val burnInOffsets: Flow<BurnInOffsets> =
+        combine(
+            keyguardInteractor.dozeAmount,
+            burnInInteractor.udfpsBurnInXOffset,
+            burnInInteractor.udfpsBurnInYOffset,
+            burnInInteractor.udfpsBurnInProgress
+        ) { dozeAmount, fullyDozingBurnInX, fullyDozingBurnInY, fullyDozingBurnInProgress ->
+            BurnInOffsets(
+                intEvaluator.evaluate(dozeAmount, 0, fullyDozingBurnInX),
+                intEvaluator.evaluate(dozeAmount, 0, fullyDozingBurnInY),
+                floatEvaluator.evaluate(dozeAmount, 0, fullyDozingBurnInProgress),
+            )
+        }
+}
+
+data class BurnInOffsets(
+    val burnInXOffset: Int, // current x burn in offset based on the aodTransitionAmount
+    val burnInYOffset: Int, // current y burn in offset based on the aodTransitionAmount
+    val burnInProgress: Float, // current progress based on the aodTransitionAmount
+)
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
deleted file mode 100644
index c45faf0..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/constants/KeyguardBouncerConstants.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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
-
-    /**
-     * This value is used for denoting the PIN length at which we want to layout the view in which
-     * PIN hinting is enabled
-     */
-    const val DEFAULT_PIN_LENGTH = 6
-
-    object ColorId {
-        const val TITLE = com.android.internal.R.attr.materialColorOnSurface
-        const val PIN_SHAPES = com.android.internal.R.attr.materialColorOnSurfaceVariant
-        const val NUM_PAD_BACKGROUND = com.android.internal.R.attr.materialColorSurfaceContainerHigh
-        const val NUM_PAD_BACKGROUND_PRESSED = com.android.internal.R.attr.materialColorPrimaryFixed
-        const val NUM_PAD_PRESSED = com.android.internal.R.attr.materialColorOnPrimaryFixed
-        const val NUM_PAD_KEY = com.android.internal.R.attr.materialColorOnSurface
-        const val NUM_PAD_BUTTON = com.android.internal.R.attr.materialColorOnSecondaryFixed
-        const val EMERGENCY_BUTTON = com.android.internal.R.attr.materialColorTertiaryFixed
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BouncerCallbackActionsModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BouncerCallbackActionsModel.kt
deleted file mode 100644
index 81cf5b4..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BouncerCallbackActionsModel.kt
+++ /dev/null
@@ -1,26 +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.shared.model
-
-import com.android.systemui.plugins.ActivityStarter
-
-/** Encapsulates callbacks to be invoked by the bouncer logic. */
-// TODO(b/243683121): Move dismiss logic from view controllers
-data class BouncerCallbackActionsModel(
-    val onDismissAction: ActivityStarter.OnDismissAction?,
-    val cancelAction: Runnable?
-)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BouncerShowMessageModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BouncerShowMessageModel.kt
deleted file mode 100644
index 05cdeaa..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BouncerShowMessageModel.kt
+++ /dev/null
@@ -1,22 +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.shared.model
-
-import android.content.res.ColorStateList
-
-/** Show a keyguard message to the bouncer. */
-data class BouncerShowMessageModel(val message: String?, val colorStateList: ColorStateList?)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
index 9e7dec4..b354cfd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.shared.model
 
 import android.hardware.face.FaceManager
+import android.os.SystemClock.elapsedRealtime
 
 /**
  * Authentication status provided by
@@ -38,8 +39,12 @@
 object FailedAuthenticationStatus : AuthenticationStatus()
 
 /** Face authentication error message */
-data class ErrorAuthenticationStatus(val msgId: Int, val msg: String? = null) :
-    AuthenticationStatus() {
+data class ErrorAuthenticationStatus(
+    val msgId: Int,
+    val msg: String? = null,
+    // present to break equality check if the same error occurs repeatedly.
+    val createdAt: Long = elapsedRealtime()
+) : AuthenticationStatus() {
     /**
      * Method that checks if [msgId] is a lockout error. A lockout error means that face
      * authentication is locked out.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
index 767fd58..0fa6f4f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
@@ -16,7 +16,9 @@
 package com.android.systemui.keyguard.shared.model
 
 /** This information will flow from the [KeyguardTransitionRepository] to control the UI layer */
-data class TransitionStep(
+data class TransitionStep
+@JvmOverloads
+constructor(
     val from: KeyguardState = KeyguardState.OFF,
     val to: KeyguardState = KeyguardState.OFF,
     val value: Float = 0f, // constrained [0.0, 1.0]
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakeSleepReason.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakeSleepReason.kt
index 51ce7ff..fb685da 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakeSleepReason.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakeSleepReason.kt
@@ -23,9 +23,12 @@
     /** The physical power button was pressed to wake up or sleep the device. */
     POWER_BUTTON,
 
-    /** The user has taped or double tapped to wake the screen */
+    /** The user has tapped or double tapped to wake the screen. */
     TAP,
 
+    /** The user performed some sort of gesture to wake the screen. */
+    GESTURE,
+
     /** Something else happened to wake up or sleep the device. */
     OTHER;
 
@@ -34,6 +37,7 @@
             return when (reason) {
                 PowerManager.WAKE_REASON_POWER_BUTTON -> POWER_BUTTON
                 PowerManager.WAKE_REASON_TAP -> TAP
+                PowerManager.WAKE_REASON_GESTURE -> GESTURE
                 else -> OTHER
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
index 52e15be..cfd9e08 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
@@ -27,7 +27,11 @@
 
     fun isStartingToSleep() = state == WakefulnessState.STARTING_TO_SLEEP
 
-    fun isStartingToSleepOrAsleep() = isStartingToSleep() || state == WakefulnessState.ASLEEP
+    private fun isAsleep() = state == WakefulnessState.ASLEEP
+
+    fun isStartingToSleepOrAsleep() = isStartingToSleep() || isAsleep()
+
+    fun isDeviceInteractive() = !isAsleep()
 
     fun isStartingToWakeOrAwake() = isStartingToWake() || state == WakefulnessState.AWAKE
 
@@ -43,6 +47,11 @@
     fun isAwakeFromTap() =
         state == WakefulnessState.STARTING_TO_WAKE && lastWakeReason == WakeSleepReason.TAP
 
+    fun isDeviceInteractiveFromTapOrGesture(): Boolean {
+        return isDeviceInteractive() &&
+            (lastWakeReason == WakeSleepReason.TAP || lastWakeReason == WakeSleepReason.GESTURE)
+    }
+
     companion object {
         fun fromWakefulnessLifecycle(wakefulnessLifecycle: WakefulnessLifecycle): WakefulnessModel {
             return WakefulnessModel(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.kt
new file mode 100644
index 0000000..ebf1beb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/adapter/UdfpsKeyguardViewControllerAdapter.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.keyguard.ui.adapter
+
+/**
+ * Temporary adapter class while
+ * [com.android.systemui.biometrics.ui.controller.UdfpsKeyguardViewController] is being refactored
+ * before [com.android.systemui.biometrics.UdfpsKeyguardViewControllerLegacy] is removed.
+ *
+ * TODO (b/278719514): Delete once udfps keyguard view is fully refactored.
+ */
+interface UdfpsKeyguardViewControllerAdapter
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 a8d662c..7d14198 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
@@ -21,12 +21,10 @@
 import android.graphics.Rect
 import android.graphics.drawable.Animatable2
 import android.util.Size
-import android.util.TypedValue
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewPropertyAnimator
 import android.widget.ImageView
-import android.widget.TextView
 import androidx.core.view.isInvisible
 import androidx.core.view.isVisible
 import androidx.core.view.updateLayoutParams
@@ -108,14 +106,10 @@
         activityStarter: ActivityStarter?,
         messageDisplayer: (Int) -> Unit,
     ): Binding {
-        val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area)
         val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container)
         val startButton: ImageView = view.requireViewById(R.id.start_button)
         val endButton: ImageView = view.requireViewById(R.id.end_button)
         val overlayContainer: View = view.requireViewById(R.id.overlay_container)
-        val indicationText: TextView = view.requireViewById(R.id.keyguard_indication_text)
-        val indicationTextBottom: TextView =
-            view.requireViewById(R.id.keyguard_indication_text_bottom)
         val settingsMenu: LaunchableLinearLayout =
             view.requireViewById(R.id.keyguard_settings_button)
 
@@ -183,7 +177,6 @@
                                 }
 
                             ambientIndicationArea?.alpha = alpha
-                            indicationArea.alpha = alpha
                         }
                     }
 
@@ -205,50 +198,23 @@
 
                     launch {
                         viewModel.indicationAreaTranslationX.collect { translationX ->
-                            indicationArea.translationX = translationX
                             ambientIndicationArea?.translationX = translationX
                         }
                     }
 
                     launch {
-                        combine(
-                                viewModel.isIndicationAreaPadded,
-                                configurationBasedDimensions.map { it.indicationAreaPaddingPx },
-                            ) { isPadded, paddingIfPaddedPx ->
-                                if (isPadded) {
-                                    paddingIfPaddedPx
-                                } else {
-                                    0
-                                }
-                            }
-                            .collect { paddingPx ->
-                                indicationArea.setPadding(paddingPx, 0, paddingPx, 0)
-                            }
-                    }
-
-                    launch {
                         configurationBasedDimensions
                             .map { it.defaultBurnInPreventionYOffsetPx }
                             .flatMapLatest { defaultBurnInOffsetY ->
                                 viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
                             }
                             .collect { translationY ->
-                                indicationArea.translationY = translationY
                                 ambientIndicationArea?.translationY = translationY
                             }
                     }
 
                     launch {
                         configurationBasedDimensions.collect { dimensions ->
-                            indicationText.setTextSize(
-                                TypedValue.COMPLEX_UNIT_PX,
-                                dimensions.indicationTextSizePx.toFloat(),
-                            )
-                            indicationTextBottom.setTextSize(
-                                TypedValue.COMPLEX_UNIT_PX,
-                                dimensions.indicationTextSizePx.toFloat(),
-                            )
-
                             startButton.updateLayoutParams<ViewGroup.LayoutParams> {
                                 width = dimensions.buttonSizePx.width
                                 height = dimensions.buttonSizePx.height
@@ -305,7 +271,7 @@
 
         return object : Binding {
             override fun getIndicationAreaAnimators(): List<ViewPropertyAnimator> {
-                return listOf(indicationArea, ambientIndicationArea).mapNotNull { it?.animate() }
+                return listOf(ambientIndicationArea).mapNotNull { it?.animate() }
             }
 
             override fun onConfigurationChanged() {
@@ -517,12 +483,6 @@
         return ConfigurationBasedDimensions(
             defaultBurnInPreventionYOffsetPx =
                 view.resources.getDimensionPixelOffset(R.dimen.default_burn_in_prevention_offset),
-            indicationAreaPaddingPx =
-                view.resources.getDimensionPixelOffset(R.dimen.keyguard_indication_area_padding),
-            indicationTextSizePx =
-                view.resources.getDimensionPixelSize(
-                    com.android.internal.R.dimen.text_size_small_material,
-                ),
             buttonSizePx =
                 Size(
                     view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width),
@@ -552,8 +512,6 @@
 
     private data class ConfigurationBasedDimensions(
         val defaultBurnInPreventionYOffsetPx: Int,
-        val indicationAreaPaddingPx: Int,
-        val indicationTextSizePx: Int,
         val buttonSizePx: Size,
     )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
deleted file mode 100644
index c1aefc7..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
+++ /dev/null
@@ -1,237 +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.ui.binder
-
-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.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.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 kotlinx.coroutines.awaitCancellation
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.launch
-
-/** Binds the bouncer container to its view model. */
-object KeyguardBouncerViewBinder {
-    @JvmStatic
-    fun bind(
-        view: ViewGroup,
-        viewModel: KeyguardBouncerViewModel,
-        primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
-        componentFactory: KeyguardBouncerComponent.Factory
-    ) {
-        // 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 = securityContainerController.currentSecurityMode
-                    return mode == KeyguardSecurityModel.SecurityMode.SimPin ||
-                        mode == KeyguardSecurityModel.SecurityMode.SimPuk
-                }
-
-                override fun getBackCallback(): OnBackAnimationCallback {
-                    return securityContainerController.backCallback
-                }
-
-                override fun shouldDismissOnMenuPressed(): Boolean {
-                    return securityContainerController.shouldEnableMenuKey()
-                }
-
-                override fun interceptMediaKey(event: KeyEvent?): Boolean {
-                    return securityContainerController.interceptMediaKey(event)
-                }
-
-                override fun dispatchBackKeyEventPreIme(): Boolean {
-                    return securityContainerController.dispatchBackKeyEventPreIme()
-                }
-
-                override fun showNextSecurityScreenOrFinish(): Boolean {
-                    return securityContainerController.dismiss(
-                        KeyguardUpdateMonitor.getCurrentUser()
-                    )
-                }
-
-                override fun resume() {
-                    securityContainerController.showPrimarySecurityScreen(/* isTurningOff= */ false)
-                    securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON)
-                }
-
-                override fun setDismissAction(
-                    onDismissAction: ActivityStarter.OnDismissAction?,
-                    cancelAction: Runnable?
-                ) {
-                    securityContainerController.setOnDismissAction(onDismissAction, cancelAction)
-                }
-
-                override fun willDismissWithActions(): Boolean {
-                    return securityContainerController.hasDismissActions()
-                }
-
-                override fun willRunDismissFromKeyguard(): Boolean {
-                    return securityContainerController.willRunDismissFromKeyguard()
-                }
-
-                override fun showPromptReason(reason: Int) {
-                    securityContainerController.showPromptReason(reason)
-                }
-            }
-        view.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.CREATED) {
-                try {
-                    viewModel.setBouncerViewDelegate(delegate)
-                    launch {
-                        viewModel.isShowing.collect { isShowing ->
-                            view.visibility = if (isShowing) View.VISIBLE else View.INVISIBLE
-                            if (isShowing) {
-                                securityContainerController.reinflateViewFlipper {
-                                    // Reset Security Container entirely.
-                                    securityContainerController.onBouncerVisibilityChanged(
-                                        /* isVisible= */ true
-                                    )
-                                    securityContainerController.showPrimarySecurityScreen(
-                                        /* turningOff= */ false
-                                    )
-                                    securityContainerController.setInitialMessage()
-                                    securityContainerController.appear()
-                                    securityContainerController.onResume(
-                                        KeyguardSecurityView.SCREEN_ON
-                                    )
-                                }
-                            } else {
-                                securityContainerController.onBouncerVisibilityChanged(
-                                    /* isVisible= */ false
-                                )
-                                securityContainerController.cancelDismissAction()
-                                securityContainerController.reset()
-                                securityContainerController.onPause()
-                            }
-                        }
-                    }
-
-                    launch {
-                        viewModel.startingToHide.collect {
-                            securityContainerController.onStartingToHide()
-                        }
-                    }
-
-                    launch {
-                        viewModel.startDisappearAnimation.collect {
-                            securityContainerController.startDisappearAnimation(it)
-                        }
-                    }
-
-                    launch {
-                        viewModel.bouncerExpansionAmount.collect { expansion ->
-                            securityContainerController.setExpansion(expansion)
-                        }
-                    }
-
-                    launch {
-                        primaryBouncerToGoneTransitionViewModel.bouncerAlpha.collect { alpha ->
-                            securityContainerController.setAlpha(alpha)
-                        }
-                    }
-
-                    launch {
-                        viewModel.bouncerExpansionAmount
-                            .filter { it == EXPANSION_VISIBLE }
-                            .collect {
-                                securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON)
-                                view.announceForAccessibility(securityContainerController.title)
-                            }
-                    }
-
-                    launch {
-                        viewModel.isInteractable.collect { isInteractable ->
-                            securityContainerController.setInteractable(isInteractable)
-                        }
-                    }
-
-                    launch {
-                        viewModel.keyguardPosition.collect { position ->
-                            securityContainerController.updateKeyguardPosition(position)
-                        }
-                    }
-
-                    launch {
-                        viewModel.updateResources.collect {
-                            securityContainerController.updateResources()
-                            viewModel.notifyUpdateResources()
-                        }
-                    }
-
-                    launch {
-                        viewModel.bouncerShowMessage.collect {
-                            securityContainerController.showMessage(
-                                it.message,
-                                it.colorStateList,
-                                /* animated= */ true
-                            )
-                            viewModel.onMessageShown()
-                        }
-                    }
-
-                    launch {
-                        viewModel.keyguardAuthenticated.collect {
-                            securityContainerController.finish(
-                                it,
-                                KeyguardUpdateMonitor.getCurrentUser()
-                            )
-                            viewModel.notifyKeyguardAuthenticated()
-                        }
-                    }
-
-                    launch {
-                        viewModel
-                            .observeOnIsBackButtonEnabled { view.systemUiVisibility }
-                            .collect { view.systemUiVisibility = it }
-                    }
-
-                    launch {
-                        viewModel.shouldUpdateSideFps.collect {
-                            viewModel.updateSideFpsVisibility()
-                        }
-                    }
-
-                    launch {
-                        viewModel.sideFpsShowing.collect {
-                            securityContainerController.updateSideFpsVisibility(it)
-                        }
-                    }
-                    awaitCancellation()
-                } finally {
-                    viewModel.setBouncerViewDelegate(null)
-                }
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
new file mode 100644
index 0000000..02e6765
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.binder
+
+import android.util.TypedValue
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.KeyguardIndicationController
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/**
+ * Binds a keyguard indication area view to its view-model.
+ *
+ * To use this properly, users should maintain a one-to-one relationship between the [View] and the
+ * view-binding, binding each view only once. It is okay and expected for the same instance of the
+ * view-model to be reused for multiple view/view-binder bindings.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+object KeyguardIndicationAreaBinder {
+
+    /** Binds the view to the view-model, continuing to update the former based on the latter. */
+    @JvmStatic
+    fun bind(
+        view: ViewGroup,
+        viewModel: KeyguardIndicationAreaViewModel,
+        indicationController: KeyguardIndicationController,
+    ): DisposableHandle {
+        val indicationArea: ViewGroup = view.requireViewById(R.id.keyguard_indication_area)
+        indicationController.setIndicationArea(indicationArea)
+
+        val indicationText: TextView = indicationArea.requireViewById(R.id.keyguard_indication_text)
+        val indicationTextBottom: TextView =
+            indicationArea.requireViewById(R.id.keyguard_indication_text_bottom)
+
+        view.clipChildren = false
+        view.clipToPadding = false
+
+        val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
+        val disposableHandle =
+            view.repeatWhenAttached {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    launch {
+                        viewModel.alpha.collect { alpha ->
+                            view.importantForAccessibility =
+                                if (alpha == 0f) {
+                                    View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                                } else {
+                                    View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+                                }
+
+                            indicationArea.alpha = alpha
+                        }
+                    }
+
+                    launch {
+                        viewModel.indicationAreaTranslationX.collect { translationX ->
+                            indicationArea.translationX = translationX
+                        }
+                    }
+
+                    launch {
+                        combine(
+                                viewModel.isIndicationAreaPadded,
+                                configurationBasedDimensions.map { it.indicationAreaPaddingPx },
+                            ) { isPadded, paddingIfPaddedPx ->
+                                if (isPadded) {
+                                    paddingIfPaddedPx
+                                } else {
+                                    0
+                                }
+                            }
+                            .collect { paddingPx ->
+                                indicationArea.setPadding(paddingPx, 0, paddingPx, 0)
+                            }
+                    }
+
+                    launch {
+                        configurationBasedDimensions
+                            .map { it.defaultBurnInPreventionYOffsetPx }
+                            .flatMapLatest { defaultBurnInOffsetY ->
+                                viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
+                            }
+                            .collect { translationY -> indicationArea.translationY = translationY }
+                    }
+
+                    launch {
+                        configurationBasedDimensions.collect { dimensions ->
+                            indicationText.setTextSize(
+                                TypedValue.COMPLEX_UNIT_PX,
+                                dimensions.indicationTextSizePx.toFloat(),
+                            )
+                            indicationTextBottom.setTextSize(
+                                TypedValue.COMPLEX_UNIT_PX,
+                                dimensions.indicationTextSizePx.toFloat(),
+                            )
+                        }
+                    }
+
+                    launch {
+                        viewModel.configurationChange.collect {
+                            configurationBasedDimensions.value = loadFromResources(view)
+                        }
+                    }
+                }
+            }
+        return disposableHandle
+    }
+
+    private fun loadFromResources(view: View): ConfigurationBasedDimensions {
+        return ConfigurationBasedDimensions(
+            defaultBurnInPreventionYOffsetPx =
+                view.resources.getDimensionPixelOffset(R.dimen.default_burn_in_prevention_offset),
+            indicationAreaPaddingPx =
+                view.resources.getDimensionPixelOffset(R.dimen.keyguard_indication_area_padding),
+            indicationTextSizePx =
+                view.resources.getDimensionPixelSize(
+                    com.android.internal.R.dimen.text_size_small_material,
+                ),
+        )
+    }
+
+    private data class ConfigurationBasedDimensions(
+        val defaultBurnInPreventionYOffsetPx: Int,
+        val indicationAreaPaddingPx: Int,
+        val indicationTextSizePx: Int,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt
new file mode 100644
index 0000000..728dd39
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.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.binder
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.airbnb.lottie.LottieAnimationView
+import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+
+@ExperimentalCoroutinesApi
+object UdfpsAodFingerprintViewBinder {
+
+    /**
+     * Drives UI for the UDFPS aod fingerprint view. See [UdfpsFingerprintViewBinder] and
+     * [UdfpsBackgroundViewBinder].
+     */
+    @JvmStatic
+    fun bind(
+        view: LottieAnimationView,
+        viewModel: UdfpsAodViewModel,
+    ) {
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    viewModel.burnInOffsets.collect { burnInOffsets ->
+                        view.progress = burnInOffsets.burnInProgress
+                        view.translationX = burnInOffsets.burnInXOffset.toFloat()
+                        view.translationY = burnInOffsets.burnInYOffset.toFloat()
+                    }
+                }
+
+                launch { viewModel.alpha.collect { alpha -> view.alpha = alpha } }
+
+                launch {
+                    viewModel.padding.collect { padding ->
+                        view.setPadding(padding, padding, padding, padding)
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt
new file mode 100644
index 0000000..26ef468
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.content.res.ColorStateList
+import android.widget.ImageView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+
+@ExperimentalCoroutinesApi
+object UdfpsBackgroundViewBinder {
+
+    /**
+     * Drives UI for the udfps background view. See [UdfpsAodFingerprintViewBinder] and
+     * [UdfpsFingerprintViewBinder].
+     */
+    @JvmStatic
+    fun bind(
+        view: ImageView,
+        viewModel: BackgroundViewModel,
+    ) {
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    viewModel.transition.collect {
+                        view.alpha = it.alpha
+                        view.scaleX = it.scale
+                        view.scaleY = it.scale
+                        view.imageTintList = ColorStateList.valueOf(it.color)
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt
new file mode 100644
index 0000000..0ab8e52
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.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.keyguard.ui.binder
+
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.airbnb.lottie.LottieAnimationView
+import com.airbnb.lottie.LottieProperty
+import com.airbnb.lottie.model.KeyPath
+import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+
+@ExperimentalCoroutinesApi
+object UdfpsFingerprintViewBinder {
+    private var udfpsIconColor = 0
+
+    /**
+     * Drives UI for the UDFPS fingerprint view when it's NOT on aod. See
+     * [UdfpsAodFingerprintViewBinder] and [UdfpsBackgroundViewBinder].
+     */
+    @JvmStatic
+    fun bind(
+        view: LottieAnimationView,
+        viewModel: FingerprintViewModel,
+    ) {
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    viewModel.transition.collect {
+                        view.alpha = it.alpha
+                        view.scaleX = it.scale
+                        view.scaleY = it.scale
+                        if (udfpsIconColor != (it.color)) {
+                            udfpsIconColor = it.color
+                            view.invalidate()
+                        }
+                    }
+                }
+
+                launch {
+                    viewModel.burnInOffsets.collect { burnInOffsets ->
+                        view.translationX = burnInOffsets.burnInXOffset.toFloat()
+                        view.translationY = burnInOffsets.burnInYOffset.toFloat()
+                    }
+                }
+
+                launch {
+                    viewModel.dozeAmount.collect { dozeAmount ->
+                        // Lottie progress represents: aod=0 to lockscreen=1
+                        view.progress = 1f - dozeAmount
+                    }
+                }
+
+                launch {
+                    viewModel.padding.collect { padding ->
+                        view.setPadding(padding, padding, padding, padding)
+                    }
+                }
+            }
+        }
+
+        // Add a callback that updates the color to `udfpsIconColor` whenever invalidate is called
+        view.addValueCallback(KeyPath("**"), LottieProperty.COLOR_FILTER) {
+            PorterDuffColorFilter(udfpsIconColor, PorterDuff.Mode.SRC_ATOP)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt
new file mode 100644
index 0000000..b568a9a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardInternalViewBinder.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.ui.binder
+
+import android.view.View
+import com.android.systemui.R
+import com.android.systemui.keyguard.ui.binder.UdfpsAodFingerprintViewBinder
+import com.android.systemui.keyguard.ui.binder.UdfpsBackgroundViewBinder
+import com.android.systemui.keyguard.ui.binder.UdfpsFingerprintViewBinder
+import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel
+import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel
+import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel
+import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardInternalViewModel
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+object UdfpsKeyguardInternalViewBinder {
+
+    @JvmStatic
+    fun bind(
+        view: View,
+        viewModel: UdfpsKeyguardInternalViewModel,
+        aodViewModel: UdfpsAodViewModel,
+        fingerprintViewModel: FingerprintViewModel,
+        backgroundViewModel: BackgroundViewModel,
+    ) {
+        view.accessibilityDelegate = viewModel.accessibilityDelegate
+
+        // bind child views
+        UdfpsAodFingerprintViewBinder.bind(view.findViewById(R.id.udfps_aod_fp), aodViewModel)
+        UdfpsFingerprintViewBinder.bind(
+            view.findViewById(R.id.udfps_lockscreen_fp),
+            fingerprintViewModel
+        )
+        UdfpsBackgroundViewBinder.bind(
+            view.findViewById(R.id.udfps_keyguard_fp_bg),
+            backgroundViewModel
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt
new file mode 100644
index 0000000..667abae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsKeyguardViewBinder.kt
@@ -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.systemui.keyguard.ui.binder
+
+import android.graphics.RectF
+import android.view.View
+import android.widget.FrameLayout
+import androidx.asynclayoutinflater.view.AsyncLayoutInflater
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.biometrics.UdfpsKeyguardView
+import com.android.systemui.biometrics.ui.binder.UdfpsKeyguardInternalViewBinder
+import com.android.systemui.keyguard.ui.viewmodel.BackgroundViewModel
+import com.android.systemui.keyguard.ui.viewmodel.FingerprintViewModel
+import com.android.systemui.keyguard.ui.viewmodel.UdfpsAodViewModel
+import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardInternalViewModel
+import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+@ExperimentalCoroutinesApi
+object UdfpsKeyguardViewBinder {
+    /**
+     * Drives UI for the keyguard UDFPS view. Inflates child views on a background thread. For view
+     * binders for its child views, see [UdfpsFingerprintViewBinder], [UdfpsBackgroundViewBinder] &
+     * [UdfpsAodFingerprintViewBinder].
+     */
+    @JvmStatic
+    fun bind(
+        view: UdfpsKeyguardView,
+        viewModel: UdfpsKeyguardViewModel,
+        udfpsKeyguardInternalViewModel: UdfpsKeyguardInternalViewModel,
+        aodViewModel: UdfpsAodViewModel,
+        fingerprintViewModel: FingerprintViewModel,
+        backgroundViewModel: BackgroundViewModel,
+    ) {
+        view.useExpandedOverlay(viewModel.useExpandedOverlay())
+
+        val layoutInflaterFinishListener =
+            AsyncLayoutInflater.OnInflateFinishedListener { inflatedInternalView, _, parent ->
+                UdfpsKeyguardInternalViewBinder.bind(
+                    inflatedInternalView,
+                    udfpsKeyguardInternalViewModel,
+                    aodViewModel,
+                    fingerprintViewModel,
+                    backgroundViewModel,
+                )
+                if (viewModel.useExpandedOverlay()) {
+                    val lp = inflatedInternalView.layoutParams as FrameLayout.LayoutParams
+                    lp.width = viewModel.sensorBounds.width()
+                    lp.height = viewModel.sensorBounds.height()
+                    val relativeToView =
+                        getBoundsRelativeToView(
+                            inflatedInternalView,
+                            RectF(viewModel.sensorBounds),
+                        )
+                    lp.setMarginsRelative(
+                        relativeToView.left.toInt(),
+                        relativeToView.top.toInt(),
+                        relativeToView.right.toInt(),
+                        relativeToView.bottom.toInt(),
+                    )
+                    parent!!.addView(inflatedInternalView, lp)
+                } else {
+                    parent!!.addView(inflatedInternalView)
+                }
+            }
+        val inflater = AsyncLayoutInflater(view.context)
+        inflater.inflate(R.layout.udfps_keyguard_view_internal, view, layoutInflaterFinishListener)
+
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                launch {
+                    combine(aodViewModel.isVisible, fingerprintViewModel.visible) {
+                            isAodVisible,
+                            isFingerprintVisible ->
+                            isAodVisible || isFingerprintVisible
+                        }
+                        .collect { view.setVisible(it) }
+                }
+            }
+        }
+    }
+
+    /**
+     * Converts coordinates of RectF relative to the screen to coordinates relative to this view.
+     *
+     * @param bounds RectF based off screen coordinates in current orientation
+     */
+    private fun getBoundsRelativeToView(view: View, bounds: RectF): RectF {
+        val pos: IntArray = view.locationOnScreen
+        return RectF(
+            bounds.left - pos[0],
+            bounds.top - pos[1],
+            bounds.right - pos[0],
+            bounds.bottom - pos[1]
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationArea.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationArea.kt
new file mode 100644
index 0000000..890d565
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardIndicationArea.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.keyguard.ui.view
+
+import android.content.Context
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.LinearLayout
+import com.android.systemui.R
+import com.android.systemui.statusbar.phone.KeyguardIndicationTextView
+
+class KeyguardIndicationArea(
+    context: Context,
+    private val attrs: AttributeSet?,
+) :
+    LinearLayout(
+        context,
+        attrs,
+    ) {
+
+    init {
+        setId(R.id.keyguard_indication_area)
+        orientation = LinearLayout.VERTICAL
+
+        addView(indicationTopRow(), LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT))
+        addView(
+            indicationBottomRow(),
+            LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
+                gravity = Gravity.CENTER_HORIZONTAL
+            }
+        )
+    }
+
+    private fun indicationTopRow(): KeyguardIndicationTextView {
+        return KeyguardIndicationTextView(context, attrs).apply {
+            id = R.id.keyguard_indication_text
+            gravity = Gravity.CENTER
+            accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_POLITE
+            setTextAppearance(R.style.TextAppearance_Keyguard_BottomArea)
+
+            val padding = R.dimen.keyguard_indication_text_padding.dp()
+            setPaddingRelative(padding, 0, padding, 0)
+        }
+    }
+
+    private fun indicationBottomRow(): KeyguardIndicationTextView {
+        return KeyguardIndicationTextView(context, attrs).apply {
+            id = R.id.keyguard_indication_text_bottom
+            gravity = Gravity.CENTER
+            accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_POLITE
+
+            setTextAppearance(R.style.TextAppearance_Keyguard_BottomArea)
+            setEllipsize(TextUtils.TruncateAt.END)
+            setAlpha(0.8f)
+            setMinHeight(R.dimen.keyguard_indication_text_min_height.dp())
+            setMaxLines(2)
+            setVisibility(View.GONE)
+
+            val padding = R.dimen.keyguard_indication_text_padding.dp()
+            setPaddingRelative(padding, 0, padding, 0)
+        }
+    }
+
+    private fun Int.dp(): Int {
+        return context.resources.getDimensionPixelSize(this)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardRootView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardRootView.kt
new file mode 100644
index 0000000..0077f2d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/KeyguardRootView.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.view
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.android.keyguard.LockIconView
+import com.android.systemui.R
+
+/** Provides a container for all keyguard ui content. */
+class KeyguardRootView(
+    context: Context,
+    private val attrs: AttributeSet?,
+) :
+    ConstraintLayout(
+        context,
+        attrs,
+    ) {
+
+    init {
+        addIndicationTextArea()
+        addLockIconView()
+    }
+
+    private fun addIndicationTextArea() {
+        val view = KeyguardIndicationArea(context, attrs)
+        addView(view)
+    }
+
+    private fun addLockIconView() {
+        val view = LockIconView(context, attrs).apply { id = R.id.lock_icon_view }
+        addView(view)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/DefaultLockscreenLayout.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/DefaultLockscreenLayout.kt
new file mode 100644
index 0000000..baaeb60
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/DefaultLockscreenLayout.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.view.layout
+
+import android.content.Context
+import android.graphics.Point
+import android.graphics.Rect
+import android.util.DisplayMetrics
+import android.view.View
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.view.WindowManager
+import androidx.annotation.VisibleForTesting
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
+import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import androidx.constraintlayout.widget.ConstraintSet.TOP
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.R
+import com.android.systemui.biometrics.AuthController
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.ui.view.KeyguardRootView
+import javax.inject.Inject
+
+/**
+ * Positions elements of the lockscreen to the default position.
+ *
+ * This will be the most common use case for phones in portrait mode.
+ */
+@SysUISingleton
+class DefaultLockscreenLayout
+@Inject
+constructor(
+    private val authController: AuthController,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val windowManager: WindowManager,
+    private val context: Context,
+) : LockscreenLayout {
+    override val id: String = DEFAULT
+
+    override fun layoutIndicationArea(rootView: KeyguardRootView) {
+        val indicationArea = rootView.findViewById<View>(R.id.keyguard_indication_area) ?: return
+
+        rootView.getConstraintSet().apply {
+            constrainWidth(indicationArea.id, MATCH_PARENT)
+            constrainHeight(indicationArea.id, WRAP_CONTENT)
+            connect(
+                indicationArea.id,
+                BOTTOM,
+                PARENT_ID,
+                BOTTOM,
+                R.dimen.keyguard_indication_margin_bottom.dp()
+            )
+            connect(indicationArea.id, START, PARENT_ID, START)
+            connect(indicationArea.id, END, PARENT_ID, END)
+            applyTo(rootView)
+        }
+    }
+
+    override fun layoutLockIcon(rootView: KeyguardRootView) {
+        val isUdfpsSupported = keyguardUpdateMonitor.isUdfpsSupported
+        val scaleFactor: Float = authController.scaleFactor
+        val mBottomPaddingPx = R.dimen.lock_icon_margin_bottom.dp()
+        val mDefaultPaddingPx = R.dimen.lock_icon_padding.dp()
+        val scaledPadding: Int = (mDefaultPaddingPx * scaleFactor).toInt()
+        val bounds = windowManager.currentWindowMetrics.bounds
+        val widthPixels = bounds.right.toFloat()
+        val heightPixels = bounds.bottom.toFloat()
+        val defaultDensity =
+            DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
+                DisplayMetrics.DENSITY_DEFAULT.toFloat()
+        val lockIconRadiusPx = (defaultDensity * 36).toInt()
+
+        if (isUdfpsSupported) {
+            authController.udfpsLocation?.let { udfpsLocation ->
+                centerLockIcon(udfpsLocation, authController.udfpsRadius, scaledPadding, rootView)
+            }
+        } else {
+            centerLockIcon(
+                Point(
+                    (widthPixels / 2).toInt(),
+                    (heightPixels - ((mBottomPaddingPx + lockIconRadiusPx) * scaleFactor)).toInt()
+                ),
+                lockIconRadiusPx * scaleFactor,
+                scaledPadding,
+                rootView
+            )
+        }
+    }
+
+    @VisibleForTesting
+    internal fun centerLockIcon(
+        center: Point,
+        radius: Float,
+        drawablePadding: Int,
+        rootView: KeyguardRootView,
+    ) {
+        val lockIconView = rootView.findViewById<View>(R.id.lock_icon_view) ?: return
+        val lockIcon = lockIconView.findViewById<View>(R.id.lock_icon) ?: return
+        lockIcon.setPadding(drawablePadding, drawablePadding, drawablePadding, drawablePadding)
+
+        val sensorRect =
+            Rect().apply {
+                set(
+                    center.x - radius.toInt(),
+                    center.y - radius.toInt(),
+                    center.x + radius.toInt(),
+                    center.y + radius.toInt(),
+                )
+            }
+
+        rootView.getConstraintSet().apply {
+            constrainWidth(lockIconView.id, sensorRect.right - sensorRect.left)
+            constrainHeight(lockIconView.id, sensorRect.bottom - sensorRect.top)
+            connect(lockIconView.id, TOP, PARENT_ID, TOP, sensorRect.top)
+            connect(lockIconView.id, START, PARENT_ID, START, sensorRect.left)
+            applyTo(rootView)
+        }
+    }
+
+    private fun Int.dp(): Int {
+        return context.resources.getDimensionPixelSize(this)
+    }
+
+    private fun ConstraintLayout.getConstraintSet(): ConstraintSet {
+        val cs = ConstraintSet()
+        cs.clone(this)
+        return cs
+    }
+
+    companion object {
+        const val DEFAULT = "default"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManager.kt
new file mode 100644
index 0000000..9bc6302
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManager.kt
@@ -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.keyguard.ui.view.layout
+
+import android.content.res.Configuration
+import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.ui.view.KeyguardRootView
+import com.android.systemui.keyguard.ui.view.layout.DefaultLockscreenLayout.Companion.DEFAULT
+import com.android.systemui.statusbar.policy.ConfigurationController
+import javax.inject.Inject
+
+/**
+ * Manages layout changes for the lockscreen.
+ *
+ * To add a layout, add an entry to the map with a unique id and call #transitionToLayout(string).
+ */
+@SysUISingleton
+class KeyguardLayoutManager
+@Inject
+constructor(
+    configurationController: ConfigurationController,
+    layouts: Set<@JvmSuppressWildcards LockscreenLayout>,
+    private val keyguardRootView: KeyguardRootView,
+) {
+    internal val layoutIdMap: Map<String, LockscreenLayout> = layouts.associateBy { it.id }
+    private var layout: LockscreenLayout? = layoutIdMap[DEFAULT]
+
+    init {
+        configurationController.addCallback(
+            object : ConfigurationController.ConfigurationListener {
+                override fun onConfigChanged(newConfig: Configuration?) {
+                    layoutViews()
+                }
+            }
+        )
+    }
+
+    /**
+     * Transitions to a layout.
+     *
+     * @param layoutId
+     * @return whether the transition has succeeded.
+     */
+    fun transitionToLayout(layoutId: String): Boolean {
+        layout = layoutIdMap[layoutId] ?: return false
+        layoutViews()
+        return true
+    }
+
+    fun layoutViews() {
+        layout?.layoutViews(keyguardRootView)
+    }
+
+    companion object {
+        const val TAG = "KeyguardLayoutManager"
+    }
+}
+
+interface LockscreenLayout {
+    val id: String
+
+    fun layoutViews(rootView: KeyguardRootView) {
+        // Clear constraints.
+        ConstraintSet()
+            .apply {
+                clone(rootView)
+                knownIds.forEach { getConstraint(it).layout.copyFrom(ConstraintSet.Layout()) }
+            }
+            .applyTo(rootView)
+        layoutIndicationArea(rootView)
+        layoutLockIcon(rootView)
+    }
+    fun layoutIndicationArea(rootView: KeyguardRootView)
+    fun layoutLockIcon(rootView: KeyguardRootView)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManagerCommandListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManagerCommandListener.kt
new file mode 100644
index 0000000..b351ea8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManagerCommandListener.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.keyguard.ui.view.layout
+
+import com.android.systemui.statusbar.commandline.Command
+import com.android.systemui.statusbar.commandline.CommandRegistry
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/** Uses $ adb shell cmd statusbar layout <LayoutId> */
+class KeyguardLayoutManagerCommandListener
+@Inject
+constructor(
+    private val commandRegistry: CommandRegistry,
+    private val keyguardLayoutManager: KeyguardLayoutManager
+) {
+    private val layoutCommand = KeyguardLayoutManagerCommand()
+
+    fun start() {
+        commandRegistry.registerCommand(COMMAND) { layoutCommand }
+    }
+
+    internal inner class KeyguardLayoutManagerCommand : Command {
+        override fun execute(pw: PrintWriter, args: List<String>) {
+            val arg = args.getOrNull(0)
+            if (arg == null || arg.lowercase() == "help") {
+                help(pw)
+                return
+            }
+
+            if (keyguardLayoutManager.transitionToLayout(arg)) {
+                pw.println("Transition succeeded!")
+            } else {
+                pw.println("Invalid argument! To see available layout ids, run:")
+                pw.println("$ adb shell cmd statusbar layout help")
+            }
+        }
+
+        override fun help(pw: PrintWriter) {
+            pw.println("Usage: $ adb shell cmd statusbar layout <layoutId>")
+            pw.println("Existing Layout Ids: ")
+            keyguardLayoutManager.layoutIdMap.forEach { entry -> pw.println("${entry.key}") }
+        }
+    }
+
+    companion object {
+        internal const val COMMAND = "layout"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/LockscreenLayoutModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/LockscreenLayoutModule.kt
new file mode 100644
index 0000000..00f93e3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/LockscreenLayoutModule.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.ui.view.layout
+
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+
+@Module
+abstract class LockscreenLayoutModule {
+    @Binds
+    @IntoSet
+    abstract fun bindDefaultLayout(
+        defaultLockscreenLayout: DefaultLockscreenLayout
+    ): LockscreenLayout
+}
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 62a1a9e..3e6f8e6 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
@@ -57,7 +57,7 @@
      * in the wallpaper picker application. This should _always_ be `false` for the real lock screen
      * experience.
      */
-    private val previewMode = MutableStateFlow(PreviewMode())
+    val previewMode = MutableStateFlow(PreviewMode())
 
     /**
      * ID of the slot that's currently selected in the preview that renders exclusively in the
@@ -101,12 +101,6 @@
                 bottomAreaInteractor.alpha.distinctUntilChanged()
             }
         }
-    /** An observable for whether the indication area should be padded. */
-    val isIndicationAreaPadded: Flow<Boolean> =
-        combine(startButton, endButton) { startButtonModel, endButtonModel ->
-                startButtonModel.isVisible || endButtonModel.isVisible
-            }
-            .distinctUntilChanged()
     /** An observable for the x-offset by which the indication area should be translated. */
     val indicationAreaTranslationX: Flow<Float> =
         bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
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
deleted file mode 100644
index 9602888..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
+++ /dev/null
@@ -1,112 +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.ui.viewmodel
-
-import android.view.View
-import com.android.systemui.keyguard.data.BouncerView
-import com.android.systemui.keyguard.data.BouncerViewDelegate
-import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
-import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-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
-@Inject
-constructor(
-    private val view: BouncerView,
-    private val interactor: PrimaryBouncerInteractor,
-) {
-    /** Observe on bouncer expansion amount. */
-    val bouncerExpansionAmount: Flow<Float> = interactor.panelExpansionAmount
-
-    /** Can the user interact with the view? */
-    val isInteractable: Flow<Boolean> = interactor.isInteractable
-
-    /** Observe whether bouncer is showing or not. */
-    val isShowing: Flow<Boolean> = interactor.isShowing
-
-    /** Observe whether bouncer is starting to hide. */
-    val startingToHide: Flow<Unit> = interactor.startingToHide
-
-    /** Observe whether we want to start the disappear animation. */
-    val startDisappearAnimation: Flow<Runnable> = interactor.startingDisappearAnimation
-
-    /** Observe whether we want to update keyguard position. */
-    val keyguardPosition: Flow<Float> = interactor.keyguardPosition
-
-    /** Observe whether we want to update resources. */
-    val updateResources: Flow<Boolean> = interactor.resourceUpdateRequests
-
-    /** Observe whether we want to set a keyguard message when the bouncer shows. */
-    val bouncerShowMessage: Flow<BouncerShowMessageModel> = interactor.showMessage
-
-    /** Observe whether keyguard is authenticated already. */
-    val keyguardAuthenticated: Flow<Boolean> = interactor.keyguardAuthenticated
-
-    /** Observe whether 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.isShowing.map {},
-            interactor.startingToHide,
-            interactor.startingDisappearAnimation.filterNotNull().map {}
-        )
-
-    /** Observe whether we want to update resources. */
-    fun notifyUpdateResources() {
-        interactor.notifyUpdatedResources()
-    }
-
-    /** Notify that keyguard authenticated was handled */
-    fun notifyKeyguardAuthenticated() {
-        interactor.notifyKeyguardAuthenticatedHandled()
-    }
-
-    /** Notifies that the message was shown. */
-    fun onMessageShown() {
-        interactor.onMessageShown()
-    }
-
-    fun updateSideFpsVisibility() {
-        interactor.updateSideFpsVisibility()
-    }
-
-    /** Observe whether back button is enabled. */
-    fun observeOnIsBackButtonEnabled(systemUiVisibility: () -> Int): Flow<Int> {
-        return interactor.isBackButtonEnabled.map { enabled ->
-            var vis: Int = systemUiVisibility()
-            vis =
-                if (enabled) {
-                    vis and View.STATUS_BAR_DISABLE_BACK.inv()
-                } else {
-                    vis or View.STATUS_BAR_DISABLE_BACK
-                }
-            vis
-        }
-    }
-
-    /** Set an abstraction that will hold reference to the ui delegate for the bouncer view. */
-    fun setBouncerViewDelegate(delegate: BouncerViewDelegate?) {
-        view.delegate = delegate
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
new file mode 100644
index 0000000..389cf76
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.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.keyguard.ui.viewmodel
+
+import com.android.systemui.doze.util.BurnInHelperWrapper
+import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+/** View-model for the keyguard indication area view */
+@OptIn(ExperimentalCoroutinesApi::class)
+class KeyguardIndicationAreaViewModel
+@Inject
+constructor(
+    private val keyguardInteractor: KeyguardInteractor,
+    private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
+    private val keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
+    private val burnInHelperWrapper: BurnInHelperWrapper,
+) {
+
+    /** Notifies when a new configuration is set */
+    val configurationChange: Flow<Unit> = keyguardInteractor.configurationChange
+
+    /** An observable for the alpha level for the entire bottom area. */
+    val alpha: Flow<Float> = keyguardBottomAreaViewModel.alpha
+
+    /** An observable for whether the indication area should be padded. */
+    val isIndicationAreaPadded: Flow<Boolean> =
+        combine(keyguardBottomAreaViewModel.startButton, keyguardBottomAreaViewModel.endButton) {
+                startButtonModel,
+                endButtonModel ->
+                startButtonModel.isVisible || endButtonModel.isVisible
+            }
+            .distinctUntilChanged()
+    /** An observable for the x-offset by which the indication area should be translated. */
+    val indicationAreaTranslationX: Flow<Float> =
+        bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
+
+    /** Returns an observable for the y-offset by which the indication area should be translated. */
+    fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow<Float> {
+        return keyguardInteractor.dozeAmount
+            .map { dozeAmount ->
+                dozeAmount *
+                    (burnInHelperWrapper.burnInOffset(
+                        /* amplitude = */ defaultBurnInOffset * 2,
+                        /* xAxis= */ false,
+                    ) - defaultBurnInOffset)
+            }
+            .distinctUntilChanged()
+    }
+}
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
index 68810f9..44e1fd1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
@@ -17,10 +17,10 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 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.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.keyguard.shared.model.ScrimAlpha
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.statusbar.SysuiStatusBarStateController
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt
new file mode 100644
index 0000000..667c2f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModel.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import android.content.Context
+import com.android.systemui.R
+import com.android.systemui.keyguard.domain.interactor.BurnInOffsets
+import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
+import javax.inject.Inject
+import kotlin.math.roundToInt
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** View-model for UDFPS AOD view. */
+@ExperimentalCoroutinesApi
+class UdfpsAodViewModel
+@Inject
+constructor(
+    val interactor: UdfpsKeyguardInteractor,
+    val context: Context,
+) {
+    val alpha: Flow<Float> = interactor.dozeAmount
+    val burnInOffsets: Flow<BurnInOffsets> = interactor.burnInOffsets
+    val isVisible: Flow<Boolean> = alpha.map { it != 0f }
+
+    // Padding between the fingerprint icon and its bounding box in pixels.
+    val padding: Flow<Int> =
+        interactor.scaleForResolution.map { scale ->
+            (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale)
+                .roundToInt()
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardInternalViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardInternalViewModel.kt
new file mode 100644
index 0000000..d894a11
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardInternalViewModel.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.biometrics.UdfpsKeyguardAccessibilityDelegate
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+class UdfpsKeyguardInternalViewModel
+@Inject
+constructor(val accessibilityDelegate: UdfpsKeyguardAccessibilityDelegate)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModel.kt
new file mode 100644
index 0000000..929f27f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModel.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.keyguard.ui.viewmodel
+
+import android.graphics.Rect
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+class UdfpsKeyguardViewModel
+@Inject
+constructor(
+    private val featureFlags: FeatureFlags,
+) {
+    var sensorBounds: Rect = Rect()
+
+    fun useExpandedOverlay(): Boolean {
+        return featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt
new file mode 100644
index 0000000..098b481
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsKeyguardViewModels.kt
@@ -0,0 +1,36 @@
+package com.android.systemui.keyguard.ui.viewmodel
+
+import android.graphics.Rect
+import com.android.systemui.biometrics.UdfpsKeyguardView
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.ui.binder.UdfpsKeyguardViewBinder
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class UdfpsKeyguardViewModels
+@Inject
+constructor(
+    private val viewModel: UdfpsKeyguardViewModel,
+    private val internalViewModel: UdfpsKeyguardInternalViewModel,
+    private val aodViewModel: UdfpsAodViewModel,
+    private val lockscreenFingerprintViewModel: FingerprintViewModel,
+    private val lockscreenBackgroundViewModel: BackgroundViewModel,
+) {
+
+    fun setSensorBounds(sensorBounds: Rect) {
+        viewModel.sensorBounds = sensorBounds
+    }
+
+    fun bindViews(view: UdfpsKeyguardView) {
+        UdfpsKeyguardViewBinder.bind(
+            view,
+            viewModel,
+            internalViewModel,
+            aodViewModel,
+            lockscreenFingerprintViewModel,
+            lockscreenBackgroundViewModel
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt
new file mode 100644
index 0000000..fd4b666
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import android.content.Context
+import androidx.annotation.ColorInt
+import com.android.settingslib.Utils.getColorAttrDefaultColor
+import com.android.systemui.R
+import com.android.systemui.keyguard.domain.interactor.BurnInOffsets
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import javax.inject.Inject
+import kotlin.math.roundToInt
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+
+/** View-model for UDFPS lockscreen views. */
+@ExperimentalCoroutinesApi
+open class UdfpsLockscreenViewModel(
+    context: Context,
+    lockscreenColorResId: Int,
+    alternateBouncerColorResId: Int,
+    transitionInteractor: KeyguardTransitionInteractor,
+) {
+    private val toLockscreen: Flow<TransitionViewModel> =
+        transitionInteractor.anyStateToLockscreenTransition.map {
+            TransitionViewModel(
+                alpha =
+                    if (it.from == KeyguardState.AOD) {
+                        it.value // animate
+                    } else {
+                        1f
+                    },
+                scale = 1f,
+                color = getColorAttrDefaultColor(context, lockscreenColorResId),
+            )
+        }
+
+    private val toAlternateBouncer: Flow<TransitionViewModel> =
+        transitionInteractor.anyStateToAlternateBouncerTransition.map {
+            TransitionViewModel(
+                alpha = 1f,
+                scale =
+                    if (visibleInKeyguardState(it.from)) {
+                        1f
+                    } else {
+                        it.value
+                    },
+                color = getColorAttrDefaultColor(context, alternateBouncerColorResId),
+            )
+        }
+
+    private val fadeOut: Flow<TransitionViewModel> =
+        merge(
+                transitionInteractor.anyStateToGoneTransition,
+                transitionInteractor.anyStateToAodTransition,
+                transitionInteractor.anyStateToOccludedTransition,
+                transitionInteractor.anyStateToPrimaryBouncerTransition,
+                transitionInteractor.anyStateToDreamingTransition,
+            )
+            .map {
+                TransitionViewModel(
+                    alpha =
+                        if (visibleInKeyguardState(it.from)) {
+                            1f - it.value
+                        } else {
+                            0f
+                        },
+                    scale = 1f,
+                    color =
+                        if (it.from == KeyguardState.ALTERNATE_BOUNCER) {
+                            getColorAttrDefaultColor(context, alternateBouncerColorResId)
+                        } else {
+                            getColorAttrDefaultColor(context, lockscreenColorResId)
+                        },
+                )
+            }
+
+    private fun visibleInKeyguardState(state: KeyguardState): Boolean {
+        return when (state) {
+            KeyguardState.OFF,
+            KeyguardState.DOZING,
+            KeyguardState.DREAMING,
+            KeyguardState.AOD,
+            KeyguardState.PRIMARY_BOUNCER,
+            KeyguardState.GONE,
+            KeyguardState.OCCLUDED -> false
+            KeyguardState.LOCKSCREEN,
+            KeyguardState.ALTERNATE_BOUNCER -> true
+        }
+    }
+
+    val transition: Flow<TransitionViewModel> =
+        merge(
+            toAlternateBouncer,
+            toLockscreen,
+            fadeOut,
+        )
+    val visible: Flow<Boolean> = transition.map { it.alpha != 0f }
+}
+
+@ExperimentalCoroutinesApi
+class FingerprintViewModel
+@Inject
+constructor(
+    val context: Context,
+    transitionInteractor: KeyguardTransitionInteractor,
+    interactor: UdfpsKeyguardInteractor,
+) :
+    UdfpsLockscreenViewModel(
+        context,
+        android.R.attr.textColorPrimary,
+        com.android.internal.R.attr.materialColorOnPrimaryFixed,
+        transitionInteractor,
+    ) {
+    val dozeAmount: Flow<Float> = interactor.dozeAmount
+    val burnInOffsets: Flow<BurnInOffsets> = interactor.burnInOffsets
+
+    // Padding between the fingerprint icon and its bounding box in pixels.
+    val padding: Flow<Int> =
+        interactor.scaleForResolution.map { scale ->
+            (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale)
+                .roundToInt()
+        }
+}
+
+@ExperimentalCoroutinesApi
+class BackgroundViewModel
+@Inject
+constructor(
+    val context: Context,
+    transitionInteractor: KeyguardTransitionInteractor,
+) :
+    UdfpsLockscreenViewModel(
+        context,
+        com.android.internal.R.attr.colorSurface,
+        com.android.internal.R.attr.materialColorPrimaryFixed,
+        transitionInteractor,
+    )
+
+data class TransitionViewModel(
+    val alpha: Float,
+    val scale: Float,
+    @ColorInt val color: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/util/IndicationHelper.kt b/packages/SystemUI/src/com/android/systemui/keyguard/util/IndicationHelper.kt
new file mode 100644
index 0000000..7129001
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/util/IndicationHelper.kt
@@ -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.keyguard.util
+
+import android.hardware.biometrics.BiometricFaceConstants
+import android.hardware.biometrics.BiometricFingerprintConstants
+import android.hardware.biometrics.BiometricSourceType
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+@SysUISingleton
+class IndicationHelper
+@Inject
+constructor(
+    val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+) {
+    fun shouldSuppressErrorMsg(biometricSource: BiometricSourceType, msgId: Int): Boolean {
+        return when (biometricSource) {
+            BiometricSourceType.FINGERPRINT ->
+                (isPrimaryAuthRequired() && !isFingerprintLockoutErrorMsg(msgId)) ||
+                    msgId == BiometricFingerprintConstants.FINGERPRINT_ERROR_CANCELED ||
+                    msgId == BiometricFingerprintConstants.FINGERPRINT_ERROR_USER_CANCELED ||
+                    msgId == BiometricFingerprintConstants.BIOMETRIC_ERROR_POWER_PRESSED
+            BiometricSourceType.FACE ->
+                (isPrimaryAuthRequired() && !isFaceLockoutErrorMsg(msgId)) ||
+                    msgId == BiometricFaceConstants.FACE_ERROR_CANCELED ||
+                    msgId == BiometricFaceConstants.FACE_ERROR_UNABLE_TO_PROCESS
+            else -> false
+        }
+    }
+
+    private fun isFingerprintLockoutErrorMsg(msgId: Int): Boolean {
+        return msgId == BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT ||
+            msgId == BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT
+    }
+
+    fun isFaceLockoutErrorMsg(msgId: Int): Boolean {
+        return msgId == BiometricFaceConstants.FACE_ERROR_LOCKOUT ||
+            msgId == BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT
+    }
+
+    private fun isPrimaryAuthRequired(): Boolean {
+        // 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 required), so it's ok to
+        // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the
+        // check of whether non-strong biometric is allowed since strong biometrics can still be
+        // used.
+        return !keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
index 34a6740..e064839 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
@@ -167,9 +167,10 @@
         registry.currentState = Lifecycle.State.DESTROYED
     }
 
-    override fun getLifecycle(): Lifecycle {
-        return registry
-    }
+    override val lifecycle: Lifecycle
+        get() {
+            return registry
+        }
 
     private fun updateState() {
         registry.currentState =
diff --git a/packages/SystemUI/src/com/android/systemui/log/BouncerLogger.kt b/packages/SystemUI/src/com/android/systemui/log/BouncerLogger.kt
index 3be4499..d4b799f 100644
--- a/packages/SystemUI/src/com/android/systemui/log/BouncerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/BouncerLogger.kt
@@ -16,8 +16,9 @@
 
 package com.android.systemui.log
 
+import com.android.systemui.bouncer.shared.model.BouncerMessageModel
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.dagger.BouncerLog
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
index fefa1b2..68cdfb6 100644
--- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
@@ -6,7 +6,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.log.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.DEBUG
 import com.android.systemui.log.dagger.FaceAuthLog
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt b/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt
index f727784..702a23e 100644
--- a/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt
@@ -21,9 +21,9 @@
 import android.graphics.RectF
 import androidx.core.graphics.toRectF
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.ERROR
+import com.android.systemui.log.core.LogLevel.INFO
 import com.android.systemui.log.dagger.ScreenDecorationsLog
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
@@ -164,4 +164,49 @@
     fun cameraProtectionEvent(@CompileTimeConstant cameraProtectionEvent: String) {
         logBuffer.log(TAG, DEBUG, cameraProtectionEvent)
     }
+
+    fun logRotationChangeDeferred(currentRot: Int, newRot: Int) {
+        logBuffer.log(
+            TAG,
+            INFO,
+            {
+                int1 = currentRot
+                int2 = newRot
+            },
+            { "Rotation changed, deferring $int2, staying at $int2" },
+        )
+    }
+
+    fun logRotationChanged(oldRot: Int, newRot: Int) {
+        logBuffer.log(
+            TAG,
+            INFO,
+            {
+                int1 = oldRot
+                int2 = newRot
+            },
+            { "Rotation changed from $int1 to $int2" }
+        )
+    }
+
+    fun logDisplaySizeChanged(currentSize: Point, newSize: Point) {
+        logBuffer.log(
+            TAG,
+            INFO,
+            {
+                str1 = currentSize.flattenToString()
+                str2 = newSize.flattenToString()
+            },
+            { "Resolution changed, deferring size change to $str2, staying at $str1" },
+        )
+    }
+
+    fun logUserSwitched(newUser: Int) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            { int1 = newUser },
+            { "UserSwitched newUserId=$int1. Updating color inversion setting" },
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/DisableFlagsRepositoryLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/DisableFlagsRepositoryLog.kt
new file mode 100644
index 0000000..e141291
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/DisableFlagsRepositoryLog.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.log.dagger
+
+import com.android.systemui.log.LogBuffer
+import javax.inject.Qualifier
+
+/** A [LogBuffer] for changes to [DisableFlagsRepository]. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class DisableFlagsRepositoryLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/DisplayMetricsRepoLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/DisplayMetricsRepoLog.kt
new file mode 100644
index 0000000..fa9ec88
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/DisplayMetricsRepoLog.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.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for display metrics related logging. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class DisplayMetricsRepoLog
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 0261ee5..b5759e3 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -224,6 +224,15 @@
                 false /* systrace */);
     }
 
+    /** Provides a logging buffer for the disable flags repository. */
+    @Provides
+    @SysUISingleton
+    @DisableFlagsRepositoryLog
+    public static LogBuffer provideDisableFlagsRepositoryLogBuffer(LogBufferFactory factory) {
+        return factory.create("DisableFlagsRepository", 40 /* maxSize */,
+                false /* systrace */);
+    }
+
     /** Provides a logging buffer for logs related to swipe up gestures. */
     @Provides
     @SysUISingleton
@@ -479,4 +488,12 @@
     public static LogBuffer provideDreamLogBuffer(LogBufferFactory factory) {
         return factory.create("DreamLog", 250);
     }
+
+    /** Provides a {@link LogBuffer} for display metrics related logs. */
+    @Provides
+    @SysUISingleton
+    @DisplayMetricsRepoLog
+    public static LogBuffer provideDisplayMetricsRepoLogBuffer(LogBufferFactory factory) {
+        return factory.create("DisplayMetricsRepo", 50);
+    }
 }
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 8d622ae..67a985e 100644
--- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt
@@ -21,8 +21,8 @@
 import com.android.systemui.Dumpable
 import com.android.systemui.common.buffer.RingBuffer
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.plugins.log.TableLogBufferBase
 import com.android.systemui.util.time.SystemClock
 import java.io.PrintWriter
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 19e1124..1e2f71f 100644
--- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
@@ -61,7 +61,7 @@
                 bgDispatcher,
                 coroutineScope,
             )
-        dumpManager.registerNormalDumpable(name, tableBuffer)
+        dumpManager.registerTableLogBuffer(name, tableBuffer)
         tableBuffer.init()
         return tableBuffer
     }
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 e2e269d..534241e 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
@@ -19,7 +19,7 @@
 import android.media.session.PlaybackState
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.dagger.MediaTimeoutListenerLog
 import javax.inject.Inject
 
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 b0389b5..23ee00d 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
@@ -122,9 +122,9 @@
                     Log.e(TAG, "Error getting package information", e)
                 }
 
-                Log.d(TAG, "Adding resume controls $desc")
+                Log.d(TAG, "Adding resume controls for ${browser.userId}: $desc")
                 mediaDataManager.addResumptionControls(
-                    currentUserId,
+                    browser.userId,
                     desc,
                     resumeAction,
                     token,
@@ -196,7 +196,11 @@
                 }
             resumeComponents.add(component to lastPlayed)
         }
-        Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}")
+        Log.d(
+            TAG,
+            "loaded resume components for $currentUserId: " +
+                "${resumeComponents.toArray().contentToString()}"
+        )
 
         if (needsUpdate) {
             // Save any missing times that we had to fill in
@@ -210,11 +214,21 @@
             return
         }
 
+        val pm = context.packageManager
         val now = systemClock.currentTimeMillis()
         resumeComponents.forEach {
             if (now.minus(it.second) <= RESUME_MEDIA_TIMEOUT) {
-                val browser = mediaBrowserFactory.create(mediaBrowserCallback, it.first)
-                browser.findRecentMedia()
+                // Verify that the service exists for this user
+                val intent = Intent(MediaBrowserService.SERVICE_INTERFACE)
+                intent.component = it.first
+                val inf = pm.resolveServiceAsUser(intent, 0, currentUserId)
+                if (inf != null) {
+                    val browser =
+                        mediaBrowserFactory.create(mediaBrowserCallback, it.first, currentUserId)
+                    browser.findRecentMedia()
+                } else {
+                    Log.d(TAG, "User $currentUserId does not have component ${it.first}")
+                }
             }
         }
     }
@@ -244,7 +258,7 @@
                 Log.d(TAG, "Checking for service component for " + data.packageName)
                 val pm = context.packageManager
                 val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
-                val resumeInfo = pm.queryIntentServices(serviceIntent, 0)
+                val resumeInfo = pm.queryIntentServicesAsUser(serviceIntent, 0, currentUserId)
 
                 val inf = resumeInfo?.filter { it.serviceInfo.packageName == data.packageName }
                 if (inf != null && inf.size > 0) {
@@ -280,13 +294,17 @@
                         browser: ResumeMediaBrowser
                     ) {
                         // Since this is a test, just save the component for later
-                        Log.d(TAG, "Can get resumable media from $componentName")
+                        Log.d(
+                            TAG,
+                            "Can get resumable media for ${browser.userId} from $componentName"
+                        )
                         mediaDataManager.setResumeAction(key, getResumeAction(componentName))
                         updateResumptionList(componentName)
                         mediaBrowser = null
                     }
                 },
-                componentName
+                componentName,
+                currentUserId
             )
         mediaBrowser?.testConnection()
     }
@@ -326,7 +344,7 @@
     /** Get a runnable which will resume media playback */
     private fun getResumeAction(componentName: ComponentName): Runnable {
         return Runnable {
-            mediaBrowser = mediaBrowserFactory.create(null, componentName)
+            mediaBrowser = mediaBrowserFactory.create(null, componentName, currentUserId)
             mediaBrowser?.restart()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java
index d460b5b..ceaccaf 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java
@@ -17,6 +17,7 @@
 package com.android.systemui.media.controls.resume;
 
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
@@ -53,6 +54,7 @@
     private final ResumeMediaBrowserLogger mLogger;
     private final ComponentName mComponentName;
     private final MediaController.Callback mMediaControllerCallback = new SessionDestroyCallback();
+    @UserIdInt private final int mUserId;
 
     private MediaBrowser mMediaBrowser;
     @Nullable private MediaController mMediaController;
@@ -62,18 +64,21 @@
      * @param context the context
      * @param callback used to report media items found
      * @param componentName Component name of the MediaBrowserService this browser will connect to
+     * @param userId ID of the current user
      */
     public ResumeMediaBrowser(
             Context context,
             @Nullable Callback callback,
             ComponentName componentName,
             MediaBrowserFactory browserFactory,
-            ResumeMediaBrowserLogger logger) {
+            ResumeMediaBrowserLogger logger,
+            @UserIdInt int userId) {
         mContext = context;
         mCallback = callback;
         mComponentName = componentName;
         mBrowserFactory = browserFactory;
         mLogger = logger;
+        mUserId = userId;
     }
 
     /**
@@ -285,6 +290,14 @@
     }
 
     /**
+     * Get the ID of the user associated with this broswer
+     * @return the user ID
+     */
+    public @UserIdInt int getUserId() {
+        return mUserId;
+    }
+
+    /**
      * Get the media session token
      * @return the token, or null if the MediaBrowser is null or disconnected
      */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java
index c558227..e374191 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media.controls.resume;
 
+import android.annotation.UserIdInt;
 import android.content.ComponentName;
 import android.content.Context;
 
@@ -42,10 +43,12 @@
      *
      * @param callback will be called on connection or error, and addTrack when media item found
      * @param componentName component to browse
+     * @param userId ID of the current user
      * @return
      */
     public ResumeMediaBrowser create(ResumeMediaBrowser.Callback callback,
-            ComponentName componentName) {
-        return new ResumeMediaBrowser(mContext, callback, componentName, mBrowserFactory, mLogger);
+            ComponentName componentName, @UserIdInt int userId) {
+        return new ResumeMediaBrowser(mContext, callback, componentName, mBrowserFactory, mLogger,
+            userId);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt
index 9e53d77..888b9c7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt
@@ -19,7 +19,7 @@
 import android.content.ComponentName
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.dagger.MediaBrowserLog
 import javax.inject.Inject
 
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 0ed2434..3dc0000 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
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.dagger.MediaCarouselControllerLog
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt
index c781b76..8f1595d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.dagger.MediaViewLog
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
index bbcf259..4171682 100644
--- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
@@ -3,7 +3,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.dagger.MediaMuteAwaitLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import javax.inject.Inject
 
 /** Log messages for [MediaMuteAwaitConnectionManager]. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
index 66399d5..46c0132 100644
--- a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
@@ -3,7 +3,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.dagger.NearbyMediaDevicesLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import javax.inject.Inject
 
 /** Log messages for [NearbyMediaDevicesManager]. */
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
index eeda1027..3c2226f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerUtils.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.media.taptotransfer.common
 
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 
 /** A helper for logging media tap-to-transfer events. */
 object MediaTttLoggerUtils {
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 206e5e3..503afd3 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
@@ -20,7 +20,7 @@
 import com.android.internal.logging.InstanceId
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.media.taptotransfer.common.MediaTttLoggerUtils
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartable.kt
new file mode 100644
index 0000000..3c50127
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartable.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.mediaprojection.taskswitcher
+
+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.mediaprojection.taskswitcher.ui.TaskSwitcherNotificationCoordinator
+import javax.inject.Inject
+
+@SysUISingleton
+class MediaProjectionTaskSwitcherCoreStartable
+@Inject
+constructor(
+    private val notificationCoordinator: TaskSwitcherNotificationCoordinator,
+    private val featureFlags: FeatureFlags,
+) : CoreStartable {
+
+    override fun start() {
+        if (featureFlags.isEnabled(Flags.PARTIAL_SCREEN_SHARING_TASK_SWITCHER)) {
+            notificationCoordinator.start()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherModule.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherModule.kt
new file mode 100644
index 0000000..22ad07e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherModule.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.mediaprojection.taskswitcher
+
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionRepository
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.TasksRepository
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface MediaProjectionTaskSwitcherModule {
+
+    @Binds fun mediaRepository(impl: MediaProjectionManagerRepository): MediaProjectionRepository
+
+    @Binds fun tasksRepository(impl: ActivityTaskManagerTasksRepository): TasksRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/model/MediaProjectionState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/model/MediaProjectionState.kt
new file mode 100644
index 0000000..9938f11
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/model/MediaProjectionState.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.taskswitcher.data.model
+
+import android.app.TaskInfo
+
+/** Represents the state of media projection. */
+sealed interface MediaProjectionState {
+    object NotProjecting : MediaProjectionState
+    object EntireScreen : MediaProjectionState
+    data class SingleTask(val task: TaskInfo) : MediaProjectionState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt
new file mode 100644
index 0000000..492d482
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.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.mediaprojection.taskswitcher.data.repository
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.ActivityTaskManager
+import android.app.TaskStackListener
+import android.os.IBinder
+import android.util.Log
+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 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.shareIn
+import kotlinx.coroutines.withContext
+
+/** Implementation of [TasksRepository] that uses [ActivityTaskManager] as the data source. */
+@SysUISingleton
+class ActivityTaskManagerTasksRepository
+@Inject
+constructor(
+    private val activityTaskManager: ActivityTaskManager,
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) : TasksRepository {
+
+    override suspend fun findRunningTaskFromWindowContainerToken(
+        windowContainerToken: IBinder
+    ): RunningTaskInfo? =
+        getRunningTasks().firstOrNull { taskInfo ->
+            taskInfo.token.asBinder() == windowContainerToken
+        }
+
+    private suspend fun getRunningTasks(): List<RunningTaskInfo> =
+        withContext(backgroundDispatcher) { activityTaskManager.getTasks(Integer.MAX_VALUE) }
+
+    override val foregroundTask: Flow<RunningTaskInfo> =
+        conflatedCallbackFlow {
+                val listener =
+                    object : TaskStackListener() {
+                        override fun onTaskMovedToFront(taskInfo: RunningTaskInfo) {
+                            Log.d(TAG, "onTaskMovedToFront: $taskInfo")
+                            trySendWithFailureLogging(taskInfo, TAG)
+                        }
+                    }
+                activityTaskManager.registerTaskStackListener(listener)
+                awaitClose { activityTaskManager.unregisterTaskStackListener(listener) }
+            }
+            .shareIn(applicationScope, SharingStarted.Lazily, replay = 1)
+
+    companion object {
+        private const val TAG = "TasksRepository"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt
new file mode 100644
index 0000000..38d4e69
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.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.mediaprojection.taskswitcher.data.repository
+
+import android.media.projection.MediaProjectionInfo
+import android.media.projection.MediaProjectionManager
+import android.os.Handler
+import android.util.Log
+import android.view.ContentRecordingSession
+import android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY
+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.Main
+import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState
+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.shareIn
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class MediaProjectionManagerRepository
+@Inject
+constructor(
+    private val mediaProjectionManager: MediaProjectionManager,
+    @Main private val handler: Handler,
+    @Application private val applicationScope: CoroutineScope,
+    private val tasksRepository: TasksRepository,
+) : MediaProjectionRepository {
+
+    override val mediaProjectionState: Flow<MediaProjectionState> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : MediaProjectionManager.Callback() {
+                        override fun onStart(info: MediaProjectionInfo?) {
+                            Log.d(TAG, "MediaProjectionManager.Callback#onStart")
+                            trySendWithFailureLogging(MediaProjectionState.NotProjecting, TAG)
+                        }
+
+                        override fun onStop(info: MediaProjectionInfo?) {
+                            Log.d(TAG, "MediaProjectionManager.Callback#onStop")
+                            trySendWithFailureLogging(MediaProjectionState.NotProjecting, TAG)
+                        }
+
+                        override fun onRecordingSessionSet(
+                            info: MediaProjectionInfo,
+                            session: ContentRecordingSession?
+                        ) {
+                            Log.d(TAG, "MediaProjectionManager.Callback#onSessionStarted: $session")
+                            launch { trySendWithFailureLogging(stateForSession(session), TAG) }
+                        }
+                    }
+                mediaProjectionManager.addCallback(callback, handler)
+                awaitClose { mediaProjectionManager.removeCallback(callback) }
+            }
+            .shareIn(scope = applicationScope, started = SharingStarted.Lazily, replay = 1)
+
+    private suspend fun stateForSession(session: ContentRecordingSession?): MediaProjectionState {
+        if (session == null) {
+            return MediaProjectionState.NotProjecting
+        }
+        if (session.contentToRecord == RECORD_CONTENT_DISPLAY || session.tokenToRecord == null) {
+            return MediaProjectionState.EntireScreen
+        }
+        val matchingTask =
+            tasksRepository.findRunningTaskFromWindowContainerToken(session.tokenToRecord)
+                ?: return MediaProjectionState.EntireScreen
+        return MediaProjectionState.SingleTask(matchingTask)
+    }
+
+    companion object {
+        private const val TAG = "MediaProjectionMngrRepo"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionRepository.kt
new file mode 100644
index 0000000..5bec692
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionRepository.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.mediaprojection.taskswitcher.data.repository
+
+import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState
+import kotlinx.coroutines.flow.Flow
+
+/** Represents a repository to retrieve and change data related to media projection. */
+interface MediaProjectionRepository {
+
+    /** Represents the current [MediaProjectionState]. */
+    val mediaProjectionState: Flow<MediaProjectionState>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/NoOpMediaProjectionRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/NoOpMediaProjectionRepository.kt
new file mode 100644
index 0000000..544eb6b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/NoOpMediaProjectionRepository.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.taskswitcher.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+
+/**
+ * No-op implementation of [MediaProjectionRepository] that does nothing. Currently used as a
+ * placeholder, while the real implementation is not completed.
+ */
+@SysUISingleton
+class NoOpMediaProjectionRepository @Inject constructor() : MediaProjectionRepository {
+
+    override val mediaProjectionState: Flow<MediaProjectionState> = emptyFlow()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/TasksRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/TasksRepository.kt
new file mode 100644
index 0000000..6a535e4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/TasksRepository.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.mediaprojection.taskswitcher.data.repository
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.os.IBinder
+import kotlinx.coroutines.flow.Flow
+
+/** Repository responsible for retrieving data related to running tasks. */
+interface TasksRepository {
+
+    /**
+     * Tries to find a [RunningTaskInfo] with a matching window container token. Returns `null` when
+     * no matching task was found.
+     */
+    suspend fun findRunningTaskFromWindowContainerToken(
+        windowContainerToken: IBinder
+    ): RunningTaskInfo?
+
+    /**
+     * Emits a stream of [RunningTaskInfo] that have been moved to the foreground.
+     *
+     * Note: when subscribing for the first time, it will not immediately emit the current
+     * foreground task. Only after a change in foreground task has occurred.
+     */
+    val foregroundTask: Flow<RunningTaskInfo>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
new file mode 100644
index 0000000..fc5cf7d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.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.mediaprojection.taskswitcher.domain.interactor
+
+import android.app.TaskInfo
+import android.content.Intent
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionRepository
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.TasksRepository
+import com.android.systemui.mediaprojection.taskswitcher.domain.model.TaskSwitchState
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/** Interactor with logic related to task switching in the context of media projection. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class TaskSwitchInteractor
+@Inject
+constructor(
+    mediaProjectionRepository: MediaProjectionRepository,
+    private val tasksRepository: TasksRepository,
+) {
+
+    /**
+     * Emits a stream of changes to the state of task switching, in the context of media projection.
+     */
+    val taskSwitchChanges: Flow<TaskSwitchState> =
+        mediaProjectionRepository.mediaProjectionState.flatMapLatest { projectionState ->
+            Log.d(TAG, "MediaProjectionState -> $projectionState")
+            when (projectionState) {
+                is MediaProjectionState.SingleTask -> {
+                    val projectedTask = projectionState.task
+                    tasksRepository.foregroundTask.map { foregroundTask ->
+                        if (hasForegroundTaskSwitched(projectedTask, foregroundTask)) {
+                            TaskSwitchState.TaskSwitched(projectedTask, foregroundTask)
+                        } else {
+                            TaskSwitchState.TaskUnchanged
+                        }
+                    }
+                }
+                is MediaProjectionState.EntireScreen,
+                is MediaProjectionState.NotProjecting -> {
+                    flowOf(TaskSwitchState.NotProjectingTask)
+                }
+            }
+        }
+
+    /**
+     * Returns whether tasks have been switched.
+     *
+     * Always returns `false` when launcher is in the foreground. The reason is that when going to
+     * recents to switch apps, launcher becomes the new foreground task, and we don't want to show
+     * the notification then.
+     */
+    private fun hasForegroundTaskSwitched(projectedTask: TaskInfo, foregroundTask: TaskInfo) =
+        projectedTask.taskId != foregroundTask.taskId && !foregroundTask.isLauncher
+
+    private val TaskInfo.isLauncher
+        get() =
+            baseIntent.hasCategory(Intent.CATEGORY_HOME) && baseIntent.action == Intent.ACTION_MAIN
+
+    companion object {
+        private const val TAG = "TaskSwitchInteractor"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/model/TaskSwitchState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/model/TaskSwitchState.kt
new file mode 100644
index 0000000..cd1258e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/model/TaskSwitchState.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.mediaprojection.taskswitcher.domain.model
+
+import android.app.TaskInfo
+
+/** Represents tha state of task switching in the context of single task media projection. */
+sealed interface TaskSwitchState {
+    /** Currently no task is being projected. */
+    object NotProjectingTask : TaskSwitchState
+    /** The foreground task is the same as the task that is currently being projected. */
+    object TaskUnchanged : TaskSwitchState
+    /** The foreground task is a different one to the task it currently being projected. */
+    data class TaskSwitched(val projectedTask: TaskInfo, val foregroundTask: TaskInfo) :
+        TaskSwitchState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt
new file mode 100644
index 0000000..a4f4076
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt
@@ -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.mediaprojection.taskswitcher.ui
+
+import android.content.Context
+import android.util.Log
+import android.widget.Toast
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState.NotShowing
+import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState.Showing
+import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.launch
+
+/** Coordinator responsible for showing/hiding the task switcher notification. */
+@SysUISingleton
+class TaskSwitcherNotificationCoordinator
+@Inject
+constructor(
+    private val context: Context,
+    @Application private val applicationScope: CoroutineScope,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    private val viewModel: TaskSwitcherNotificationViewModel,
+) {
+
+    fun start() {
+        applicationScope.launch {
+            viewModel.uiState.flowOn(mainDispatcher).collect { uiState ->
+                Log.d(TAG, "uiState -> $uiState")
+                when (uiState) {
+                    is Showing -> showNotification(uiState)
+                    is NotShowing -> hideNotification()
+                }
+            }
+        }
+    }
+
+    private fun showNotification(uiState: Showing) {
+        val text =
+            """
+            Sharing pauses when you switch apps.
+            Share this app instead.
+            Switch back.
+            """
+                .trimIndent()
+        // TODO(b/286201515): Create actual notification.
+        Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
+    }
+
+    private fun hideNotification() {}
+
+    companion object {
+        private const val TAG = "TaskSwitchNotifCoord"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/model/TaskSwitcherNotificationUiState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/model/TaskSwitcherNotificationUiState.kt
new file mode 100644
index 0000000..21aee72
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/model/TaskSwitcherNotificationUiState.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.mediaprojection.taskswitcher.ui.model
+
+import android.app.TaskInfo
+
+/** Represents the UI state for the task switcher notification. */
+sealed interface TaskSwitcherNotificationUiState {
+    /** The notification should not be shown. */
+    object NotShowing : TaskSwitcherNotificationUiState
+    /** The notification should be shown. */
+    data class Showing(
+        val projectedTask: TaskInfo,
+        val foregroundTask: TaskInfo,
+    ) : TaskSwitcherNotificationUiState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt
new file mode 100644
index 0000000..d9754d4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.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.mediaprojection.taskswitcher.ui.viewmodel
+
+import android.util.Log
+import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor
+import com.android.systemui.mediaprojection.taskswitcher.domain.model.TaskSwitchState
+import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+class TaskSwitcherNotificationViewModel @Inject constructor(interactor: TaskSwitchInteractor) {
+
+    val uiState: Flow<TaskSwitcherNotificationUiState> =
+        interactor.taskSwitchChanges.map { taskSwitchChange ->
+            Log.d(TAG, "taskSwitchChange: $taskSwitchChange")
+            when (taskSwitchChange) {
+                is TaskSwitchState.TaskSwitched -> {
+                    TaskSwitcherNotificationUiState.Showing(
+                        projectedTask = taskSwitchChange.projectedTask,
+                        foregroundTask = taskSwitchChange.foregroundTask,
+                    )
+                }
+                is TaskSwitchState.NotProjectingTask,
+                is TaskSwitchState.TaskUnchanged -> {
+                    TaskSwitcherNotificationUiState.NotShowing
+                }
+            }
+        }
+
+    companion object {
+        private const val TAG = "TaskSwitchNotifVM"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiState.java b/packages/SystemUI/src/com/android/systemui/model/SysUiState.java
index 8d80990..07846b5 100644
--- a/packages/SystemUI/src/com/android/systemui/model/SysUiState.java
+++ b/packages/SystemUI/src/com/android/systemui/model/SysUiState.java
@@ -114,7 +114,7 @@
         pw.print("  mSysUiStateFlags="); pw.println(mFlags);
         pw.println("    " + QuickStepContract.getSystemUiStateString(mFlags));
         pw.print("    backGestureDisabled=");
-        pw.println(QuickStepContract.isBackGestureDisabled(mFlags));
+        pw.println(QuickStepContract.isBackGestureDisabled(mFlags, false /* forTrackpad */));
         pw.print("    assistantGestureDisabled=");
         pw.println(QuickStepContract.isAssistantGestureDisabled(mFlags));
     }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
index 1da8718..8225c47 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
@@ -51,7 +51,6 @@
 import android.view.IWindowManager;
 import android.view.View;
 import android.view.WindowInsets;
-import android.view.WindowManagerGlobal;
 import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.NonNull;
@@ -69,10 +68,13 @@
 import com.android.systemui.settings.UserTracker;
 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.BarTransitions.TransitionMode;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
+import dagger.Lazy;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -80,8 +82,6 @@
 
 import javax.inject.Inject;
 
-import dagger.Lazy;
-
 /**
  * Extracts shared elements between navbar and taskbar delegate to de-dupe logic and help them
  * experience the joys of friendship.
@@ -111,6 +111,7 @@
     private final AccessibilityButtonTargetsObserver mAccessibilityButtonTargetsObserver;
     private final List<NavbarTaskbarStateUpdater> mStateListeners = new ArrayList<>();
     private final Context mContext;
+    private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final CommandQueue mCommandQueue;
     private final ContentResolver mContentResolver;
     private final EdgeBackGestureHandler mEdgeBackGestureHandler;
@@ -182,9 +183,11 @@
             IWindowManager wm,
             UserTracker userTracker,
             DisplayTracker displayTracker,
+            NotificationShadeWindowController notificationShadeWindowController,
             DumpManager dumpManager,
             CommandQueue commandQueue) {
         mContext = context;
+        mNotificationShadeWindowController = notificationShadeWindowController;
         mCommandQueue = commandQueue;
         mContentResolver = mContext.getContentResolver();
         mAccessibilityManager = accessibilityManager;
@@ -460,11 +463,7 @@
      * {@link InputMethodService} and the keyguard states.
      */
     public boolean isImeShown(int vis) {
-        View shadeWindowView = null;
-        if (mCentralSurfacesOptionalLazy.get().isPresent()) {
-            shadeWindowView =
-                    mCentralSurfacesOptionalLazy.get().get().getNotificationShadeWindowView();
-        }
+        View shadeWindowView =  mNotificationShadeWindowController.getWindowRootView();
         boolean isKeyguardShowing = mKeyguardStateController.isShowing();
         boolean imeVisibleOnShade = shadeWindowView != null && shadeWindowView.isAttachedToWindow()
                 && shadeWindowView.getRootWindowInsets().isVisible(WindowInsets.Type.ime());
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java
index 63276fe..99daf36 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java
@@ -28,18 +28,21 @@
 import android.os.PatternMatcher;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.Trace;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.provider.Settings.Secure;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -64,22 +67,21 @@
     private Context mCurrentUserContext;
     private final IOverlayManager mOverlayManager;
     private final Executor mUiBgExecutor;
+    private final UserTracker mUserTracker;
 
     private ArrayList<ModeChangedListener> mListeners = new ArrayList<>();
 
-    private final DeviceProvisionedController.DeviceProvisionedListener mDeviceProvisionedCallback =
-            new DeviceProvisionedController.DeviceProvisionedListener() {
-                @Override
-                public void onUserSwitched() {
-                    if (DEBUG) {
-                        Log.d(TAG, "onUserSwitched: "
-                                + ActivityManagerWrapper.getInstance().getCurrentUserId());
-                    }
+    private final UserTracker.Callback mUserTrackerCallback = new UserTracker.Callback() {
+        @Override
+        public void onUserChanged(int newUser, @NonNull Context userContext) {
+            if (DEBUG) {
+                Log.d(TAG, "onUserChanged: "
+                        + newUser);
+            }
 
-                    // Update the nav mode for the current user
-                    updateCurrentInteractionMode(true /* notify */);
-                }
-            };
+            updateCurrentInteractionMode(true /* notify */);
+        }
+    };
 
     // The primary user SysUI process doesn't get AppInfo changes from overlay package changes for
     // the secondary user (b/158613864), so we need to update the interaction mode here as well
@@ -97,19 +99,20 @@
 
     @Inject
     public NavigationModeController(Context context,
-            DeviceProvisionedController deviceProvisionedController,
             ConfigurationController configurationController,
+            UserTracker userTracker,
+            @Main Executor mainExecutor,
             @UiBackground Executor uiBgExecutor,
             DumpManager dumpManager) {
         mContext = context;
         mCurrentUserContext = context;
+        mUserTracker = userTracker;
+        mUserTracker.addCallback(mUserTrackerCallback, mainExecutor);
         mOverlayManager = IOverlayManager.Stub.asInterface(
                 ServiceManager.getService(Context.OVERLAY_SERVICE));
         mUiBgExecutor = uiBgExecutor;
         dumpManager.registerDumpable(getClass().getSimpleName(), this);
 
-        deviceProvisionedController.addCallback(mDeviceProvisionedCallback);
-
         IntentFilter overlayFilter = new IntentFilter(ACTION_OVERLAY_CHANGED);
         overlayFilter.addDataScheme("package");
         overlayFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL);
@@ -129,6 +132,7 @@
     }
 
     public void updateCurrentInteractionMode(boolean notify) {
+        Trace.beginSection("NMC#updateCurrentInteractionMode");
         mCurrentUserContext = getCurrentUserContext();
         int mode = getCurrentInteractionMode(mCurrentUserContext);
         mUiBgExecutor.execute(() ->
@@ -144,6 +148,7 @@
                 mListeners.get(i).onNavigationModeChanged(mode);
             }
         }
+        Trace.endSection();
     }
 
     public int addListener(ModeChangedListener listener) {
@@ -171,7 +176,7 @@
     }
 
     public Context getCurrentUserContext() {
-        int userId = ActivityManagerWrapper.getInstance().getCurrentUserId();
+        int userId = mUserTracker.getUserId();
         if (DEBUG) {
             Log.d(TAG, "getCurrentUserContext: contextUser=" + mContext.getUserId()
                     + " currentUser=" + userId);
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 8b3d7a6..a8af67a 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -15,11 +15,13 @@
  */
 package com.android.systemui.navigationbar.gestural;
 
+import static android.view.InputDevice.SOURCE_MOUSE;
+import static android.view.InputDevice.SOURCE_TOUCHPAD;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
 
 import static com.android.systemui.classifier.Classifier.BACK_GESTURE;
-import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadFourFingerSwipe;
-import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadMultiFingerSwipe;
+import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll;
+import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe;
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
@@ -37,6 +39,7 @@
 import android.graphics.Region;
 import android.hardware.input.InputManager;
 import android.icu.text.SimpleDateFormat;
+import android.os.Handler;
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -195,6 +198,7 @@
     private final ViewConfiguration mViewConfiguration;
     private final WindowManager mWindowManager;
     private final IWindowManager mWindowManagerService;
+    private final InputManager mInputManager;
     private final Optional<Pip> mPipOptional;
     private final Optional<DesktopMode> mDesktopModeOptional;
     private final FalsingManager mFalsingManager;
@@ -206,6 +210,7 @@
     private final int mDisplayId;
 
     private final Executor mMainExecutor;
+    private final Handler mMainHandler;
     private final Executor mBackgroundExecutor;
 
     private final Rect mPipExcludedBounds = new Rect();
@@ -249,13 +254,17 @@
     private boolean mDeferSetIsOnLeftEdge;
 
     private boolean mIsAttached;
-    private boolean mIsGesturalModeEnabled;
+    private boolean mIsGestureHandlingEnabled;
+    private boolean mIsTrackpadConnected;
+    private boolean mInGestureNavMode;
+    private boolean mUsingThreeButtonNav;
     private boolean mIsEnabled;
     private boolean mIsNavBarShownTransiently;
     private boolean mIsBackGestureAllowed;
     private boolean mGestureBlockingActivityRunning;
     private boolean mIsNewBackAffordanceEnabled;
     private boolean mIsTrackpadGestureFeaturesEnabled;
+    private boolean mIsTrackpadThreeFingerSwipe;
     private boolean mIsButtonForcedVisible;
 
     private InputMonitor mInputMonitor;
@@ -349,12 +358,48 @@
                 }
             };
 
+    private final InputManager.InputDeviceListener mInputDeviceListener =
+            new InputManager.InputDeviceListener() {
+
+        // Only one trackpad can be connected to a device at a time, since it takes over the
+        // only USB port.
+        private int mTrackpadDeviceId;
+
+        @Override
+        public void onInputDeviceAdded(int deviceId) {
+            if (isTrackpadDevice(deviceId)) {
+                mTrackpadDeviceId = deviceId;
+                update(true /* isTrackpadConnected */);
+            }
+        }
+
+        @Override
+        public void onInputDeviceChanged(int deviceId) { }
+
+        @Override
+        public void onInputDeviceRemoved(int deviceId) {
+            if (mTrackpadDeviceId == deviceId) {
+                update(false /* isTrackpadConnected */);
+            }
+        }
+
+        private void update(boolean isTrackpadConnected) {
+            boolean isPreviouslyTrackpadConnected = mIsTrackpadConnected;
+            mIsTrackpadConnected = isTrackpadConnected;
+            if (isPreviouslyTrackpadConnected != mIsTrackpadConnected) {
+                updateIsEnabled();
+                updateCurrentUserResources();
+            }
+        }
+    };
+
     EdgeBackGestureHandler(
             Context context,
             OverviewProxyService overviewProxyService,
             SysUiState sysUiState,
             PluginManager pluginManager,
             @Main Executor executor,
+            @Main Handler handler,
             @Background Executor backgroundExecutor,
             UserTracker userTracker,
             ProtoTracer protoTracer,
@@ -363,6 +408,7 @@
             ViewConfiguration viewConfiguration,
             WindowManager windowManager,
             IWindowManager windowManagerService,
+            InputManager inputManager,
             Optional<Pip> pipOptional,
             Optional<DesktopMode> desktopModeOptional,
             FalsingManager falsingManager,
@@ -373,6 +419,7 @@
         mContext = context;
         mDisplayId = context.getDisplayId();
         mMainExecutor = executor;
+        mMainHandler = handler;
         mBackgroundExecutor = backgroundExecutor;
         mUserTracker = userTracker;
         mOverviewProxyService = overviewProxyService;
@@ -384,6 +431,7 @@
         mViewConfiguration = viewConfiguration;
         mWindowManager = windowManager;
         mWindowManagerService = windowManagerService;
+        mInputManager = inputManager;
         mPipOptional = pipOptional;
         mDesktopModeOptional = desktopModeOptional;
         mFalsingManager = falsingManager;
@@ -392,6 +440,8 @@
         mFeatureFlags = featureFlags;
         mLightBarControllerProvider = lightBarControllerProvider;
         mLastReportedConfig.setTo(mContext.getResources().getConfiguration());
+        mIsTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled(
+                Flags.TRACKPAD_GESTURE_FEATURES);
         ComponentName recentsComponentName = ComponentName.unflattenFromString(
                 context.getString(com.android.internal.R.string.config_recentsComponentName));
         if (recentsComponentName != null) {
@@ -423,7 +473,7 @@
                 ViewConfiguration.getLongPressTimeout());
 
         mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver(
-                mContext.getMainThreadHandler(), mContext, this::onNavigationSettingsChanged);
+                mMainHandler, mContext, this::onNavigationSettingsChanged);
 
         updateCurrentUserResources();
     }
@@ -510,6 +560,13 @@
         mProtoTracer.add(this);
         mOverviewProxyService.addCallback(mQuickSwitchListener);
         mSysUiState.addCallback(mSysUiStateCallback);
+        if (mIsTrackpadGestureFeaturesEnabled) {
+            mInputManager.registerInputDeviceListener(mInputDeviceListener, mMainHandler);
+            int [] inputDevices = mInputManager.getInputDeviceIds();
+            for (int inputDeviceId : inputDevices) {
+                mInputDeviceListener.onInputDeviceAdded(inputDeviceId);
+            }
+        }
         updateIsEnabled();
         mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
     }
@@ -522,6 +579,7 @@
         mProtoTracer.remove(this);
         mOverviewProxyService.removeCallback(mQuickSwitchListener);
         mSysUiState.removeCallback(mSysUiStateCallback);
+        mInputManager.unregisterInputDeviceListener(mInputDeviceListener);
         updateIsEnabled();
         mUserTracker.removeCallback(mUserChangedCallback);
     }
@@ -530,7 +588,8 @@
      * @see NavigationModeController.ModeChangedListener#onNavigationModeChanged
      */
     public void onNavigationModeChanged(int mode) {
-        mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mode);
+        mUsingThreeButtonNav = QuickStepContract.isLegacyMode(mode);
+        mInGestureNavMode = QuickStepContract.isGesturalMode(mode);
         updateIsEnabled();
         updateCurrentUserResources();
     }
@@ -553,83 +612,83 @@
     private void updateIsEnabled() {
         try {
             Trace.beginSection("EdgeBackGestureHandler#updateIsEnabled");
-            updateIsEnabledTraced();
+
+            mIsGestureHandlingEnabled =
+                    mInGestureNavMode || (mIsTrackpadGestureFeaturesEnabled && mUsingThreeButtonNav
+                            && mIsTrackpadConnected);
+            boolean isEnabled = mIsAttached && mIsGestureHandlingEnabled;
+            if (isEnabled == mIsEnabled) {
+                return;
+            }
+            mIsEnabled = isEnabled;
+            disposeInputChannel();
+
+            if (mEdgeBackPlugin != null) {
+                mEdgeBackPlugin.onDestroy();
+                mEdgeBackPlugin = null;
+            }
+
+            if (!mIsEnabled) {
+                mGestureNavigationSettingsObserver.unregister();
+                if (DEBUG_MISSING_GESTURE) {
+                    Log.d(DEBUG_MISSING_GESTURE_TAG, "Unregister display listener");
+                }
+                mPluginManager.removePluginListener(this);
+                TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
+                        mTaskStackListener);
+                DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener);
+                mPipOptional.ifPresent(pip -> pip.setOnIsInPipStateChangedListener(null));
+
+                try {
+                    mWindowManagerService.unregisterSystemGestureExclusionListener(
+                            mGestureExclusionListener, mDisplayId);
+                } catch (RemoteException | IllegalArgumentException e) {
+                    Log.e(TAG, "Failed to unregister window manager callbacks", e);
+                }
+
+            } else {
+                mGestureNavigationSettingsObserver.register();
+                updateDisplaySize();
+                if (DEBUG_MISSING_GESTURE) {
+                    Log.d(DEBUG_MISSING_GESTURE_TAG, "Register display listener");
+                }
+                TaskStackChangeListeners.getInstance().registerTaskStackListener(
+                        mTaskStackListener);
+                DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
+                        mMainExecutor::execute, mOnPropertiesChangedListener);
+                mPipOptional.ifPresent(pip -> pip.setOnIsInPipStateChangedListener(
+                        mOnIsInPipStateChangedListener));
+                mDesktopModeOptional.ifPresent(
+                        dm -> dm.addDesktopGestureExclusionRegionListener(
+                                mDesktopCornersChangedListener, mMainExecutor));
+
+                try {
+                    mWindowManagerService.registerSystemGestureExclusionListener(
+                            mGestureExclusionListener, mDisplayId);
+                } catch (RemoteException | IllegalArgumentException e) {
+                    Log.e(TAG, "Failed to register window manager callbacks", e);
+                }
+
+                // Register input event receiver
+                mInputMonitor = mContext.getSystemService(InputManager.class).monitorGestureInput(
+                        "edge-swipe", mDisplayId);
+                mInputEventReceiver = new InputChannelCompat.InputEventReceiver(
+                        mInputMonitor.getInputChannel(), Looper.getMainLooper(),
+                        Choreographer.getInstance(), this::onInputEvent);
+
+                // Add a nav bar panel window
+                mIsNewBackAffordanceEnabled = mFeatureFlags.isEnabled(Flags.NEW_BACK_AFFORDANCE);
+                resetEdgeBackPlugin();
+                mPluginManager.addPluginListener(
+                        this, NavigationEdgeBackPlugin.class, /*allowMultiple=*/ false);
+            }
+            // Update the ML model resources.
+            updateMLModelState();
         } finally {
             Trace.endSection();
         }
     }
 
-    private void updateIsEnabledTraced() {
-        boolean isEnabled = mIsAttached && mIsGesturalModeEnabled;
-        if (isEnabled == mIsEnabled) {
-            return;
-        }
-        mIsEnabled = isEnabled;
-        disposeInputChannel();
-
-        if (mEdgeBackPlugin != null) {
-            mEdgeBackPlugin.onDestroy();
-            mEdgeBackPlugin = null;
-        }
-
-        if (!mIsEnabled) {
-            mGestureNavigationSettingsObserver.unregister();
-            if (DEBUG_MISSING_GESTURE) {
-                Log.d(DEBUG_MISSING_GESTURE_TAG, "Unregister display listener");
-            }
-            mPluginManager.removePluginListener(this);
-            TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
-            DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener);
-            mPipOptional.ifPresent(pip -> pip.setOnIsInPipStateChangedListener(null));
-
-            try {
-                mWindowManagerService.unregisterSystemGestureExclusionListener(
-                        mGestureExclusionListener, mDisplayId);
-            } catch (RemoteException | IllegalArgumentException e) {
-                Log.e(TAG, "Failed to unregister window manager callbacks", e);
-            }
-
-        } else {
-            mGestureNavigationSettingsObserver.register();
-            updateDisplaySize();
-            if (DEBUG_MISSING_GESTURE) {
-                Log.d(DEBUG_MISSING_GESTURE_TAG, "Register display listener");
-            }
-            TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
-            DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
-                    mMainExecutor::execute, mOnPropertiesChangedListener);
-            mPipOptional.ifPresent(
-                    pip -> pip.setOnIsInPipStateChangedListener(mOnIsInPipStateChangedListener));
-            mDesktopModeOptional.ifPresent(
-                    dm -> dm.addDesktopGestureExclusionRegionListener(
-                            mDesktopCornersChangedListener, mMainExecutor));
-
-            try {
-                mWindowManagerService.registerSystemGestureExclusionListener(
-                        mGestureExclusionListener, mDisplayId);
-            } catch (RemoteException | IllegalArgumentException e) {
-                Log.e(TAG, "Failed to register window manager callbacks", e);
-            }
-
-            // Register input event receiver
-            mInputMonitor = mContext.getSystemService(InputManager.class).monitorGestureInput(
-                    "edge-swipe", mDisplayId);
-            mInputEventReceiver = new InputChannelCompat.InputEventReceiver(
-                    mInputMonitor.getInputChannel(), Looper.getMainLooper(),
-                    Choreographer.getInstance(), this::onInputEvent);
-
-            // Add a nav bar panel window
-            mIsNewBackAffordanceEnabled = mFeatureFlags.isEnabled(Flags.NEW_BACK_AFFORDANCE);
-            mIsTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled(
-                    Flags.TRACKPAD_GESTURE_FEATURES);
-            resetEdgeBackPlugin();
-            mPluginManager.addPluginListener(
-                    this, NavigationEdgeBackPlugin.class, /*allowMultiple=*/ false);
-        }
-        // Update the ML model resources.
-        updateMLModelState();
-    }
-
     @Override
     public void onPluginConnected(NavigationEdgeBackPlugin plugin, Context context) {
         setEdgeBackPlugin(plugin);
@@ -704,9 +763,9 @@
     }
 
     private void updateMLModelState() {
-        boolean newState =
-                mIsGesturalModeEnabled && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
-                        SystemUiDeviceConfigFlags.USE_BACK_GESTURE_ML_MODEL, false);
+        boolean newState = mIsGestureHandlingEnabled && DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.USE_BACK_GESTURE_ML_MODEL, false);
 
         if (newState == mUseMLModel) {
             return;
@@ -825,6 +884,15 @@
                 mDisplaySize.y - insets.bottom);
     }
 
+    private boolean isTrackpadDevice(int deviceId) {
+        InputDevice inputDevice = mInputManager.getInputDevice(deviceId);
+        if (inputDevice == null) {
+            return false;
+        }
+        return inputDevice.getSources() == (InputDevice.SOURCE_MOUSE
+                | InputDevice.SOURCE_TOUCHPAD);
+    }
+
     private boolean desktopExcludeRegionContains(int x, int y) {
         return mDesktopModeExcludeRegion.contains(x, y);
     }
@@ -919,18 +987,21 @@
                 (int) mEndPoint.x, (int) mEndPoint.y,
                 mEdgeWidthLeft + mLeftInset,
                 mDisplaySize.x - (mEdgeWidthRight + mRightInset),
-                mUseMLModel ? mMLResults : -2, logPackageName);
+                mUseMLModel ? mMLResults : -2, logPackageName,
+                mIsTrackpadThreeFingerSwipe ? SysUiStatsLog.BACK_GESTURE__INPUT_TYPE__TRACKPAD
+                        : SysUiStatsLog.BACK_GESTURE__INPUT_TYPE__TOUCH);
     }
 
     private void onMotionEvent(MotionEvent ev) {
         int action = ev.getActionMasked();
-        boolean isTrackpadMultiFingerSwipe = isTrackpadMultiFingerSwipe(
-                mIsTrackpadGestureFeaturesEnabled, ev);
         if (action == MotionEvent.ACTION_DOWN) {
             if (DEBUG_MISSING_GESTURE) {
                 Log.d(DEBUG_MISSING_GESTURE_TAG, "Start gesture: " + ev);
             }
 
+            mIsTrackpadThreeFingerSwipe = isTrackpadThreeFingerSwipe(
+                    mIsTrackpadGestureFeaturesEnabled, ev);
+
             // ACTION_UP or ACTION_CANCEL is not guaranteed to be called before a new
             // ACTION_DOWN, in that case we should just reuse the old instance.
             mVelocityTracker.clear();
@@ -938,7 +1009,7 @@
             // Verify if this is in within the touch region and we aren't in immersive mode, and
             // either the bouncer is showing or the notification panel is hidden
             mInputEventReceiver.setBatchingEnabled(false);
-            if (isTrackpadMultiFingerSwipe) {
+            if (mIsTrackpadThreeFingerSwipe) {
                 // Since trackpad gestures don't have zones, this will be determined later by the
                 // direction of the gesture. {@code mIsOnLeftEdge} is set to false to begin with.
                 mDeferSetIsOnLeftEdge = true;
@@ -950,20 +1021,27 @@
             mLogGesture = false;
             mInRejectedExclusion = false;
             boolean isWithinInsets = isWithinInsets((int) ev.getX(), (int) ev.getY());
-            // Trackpad back gestures don't have zones, so we don't need to check if the down event
-            // is within insets.
-            mAllowGesture = !mDisabledForQuickstep && mIsBackGestureAllowed
-                    && (isTrackpadMultiFingerSwipe || isWithinInsets)
+            boolean isBackAllowedCommon = !mDisabledForQuickstep && mIsBackGestureAllowed
                     && !mGestureBlockingActivityRunning
-                    && !QuickStepContract.isBackGestureDisabled(mSysUiFlags)
-                    && (isValidTrackpadBackGesture(isTrackpadMultiFingerSwipe)
-                        || isWithinTouchRegion((int) ev.getX(), (int) ev.getY()));
+                    && !QuickStepContract.isBackGestureDisabled(mSysUiFlags,
+                            mIsTrackpadThreeFingerSwipe)
+                    && !isTrackpadScroll(mIsTrackpadGestureFeaturesEnabled, ev);
+            if (mIsTrackpadThreeFingerSwipe) {
+                // Trackpad back gestures don't have zones, so we don't need to check if the down
+                // event is within insets.
+                mAllowGesture = isBackAllowedCommon && isValidTrackpadBackGesture(
+                        true /* isTrackpadEvent */);
+            } else {
+                mAllowGesture = isBackAllowedCommon && !mUsingThreeButtonNav && isWithinInsets
+                    && isWithinTouchRegion((int) ev.getX(), (int) ev.getY())
+                    && !isButtonPressFromTrackpad(ev);
+            }
             if (mAllowGesture) {
                 mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge);
                 mEdgeBackPlugin.onMotionEvent(ev);
                 dispatchToBackAnimation(ev);
             }
-            if (mLogGesture || isTrackpadMultiFingerSwipe) {
+            if (mLogGesture || mIsTrackpadThreeFingerSwipe) {
                 mDownPoint.set(ev.getX(), ev.getY());
                 mEndPoint.set(-1, -1);
                 mThresholdCrossed = false;
@@ -974,20 +1052,20 @@
             mTmpLogDate.setTime(curTime);
             String curTimeStr = mLogDateFormat.format(mTmpLogDate);
             (isWithinInsets ? mGestureLogInsideInsets : mGestureLogOutsideInsets).log(String.format(
-                    "Gesture [%d [%s],alw=%B, mltf=%B, left=%B, defLeft=%B, backAlw=%B, disbld=%B,"
+                    "Gesture [%d [%s],alw=%B, t3fs=%B, left=%B, defLeft=%B, backAlw=%B, disbld=%B,"
                             + " qsDisbld=%b, blkdAct=%B, pip=%B,"
                             + " disp=%s, wl=%d, il=%d, wr=%d, ir=%d, excl=%s]",
-                    curTime, curTimeStr, mAllowGesture, isTrackpadMultiFingerSwipe,
+                    curTime, curTimeStr, mAllowGesture, mIsTrackpadThreeFingerSwipe,
                     mIsOnLeftEdge, mDeferSetIsOnLeftEdge, mIsBackGestureAllowed,
-                    QuickStepContract.isBackGestureDisabled(mSysUiFlags), mDisabledForQuickstep,
-                    mGestureBlockingActivityRunning, mIsInPip, mDisplaySize,
+                    QuickStepContract.isBackGestureDisabled(mSysUiFlags,
+                            mIsTrackpadThreeFingerSwipe),
+                    mDisabledForQuickstep, mGestureBlockingActivityRunning, mIsInPip, mDisplaySize,
                     mEdgeWidthLeft, mLeftInset, mEdgeWidthRight, mRightInset, mExcludeRegion));
         } else if (mAllowGesture || mLogGesture) {
             if (!mThresholdCrossed) {
                 mEndPoint.x = (int) ev.getX();
                 mEndPoint.y = (int) ev.getY();
-                if (action == MotionEvent.ACTION_POINTER_DOWN && (!isTrackpadMultiFingerSwipe
-                        || isTrackpadFourFingerSwipe(mIsTrackpadGestureFeaturesEnabled, ev))) {
+                if (action == MotionEvent.ACTION_POINTER_DOWN && !mIsTrackpadThreeFingerSwipe) {
                     if (mAllowGesture) {
                         logGesture(SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE_MULTI_TOUCH);
                         if (DEBUG_MISSING_GESTURE) {
@@ -999,11 +1077,7 @@
                     mLogGesture = false;
                     return;
                 } else if (action == MotionEvent.ACTION_MOVE) {
-                    if (isTrackpadFourFingerSwipe(isTrackpadMultiFingerSwipe, ev)) {
-                        cancelGesture(ev);
-                        return;
-                    }
-                    if (isTrackpadMultiFingerSwipe && mDeferSetIsOnLeftEdge) {
+                    if (mIsTrackpadThreeFingerSwipe && mDeferSetIsOnLeftEdge) {
                         // mIsOnLeftEdge is determined by the relative position between the down
                         // and the current motion event for trackpad gestures instead of zoning.
                         mIsOnLeftEdge = mEndPoint.x > mDownPoint.x;
@@ -1065,6 +1139,13 @@
         mProtoTracer.scheduleFrameUpdate();
     }
 
+    private boolean isButtonPressFromTrackpad(MotionEvent ev) {
+        // We don't allow back for button press from the trackpad, and yet we do with a mouse.
+        int sources = InputManager.getInstance().getInputDevice(ev.getDeviceId()).getSources();
+        int sourceTrackpad = (SOURCE_MOUSE | SOURCE_TOUCHPAD);
+        return (sources & sourceTrackpad) == sourceTrackpad && ev.getButtonState() != 0;
+    }
+
     private void dispatchToBackAnimation(MotionEvent event) {
         if (mBackAnimation != null) {
             mVelocityTracker.addMovement(event);
@@ -1159,7 +1240,7 @@
         pw.println("  mIsEnabled=" + mIsEnabled);
         pw.println("  mIsAttached=" + mIsAttached);
         pw.println("  mIsBackGestureAllowed=" + mIsBackGestureAllowed);
-        pw.println("  mIsGesturalModeEnabled=" + mIsGesturalModeEnabled);
+        pw.println("  mIsGestureHandlingEnabled=" + mIsGestureHandlingEnabled);
         pw.println("  mIsNavBarShownTransiently=" + mIsNavBarShownTransiently);
         pw.println("  mGestureBlockingActivityRunning=" + mGestureBlockingActivityRunning);
         pw.println("  mAllowGesture=" + mAllowGesture);
@@ -1184,6 +1265,8 @@
         pw.println("  mPredictionLog=" + String.join("\n", mPredictionLog));
         pw.println("  mGestureLogInsideInsets=" + String.join("\n", mGestureLogInsideInsets));
         pw.println("  mGestureLogOutsideInsets=" + String.join("\n", mGestureLogOutsideInsets));
+        pw.println("  mIsTrackpadConnected=" + mIsTrackpadConnected);
+        pw.println("  mUsingThreeButtonNav=" + mUsingThreeButtonNav);
         pw.println("  mEdgeBackPlugin=" + mEdgeBackPlugin);
         if (mEdgeBackPlugin != null) {
             mEdgeBackPlugin.dump(pw);
@@ -1233,6 +1316,7 @@
         private final SysUiState mSysUiState;
         private final PluginManager mPluginManager;
         private final Executor mExecutor;
+        private final Handler mHandler;
         private final Executor mBackgroundExecutor;
         private final UserTracker mUserTracker;
         private final ProtoTracer mProtoTracer;
@@ -1241,6 +1325,7 @@
         private final ViewConfiguration mViewConfiguration;
         private final WindowManager mWindowManager;
         private final IWindowManager mWindowManagerService;
+        private final InputManager mInputManager;
         private final Optional<Pip> mPipOptional;
         private final Optional<DesktopMode> mDesktopModeOptional;
         private final FalsingManager mFalsingManager;
@@ -1255,6 +1340,7 @@
                        SysUiState sysUiState,
                        PluginManager pluginManager,
                        @Main Executor executor,
+                       @Main Handler handler,
                        @Background Executor backgroundExecutor,
                        UserTracker userTracker,
                        ProtoTracer protoTracer,
@@ -1263,6 +1349,7 @@
                        ViewConfiguration viewConfiguration,
                        WindowManager windowManager,
                        IWindowManager windowManagerService,
+                       InputManager inputManager,
                        Optional<Pip> pipOptional,
                        Optional<DesktopMode> desktopModeOptional,
                        FalsingManager falsingManager,
@@ -1275,6 +1362,7 @@
             mSysUiState = sysUiState;
             mPluginManager = pluginManager;
             mExecutor = executor;
+            mHandler = handler;
             mBackgroundExecutor = backgroundExecutor;
             mUserTracker = userTracker;
             mProtoTracer = protoTracer;
@@ -1283,6 +1371,7 @@
             mViewConfiguration = viewConfiguration;
             mWindowManager = windowManager;
             mWindowManagerService = windowManagerService;
+            mInputManager = inputManager;
             mPipOptional = pipOptional;
             mDesktopModeOptional = desktopModeOptional;
             mFalsingManager = falsingManager;
@@ -1300,6 +1389,7 @@
                     mSysUiState,
                     mPluginManager,
                     mExecutor,
+                    mHandler,
                     mBackgroundExecutor,
                     mUserTracker,
                     mProtoTracer,
@@ -1308,6 +1398,7 @@
                     mViewConfiguration,
                     mWindowManager,
                     mWindowManagerService,
+                    mInputManager,
                     mPipOptional,
                     mDesktopModeOptional,
                     mFalsingManager,
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java
index 50e8aa7..10a88c8 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java
@@ -16,27 +16,24 @@
 
 package com.android.systemui.navigationbar.gestural;
 
+import static android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT;
 import static android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE;
+import static android.view.MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE;
 
 import android.view.MotionEvent;
 
 public final class Utilities {
 
-    public static boolean isTrackpadMultiFingerSwipe(boolean isTrackpadGestureFeaturesEnabled,
+    public static boolean isTrackpadScroll(boolean isTrackpadGestureFeaturesEnabled,
             MotionEvent event) {
         return isTrackpadGestureFeaturesEnabled
-                && event.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE;
+                && event.getClassification() == CLASSIFICATION_TWO_FINGER_SWIPE;
     }
 
     public static boolean isTrackpadThreeFingerSwipe(boolean isTrackpadGestureFeaturesEnabled,
             MotionEvent event) {
-        return isTrackpadMultiFingerSwipe(isTrackpadGestureFeaturesEnabled, event)
-                && event.getPointerCount() == 3;
-    }
-
-    public static boolean isTrackpadFourFingerSwipe(boolean isTrackpadGestureFeaturesEnabled,
-            MotionEvent event) {
-        return isTrackpadMultiFingerSwipe(isTrackpadGestureFeaturesEnabled, event)
-                && event.getPointerCount() == 4;
+        return isTrackpadGestureFeaturesEnabled
+                && event.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE
+                && event.getAxisValue(AXIS_GESTURE_SWIPE_FINGER_COUNT) == 3;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl b/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl
new file mode 100644
index 0000000..3e947d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl
@@ -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;
+
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.os.UserHandle;
+
+/** A service to help with controlling the state of notes app bubble through the system user. */
+interface INoteTaskBubblesService {
+
+    boolean areBubblesAvailable();
+
+    void showOrHideAppBubble(in Intent intent, in UserHandle userHandle, in Icon icon);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt
new file mode 100644
index 0000000..ec205f8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.Service
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.os.IBinder
+import android.os.UserHandle
+import com.android.internal.infra.ServiceConnector
+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.DebugLogger.debugLog
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/**
+ * A utility class to help interact with [Bubbles] as system user. The SysUI instance running as
+ * system user is the only instance that has the instance of [Bubbles] that manages the notes app
+ * bubble for all users.
+ *
+ * <p>Note: This class is made overridable so that a fake can be created for as mocking suspending
+ * functions is not supported by the Android tree's version of mockito.
+ */
+@SysUISingleton
+open class NoteTaskBubblesController
+@Inject
+constructor(
+    @Application private val context: Context,
+    @Background private val bgDispatcher: CoroutineDispatcher
+) {
+
+    private val serviceConnector: ServiceConnector<INoteTaskBubblesService> =
+        ServiceConnector.Impl(
+            context,
+            Intent(context, NoteTaskBubblesService::class.java),
+            Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE,
+            UserHandle.USER_SYSTEM,
+            INoteTaskBubblesService.Stub::asInterface
+        )
+
+    /** Returns whether notes app bubble is supported. */
+    open suspend fun areBubblesAvailable(): Boolean =
+        withContext(bgDispatcher) {
+            suspendCoroutine { continuation ->
+                serviceConnector
+                    .postForResult { it.areBubblesAvailable() }
+                    .whenComplete { available, error ->
+                        if (error != null) {
+                            debugLog(error = error) { "Failed to query Bubbles as system user." }
+                        }
+                        continuation.resume(available ?: false)
+                    }
+            }
+        }
+
+    /** Calls the [Bubbles.showOrHideAppBubble] API as [UserHandle.USER_SYSTEM]. */
+    open suspend fun showOrHideAppBubble(
+        intent: Intent,
+        userHandle: UserHandle,
+        icon: Icon
+    ) {
+        withContext(bgDispatcher) {
+            serviceConnector
+                .post { it.showOrHideAppBubble(intent, userHandle, icon) }
+                .whenComplete { _, error ->
+                    if (error != null) {
+                        debugLog(error = error) {
+                            "Failed to show notes app bubble for intent $intent, " +
+                                "user $userHandle, and icon $icon."
+                        }
+                    } else {
+                        debugLog {
+                            "Call to show notes app bubble for intent $intent, " +
+                                "user $userHandle, and icon $icon successful."
+                        }
+                    }
+                }
+        }
+    }
+
+    /**
+     * A helper service to call [Bubbles] APIs that should always be called from the system user
+     * instance of SysUI.
+     *
+     * <p>Note: This service always runs in the SysUI process running on the system user
+     * irrespective of which user started the service. This is required so that the correct instance
+     * of {@link Bubbles} is injected. This is set via attribute {@code android:singleUser=”true”}
+     * in AndroidManifest.
+     */
+    class NoteTaskBubblesService
+    @Inject
+    constructor(private val mOptionalBubbles: Optional<Bubbles>) : Service() {
+
+        override fun onBind(intent: Intent): IBinder {
+            return object : INoteTaskBubblesService.Stub() {
+                override fun areBubblesAvailable() = mOptionalBubbles.isPresent
+
+                override fun showOrHideAppBubble(
+                    intent: Intent,
+                    userHandle: UserHandle,
+                    icon: Icon
+                ) {
+                    mOptionalBubbles.ifPresentOrElse(
+                        { bubbles -> bubbles.showOrHideAppBubble(intent, userHandle, icon) },
+                        {
+                            debugLog {
+                                "Failed to show or hide bubble for intent $intent," +
+                                    "user $user, and icon $icon as bubble is empty."
+                            }
+                        }
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index ccfbaf1..48790c2 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -33,25 +33,26 @@
 import android.graphics.drawable.Icon
 import android.os.UserHandle
 import android.os.UserManager
+import android.provider.Settings
 import android.widget.Toast
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled
 import com.android.systemui.log.DebugLogger.debugLog
 import com.android.systemui.notetask.NoteTaskRoleManagerExt.createNoteShortcutInfoAsUser
 import com.android.systemui.notetask.NoteTaskRoleManagerExt.getDefaultRoleHolderAsUser
 import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
-import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.shared.system.ActivityManagerKt.isInForeground
-import com.android.systemui.util.kotlin.getOrNull
+import com.android.systemui.util.settings.SecureSettings
 import com.android.wm.shell.bubbles.Bubble
-import com.android.wm.shell.bubbles.Bubbles
 import com.android.wm.shell.bubbles.Bubbles.BubbleExpandListener
-import java.util.Optional
 import java.util.concurrent.atomic.AtomicReference
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 
 /**
  * Entry point for creating and managing note.
@@ -69,13 +70,15 @@
     private val shortcutManager: ShortcutManager,
     private val resolver: NoteTaskInfoResolver,
     private val eventLogger: NoteTaskEventLogger,
-    private val optionalBubbles: Optional<Bubbles>,
+    private val noteTaskBubblesController: NoteTaskBubblesController,
     private val userManager: UserManager,
     private val keyguardManager: KeyguardManager,
     private val activityManager: ActivityManager,
     @NoteTaskEnabledKey private val isEnabled: Boolean,
     private val devicePolicyManager: DevicePolicyManager,
     private val userTracker: UserTracker,
+    private val secureSettings: SecureSettings,
+    @Application private val applicationScope: CoroutineScope
 ) {
 
     @VisibleForTesting val infoReference = AtomicReference<NoteTaskInfo?>()
@@ -100,18 +103,6 @@
         }
     }
 
-    /** Starts [LaunchNoteTaskProxyActivity] on the given [user]. */
-    fun startNoteTaskProxyActivityForUser(user: UserHandle) {
-        context.startActivityAsUser(
-            Intent().apply {
-                component =
-                    ComponentName(context, LaunchNoteTaskManagedProfileProxyActivity::class.java)
-                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-            },
-            user
-        )
-    }
-
     /** Starts the notes role setting. */
     fun startNotesRoleSetting(activityContext: Context, entryPoint: NoteTaskEntryPoint?) {
         val user =
@@ -146,7 +137,7 @@
             userTracker.userProfiles.firstOrNull { userManager.isManagedProfile(it.id) }?.userHandle
                 ?: userTracker.userHandle
         } else {
-            userTracker.userHandle
+            secureSettings.preferredUser
         }
 
     /**
@@ -175,7 +166,19 @@
     ) {
         if (!isEnabled) return
 
-        val bubbles = optionalBubbles.getOrNull() ?: return
+        applicationScope.launch { awaitShowNoteTaskAsUser(entryPoint, user) }
+    }
+
+    private suspend fun awaitShowNoteTaskAsUser(
+        entryPoint: NoteTaskEntryPoint,
+        user: UserHandle,
+    ) {
+        if (!isEnabled) return
+
+        if (!noteTaskBubblesController.areBubblesAvailable()) {
+            debugLog { "Bubbles not available in the system user SysUI instance" }
+            return
+        }
 
         // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
         if (!userManager.isUserUnlocked) return
@@ -210,7 +213,7 @@
                     val intent = createNoteTaskIntent(info)
                     val icon =
                         Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
-                    bubbles.showOrHideAppBubble(intent, user, icon)
+                    noteTaskBubblesController.showOrHideAppBubble(intent, user, icon)
                     // App bubble logging happens on `onBubbleExpandChanged`.
                     debugLog { "onShowNoteTask - opened as app bubble: $info" }
                 }
@@ -284,15 +287,55 @@
     }
 
     /**
+     * Like [updateNoteTaskAsUser] but automatically apply to the current user and all its work
+     * profiles.
+     *
+     * @see updateNoteTaskAsUser
+     * @see UserTracker.userHandle
+     * @see UserTracker.userProfiles
+     */
+    fun updateNoteTaskForCurrentUserAndManagedProfiles() {
+        updateNoteTaskAsUser(userTracker.userHandle)
+        for (profile in userTracker.userProfiles) {
+            if (userManager.isManagedProfile(profile.id)) {
+                updateNoteTaskAsUser(profile.userHandle)
+            }
+        }
+    }
+
+    /**
      * Updates all [NoteTaskController] related information, including but not exclusively the
      * widget shortcut created by the [user] - by default it will use the current user.
      *
+     * If the user is not current user, the update will be dispatched to run in that user's process.
+     *
      * Keep in mind the shortcut API has a
      * [rate limiting](https://developer.android.com/develop/ui/views/launch/shortcuts/managing-shortcuts#rate-limiting)
      * and may not be updated in real-time. To reduce the chance of stale shortcuts, we run the
      * function during System UI initialization.
      */
     fun updateNoteTaskAsUser(user: UserHandle) {
+        if (!userManager.isUserUnlocked(user)) {
+            debugLog { "updateNoteTaskAsUser call but user locked: user=$user" }
+            return
+        }
+
+        if (user == userTracker.userHandle) {
+            updateNoteTaskAsUserInternal(user)
+        } else {
+            // TODO(b/278729185): Replace fire and forget service with a bounded service.
+            val intent = NoteTaskControllerUpdateService.createIntent(context)
+            context.startServiceAsUser(intent, user)
+        }
+    }
+
+    @InternalNoteTaskApi
+    fun updateNoteTaskAsUserInternal(user: UserHandle) {
+        if (!userManager.isUserUnlocked(user)) {
+            debugLog { "updateNoteTaskAsUserInternal call but user locked: user=$user" }
+            return
+        }
+
         val packageName = roleManager.getDefaultRoleHolderAsUser(ROLE_NOTES, user)
         val hasNotesRoleHolder = isEnabled && !packageName.isNullOrEmpty()
 
@@ -310,20 +353,20 @@
     /** @see OnRoleHoldersChangedListener */
     fun onRoleHoldersChanged(roleName: String, user: UserHandle) {
         if (roleName != ROLE_NOTES) return
-        if (!userManager.isUserUnlocked(user)) {
-            debugLog { "onRoleHoldersChanged call but user locked: role=$roleName, user=$user" }
-            return
-        }
 
-        if (user == userTracker.userHandle) {
-            updateNoteTaskAsUser(user)
-        } else {
-            // TODO(b/278729185): Replace fire and forget service with a bounded service.
-            val intent = NoteTaskControllerUpdateService.createIntent(context)
-            context.startServiceAsUser(intent, user)
-        }
+        updateNoteTaskAsUser(user)
     }
 
+    private val SecureSettings.preferredUser: UserHandle
+        get() {
+            val userId =
+                secureSettings.getInt(
+                    Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
+                    userTracker.userHandle.identifier,
+                )
+            return UserHandle.of(userId)
+        }
+
     companion object {
         val TAG = NoteTaskController::class.simpleName.orEmpty()
 
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt
index 26b35cc..3e352af 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt
@@ -44,7 +44,7 @@
     override fun onCreate() {
         super.onCreate()
         // TODO(b/278729185): Replace fire and forget service with a bounded service.
-        controller.updateNoteTaskAsUser(user)
+        controller.updateNoteTaskAsUserInternal(user)
         stopSelf()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index 221ff65..fe1034a 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -15,7 +15,10 @@
  */
 package com.android.systemui.notetask
 
+import android.app.role.OnRoleHoldersChangedListener
 import android.app.role.RoleManager
+import android.content.Context
+import android.content.pm.UserInfo
 import android.os.UserHandle
 import android.view.KeyEvent
 import android.view.KeyEvent.KEYCODE_N
@@ -54,6 +57,7 @@
         initializeHandleSystemKey()
         initializeOnRoleHoldersChanged()
         initializeOnUserUnlocked()
+        initializeUserTracker()
     }
 
     /**
@@ -79,7 +83,7 @@
     private fun initializeOnRoleHoldersChanged() {
         roleManager.addOnRoleHoldersChangedListenerAsUser(
             backgroundExecutor,
-            controller::onRoleHoldersChanged,
+            callbacks,
             UserHandle.ALL,
         )
     }
@@ -93,18 +97,41 @@
      */
     private fun initializeOnUserUnlocked() {
         if (keyguardUpdateMonitor.isUserUnlocked(userTracker.userId)) {
-            controller.setNoteTaskShortcutEnabled(true, userTracker.userHandle)
-        } else {
-            keyguardUpdateMonitor.registerCallback(onUserUnlockedCallback)
+            controller.updateNoteTaskForCurrentUserAndManagedProfiles()
         }
+        keyguardUpdateMonitor.registerCallback(callbacks)
     }
 
-    // KeyguardUpdateMonitor.registerCallback uses a weak reference, so we need a hard reference.
-    private val onUserUnlockedCallback =
-        object : KeyguardUpdateMonitorCallback() {
+    private fun initializeUserTracker() {
+        userTracker.addCallback(callbacks, backgroundExecutor)
+    }
+
+    // Some callbacks use a weak reference, so we play safe and keep a hard reference to them all.
+    private val callbacks =
+        object :
+            KeyguardUpdateMonitorCallback(),
+            CommandQueue.Callbacks,
+            UserTracker.Callback,
+            OnRoleHoldersChangedListener {
+
+            override fun handleSystemKey(key: KeyEvent) {
+                key.toNoteTaskEntryPointOrNull()?.let(controller::showNoteTask)
+            }
+
+            override fun onRoleHoldersChanged(roleName: String, user: UserHandle) {
+                controller.onRoleHoldersChanged(roleName, user)
+            }
+
             override fun onUserUnlocked() {
-                controller.setNoteTaskShortcutEnabled(true, userTracker.userHandle)
-                keyguardUpdateMonitor.removeCallback(this)
+                controller.updateNoteTaskForCurrentUserAndManagedProfiles()
+            }
+
+            override fun onUserChanged(newUser: Int, userContext: Context) {
+                controller.updateNoteTaskForCurrentUserAndManagedProfiles()
+            }
+
+            override fun onProfilesChanged(profiles: List<UserInfo>) {
+                controller.updateNoteTaskForCurrentUserAndManagedProfiles()
             }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
index 109cfee..c0e688f 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -26,7 +26,6 @@
 import com.android.systemui.notetask.quickaffordance.NoteTaskQuickAffordanceModule
 import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
 import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity
-import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
@@ -40,12 +39,12 @@
     @[Binds IntoMap ClassKey(NoteTaskControllerUpdateService::class)]
     fun NoteTaskControllerUpdateService.bindNoteTaskControllerUpdateService(): Service
 
+    @[Binds IntoMap ClassKey(NoteTaskBubblesController.NoteTaskBubblesService::class)]
+    fun NoteTaskBubblesController.NoteTaskBubblesService.bindNoteTaskBubblesService(): Service
+
     @[Binds IntoMap ClassKey(LaunchNoteTaskActivity::class)]
     fun LaunchNoteTaskActivity.bindNoteTaskLauncherActivity(): Activity
 
-    @[Binds IntoMap ClassKey(LaunchNoteTaskManagedProfileProxyActivity::class)]
-    fun LaunchNoteTaskManagedProfileProxyActivity.bindNoteTaskLauncherProxyActivity(): Activity
-
     @[Binds IntoMap ClassKey(LaunchNotesRoleSettingsTrampolineActivity::class)]
     fun LaunchNotesRoleSettingsTrampolineActivity.bindLaunchNotesRoleSettingsTrampolineActivity():
         Activity
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt
index 754c365..4d30634 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt
@@ -19,6 +19,7 @@
 import android.app.role.RoleManager
 import android.app.role.RoleManager.ROLE_NOTES
 import android.content.Context
+import android.content.pm.PackageManager
 import android.content.pm.ShortcutInfo
 import android.graphics.drawable.Icon
 import android.os.PersistableBundle
@@ -42,21 +43,42 @@
         context: Context,
         user: UserHandle,
     ): ShortcutInfo {
+        val packageName = getDefaultRoleHolderAsUser(ROLE_NOTES, user)
+
         val extras = PersistableBundle()
-        getDefaultRoleHolderAsUser(ROLE_NOTES, user)?.let { packageName ->
+        if (packageName != null) {
             // Set custom app badge using the icon from ROLES_NOTES default app.
             extras.putString(NoteTaskController.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE, packageName)
         }
 
+        val shortLabel = context.getString(R.string.note_task_button_label)
+
+        val applicationLabel = context.packageManager.getApplicationLabel(packageName)
+        val longLabel =
+            if (applicationLabel == null) {
+                shortLabel
+            } else {
+                context.getString(
+                    R.string.note_task_shortcut_long_label,
+                    applicationLabel,
+                )
+            }
+
         val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
 
         return ShortcutInfo.Builder(context, NoteTaskController.SHORTCUT_ID)
             .setIntent(LaunchNoteTaskActivity.createIntent(context))
             .setActivity(LaunchNoteTaskActivity.createComponent(context))
-            .setShortLabel(context.getString(R.string.note_task_button_label))
+            .setShortLabel(shortLabel)
+            .setLongLabel(longLabel)
             .setLongLived(true)
             .setIcon(icon)
             .setExtras(extras)
             .build()
     }
+
+    private fun PackageManager.getApplicationLabel(packageName: String?): String? =
+        runCatching { getApplicationInfo(packageName, /* flags= */ 0)!! }
+            .getOrNull()
+            ?.let { info -> getApplicationLabel(info).toString() }
 }
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 7ef149d..d00a79e 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
@@ -20,67 +20,24 @@
 import android.content.Context
 import android.content.Intent
 import android.os.Bundle
-import android.os.UserHandle
-import android.os.UserManager
 import androidx.activity.ComponentActivity
-import com.android.systemui.log.DebugLogger.debugLog
 import com.android.systemui.notetask.NoteTaskController
 import com.android.systemui.notetask.NoteTaskEntryPoint
-import com.android.systemui.settings.UserTracker
 import javax.inject.Inject
 
 /** Activity responsible for launching the note experience, and finish. */
-class LaunchNoteTaskActivity
-@Inject
-constructor(
-    private val controller: NoteTaskController,
-    private val userManager: UserManager,
-    private val userTracker: UserTracker,
-) : ComponentActivity() {
+class LaunchNoteTaskActivity @Inject constructor(private val controller: NoteTaskController) :
+    ComponentActivity() {
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-
-        // Under the hood, notes app shortcuts are shown in a floating window, called Bubble.
-        // Bubble API is only available in the main user but not work profile.
-        //
-        // On devices with work profile (WP), SystemUI provides both personal notes app shortcuts &
-        // work profile notes app shortcuts. In order to make work profile notes app shortcuts to
-        // show in Bubble, a few redirections across users are required:
-        // 1. When `LaunchNoteTaskActivity` is started in the work profile user, we launch
-        //    `LaunchNoteTaskManagedProfileProxyActivity` on the main user, which has access to the
-        //    Bubble API.
-        // 2. `LaunchNoteTaskManagedProfileProxyActivity` calls `Bubble#showOrHideAppBubble` with
-        //     the work profile user ID.
-        // 3. Bubble renders the work profile notes app activity in a floating window, which is
-        //    hosted in the main user.
-        //
-        //            WP                                main user
-        //  ------------------------          -------------------------------------------
-        // | LaunchNoteTaskActivity |   ->   | LaunchNoteTaskManagedProfileProxyActivity |
-        //  ------------------------          -------------------------------------------
-        //                                                        |
-        //                 main user                              |
-        //         ----------------------------                   |
-        //        | Bubble#showOrHideAppBubble |   <--------------
-        //        |      (with WP user ID)     |
-        //         ----------------------------
-        val mainUser: UserHandle? = userManager.mainUser
-        if (userManager.isManagedProfile) {
-            if (mainUser == null) {
-                debugLog { "Can't find the main user. Skipping the notes app launch." }
+        val entryPoint =
+            if (isInMultiWindowMode) {
+                NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE
             } else {
-                controller.startNoteTaskProxyActivityForUser(mainUser)
+                NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT
             }
-        } else {
-            val entryPoint =
-                if (isInMultiWindowMode) {
-                    NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE
-                } else {
-                    NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT
-                }
-            controller.showNoteTask(entryPoint)
-        }
+        controller.showNoteTaskAsUser(entryPoint, user)
         finish()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt
deleted file mode 100644
index 3259b0d..0000000
--- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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.shortcut
-
-import android.os.Build
-import android.os.Bundle
-import android.os.UserManager
-import android.util.Log
-import androidx.activity.ComponentActivity
-import com.android.systemui.notetask.NoteTaskController
-import com.android.systemui.notetask.NoteTaskEntryPoint
-import com.android.systemui.settings.UserTracker
-import javax.inject.Inject
-
-/**
- * An internal proxy activity that starts notes app in the work profile.
- *
- * If there is no work profile, this activity finishes gracefully.
- *
- * This activity MUST NOT be exported because that would expose the INTERACT_ACROSS_USER privilege
- * to any apps.
- */
-class LaunchNoteTaskManagedProfileProxyActivity
-@Inject
-constructor(
-    private val controller: NoteTaskController,
-    private val userTracker: UserTracker,
-    private val userManager: UserManager,
-) : ComponentActivity() {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-        val managedProfileUser =
-            userTracker.userProfiles.firstOrNull { userManager.isManagedProfile(it.id) }
-
-        if (managedProfileUser == null) {
-            logDebug { "Fail to find the work profile user." }
-        } else {
-            controller.showNoteTaskAsUser(
-                entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT,
-                user = managedProfileUser.userHandle
-            )
-        }
-        finish()
-    }
-}
-
-private inline fun logDebug(message: () -> String) {
-    if (Build.IS_DEBUGGABLE) {
-        Log.d(NoteTaskController.TAG, message())
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt b/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt
index b2e04bb..69cb611 100644
--- a/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt
@@ -26,6 +26,8 @@
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
@@ -34,13 +36,18 @@
 interface PowerRepository {
     /** Whether the device is interactive. Starts with the current state. */
     val isInteractive: Flow<Boolean>
+
+    /** Wakes up the device. */
+    fun wakeUp(why: String, @PowerManager.WakeReason wakeReason: Int)
 }
 
 @SysUISingleton
 class PowerRepositoryImpl
 @Inject
 constructor(
-    manager: PowerManager,
+    private val manager: PowerManager,
+    @Application private val applicationContext: Context,
+    private val systemClock: SystemClock,
     dispatcher: BroadcastDispatcher,
 ) : PowerRepository {
 
@@ -68,6 +75,14 @@
         awaitClose { dispatcher.unregisterReceiver(receiver) }
     }
 
+    override fun wakeUp(why: String, wakeReason: Int) {
+        manager.wakeUp(
+            systemClock.uptimeMillis(),
+            wakeReason,
+            "${applicationContext.packageName}:$why",
+        )
+    }
+
     companion object {
         private const val TAG = "PowerRepository"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
index 3f799f7..c13476f 100644
--- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
@@ -17,8 +17,12 @@
 
 package com.android.systemui.power.domain.interactor
 
+import android.os.PowerManager
+import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.power.data.repository.PowerRepository
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 
@@ -27,8 +31,27 @@
 class PowerInteractor
 @Inject
 constructor(
-    repository: PowerRepository,
+    private val repository: PowerRepository,
+    private val falsingCollector: FalsingCollector,
+    private val screenOffAnimationController: ScreenOffAnimationController,
+    private val statusBarStateController: StatusBarStateController,
 ) {
     /** Whether the screen is on or off. */
     val isInteractive: Flow<Boolean> = repository.isInteractive
+
+    /**
+     * Wakes up the device if the device was dozing.
+     *
+     * @param why a string explaining why we're waking the device for debugging purposes. Should be
+     *   in SCREAMING_SNAKE_CASE.
+     * @param wakeReason the PowerManager-based reason why we're waking the device.
+     */
+    fun wakeUpIfDozing(why: String, @PowerManager.WakeReason wakeReason: Int) {
+        if (
+            statusBarStateController.isDozing && screenOffAnimationController.allowWakeUpIfDozing()
+        ) {
+            repository.wakeUp(why, wakeReason)
+            falsingCollector.onScreenOnFromTouch()
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
index 79167f2..c3b5db4 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
@@ -25,7 +25,7 @@
 import android.widget.LinearLayout
 import com.android.settingslib.Utils
 import com.android.systemui.R
-import com.android.systemui.animation.LaunchableFrameLayout
+import com.android.systemui.animation.view.LaunchableFrameLayout
 import com.android.systemui.statusbar.events.BackgroundAnimatableView
 
 class OngoingPrivacyChip @JvmOverloads constructor(
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
index e56106d..f934346 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
@@ -20,8 +20,8 @@
 import android.permission.PermissionGroupUsage
 import com.android.systemui.log.dagger.PrivacyLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogMessage
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogMessage
 import com.android.systemui.privacy.PrivacyDialog
 import com.android.systemui.privacy.PrivacyItem
 import java.util.Locale
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index b8c2fad..8d9475d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -52,7 +52,6 @@
 import com.android.systemui.compose.ComposeFacade;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.qs.QSContainerController;
@@ -755,8 +754,7 @@
             // Alpha progress should be linear on lockscreen shade expansion.
             return progress;
         }
-        if (mIsSmallScreen || !mFeatureFlags.isEnabled(
-                Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) {
+        if (mIsSmallScreen) {
             return ShadeInterpolation.getContentAlpha(progress);
         } else {
             return mLargeScreenShadeInterpolator.getQsAlpha(progress);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
index cd52ec2..6563e42 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
@@ -2,7 +2,7 @@
 
 import com.android.systemui.log.dagger.QSFragmentDisableLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import javax.inject.Inject
 
@@ -34,7 +34,6 @@
             },
             {
                 disableFlagsLogger.getDisableFlagsString(
-                    old = null,
                     new = DisableFlagsLogger.DisableState(int1, int2),
                     newAfterLocalModification =
                         DisableFlagsLogger.DisableState(long1.toInt(), long2.toInt())
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index 868dbfa..432147f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -52,8 +52,8 @@
 import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
 import com.android.systemui.settings.UserFileManager;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.phone.AutoTileManager;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
 import com.android.systemui.util.settings.SecureSettings;
@@ -66,7 +66,6 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Objects;
-import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.function.Predicate;
@@ -108,7 +107,7 @@
     private AutoTileManager mAutoTiles;
     private final ArrayList<QSFactory> mQsFactories = new ArrayList<>();
     private int mCurrentUser;
-    private final Optional<CentralSurfaces> mCentralSurfacesOptional;
+    private final ShadeController mShadeController;
     private Context mUserContext;
     private UserTracker mUserTracker;
     private SecureSettings mSecureSettings;
@@ -129,7 +128,7 @@
             PluginManager pluginManager,
             TunerService tunerService,
             Provider<AutoTileManager> autoTiles,
-            Optional<CentralSurfaces> centralSurfacesOptional,
+            ShadeController shadeController,
             QSLogger qsLogger,
             UserTracker userTracker,
             SecureSettings secureSettings,
@@ -148,11 +147,12 @@
         mUserFileManager = userFileManager;
         mFeatureFlags = featureFlags;
 
-        mCentralSurfacesOptional = centralSurfacesOptional;
+        mShadeController = shadeController;
 
         mQsFactories.add(defaultFactory);
         pluginManager.addPluginListener(this, QSFactory.class, true);
         mUserTracker = userTracker;
+        mCurrentUser = userTracker.getUserId();
         mSecureSettings = secureSettings;
         mCustomTileStatePersister = customTileStatePersister;
 
@@ -162,7 +162,9 @@
             // finishes before creating any tiles.
             tunerService.addTunable(this, TILES_SETTING);
             // AutoTileManager can modify mTiles so make sure mTiles has already been initialized.
-            mAutoTiles = autoTiles.get();
+            if (!mFeatureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD)) {
+                mAutoTiles = autoTiles.get();
+            }
         });
     }
 
@@ -209,17 +211,17 @@
 
     @Override
     public void collapsePanels() {
-        mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateCollapsePanels);
+        mShadeController.postAnimateCollapseShade();
     }
 
     @Override
     public void forceCollapsePanels() {
-        mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateForceCollapsePanels);
+        mShadeController.postAnimateForceCollapseShade();
     }
 
     @Override
     public void openPanels() {
-        mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateOpenPanels);
+        mShadeController.postAnimateExpandQs();
     }
 
     @Override
@@ -273,6 +275,13 @@
         if (!TILES_SETTING.equals(key)) {
             return;
         }
+        int currentUser = mUserTracker.getUserId();
+        if (currentUser != mCurrentUser) {
+            mUserContext = mUserTracker.getUserContext();
+            if (mAutoTiles != null) {
+                mAutoTiles.changeUser(UserHandle.of(currentUser));
+            }
+        }
         // Do not process tiles if the flag is enabled.
         if (mFeatureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {
             return;
@@ -281,13 +290,6 @@
             newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode);
         }
         final List<String> tileSpecs = loadTileSpecs(mContext, newValue);
-        int currentUser = mUserTracker.getUserId();
-        if (currentUser != mCurrentUser) {
-            mUserContext = mUserTracker.getUserContext();
-            if (mAutoTiles != null) {
-                mAutoTiles.changeUser(UserHandle.of(currentUser));
-            }
-        }
         if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return;
         Log.d(TAG, "Recreating tiles: " + tileSpecs);
         mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach(
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 dffe7fd..03de3a0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
@@ -19,9 +19,9 @@
 import static com.android.systemui.qs.dagger.QSFlagsModule.RBC_AVAILABLE;
 
 import android.content.Context;
-import android.hardware.display.NightDisplayListener;
 import android.os.Handler;
 
+import com.android.systemui.dagger.NightDisplayListenerModule;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.media.dagger.MediaModule;
@@ -41,14 +41,14 @@
 import com.android.systemui.statusbar.policy.WalletController;
 import com.android.systemui.util.settings.SecureSettings;
 
-import dagger.Module;
-import dagger.Provides;
-import dagger.multibindings.Multibinds;
-
 import java.util.Map;
 
 import javax.inject.Named;
 
+import dagger.Module;
+import dagger.Provides;
+import dagger.multibindings.Multibinds;
+
 /**
  * Module for QS dependencies
  */
@@ -79,7 +79,7 @@
             HotspotController hotspotController,
             DataSaverController dataSaverController,
             ManagedProfileController managedProfileController,
-            NightDisplayListener nightDisplayListener,
+            NightDisplayListenerModule.Builder nightDisplayListenerBuilder,
             CastController castController,
             ReduceBrightColorsController reduceBrightColorsController,
             DeviceControlsController deviceControlsController,
@@ -95,7 +95,7 @@
                 hotspotController,
                 dataSaverController,
                 managedProfileController,
-                nightDisplayListener,
+                nightDisplayListenerBuilder,
                 castController,
                 reduceBrightColorsController,
                 deviceControlsController,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialog.kt
index 6265b3c..3432628 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialog.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.graphics.drawable.Icon
+import android.view.ContextThemeWrapper
 import android.view.LayoutInflater
 import android.view.ViewGroup
 import android.widget.TextView
@@ -66,7 +67,8 @@
     }
 
     private fun createTileView(tileData: TileData): QSTileView {
-        val tile = QSTileViewImpl(context, QSIconViewImpl(context), true)
+        val themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
+        val tile = QSTileViewImpl(themedContext, QSIconViewImpl(themedContext), true)
         val state = QSTile.BooleanState().apply {
             label = tileData.label
             handlesLongClick = false
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
index 9c9ad33..3c53d77 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
@@ -244,8 +244,8 @@
 
         val backgroundResource =
             when (model.backgroundColor) {
-                R.attr.offStateColor -> R.drawable.qs_footer_action_circle
-                com.android.internal.R.attr.colorAccent -> R.drawable.qs_footer_action_circle_color
+                R.attr.shadeInactive -> R.drawable.qs_footer_action_circle
+                R.attr.shadeActive -> R.drawable.qs_footer_action_circle_color
                 else -> error("Unsupported icon background resource ${model.backgroundColor}")
             }
         buttonView.setBackgroundResource(backgroundResource)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
index b3596a2..32146b5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
@@ -145,8 +145,12 @@
                 R.drawable.ic_settings,
                 ContentDescription.Resource(R.string.accessibility_quick_settings_settings)
             ),
-            iconTint = null,
-            backgroundColor = R.attr.offStateColor,
+            iconTint =
+                Utils.getColorAttrDefaultColor(
+                    context,
+                    R.attr.onShadeInactiveVariant,
+                ),
+            backgroundColor = R.attr.shadeInactive,
             this::onSettingsButtonClicked,
         )
 
@@ -162,9 +166,9 @@
                 iconTint =
                     Utils.getColorAttrDefaultColor(
                         context,
-                        com.android.internal.R.attr.textColorOnAccent,
+                        R.attr.onShadeActive,
                     ),
-                backgroundColor = com.android.internal.R.attr.colorAccent,
+                backgroundColor = R.attr.shadeActive,
                 this::onPowerButtonClicked,
             )
         } else {
@@ -264,7 +268,7 @@
                     ),
                 ),
             iconTint = null,
-            backgroundColor = R.attr.offStateColor,
+            backgroundColor = R.attr.shadeInactive,
             onClick = this::onUserSwitcherClicked,
         )
     }
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 c00a81c..39745c8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -24,9 +24,9 @@
 import com.android.systemui.log.ConstantStringsLogger
 import com.android.systemui.log.ConstantStringsLoggerImpl
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.VERBOSE
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.ERROR
+import com.android.systemui.log.core.LogLevel.VERBOSE
 import com.android.systemui.log.dagger.QSConfigLog
 import com.android.systemui.log.dagger.QSLog
 import com.android.systemui.plugins.qs.QSTile
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt
new file mode 100644
index 0000000..adea26e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.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.qs.pipeline.dagger
+
+import android.content.res.Resources
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.pipeline.domain.autoaddable.AutoAddableSetting
+import com.android.systemui.qs.pipeline.domain.autoaddable.AutoAddableSettingList
+import com.android.systemui.qs.pipeline.domain.autoaddable.CastAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.DataSaverAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.DeviceControlsAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.HotspotAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.NightDisplayAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.ReduceBrightColorsAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.WalletAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.WorkTileAutoAddable
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ElementsIntoSet
+import dagger.multibindings.IntoSet
+
+@Module
+interface BaseAutoAddableModule {
+
+    companion object {
+        @Provides
+        @ElementsIntoSet
+        fun providesAutoAddableSetting(
+            @Main resources: Resources,
+            autoAddableSettingFactory: AutoAddableSetting.Factory
+        ): Set<AutoAddable> {
+            return AutoAddableSettingList.parseSettingsResource(
+                    resources,
+                    autoAddableSettingFactory
+                )
+                .toSet()
+        }
+    }
+
+    @Binds @IntoSet fun bindCastAutoAddable(impl: CastAutoAddable): AutoAddable
+
+    @Binds @IntoSet fun bindDataSaverAutoAddable(impl: DataSaverAutoAddable): AutoAddable
+
+    @Binds @IntoSet fun bindDeviceControlsAutoAddable(impl: DeviceControlsAutoAddable): AutoAddable
+
+    @Binds @IntoSet fun bindHotspotAutoAddable(impl: HotspotAutoAddable): AutoAddable
+
+    @Binds @IntoSet fun bindNightDisplayAutoAddable(impl: NightDisplayAutoAddable): AutoAddable
+
+    @Binds
+    @IntoSet
+    fun bindReduceBrightColorsAutoAddable(impl: ReduceBrightColorsAutoAddable): AutoAddable
+
+    @Binds @IntoSet fun bindWalletAutoAddable(impl: WalletAutoAddable): AutoAddable
+
+    @Binds @IntoSet fun bindWorkModeAutoAddable(impl: WorkTileAutoAddable): AutoAddable
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddLog.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddLog.kt
new file mode 100644
index 0000000..91cb5bb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddLog.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.pipeline.dagger
+
+import javax.inject.Qualifier
+
+/** A [LogBuffer] for the QS pipeline to track auto-added tiles */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class QSAutoAddLog
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddModule.kt
index 9979228..a010ac4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddModule.kt
@@ -16,13 +16,40 @@
 
 package com.android.systemui.qs.pipeline.dagger
 
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.qs.pipeline.data.repository.AutoAddRepository
 import com.android.systemui.qs.pipeline.data.repository.AutoAddSettingRepository
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
 import dagger.Binds
 import dagger.Module
+import dagger.Provides
+import dagger.multibindings.Multibinds
 
-@Module
+@Module(
+    includes =
+        [
+            BaseAutoAddableModule::class,
+        ]
+)
 abstract class QSAutoAddModule {
 
     @Binds abstract fun bindAutoAddRepository(impl: AutoAddSettingRepository): AutoAddRepository
+
+    @Multibinds abstract fun providesAutoAddableSet(): Set<AutoAddable>
+
+    companion object {
+        /**
+         * Provides a logging buffer for all logs related to the new Quick Settings pipeline to log
+         * auto added tiles.
+         */
+        @Provides
+        @SysUISingleton
+        @QSAutoAddLog
+        fun provideQSAutoAddLogBuffer(factory: LogBufferFactory): LogBuffer {
+            return factory.create(QSPipelineLogger.AUTO_ADD_TAG, maxSize = 100, systrace = false)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt
index d7ae575..a4600fb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt
@@ -26,7 +26,7 @@
 import com.android.systemui.qs.pipeline.data.repository.TileSpecSettingsRepository
 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractorImpl
-import com.android.systemui.qs.pipeline.prototyping.PrototypeCoreStartable
+import com.android.systemui.qs.pipeline.domain.startable.QSPipelineCoreStartable
 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
 import dagger.Binds
 import dagger.Module
@@ -53,8 +53,8 @@
 
     @Binds
     @IntoMap
-    @ClassKey(PrototypeCoreStartable::class)
-    abstract fun providePrototypeCoreStartable(startable: PrototypeCoreStartable): CoreStartable
+    @ClassKey(QSPipelineCoreStartable::class)
+    abstract fun provideCoreStartable(startable: QSPipelineCoreStartable): CoreStartable
 
     companion object {
         /**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt
index ad8bfea..c56ca8c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt
@@ -19,5 +19,5 @@
 import java.lang.annotation.RetentionPolicy
 import javax.inject.Qualifier
 
-/** A {@link LogBuffer} for the new QS Pipeline for logging changes to the set of current tiles. */
+/** A [LogBuffer] for the new QS Pipeline for logging changes to the set of current tiles. */
 @Qualifier @MustBeDocumented @Retention(RetentionPolicy.RUNTIME) annotation class QSTileListLog
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
index 498f403..6e7e099 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
@@ -51,12 +51,26 @@
 @Inject
 constructor(
     @Application private val applicationContext: Context,
-    private val packageManager: PackageManager,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) : InstalledTilesComponentRepository {
 
-    override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> =
-        conflatedCallbackFlow {
+    override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> {
+        /*
+         * In order to query [PackageManager] for different users, this implementation will call
+         * [Context.createContextAsUser] and retrieve the [PackageManager] from that context.
+         */
+        val packageManager =
+            if (applicationContext.userId == userId) {
+                applicationContext.packageManager
+            } else {
+                applicationContext
+                    .createContextAsUser(
+                        UserHandle.of(userId),
+                        /* flags */ 0,
+                    )
+                    .packageManager
+            }
+        return conflatedCallbackFlow {
                 val receiver =
                     object : BroadcastReceiver() {
                         override fun onReceive(context: Context?, intent: Intent?) {
@@ -74,12 +88,13 @@
                 awaitClose { applicationContext.unregisterReceiver(receiver) }
             }
             .onStart { emit(Unit) }
-            .map { reloadComponents(userId) }
+            .map { reloadComponents(userId, packageManager) }
             .distinctUntilChanged()
             .flowOn(backgroundDispatcher)
+    }
 
     @WorkerThread
-    private fun reloadComponents(userId: Int): Set<ComponentName> {
+    private fun reloadComponents(userId: Int, packageManager: PackageManager): Set<ComponentName> {
         return packageManager
             .queryIntentServicesAsUser(INTENT, FLAGS, userId)
             .mapNotNull { it.serviceInfo }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSetting.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSetting.kt
new file mode 100644
index 0000000..45129b9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSetting.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.pipeline.domain.autoaddable
+
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.Objects
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+
+/**
+ * It tracks a specific `Secure` int [setting] and when its value changes to non-zero, it will emit
+ * a [AutoAddSignal.Add] for [spec].
+ */
+class AutoAddableSetting
+@AssistedInject
+constructor(
+    private val secureSettings: SecureSettings,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    @Assisted private val setting: String,
+    @Assisted private val spec: TileSpec,
+) : AutoAddable {
+
+    override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+        return secureSettings
+            .observerFlow(userId, setting)
+            .onStart { emit(Unit) }
+            .map { secureSettings.getIntForUser(setting, 0, userId) != 0 }
+            .distinctUntilChanged()
+            .filter { it }
+            .map { AutoAddSignal.Add(spec) }
+            .flowOn(bgDispatcher)
+    }
+
+    override val autoAddTracking = AutoAddTracking.IfNotAdded(spec)
+
+    override val description = "AutoAddableSetting: $setting:$spec ($autoAddTracking)"
+
+    override fun equals(other: Any?): Boolean {
+        return other is AutoAddableSetting && spec == other.spec && setting == other.setting
+    }
+
+    override fun hashCode(): Int {
+        return Objects.hash(spec, setting)
+    }
+
+    override fun toString(): String {
+        return description
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(setting: String, spec: TileSpec): AutoAddableSetting
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingList.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingList.kt
new file mode 100644
index 0000000..b1c7433
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingList.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.pipeline.domain.autoaddable
+
+import android.content.res.Resources
+import android.util.Log
+import com.android.systemui.R
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+object AutoAddableSettingList {
+
+    /** Parses [R.array.config_quickSettingsAutoAdd] into a collection of [AutoAddableSetting]. */
+    fun parseSettingsResource(
+        resources: Resources,
+        autoAddableSettingFactory: AutoAddableSetting.Factory,
+    ): Iterable<AutoAddable> {
+        val autoAddList = resources.getStringArray(R.array.config_quickSettingsAutoAdd)
+        return autoAddList.mapNotNull {
+            val elements = it.split(SETTING_SEPARATOR, limit = 2)
+            if (elements.size == 2) {
+                val setting = elements[0]
+                val spec = elements[1]
+                val tileSpec = TileSpec.create(spec)
+                if (tileSpec == TileSpec.Invalid) {
+                    Log.w(TAG, "Malformed item in array: $it")
+                    null
+                } else {
+                    autoAddableSettingFactory.create(setting, TileSpec.create(spec))
+                }
+            } else {
+                Log.w(TAG, "Malformed item in array: $it")
+                null
+            }
+        }
+    }
+
+    private const val SETTING_SEPARATOR = ":"
+    private const val TAG = "AutoAddableSettingList"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt
new file mode 100644
index 0000000..88a49ee
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.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.qs.pipeline.domain.autoaddable
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.statusbar.policy.CallbackController
+import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Generic [AutoAddable] for tiles that are added based on a signal from a [CallbackController]. */
+abstract class CallbackControllerAutoAddable<
+    Callback : Any, Controller : CallbackController<Callback>>(
+    private val controller: Controller,
+) : AutoAddable {
+
+    /** [TileSpec] for the tile to add. */
+    protected abstract val spec: TileSpec
+
+    /**
+     * Callback to be used to determine when to add the tile. When the callback determines that the
+     * feature has been enabled, it should call [sendAdd].
+     */
+    protected abstract fun ProducerScope<AutoAddSignal>.getCallback(): Callback
+
+    /** Sends an [AutoAddSignal.Add] for [spec]. */
+    protected fun ProducerScope<AutoAddSignal>.sendAdd() {
+        trySend(AutoAddSignal.Add(spec))
+    }
+
+    final override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+        return conflatedCallbackFlow {
+            val callback = getCallback()
+            controller.addCallback(callback)
+
+            awaitClose { controller.removeCallback(callback) }
+        }
+    }
+
+    override val autoAddTracking: AutoAddTracking
+        get() = AutoAddTracking.IfNotAdded(spec)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddable.kt
new file mode 100644
index 0000000..b5bef9f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddable.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.qs.pipeline.domain.autoaddable
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.CastTile
+import com.android.systemui.statusbar.policy.CastController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.ProducerScope
+
+/**
+ * [AutoAddable] for [CastTile.TILE_SPEC].
+ *
+ * It will send a signal to add the tile when there's a casting device connected or connecting.
+ */
+@SysUISingleton
+class CastAutoAddable
+@Inject
+constructor(
+    private val controller: CastController,
+) : CallbackControllerAutoAddable<CastController.Callback, CastController>(controller) {
+
+    override val spec: TileSpec
+        get() = TileSpec.create(CastTile.TILE_SPEC)
+
+    override fun ProducerScope<AutoAddSignal>.getCallback(): CastController.Callback {
+        return CastController.Callback {
+            val isCasting =
+                controller.castDevices.any {
+                    it.state == CastController.CastDevice.STATE_CONNECTED ||
+                        it.state == CastController.CastDevice.STATE_CONNECTING
+                }
+            if (isCasting) {
+                sendAdd()
+            }
+        }
+    }
+
+    override val description = "CastAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddable.kt
new file mode 100644
index 0000000..a877aee
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddable.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.pipeline.domain.autoaddable
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.DataSaverTile
+import com.android.systemui.statusbar.policy.DataSaverController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.ProducerScope
+
+/**
+ * [AutoAddable] for [DataSaverTile.TILE_SPEC].
+ *
+ * It will send a signal to add the tile when data saver is enabled.
+ */
+@SysUISingleton
+class DataSaverAutoAddable
+@Inject
+constructor(
+    dataSaverController: DataSaverController,
+) :
+    CallbackControllerAutoAddable<DataSaverController.Listener, DataSaverController>(
+        dataSaverController
+    ) {
+
+    override val spec
+        get() = TileSpec.create(DataSaverTile.TILE_SPEC)
+
+    override fun ProducerScope<AutoAddSignal>.getCallback(): DataSaverController.Listener {
+        return DataSaverController.Listener { enabled ->
+            if (enabled) {
+                sendAdd()
+            }
+        }
+    }
+
+    override val description = "DataSaverAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt
new file mode 100644
index 0000000..76bfad9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.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.qs.pipeline.domain.autoaddable
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.DeviceControlsTile
+import com.android.systemui.statusbar.policy.DeviceControlsController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * [AutoAddable] for [DeviceControlsTile.TILE_SPEC].
+ *
+ * It will send a signal to add the tile when updating to a device that supports device controls. It
+ * will send a signal to remove the tile when the device does not support controls.
+ */
+@SysUISingleton
+class DeviceControlsAutoAddable
+@Inject
+constructor(
+    private val deviceControlsController: DeviceControlsController,
+) : AutoAddable {
+
+    private val spec = TileSpec.create(DeviceControlsTile.TILE_SPEC)
+
+    override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+        return conflatedCallbackFlow {
+            val callback =
+                object : DeviceControlsController.Callback {
+                    override fun onControlsUpdate(position: Int?) {
+                        position?.let { trySend(AutoAddSignal.Add(spec, position)) }
+                        deviceControlsController.removeCallback()
+                    }
+
+                    override fun removeControlsAutoTracker() {
+                        trySend(AutoAddSignal.Remove(spec))
+                    }
+                }
+
+            deviceControlsController.setCallback(callback)
+
+            awaitClose { deviceControlsController.removeCallback() }
+        }
+    }
+
+    override val autoAddTracking: AutoAddTracking
+        get() = AutoAddTracking.Always
+
+    override val description = "DeviceControlsAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddable.kt
new file mode 100644
index 0000000..9c59e12
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddable.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.pipeline.domain.autoaddable
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.HotspotTile
+import com.android.systemui.statusbar.policy.HotspotController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.ProducerScope
+
+/**
+ * [AutoAddable] for [HotspotTile.TILE_SPEC].
+ *
+ * It will send a signal to add the tile when hotspot is enabled.
+ */
+@SysUISingleton
+class HotspotAutoAddable
+@Inject
+constructor(
+    hotspotController: HotspotController,
+) :
+    CallbackControllerAutoAddable<HotspotController.Callback, HotspotController>(
+        hotspotController
+    ) {
+
+    override val spec
+        get() = TileSpec.create(HotspotTile.TILE_SPEC)
+
+    override fun ProducerScope<AutoAddSignal>.getCallback(): HotspotController.Callback {
+        return HotspotController.Callback { enabled, _ ->
+            if (enabled) {
+                sendAdd()
+            }
+        }
+    }
+
+    override val description = "HotspotAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt
new file mode 100644
index 0000000..31ea734
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt
@@ -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.qs.pipeline.domain.autoaddable
+
+import android.content.Context
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.NightDisplayTile
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * [AutoAddable] for [NightDisplayTile.TILE_SPEC].
+ *
+ * It will send a signal to add the tile when night display is enabled or when the auto mode changes
+ * to one that supports night display.
+ */
+@SysUISingleton
+class NightDisplayAutoAddable
+@Inject
+constructor(
+    private val nightDisplayListenerBuilder: NightDisplayListenerModule.Builder,
+    context: Context,
+) : AutoAddable {
+
+    private val enabled = ColorDisplayManager.isNightDisplayAvailable(context)
+    private val spec = TileSpec.create(NightDisplayTile.TILE_SPEC)
+
+    override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+        return conflatedCallbackFlow {
+            val nightDisplayListener = nightDisplayListenerBuilder.setUser(userId).build()
+
+            val callback =
+                object : NightDisplayListener.Callback {
+                    override fun onActivated(activated: Boolean) {
+                        if (activated) {
+                            sendAdd()
+                        }
+                    }
+
+                    override fun onAutoModeChanged(autoMode: Int) {
+                        if (
+                            autoMode == ColorDisplayManager.AUTO_MODE_CUSTOM_TIME ||
+                                autoMode == ColorDisplayManager.AUTO_MODE_TWILIGHT
+                        ) {
+                            sendAdd()
+                        }
+                    }
+
+                    private fun sendAdd() {
+                        trySend(AutoAddSignal.Add(spec))
+                    }
+                }
+
+            nightDisplayListener.setCallback(callback)
+
+            awaitClose { nightDisplayListener.setCallback(null) }
+        }
+    }
+
+    override val autoAddTracking =
+        if (enabled) {
+            AutoAddTracking.IfNotAdded(spec)
+        } else {
+            AutoAddTracking.Disabled
+        }
+
+    override val description = "NightDisplayAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt
new file mode 100644
index 0000000..267e2b7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.pipeline.domain.autoaddable
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.ReduceBrightColorsController
+import com.android.systemui.qs.dagger.QSFlagsModule.RBC_AVAILABLE
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.ReduceBrightColorsTile
+import javax.inject.Inject
+import javax.inject.Named
+import kotlinx.coroutines.channels.ProducerScope
+
+/**
+ * [AutoAddable] for [ReduceBrightColorsTile.TILE_SPEC].
+ *
+ * It will send a signal to add the tile when reduce bright colors is enabled.
+ */
+@SysUISingleton
+class ReduceBrightColorsAutoAddable
+@Inject
+constructor(
+    controller: ReduceBrightColorsController,
+    @Named(RBC_AVAILABLE) private val available: Boolean,
+) :
+    CallbackControllerAutoAddable<
+        ReduceBrightColorsController.Listener, ReduceBrightColorsController
+    >(controller) {
+
+    override val spec: TileSpec
+        get() = TileSpec.create(ReduceBrightColorsTile.TILE_SPEC)
+
+    override fun ProducerScope<AutoAddSignal>.getCallback(): ReduceBrightColorsController.Listener {
+        return object : ReduceBrightColorsController.Listener {
+            override fun onActivated(activated: Boolean) {
+                if (activated) {
+                    sendAdd()
+                }
+            }
+        }
+    }
+
+    override val autoAddTracking
+        get() =
+            if (available) {
+                super.autoAddTracking
+            } else {
+                AutoAddTracking.Disabled
+            }
+
+    override val description = "ReduceBrightColorsAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt
new file mode 100644
index 0000000..58a31bc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt
@@ -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.qs.pipeline.domain.autoaddable
+
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.text.TextUtils
+import com.android.systemui.R
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.statusbar.policy.SafetyController
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.withContext
+
+/**
+ * [AutoAddable] for the safety tile.
+ *
+ * It will send a signal to add the tile when the feature is enabled, indicating the component
+ * corresponding to the tile. If the feature is disabled, it will send a signal to remove the tile.
+ */
+@SysUISingleton
+class SafetyCenterAutoAddable
+@Inject
+constructor(
+    private val safetyController: SafetyController,
+    private val packageManager: PackageManager,
+    @Main private val resources: Resources,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+) : AutoAddable {
+
+    private suspend fun getSpec(): TileSpec? {
+        val specClass = resources.getString(R.string.safety_quick_settings_tile_class)
+        return if (TextUtils.isEmpty(specClass)) {
+            null
+        } else {
+            val packageName =
+                withContext(bgDispatcher) { packageManager.permissionControllerPackageName }
+            TileSpec.create(ComponentName(packageName, specClass))
+        }
+    }
+
+    override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+        return conflatedCallbackFlow {
+            val spec = getSpec()
+            if (spec != null) {
+                // If not added, we always try to add it
+                trySend(AutoAddSignal.Add(spec))
+                val listener =
+                    SafetyController.Listener { isSafetyCenterEnabled ->
+                        if (isSafetyCenterEnabled) {
+                            trySend(AutoAddSignal.Add(spec))
+                        } else {
+                            trySend(AutoAddSignal.Remove(spec))
+                        }
+                    }
+
+                safetyController.addCallback(listener)
+
+                awaitClose { safetyController.removeCallback(listener) }
+            } else {
+                awaitClose {}
+            }
+        }
+    }
+
+    override val autoAddTracking: AutoAddTracking
+        get() = AutoAddTracking.Always
+
+    override val description = "SafetyCenterAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddable.kt
new file mode 100644
index 0000000..b3bc25f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddable.kt
@@ -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.systemui.qs.pipeline.domain.autoaddable
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.QuickAccessWalletTile
+import com.android.systemui.statusbar.policy.WalletController
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+
+/**
+ * [AutoAddable] for [QuickAccessWalletTile.TILE_SPEC].
+ *
+ * It will always try to add the tile if [WalletController.getWalletPosition] is non-null.
+ */
+@SysUISingleton
+class WalletAutoAddable
+@Inject
+constructor(
+    private val walletController: WalletController,
+) : AutoAddable {
+
+    private val spec = TileSpec.create(QuickAccessWalletTile.TILE_SPEC)
+
+    override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+        return flow {
+            val position = walletController.getWalletPosition()
+            if (position != null) {
+                emit(AutoAddSignal.Add(spec, position))
+            }
+        }
+    }
+
+    override val autoAddTracking: AutoAddTracking
+        get() = AutoAddTracking.IfNotAdded(spec)
+
+    override val description = "WalletAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt
new file mode 100644
index 0000000..5e3c348
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.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.qs.pipeline.domain.autoaddable
+
+import android.content.pm.UserInfo
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.WorkModeTile
+import com.android.systemui.settings.UserTracker
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * [AutoAddable] for [WorkModeTile.TILE_SPEC].
+ *
+ * It will send a signal to add the tile when there is a managed profile for the current user, and a
+ * signal to remove it if there is not.
+ */
+@SysUISingleton
+class WorkTileAutoAddable @Inject constructor(private val userTracker: UserTracker) : AutoAddable {
+
+    private val spec = TileSpec.create(WorkModeTile.TILE_SPEC)
+
+    override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+        return conflatedCallbackFlow {
+            fun maybeSend(profiles: List<UserInfo>) {
+                if (profiles.any { it.id == userId }) {
+                    // We are looking at the profiles of the correct user.
+                    if (profiles.any { it.isManagedProfile }) {
+                        trySend(AutoAddSignal.Add(spec))
+                    } else {
+                        trySend(AutoAddSignal.Remove(spec))
+                    }
+                }
+            }
+
+            val callback =
+                object : UserTracker.Callback {
+                    override fun onProfilesChanged(profiles: List<UserInfo>) {
+                        maybeSend(profiles)
+                    }
+                }
+
+            userTracker.addCallback(callback) { it.run() }
+            maybeSend(userTracker.userProfiles)
+
+            awaitClose { userTracker.removeCallback(callback) }
+        }
+    }
+
+    override val autoAddTracking = AutoAddTracking.Always
+
+    override val description = "WorkTileAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractor.kt
new file mode 100644
index 0000000..b747393
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractor.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.pipeline.domain.interactor
+
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.qs.pipeline.data.repository.AutoAddRepository
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.indentIfPossible
+import java.io.PrintWriter
+import java.util.concurrent.atomic.AtomicBoolean
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.launch
+
+/**
+ * Collects the signals coming from all registered [AutoAddable] and adds/removes tiles accordingly.
+ */
+@SysUISingleton
+class AutoAddInteractor
+@Inject
+constructor(
+    private val autoAddables: Set<@JvmSuppressWildcards AutoAddable>,
+    private val repository: AutoAddRepository,
+    private val dumpManager: DumpManager,
+    private val qsPipelineLogger: QSPipelineLogger,
+    @Application private val scope: CoroutineScope,
+) : Dumpable {
+
+    private val initialized = AtomicBoolean(false)
+
+    /** Start collection of signals following the user from [currentTilesInteractor]. */
+    fun init(currentTilesInteractor: CurrentTilesInteractor) {
+        if (!initialized.compareAndSet(false, true)) {
+            return
+        }
+
+        dumpManager.registerNormalDumpable(TAG, this)
+
+        scope.launch {
+            currentTilesInteractor.userId.collectLatest { userId ->
+                coroutineScope {
+                    val previouslyAdded = repository.autoAddedTiles(userId).stateIn(this)
+
+                    autoAddables
+                        .map { addable ->
+                            val autoAddSignal = addable.autoAddSignal(userId)
+                            when (val lifecycle = addable.autoAddTracking) {
+                                is AutoAddTracking.Always -> autoAddSignal
+                                is AutoAddTracking.Disabled -> emptyFlow()
+                                is AutoAddTracking.IfNotAdded -> {
+                                    if (lifecycle.spec !in previouslyAdded.value) {
+                                        autoAddSignal.filterIsInstance<AutoAddSignal.Add>().take(1)
+                                    } else {
+                                        emptyFlow()
+                                    }
+                                }
+                            }
+                        }
+                        .merge()
+                        .collect { signal ->
+                            when (signal) {
+                                is AutoAddSignal.Add -> {
+                                    if (signal.spec !in previouslyAdded.value) {
+                                        currentTilesInteractor.addTile(signal.spec, signal.position)
+                                        qsPipelineLogger.logTileAutoAdded(
+                                            userId,
+                                            signal.spec,
+                                            signal.position
+                                        )
+                                        repository.markTileAdded(userId, signal.spec)
+                                    }
+                                }
+                                is AutoAddSignal.Remove -> {
+                                    currentTilesInteractor.removeTiles(setOf(signal.spec))
+                                    qsPipelineLogger.logTileAutoRemoved(userId, signal.spec)
+                                    repository.unmarkTileAdded(userId, signal.spec)
+                                }
+                            }
+                        }
+                }
+            }
+        }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        with(pw.asIndenting()) {
+            println("AutoAddables:")
+            indentIfPossible { autoAddables.forEach { println(it.description) } }
+        }
+    }
+
+    companion object {
+        private const val TAG = "AutoAddInteractor"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractor.kt
index 260caa7..fa6de8d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractor.kt
@@ -16,8 +16,7 @@
 package com.android.systemui.qs.pipeline.domain.interactor
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.phone.CentralSurfaces
-import java.util.Optional
+import com.android.systemui.shade.ShadeController
 import javax.inject.Inject
 
 /** Encapsulates business logic for interacting with the QS panel. */
@@ -37,17 +36,17 @@
 class PanelInteractorImpl
 @Inject
 constructor(
-    private val centralSurfaces: Optional<CentralSurfaces>,
+    private val shadeController: ShadeController,
 ) : PanelInteractor {
     override fun collapsePanels() {
-        centralSurfaces.ifPresent { it.postAnimateCollapsePanels() }
+        shadeController.postAnimateCollapseShade()
     }
 
     override fun forceCollapsePanels() {
-        centralSurfaces.ifPresent { it.postAnimateForceCollapsePanels() }
+        shadeController.postAnimateForceCollapseShade()
     }
 
     override fun openPanels() {
-        centralSurfaces.ifPresent { it.postAnimateOpenPanels() }
+        shadeController.postAnimateExpandQs()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddSignal.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddSignal.kt
new file mode 100644
index 0000000..ed7b8bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddSignal.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.qs.pipeline.domain.model
+
+import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+/** Signal indicating when a tile needs to be auto-added or removed */
+sealed interface AutoAddSignal {
+    /** Tile for this object */
+    val spec: TileSpec
+
+    /** Signal for auto-adding a tile at [position]. */
+    data class Add(
+        override val spec: TileSpec,
+        val position: Int = POSITION_AT_END,
+    ) : AutoAddSignal
+
+    /** Signal for removing a tile. */
+    data class Remove(
+        override val spec: TileSpec,
+    ) : AutoAddSignal
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddTracking.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddTracking.kt
new file mode 100644
index 0000000..154d045
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddTracking.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.qs.pipeline.domain.model
+
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+/** Strategy for when to track a particular [AutoAddable]. */
+sealed interface AutoAddTracking {
+
+    /**
+     * Indicates that the signals from the associated [AutoAddable] should all be collected and
+     * reacted accordingly. It may have [AutoAddSignal.Add] and [AutoAddSignal.Remove].
+     */
+    object Always : AutoAddTracking {
+        override fun toString(): String {
+            return "Always"
+        }
+    }
+
+    /**
+     * Indicates that the associated [AutoAddable] is [Disabled] and doesn't need to be collected.
+     */
+    object Disabled : AutoAddTracking {
+        override fun toString(): String {
+            return "Disabled"
+        }
+    }
+
+    /**
+     * Only the first [AutoAddSignal.Add] for each flow of signals needs to be collected, and only
+     * if the tile hasn't been auto-added yet. The associated [AutoAddable] will only emit
+     * [AutoAddSignal.Add].
+     */
+    data class IfNotAdded(val spec: TileSpec) : AutoAddTracking
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddable.kt
new file mode 100644
index 0000000..61fe5b4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddable.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.pipeline.domain.model
+
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Tracks conditions for auto-adding or removing specific tiles.
+ *
+ * When creating a new [AutoAddable], it needs to be registered in a [Module] like
+ * [BaseAutoAddableModule], for example:
+ * ```
+ * @Binds
+ * @IntoSet
+ * fun providesMyAutoAddable(autoAddable: MyAutoAddable): AutoAddable
+ * ```
+ */
+interface AutoAddable {
+
+    /**
+     * Signals associated with a particular user indicating whether a particular tile needs to be
+     * auto-added or auto-removed.
+     */
+    fun autoAddSignal(userId: Int): Flow<AutoAddSignal>
+
+    /**
+     * Lifecycle for this object. It indicates in which cases [autoAddSignal] should be collected
+     */
+    val autoAddTracking: AutoAddTracking
+
+    /** Human readable description */
+    val description: String
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
new file mode 100644
index 0000000..224fc1a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
@@ -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.qs.pipeline.domain.startable
+
+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.qs.pipeline.domain.interactor.AutoAddInteractor
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
+import javax.inject.Inject
+
+@SysUISingleton
+class QSPipelineCoreStartable
+@Inject
+constructor(
+    private val currentTilesInteractor: CurrentTilesInteractor,
+    private val autoAddInteractor: AutoAddInteractor,
+    private val featureFlags: FeatureFlags,
+) : CoreStartable {
+
+    override fun start() {
+        if (
+            featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST) &&
+                featureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD)
+        ) {
+            autoAddInteractor.init(currentTilesInteractor)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt
deleted file mode 100644
index bbd7234..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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.pipeline.prototyping
-
-import android.util.Log
-import com.android.systemui.CoreStartable
-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.qs.pipeline.data.repository.AutoAddRepository
-import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import com.android.systemui.statusbar.commandline.Command
-import com.android.systemui.statusbar.commandline.CommandRegistry
-import com.android.systemui.user.data.repository.UserRepository
-import java.io.PrintWriter
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.launch
-
-/**
- * Class for observing results while prototyping.
- *
- * The flows do their own logging, so we just need to make sure that they collect.
- *
- * This will be torn down together with the last of the new pipeline flags remaining here.
- */
-// TODO(b/270385608)
-@SysUISingleton
-class PrototypeCoreStartable
-@Inject
-constructor(
-    private val tileSpecRepository: TileSpecRepository,
-    private val autoAddRepository: AutoAddRepository,
-    private val userRepository: UserRepository,
-    private val featureFlags: FeatureFlags,
-    @Application private val scope: CoroutineScope,
-    private val commandRegistry: CommandRegistry,
-) : CoreStartable {
-
-    @OptIn(ExperimentalCoroutinesApi::class)
-    override fun start() {
-        if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {
-            scope.launch {
-                userRepository.selectedUserInfo
-                    .flatMapLatest { user -> tileSpecRepository.tilesSpecs(user.id) }
-                    .collect {}
-            }
-            if (featureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD)) {
-                scope.launch {
-                    userRepository.selectedUserInfo
-                        .flatMapLatest { user -> autoAddRepository.autoAddedTiles(user.id) }
-                        .collect { tiles -> Log.d(TAG, "Auto-added tiles: $tiles") }
-                }
-            }
-            commandRegistry.registerCommand(COMMAND, ::CommandExecutor)
-        }
-    }
-
-    private inner class CommandExecutor : Command {
-        override fun execute(pw: PrintWriter, args: List<String>) {
-            if (args.size < 2) {
-                pw.println("Error: needs at least two arguments")
-                return
-            }
-            val spec = TileSpec.create(args[1])
-            if (spec == TileSpec.Invalid) {
-                pw.println("Error: Invalid tile spec ${args[1]}")
-            }
-            if (args[0] == "add") {
-                performAdd(args, spec)
-                pw.println("Requested tile added")
-            } else if (args[0] == "remove") {
-                performRemove(args, spec)
-                pw.println("Requested tile removed")
-            } else {
-                pw.println("Error: unknown command")
-            }
-        }
-
-        private fun performAdd(args: List<String>, spec: TileSpec) {
-            val position = args.getOrNull(2)?.toInt() ?: TileSpecRepository.POSITION_AT_END
-            val user = args.getOrNull(3)?.toInt() ?: userRepository.getSelectedUserInfo().id
-            scope.launch { tileSpecRepository.addTile(user, spec, position) }
-        }
-
-        private fun performRemove(args: List<String>, spec: TileSpec) {
-            val user = args.getOrNull(2)?.toInt() ?: userRepository.getSelectedUserInfo().id
-            scope.launch { tileSpecRepository.removeTiles(user, listOf(spec)) }
-        }
-
-        override fun help(pw: PrintWriter) {
-            pw.println("Usage: adb shell cmd statusbar $COMMAND:")
-            pw.println("  add <spec> [position] [user]")
-            pw.println("  remove <spec> [user]")
-        }
-    }
-
-    companion object {
-        private const val COMMAND = "qs-pipeline"
-        private const val TAG = "PrototypeCoreStartable"
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt
index af1cd09..11b5dd7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt
@@ -52,7 +52,11 @@
     internal constructor(
         override val spec: String,
         val componentName: ComponentName,
-    ) : TileSpec(spec)
+    ) : TileSpec(spec) {
+        override fun toString(): String {
+            return "CustomTileSpec(${componentName.toShortString()})"
+        }
+    }
 
     companion object {
         /** Create a [TileSpec] from the string [spec]. */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
index 8318ec9..573cb715 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
@@ -18,7 +18,8 @@
 
 import android.annotation.UserIdInt
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.qs.pipeline.dagger.QSAutoAddLog
 import com.android.systemui.qs.pipeline.dagger.QSTileListLog
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import javax.inject.Inject
@@ -32,10 +33,12 @@
 @Inject
 constructor(
     @QSTileListLog private val tileListLogBuffer: LogBuffer,
+    @QSAutoAddLog private val tileAutoAddLogBuffer: LogBuffer,
 ) {
 
     companion object {
         const val TILE_LIST_TAG = "QSTileListLog"
+        const val AUTO_ADD_TAG = "QSAutoAddableLog"
     }
 
     /**
@@ -136,6 +139,31 @@
         )
     }
 
+    fun logTileAutoAdded(userId: Int, spec: TileSpec, position: Int) {
+        tileAutoAddLogBuffer.log(
+            AUTO_ADD_TAG,
+            LogLevel.DEBUG,
+            {
+                int1 = userId
+                int2 = position
+                str1 = spec.toString()
+            },
+            { "Tile $str1 auto added for user $int1 at position $int2" }
+        )
+    }
+
+    fun logTileAutoRemoved(userId: Int, spec: TileSpec) {
+        tileAutoAddLogBuffer.log(
+            AUTO_ADD_TAG,
+            LogLevel.DEBUG,
+            {
+                int1 = userId
+                str1 = spec.toString()
+            },
+            { "Tile $str1 auto removed for user $int1" }
+        )
+    }
+
     /** Reasons for destroying an existing tile. */
     enum class TileDestroyedReason(val readable: String) {
         TILE_REMOVED("Tile removed from  current set"),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
index cd69f4e..7e45491 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
@@ -130,6 +130,11 @@
                 d.setLayoutDirection(getLayoutDirection());
             }
 
+            final Drawable lastDrawable = iv.getDrawable();
+            if (lastDrawable instanceof Animatable2) {
+                ((Animatable2) lastDrawable).clearAnimationCallbacks();
+            }
+
             if (iv instanceof SlashImageView) {
                 ((SlashImageView) iv).setAnimationEnabled(shouldAnimate);
                 ((SlashImageView) iv).setState(null, d);
@@ -243,13 +248,11 @@
      */
     private static int getIconColorForState(Context context, QSTile.State state) {
         if (state.disabledByPolicy || state.state == Tile.STATE_UNAVAILABLE) {
-            return Utils.getColorAttrDefaultColor(
-                    context, com.android.internal.R.attr.textColorTertiary);
+            return Utils.getColorAttrDefaultColor(context, R.attr.outline);
         } else if (state.state == Tile.STATE_INACTIVE) {
-            return Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary);
+            return Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactiveVariant);
         } else if (state.state == Tile.STATE_ACTIVE) {
-            return Utils.getColorAttrDefaultColor(context,
-                    com.android.internal.R.attr.textColorOnAccent);
+            return Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive);
         } else {
             Log.e("QSIconView", "Invalid state " + state);
             return 0;
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 2a9e7d0..1ca2a96 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -330,7 +330,9 @@
         final int eventId = mClickEventId++;
         mQSLogger.logTileLongClick(mTileSpec, mStatusBarStateController.getState(), mState.state,
                 eventId);
-        mHandler.obtainMessage(H.LONG_CLICK, eventId, 0, view).sendToTarget();
+        if (!mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
+            mHandler.obtainMessage(H.LONG_CLICK, eventId, 0, view).sendToTarget();
+        }
     }
 
     public LogMaker populate(LogMaker logMaker) {
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 b806683..d81e4c2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -29,6 +29,7 @@
 import android.service.quicksettings.Tile
 import android.text.TextUtils
 import android.util.Log
+import android.util.TypedValue
 import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.View
@@ -92,24 +93,21 @@
             updateHeight()
         }
 
-    private val colorActive = Utils.getColorAttrDefaultColor(context,
-            com.android.internal.R.attr.colorAccentPrimary)
-    private val colorInactive = Utils.getColorAttrDefaultColor(context, R.attr.offStateColor)
-    private val colorUnavailable = Utils.applyAlpha(UNAVAILABLE_ALPHA, colorInactive)
+    private val colorActive = Utils.getColorAttrDefaultColor(context, R.attr.shadeActive)
+    private val colorInactive = Utils.getColorAttrDefaultColor(context, R.attr.shadeInactive)
+    private val colorUnavailable = Utils.getColorAttrDefaultColor(context, R.attr.shadeDisabled)
 
-    private val colorLabelActive =
-            Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.textColorOnAccent)
-    private val colorLabelInactive =
-            Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary)
+    private val colorLabelActive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive)
+    private val colorLabelInactive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactive)
     private val colorLabelUnavailable =
-        Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.textColorTertiary)
+        Utils.getColorAttrDefaultColor(context, R.attr.outline)
 
     private val colorSecondaryLabelActive =
-            Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondaryInverse)
+        Utils.getColorAttrDefaultColor(context, R.attr.onShadeActiveVariant)
     private val colorSecondaryLabelInactive =
-            Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary)
+            Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactiveVariant)
     private val colorSecondaryLabelUnavailable =
-        Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.textColorTertiary)
+        Utils.getColorAttrDefaultColor(context, R.attr.outline)
 
     private lateinit var label: TextView
     protected lateinit var secondaryLabel: TextView
@@ -151,6 +149,11 @@
     private val locInScreen = IntArray(2)
 
     init {
+        val typedValue = TypedValue()
+        if (!getContext().theme.resolveAttribute(R.attr.isQsTheme, typedValue, true)) {
+            throw IllegalStateException("QSViewImpl must be inflated with a theme that contains " +
+                    "Theme.SystemUI.QuickSettings")
+        }
         setId(generateViewId())
         orientation = LinearLayout.HORIZONTAL
         gravity = Gravity.CENTER_VERTICAL or Gravity.START
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 eef4c1d..b5e6a0f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
@@ -166,17 +166,6 @@
     }
 
     @Override
-    protected void handleLongClick(@Nullable View view) {
-        try {
-            // Need to wake on long click so bouncer->settings works.
-            mDreamManager.awaken();
-        } catch (RemoteException e) {
-            Log.e(LOG_TAG, "Can't awaken", e);
-        }
-        super.handleLongClick(view);
-    }
-
-    @Override
     protected void handleUpdateState(BooleanState state, Object arg) {
         state.label = getTileLabel();
         state.secondaryLabel = getActiveDreamName();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt
index c013486..423fa80 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt
@@ -38,7 +38,9 @@
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.settings.SecureSettings
 import com.android.systemui.util.settings.SystemSettings
@@ -57,11 +59,13 @@
     statusBarStateController: StatusBarStateController,
     activityStarter: ActivityStarter,
     qsLogger: QSLogger,
+    private val keyguardStateController: KeyguardStateController,
     private val dialogLaunchAnimator: DialogLaunchAnimator,
     private val systemSettings: SystemSettings,
     private val secureSettings: SecureSettings,
     private val systemClock: SystemClock,
     private val featureFlags: FeatureFlags,
+    private val userTracker: UserTracker,
     @Background private val backgroundDelayableExecutor: DelayableExecutor
 ) :
     QSTileImpl<QSTile.State?>(
@@ -86,26 +90,40 @@
     }
 
     override fun handleClick(view: View?) {
-        mUiHandler.post {
+        // We animate from the touched view only if we are not on the keyguard
+        val animateFromView: Boolean = view != null && !keyguardStateController.isShowing
+
+        val runnable = Runnable {
             val dialog: SystemUIDialog =
                 FontScalingDialog(
                     mContext,
                     systemSettings,
                     secureSettings,
                     systemClock,
+                    userTracker,
                     mainHandler,
                     backgroundDelayableExecutor
                 )
-            if (view != null) {
+            if (animateFromView) {
                 dialogLaunchAnimator.showFromView(
                     dialog,
-                    view,
+                    view!!,
                     DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)
                 )
             } else {
                 dialog.show()
             }
         }
+
+        mainHandler.post {
+            mActivityStarter.executeRunnableDismissingKeyguard(
+                runnable,
+                /* cancelAction= */ null,
+                /* dismissShade= */ true,
+                /* afterKeyguardGone= */ true,
+                /* deferred= */ false
+            )
+        }
     }
 
     override fun handleUpdateState(state: QSTile.State?, arg: Any?) {
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 ad65845..6e1ef91 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
@@ -284,7 +284,6 @@
         mHandler.removeCallbacks(mHideProgressBarRunnable);
         mHandler.removeCallbacks(mHideSearchingRunnable);
         mMobileNetworkLayout.setOnClickListener(null);
-        mMobileDataToggle.setOnCheckedChangeListener(null);
         mConnectedWifListLayout.setOnClickListener(null);
         if (mSecondaryMobileNetworkLayout != null) {
             mSecondaryMobileNetworkLayout.setOnClickListener(null);
@@ -349,18 +348,16 @@
             }
             mInternetDialogController.connectCarrierNetwork();
         });
-        mMobileDataToggle.setOnCheckedChangeListener(
-                (buttonView, isChecked) -> {
-                    if (!isChecked && shouldShowMobileDialog()) {
-                        showTurnOffMobileDialog();
-                    } else if (!shouldShowMobileDialog()) {
-                        if (mInternetDialogController.isMobileDataEnabled() == isChecked) {
-                            return;
-                        }
-                        mInternetDialogController.setMobileDataEnabled(mContext, mDefaultDataSubId,
-                                isChecked, false);
-                    }
-                });
+        mMobileDataToggle.setOnClickListener(v -> {
+            boolean isChecked = mMobileDataToggle.isChecked();
+            if (!isChecked && shouldShowMobileDialog()) {
+                mMobileDataToggle.setChecked(true);
+                showTurnOffMobileDialog();
+            } else if (mInternetDialogController.isMobileDataEnabled() != isChecked) {
+                mInternetDialogController.setMobileDataEnabled(mContext, mDefaultDataSubId,
+                        isChecked, false);
+            }
+        });
         mConnectedWifListLayout.setOnClickListener(this::onClickConnectedWifi);
         mSeeAllLayout.setOnClickListener(this::onClickSeeMoreButton);
         mWiFiToggle.setOnCheckedChangeListener(
@@ -686,9 +683,7 @@
         mAlertDialog = 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, (d, w) -> {
-                    mMobileDataToggle.setChecked(true);
-                })
+                .setNegativeButton(android.R.string.cancel, (d, w) -> {})
                 .setPositiveButton(
                         com.android.internal.R.string.alert_windows_notification_turn_off_action,
                         (d, w) -> {
@@ -698,7 +693,6 @@
                             Prefs.putBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, true);
                         })
                 .create();
-        mAlertDialog.setOnCancelListener(dialog -> mMobileDataToggle.setChecked(true));
         mAlertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
         SystemUIDialog.setShowForAllUsers(mAlertDialog, true);
         SystemUIDialog.registerDismissListener(mAlertDialog);
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 abeb5af..ec12580 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
@@ -172,7 +172,9 @@
     private Executor mExecutor;
     private AccessPointController mAccessPointController;
     private IntentFilter mConnectionStateFilter;
-    private InternetDialogCallback mCallback;
+    @VisibleForTesting
+    @Nullable
+    InternetDialogCallback mCallback;
     private UiEventLogger mUiEventLogger;
     private BroadcastDispatcher mBroadcastDispatcher;
     private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@@ -215,12 +217,16 @@
             new KeyguardUpdateMonitorCallback() {
                 @Override
                 public void onRefreshCarrierInfo() {
-                    mCallback.onRefreshCarrierInfo();
+                    if (mCallback != null) {
+                        mCallback.onRefreshCarrierInfo();
+                    }
                 }
 
                 @Override
                 public void onSimStateChanged(int subId, int slotId, int simState) {
-                    mCallback.onSimStateChanged();
+                    if (mCallback != null) {
+                        mCallback.onSimStateChanged();
+                    }
                 }
             };
 
@@ -331,6 +337,7 @@
         mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateCallback);
         mConnectivityManager.unregisterNetworkCallback(mConnectivityManagerNetworkCallback);
         mConnectedWifiInternetMonitor.unregisterCallback();
+        mCallback = null;
     }
 
     @VisibleForTesting
@@ -724,7 +731,7 @@
         ActivityLaunchAnimator.Controller controller =
                 mDialogLaunchAnimator.createActivityLaunchController(view);
 
-        if (controller == null) {
+        if (controller == null && mCallback != null) {
             mCallback.dismissDialog();
         }
 
@@ -1095,7 +1102,9 @@
             mHasWifiEntries = false;
         }
 
-        mCallback.onAccessPointsChanged(wifiEntries, connectedEntry, hasMoreWifiEntries);
+        if (mCallback != null) {
+            mCallback.onAccessPointsChanged(wifiEntries, connectedEntry, hasMoreWifiEntries);
+        }
     }
 
     @Override
@@ -1117,34 +1126,46 @@
 
         @Override
         public void onServiceStateChanged(@NonNull ServiceState serviceState) {
-            mCallback.onServiceStateChanged(serviceState);
+            if (mCallback != null) {
+                mCallback.onServiceStateChanged(serviceState);
+            }
         }
 
         @Override
         public void onDataConnectionStateChanged(int state, int networkType) {
-            mCallback.onDataConnectionStateChanged(state, networkType);
+            if (mCallback != null) {
+                mCallback.onDataConnectionStateChanged(state, networkType);
+            }
         }
 
         @Override
         public void onSignalStrengthsChanged(@NonNull SignalStrength signalStrength) {
-            mCallback.onSignalStrengthsChanged(signalStrength);
+            if (mCallback != null) {
+                mCallback.onSignalStrengthsChanged(signalStrength);
+            }
         }
 
         @Override
         public void onDisplayInfoChanged(@NonNull TelephonyDisplayInfo telephonyDisplayInfo) {
             mSubIdTelephonyDisplayInfoMap.put(mSubId, telephonyDisplayInfo);
-            mCallback.onDisplayInfoChanged(telephonyDisplayInfo);
+            if (mCallback != null) {
+                mCallback.onDisplayInfoChanged(telephonyDisplayInfo);
+            }
         }
 
         @Override
         public void onUserMobileDataStateChanged(boolean enabled) {
-            mCallback.onUserMobileDataStateChanged(enabled);
+            if (mCallback != null) {
+                mCallback.onUserMobileDataStateChanged(enabled);
+            }
         }
 
         @Override
         public void onCarrierNetworkChange(boolean active) {
             mCarrierNetworkChangeMode = active;
-            mCallback.onCarrierNetworkChange(active);
+            if (mCallback != null) {
+                mCallback.onCarrierNetworkChange(active);
+            }
         }
     }
 
@@ -1171,14 +1192,18 @@
                 scanWifiAccessPoints();
             }
             // update UI
-            mCallback.onCapabilitiesChanged(network, capabilities);
+            if (mCallback != null) {
+                mCallback.onCapabilitiesChanged(network, capabilities);
+            }
         }
 
         @Override
         @WorkerThread
         public void onLost(@NonNull Network network) {
             mHasEthernet = false;
-            mCallback.onLost(network);
+            if (mCallback != null) {
+                mCallback.onLost(network);
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 07259c2..e7dde66 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -200,8 +200,8 @@
 
         // TODO: change the method signature to use (boolean inputFocusTransferStarted)
         @Override
-        public void onStatusBarMotionEvent(MotionEvent event) {
-            verifyCallerAndClearCallingIdentity("onStatusBarMotionEvent", () -> {
+        public void onStatusBarTouchEvent(MotionEvent event) {
+            verifyCallerAndClearCallingIdentity("onStatusBarTouchEvent", () -> {
                 // TODO move this logic to message queue
                 mCentralSurfacesOptionalLazy.get().ifPresent(centralSurfaces -> {
                     if (event.getActionMasked() == ACTION_DOWN) {
@@ -236,6 +236,13 @@
         }
 
         @Override
+        public void onStatusBarTrackpadEvent(MotionEvent event) {
+            verifyCallerAndClearCallingIdentityPostMain("onStatusBarTrackpadEvent", () ->
+                    mCentralSurfacesOptionalLazy.get().ifPresent(centralSurfaces ->
+                            centralSurfaces.onStatusBarTrackpadEvent(event)));
+        }
+
+        @Override
         public void onBackPressed() {
             verifyCallerAndClearCallingIdentityPostMain("onBackPressed", () -> {
                 sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
@@ -345,7 +352,7 @@
 
         @Override
         public void expandNotificationPanel() {
-            verifyCallerAndClearCallingIdentity("expandNotificationPanel",
+            verifyCallerAndClearCallingIdentityPostMain("expandNotificationPanel",
                     () -> mCommandQueue.handleSystemKey(new KeyEvent(KeyEvent.ACTION_DOWN,
                             KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN)));
         }
@@ -353,7 +360,7 @@
         @Override
         public void toggleNotificationPanel() {
             verifyCallerAndClearCallingIdentityPostMain("toggleNotificationPanel", () ->
-                    mCentralSurfacesOptionalLazy.get().ifPresent(CentralSurfaces::togglePanel));
+                    mCommandQueue.togglePanel());
         }
 
         private boolean verifyCaller(String reason) {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index 752471d..0a9839e 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.scene
 
-import com.android.systemui.scene.data.model.SceneContainerConfigModule
+import com.android.systemui.scene.shared.model.SceneContainerConfigModule
 import com.android.systemui.scene.ui.composable.SceneModule
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModelModule
 import dagger.Module
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfig.kt b/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfig.kt
deleted file mode 100644
index d0769eb..0000000
--- a/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfig.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.scene.data.model
-
-import com.android.systemui.scene.shared.model.SceneKey
-
-/** Models the configuration of a single scene container. */
-data class SceneContainerConfig(
-    /** Container name. Must be unique across all containers in System UI. */
-    val name: String,
-
-    /**
-     * The keys to all scenes in the container, sorted by z-order such that the last one renders on
-     * top of all previous ones. Scene keys within the same container must not repeat but it's okay
-     * to have the same scene keys in different containers.
-     */
-    val sceneKeys: List<SceneKey>,
-
-    /**
-     * The key of the scene that is the initial current scene when the container is first set up,
-     * before taking any application state in to account.
-     */
-    val initialSceneKey: SceneKey,
-) {
-    init {
-        check(sceneKeys.isNotEmpty()) { "A container must have at least one scene key." }
-
-        check(sceneKeys.contains(initialSceneKey)) {
-            "The initial key \"$initialSceneKey\" is not present in this container."
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfigModule.kt b/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfigModule.kt
deleted file mode 100644
index 0af8094..0000000
--- a/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfigModule.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.scene.data.model
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.scene.shared.model.SceneContainerNames
-import com.android.systemui.scene.shared.model.SceneKey
-import dagger.Module
-import dagger.Provides
-import javax.inject.Named
-
-@Module
-object SceneContainerConfigModule {
-
-    @Provides
-    fun containerConfigs(): Map<String, SceneContainerConfig> {
-        return mapOf(
-            SceneContainerNames.SYSTEM_UI_DEFAULT to
-                SceneContainerConfig(
-                    name = SceneContainerNames.SYSTEM_UI_DEFAULT,
-                    // Note that this list is in z-order. The first one is the bottom-most and the
-                    // last
-                    // one is top-most.
-                    sceneKeys =
-                        listOf(
-                            SceneKey.Gone,
-                            SceneKey.Lockscreen,
-                            SceneKey.Bouncer,
-                            SceneKey.Shade,
-                            SceneKey.QuickSettings,
-                        ),
-                    initialSceneKey = SceneKey.Lockscreen,
-                ),
-        )
-    }
-
-    @Provides
-    @SysUISingleton
-    @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
-    fun provideDefaultSceneContainerConfig(
-        configs: Map<String, SceneContainerConfig>,
-    ): SceneContainerConfig {
-        return checkNotNull(configs[SceneContainerNames.SYSTEM_UI_DEFAULT]) {
-            "No SceneContainerConfig named \"${SceneContainerNames.SYSTEM_UI_DEFAULT}\"."
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
index 61b162b..1ebeced 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.scene.data.repository
 
-import com.android.systemui.scene.data.model.SceneContainerConfig
+import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
 import javax.inject.Inject
@@ -28,11 +28,9 @@
 class SceneContainerRepository
 @Inject
 constructor(
-    containerConfigurations: Set<SceneContainerConfig>,
+    private val containerConfigByName: Map<String, SceneContainerConfig>,
 ) {
 
-    private val containerConfigByName: Map<String, SceneContainerConfig> =
-        containerConfigurations.associateBy { config -> config.name }
     private val containerVisibilityByName: Map<String, MutableStateFlow<Boolean>> =
         containerConfigByName
             .map { (containerName, _) -> containerName to MutableStateFlow(true) }
@@ -48,21 +46,6 @@
             .map { (containerName, _) -> containerName to MutableStateFlow(1f) }
             .toMap()
 
-    init {
-        val repeatedContainerNames =
-            containerConfigurations
-                .groupingBy { config -> config.name }
-                .eachCount()
-                .filter { (_, count) -> count > 1 }
-        check(repeatedContainerNames.isEmpty()) {
-            "Container names must be unique. The following container names appear more than once: ${
-                repeatedContainerNames
-                        .map { (name, count) -> "\"$name\" appears $count times" }
-                        .joinToString(", ")
-            }"
-        }
-    }
-
     /**
      * Returns the keys to all scenes in the container with the given name.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerConfig.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerConfig.kt
new file mode 100644
index 0000000..0327edb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerConfig.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.scene.shared.model
+
+/** Models the configuration of a single scene container. */
+data class SceneContainerConfig(
+    /** Container name. Must be unique across all containers in System UI. */
+    val name: String,
+
+    /**
+     * The keys to all scenes in the container, sorted by z-order such that the last one renders on
+     * top of all previous ones. Scene keys within the same container must not repeat but it's okay
+     * to have the same scene keys in different containers.
+     */
+    val sceneKeys: List<SceneKey>,
+
+    /**
+     * The key of the scene that is the initial current scene when the container is first set up,
+     * before taking any application state in to account.
+     */
+    val initialSceneKey: SceneKey,
+) {
+    init {
+        check(sceneKeys.isNotEmpty()) { "A container must have at least one scene key." }
+
+        check(sceneKeys.contains(initialSceneKey)) {
+            "The initial key \"$initialSceneKey\" is not present in this container."
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerConfigModule.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerConfigModule.kt
new file mode 100644
index 0000000..7562a5a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneContainerConfigModule.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.scene.shared.model
+
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Module
+import dagger.Provides
+import javax.inject.Named
+
+@Module
+object SceneContainerConfigModule {
+
+    @Provides
+    fun containerConfigs(): Map<String, SceneContainerConfig> {
+        return mapOf(
+            SceneContainerNames.SYSTEM_UI_DEFAULT to
+                SceneContainerConfig(
+                    name = SceneContainerNames.SYSTEM_UI_DEFAULT,
+                    // Note that this list is in z-order. The first one is the bottom-most and the
+                    // last
+                    // one is top-most.
+                    sceneKeys =
+                        listOf(
+                            SceneKey.Gone,
+                            SceneKey.Lockscreen,
+                            SceneKey.Bouncer,
+                            SceneKey.Shade,
+                            SceneKey.QuickSettings,
+                        ),
+                    initialSceneKey = SceneKey.Lockscreen,
+                ),
+        )
+    }
+
+    @Provides
+    @SysUISingleton
+    @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+    fun provideDefaultSceneContainerConfig(
+        configs: Map<String, SceneContainerConfig>,
+    ): SceneContainerConfig {
+        return checkNotNull(configs[SceneContainerNames.SYSTEM_UI_DEFAULT]) {
+            "No SceneContainerConfig named \"${SceneContainerNames.SYSTEM_UI_DEFAULT}\"."
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
new file mode 100644
index 0000000..2ad5429
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
@@ -0,0 +1,74 @@
+package com.android.systemui.scene.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.OnBackPressedDispatcherOwner
+import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.scene.shared.model.Scene
+import com.android.systemui.scene.shared.model.SceneContainerConfig
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import kotlinx.coroutines.launch
+
+/** A root view of the main SysUI window that supports scenes. */
+class SceneWindowRootView(
+    context: Context,
+    attrs: AttributeSet?,
+) :
+    WindowRootView(
+        context,
+        attrs,
+    ) {
+    fun init(
+        viewModel: SceneContainerViewModel,
+        containerConfig: SceneContainerConfig,
+        scenes: Set<Scene>,
+    ) {
+        val unsortedSceneByKey: Map<SceneKey, Scene> = scenes.associateBy { scene -> scene.key }
+        val sortedSceneByKey: Map<SceneKey, Scene> = buildMap {
+            containerConfig.sceneKeys.forEach { sceneKey ->
+                val scene =
+                    checkNotNull(unsortedSceneByKey[sceneKey]) {
+                        "Scene not found for key \"$sceneKey\"!"
+                    }
+
+                put(sceneKey, scene)
+            }
+        }
+
+        repeatWhenAttached {
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    setViewTreeOnBackPressedDispatcherOwner(
+                        object : OnBackPressedDispatcherOwner {
+                            override val onBackPressedDispatcher =
+                                OnBackPressedDispatcher().apply {
+                                    setOnBackInvokedDispatcher(viewRootImpl.onBackInvokedDispatcher)
+                                }
+
+                            override val lifecycle: Lifecycle =
+                                this@repeatWhenAttached.lifecycle
+                        }
+                    )
+
+                    addView(
+                        ComposeFacade.createSceneContainerView(
+                            context = context,
+                            viewModel = viewModel,
+                            sceneByKey = sortedSceneByKey,
+                        )
+                    )
+                }
+
+                // Here when destroyed.
+                removeAllViews()
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
new file mode 100644
index 0000000..11e1eb9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
@@ -0,0 +1,53 @@
+package com.android.systemui.scene.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.FrameLayout
+import com.android.systemui.compose.ComposeFacade
+
+/**
+ * A view that can serve as the root of the main SysUI window (but might not, see below for more
+ * information regarding this confusing comment).
+ *
+ * Naturally, only one view may ever be at the root of a view hierarchy tree. Under certain
+ * conditions, the view hierarchy tree in the scene-containing window view may actually have one
+ * [WindowRootView] acting as the true root view and another [WindowRootView] which doesn't and is,
+ * instead, a child of the true root view. To discern which one is which, please use the [isRoot]
+ * method.
+ */
+open class WindowRootView(
+    context: Context,
+    attrs: AttributeSet?,
+) :
+    FrameLayout(
+        context,
+        attrs,
+    ) {
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+
+        if (ComposeFacade.isComposeAvailable() && isRoot()) {
+            ComposeFacade.composeInitializer().onAttachedToWindow(this)
+        }
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+
+        if (ComposeFacade.isComposeAvailable() && isRoot()) {
+            ComposeFacade.composeInitializer().onDetachedFromWindow(this)
+        }
+    }
+
+    /**
+     * Returns `true` if this view is the true root of the view-hierarchy; `false` otherwise.
+     *
+     * Please see the class-level documentation to understand why this is possible.
+     */
+    private fun isRoot(): Boolean {
+        // TODO(b/283300105): remove this check once there's only one subclass of WindowRootView.
+        return parent.let { it !is View || it.id == android.R.id.content }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java
index afc8bff..7de22b1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java
@@ -19,7 +19,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
-import android.os.UserManager;
+import android.os.UserHandle;
 
 import androidx.annotation.Nullable;
 
@@ -39,15 +39,14 @@
     private final DisplayTracker mDisplayTracker;
 
     @Inject
-    AppClipsCrossProcessHelper(@Application Context context, UserManager userManager,
-            DisplayTracker displayTracker) {
+    AppClipsCrossProcessHelper(@Application Context context, DisplayTracker displayTracker) {
         // Start a service as main user so that even if the app clips activity is running as work
         // profile user the service is able to use correct instance of Bubbles to grab a screenshot
         // excluding the bubble layer.
         mProxyConnector = new ServiceConnector.Impl<>(context,
                 new Intent(context, AppClipsScreenshotHelperService.class),
                 Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY
-                        | Context.BIND_NOT_VISIBLE, userManager.getMainUser().getIdentifier(),
+                        | Context.BIND_NOT_VISIBLE, UserHandle.USER_SYSTEM,
                 IAppClipsScreenshotHelperService.Stub::asInterface);
         mDisplayTracker = displayTracker;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java
index 4c01315..4cc2f62 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java
@@ -33,6 +33,11 @@
 /**
  * A helper service that runs in SysUI process and helps {@link AppClipsActivity} which runs in its
  * own separate process take a screenshot.
+ *
+ * <p>Note: This service always runs in the SysUI process running on the system user irrespective of
+ * which user started the service. This is required so that the correct instance of {@link Bubbles}
+ * instance is injected. This is set via attribute {@code android:singleUser=”true”} in
+ * AndroidManifest.
  */
 public class AppClipsScreenshotHelperService extends Service {
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
index 3949492..dce8c81 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
@@ -16,6 +16,11 @@
 
 package com.android.systemui.screenshot.appclips;
 
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
+
 import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
 
 import android.app.Activity;
@@ -25,17 +30,12 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.Intent.CaptureContentForNoteStatusCodes;
 import android.content.res.Resources;
 import android.os.IBinder;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.util.Log;
 
 import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
 
-import com.android.internal.infra.AndroidFuture;
-import com.android.internal.infra.ServiceConnector;
 import com.android.internal.statusbar.IAppClipsService;
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Application;
@@ -43,73 +43,36 @@
 import com.android.wm.shell.bubbles.Bubbles;
 
 import java.util.Optional;
-import java.util.concurrent.ExecutionException;
 
 import javax.inject.Inject;
 
 /**
  * A service that communicates with {@link StatusBarManager} to support the
- * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} API.
+ * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} API. Also used by
+ * {@link AppClipsTrampolineActivity} to query if an app should be allowed to user App Clips.
+ *
+ * <p>Note: This service always runs in the SysUI process running on the system user irrespective of
+ * which user started the service. This is required so that the correct instance of {@link Bubbles}
+ * instance is injected. This is set via attribute {@code android:singleUser=”true”} in
+ * AndroidManifest.
  */
 public class AppClipsService extends Service {
 
-    private static final String TAG = AppClipsService.class.getSimpleName();
-
     @Application private final Context mContext;
     private final FeatureFlags mFeatureFlags;
     private final Optional<Bubbles> mOptionalBubbles;
     private final DevicePolicyManager mDevicePolicyManager;
-    private final UserManager mUserManager;
-
     private final boolean mAreTaskAndTimeIndependentPrerequisitesMet;
 
-    @VisibleForTesting()
-    @Nullable ServiceConnector<IAppClipsService> mProxyConnectorToMainProfile;
-
     @Inject
     public AppClipsService(@Application Context context, FeatureFlags featureFlags,
-            Optional<Bubbles> optionalBubbles, DevicePolicyManager devicePolicyManager,
-            UserManager userManager) {
+            Optional<Bubbles> optionalBubbles, DevicePolicyManager devicePolicyManager) {
         mContext = context;
         mFeatureFlags = featureFlags;
         mOptionalBubbles = optionalBubbles;
         mDevicePolicyManager = devicePolicyManager;
-        mUserManager = userManager;
-
-        // The consumer of this service are apps that call through StatusBarManager API to query if
-        // it can use app clips API. Since these apps can be launched as work profile users, this
-        // service will start as work profile user. SysUI doesn't share injected instances for
-        // different users. This is why the bubbles instance injected will be incorrect. As the apps
-        // don't generally have permission to connect to a service running as different user, we
-        // start a proxy connection to communicate with the main user's version of this service.
-        if (mUserManager.isManagedProfile()) {
-            // No need to check for prerequisites in this case as those are incorrect for work
-            // profile user instance of the service and the main user version of the service will
-            // take care of this check.
-            mAreTaskAndTimeIndependentPrerequisitesMet = false;
-
-            // Get the main user so that we can connect to the main user's version of the service.
-            UserHandle mainUser = mUserManager.getMainUser();
-            if (mainUser == null) {
-                // If main user is not available there isn't much we can do, no apps can use app
-                // clips.
-                return;
-            }
-
-            // Set up the connection to be used later during onBind callback.
-            mProxyConnectorToMainProfile =
-                    new ServiceConnector.Impl<>(
-                            context,
-                            new Intent(context, AppClipsService.class),
-                            Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY
-                                    | Context.BIND_NOT_VISIBLE,
-                            mainUser.getIdentifier(),
-                            IAppClipsService.Stub::asInterface);
-            return;
-        }
 
         mAreTaskAndTimeIndependentPrerequisitesMet = checkIndependentVariables();
-        mProxyConnectorToMainProfile = null;
     }
 
     private boolean checkIndependentVariables() {
@@ -144,40 +107,25 @@
         return new IAppClipsService.Stub() {
             @Override
             public boolean canLaunchCaptureContentActivityForNote(int taskId) {
-                // In case of managed profile, use the main user's instance of the service. Callers
-                // cannot directly connect to the main user's instance as they may not have the
-                // permission to interact across users.
-                if (mUserManager.isManagedProfile()) {
-                    return canLaunchCaptureContentActivityForNoteFromMainUser(taskId);
-                }
+                return canLaunchCaptureContentActivityForNoteInternal(taskId)
+                        == CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
+            }
 
+            @Override
+            @CaptureContentForNoteStatusCodes
+            public int canLaunchCaptureContentActivityForNoteInternal(int taskId) {
                 if (!mAreTaskAndTimeIndependentPrerequisitesMet) {
-                    return false;
+                    return CAPTURE_CONTENT_FOR_NOTE_FAILED;
                 }
 
                 if (!mOptionalBubbles.get().isAppBubbleTaskId(taskId)) {
-                    return false;
+                    return CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
                 }
 
-                return !mDevicePolicyManager.getScreenCaptureDisabled(null);
+                return mDevicePolicyManager.getScreenCaptureDisabled(null)
+                        ? CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN
+                        : CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
             }
         };
     }
-
-    /** Returns whether the app clips API can be used by querying the service as the main user. */
-    private boolean canLaunchCaptureContentActivityForNoteFromMainUser(int taskId) {
-        if (mProxyConnectorToMainProfile == null) {
-            return false;
-        }
-
-        try {
-            AndroidFuture<Boolean> future = mProxyConnectorToMainProfile.postForResult(
-                    service -> service.canLaunchCaptureContentActivityForNote(taskId));
-            return future.get();
-        } catch (ExecutionException | InterruptedException e) {
-            Log.d(TAG, "Exception from service\n" + e);
-        }
-
-        return false;
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
index f00803c..6e5cef4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
@@ -22,41 +22,41 @@
 import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
 import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE;
 
-import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
 import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED;
 
 import android.app.Activity;
-import android.app.admin.DevicePolicyManager;
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
+import android.content.Intent.CaptureContentForNoteStatusCodes;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.ApplicationInfoFlags;
 import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.UserInfo;
-import android.content.res.Resources;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Parcel;
 import android.os.ResultReceiver;
 import android.os.UserHandle;
-import android.os.UserManager;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.internal.infra.AndroidFuture;
+import com.android.internal.infra.ServiceConnector;
 import com.android.internal.logging.UiEventLogger;
+import com.android.internal.statusbar.IAppClipsService;
 import com.android.systemui.R;
+import com.android.systemui.broadcast.BroadcastSender;
+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.notetask.NoteTaskController;
 import com.android.systemui.notetask.NoteTaskEntryPoint;
-import com.android.systemui.settings.UserTracker;
-import com.android.wm.shell.bubbles.Bubbles;
 
-import java.util.Optional;
+import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
@@ -82,39 +82,57 @@
     private static final String TAG = AppClipsTrampolineActivity.class.getSimpleName();
     static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
     static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI";
-    @VisibleForTesting
-    static final String EXTRA_USE_WP_USER = TAG + "USE_WP_USER";
     static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE";
     static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER";
     static final String EXTRA_CALLING_PACKAGE_NAME = TAG + "CALLING_PACKAGE_NAME";
     private static final ApplicationInfoFlags APPLICATION_INFO_FLAGS = ApplicationInfoFlags.of(0);
 
-    private final DevicePolicyManager mDevicePolicyManager;
-    private final FeatureFlags mFeatureFlags;
-    private final Optional<Bubbles> mOptionalBubbles;
     private final NoteTaskController mNoteTaskController;
     private final PackageManager mPackageManager;
-    private final UserTracker mUserTracker;
     private final UiEventLogger mUiEventLogger;
-    private final UserManager mUserManager;
+    private final BroadcastSender mBroadcastSender;
+    @Background
+    private final Executor mBgExecutor;
+    @Main
+    private final Executor mMainExecutor;
     private final ResultReceiver mResultReceiver;
 
+    private final ServiceConnector<IAppClipsService> mAppClipsServiceConnector;
+
+    private UserHandle mUserHandle;
     private Intent mKillAppClipsBroadcastIntent;
-    private UserHandle mNotesAppUser;
 
     @Inject
-    public AppClipsTrampolineActivity(DevicePolicyManager devicePolicyManager, FeatureFlags flags,
-            Optional<Bubbles> optionalBubbles, NoteTaskController noteTaskController,
-            PackageManager packageManager, UserTracker userTracker, UiEventLogger uiEventLogger,
-            UserManager userManager, @Main Handler mainHandler) {
-        mDevicePolicyManager = devicePolicyManager;
-        mFeatureFlags = flags;
-        mOptionalBubbles = optionalBubbles;
+    public AppClipsTrampolineActivity(@Application Context context,
+            NoteTaskController noteTaskController, PackageManager packageManager,
+            UiEventLogger uiEventLogger, BroadcastSender broadcastSender,
+            @Background Executor bgExecutor, @Main Executor mainExecutor,
+            @Main Handler mainHandler) {
         mNoteTaskController = noteTaskController;
         mPackageManager = packageManager;
-        mUserTracker = userTracker;
         mUiEventLogger = uiEventLogger;
-        mUserManager = userManager;
+        mBroadcastSender = broadcastSender;
+        mBgExecutor = bgExecutor;
+        mMainExecutor = mainExecutor;
+
+        mResultReceiver = createResultReceiver(mainHandler);
+        mAppClipsServiceConnector = createServiceConnector(context);
+    }
+
+    /** A constructor used only for testing to verify interactions with {@link ServiceConnector}. */
+    @VisibleForTesting
+    AppClipsTrampolineActivity(ServiceConnector<IAppClipsService> appClipsServiceConnector,
+            NoteTaskController noteTaskController, PackageManager packageManager,
+            UiEventLogger uiEventLogger, BroadcastSender broadcastSender,
+            @Background Executor bgExecutor, @Main Executor mainExecutor,
+            @Main Handler mainHandler) {
+        mAppClipsServiceConnector = appClipsServiceConnector;
+        mNoteTaskController = noteTaskController;
+        mPackageManager = packageManager;
+        mUiEventLogger = uiEventLogger;
+        mBroadcastSender = broadcastSender;
+        mBgExecutor = bgExecutor;
+        mMainExecutor = mainExecutor;
 
         mResultReceiver = createResultReceiver(mainHandler);
     }
@@ -127,62 +145,62 @@
             return;
         }
 
-        if (mUserManager.isManagedProfile()) {
-            maybeStartActivityForWPUser();
-            finish();
+        mUserHandle = getUser();
+
+        mBgExecutor.execute(() -> {
+            AndroidFuture<Integer> statusCodeFuture = mAppClipsServiceConnector.postForResult(
+                    service -> service.canLaunchCaptureContentActivityForNoteInternal(getTaskId()));
+            statusCodeFuture.whenCompleteAsync(this::handleAppClipsStatusCode, mMainExecutor);
+        });
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (isFinishing() && mKillAppClipsBroadcastIntent != null) {
+            mBroadcastSender.sendBroadcast(mKillAppClipsBroadcastIntent, PERMISSION_SELF);
+        }
+
+        super.onDestroy();
+    }
+
+    private void handleAppClipsStatusCode(@CaptureContentForNoteStatusCodes int statusCode,
+            Throwable error) {
+        if (isFinishing()) {
+            // It's too late, trampoline activity is finishing or already finished. Return early.
             return;
         }
 
-        if (!mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)) {
-            finish();
+        if (error != null) {
+            Log.d(TAG, "Error querying app clips service", error);
+            setErrorResultAndFinish(statusCode);
             return;
         }
 
-        if (mOptionalBubbles.isEmpty()) {
-            setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
-            return;
-        }
+        switch (statusCode) {
+            case CAPTURE_CONTENT_FOR_NOTE_SUCCESS:
+                launchAppClipsActivity();
+                break;
 
-        if (!mOptionalBubbles.get().isAppBubbleTaskId(getTaskId())) {
-            setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED);
-            return;
+            case CAPTURE_CONTENT_FOR_NOTE_FAILED:
+            case CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED:
+            case CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN:
+            default:
+                setErrorResultAndFinish(statusCode);
         }
+    }
 
-        if (mDevicePolicyManager.getScreenCaptureDisabled(null)) {
-            setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN);
-            return;
-        }
-
-        ComponentName componentName;
-        try {
-            componentName = ComponentName.unflattenFromString(
+    private void launchAppClipsActivity() {
+        ComponentName componentName = ComponentName.unflattenFromString(
                     getString(R.string.config_screenshotAppClipsActivityComponent));
-        } catch (Resources.NotFoundException e) {
-            setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
-            return;
-        }
-
-        if (componentName == null || componentName.getPackageName().isEmpty()
-                || componentName.getClassName().isEmpty()) {
-            setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
-            return;
-        }
-
-        mNotesAppUser = getUser();
-        if (getIntent().getBooleanExtra(EXTRA_USE_WP_USER, /* defaultValue= */ false)) {
-            // Get the work profile user internally instead of passing around via intent extras as
-            // this activity is exported apps could potentially mess around with intent extras.
-            mNotesAppUser = getWorkProfileUser().orElse(mNotesAppUser);
-        }
-
         String callingPackageName = getCallingPackage();
-        Intent intent = new Intent().setComponent(componentName)
+
+        Intent intent = new Intent()
+                .setComponent(componentName)
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                 .putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver)
                 .putExtra(EXTRA_CALLING_PACKAGE_NAME, callingPackageName);
         try {
-            // Start the App Clips activity for the user corresponding to the notes app user.
-            startActivityAsUser(intent, mNotesAppUser);
+            startActivity(intent);
 
             // Set up the broadcast intent that will inform the above App Clips activity to finish
             // when this trampoline activity is finished.
@@ -198,39 +216,6 @@
         }
     }
 
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-
-        if (isFinishing() && mKillAppClipsBroadcastIntent != null) {
-            sendBroadcast(mKillAppClipsBroadcastIntent, PERMISSION_SELF);
-        }
-    }
-
-    private Optional<UserHandle> getWorkProfileUser() {
-        return mUserTracker.getUserProfiles().stream()
-                .filter(profile -> mUserManager.isManagedProfile(profile.id))
-                .findFirst()
-                .map(UserInfo::getUserHandle);
-    }
-
-    private void maybeStartActivityForWPUser() {
-        UserHandle mainUser = mUserManager.getMainUser();
-        if (mainUser == null) {
-            setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
-            return;
-        }
-
-        // Start the activity as the main user with activity result forwarding. Set the intent extra
-        // so that the newly started trampoline activity starts the actual app clips activity as the
-        // work profile user. Starting the app clips activity as the work profile user is required
-        // to save the screenshot in work profile user storage and grant read permission to the URI.
-        startActivityAsUser(
-                new Intent(this, AppClipsTrampolineActivity.class)
-                        .putExtra(EXTRA_USE_WP_USER, /* value= */ true)
-                        .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT), mainUser);
-    }
-
     private void setErrorResultAndFinish(int errorCode) {
         setResult(RESULT_OK,
                 new Intent().putExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, errorCode));
@@ -241,7 +226,7 @@
         int callingPackageUid = 0;
         try {
             callingPackageUid = mPackageManager.getApplicationInfoAsUser(callingPackageName,
-                    APPLICATION_INFO_FLAGS, mNotesAppUser.getIdentifier()).uid;
+                    APPLICATION_INFO_FLAGS, mUserHandle.getIdentifier()).uid;
         } catch (NameNotFoundException e) {
             Log.d(TAG, "Couldn't find notes app UID " + e);
         }
@@ -281,7 +266,7 @@
             mKillAppClipsBroadcastIntent = null;
 
             // Expand the note bubble before returning the result.
-            mNoteTaskController.showNoteTaskAsUser(NoteTaskEntryPoint.APP_CLIPS, mNotesAppUser);
+            mNoteTaskController.showNoteTaskAsUser(NoteTaskEntryPoint.APP_CLIPS, mUserHandle);
             setResult(RESULT_OK, convertedData);
             finish();
         }
@@ -298,11 +283,18 @@
         appClipsResultReceiver.writeToParcel(parcel, 0);
         parcel.setDataPosition(0);
 
-        ResultReceiver resultReceiver  = ResultReceiver.CREATOR.createFromParcel(parcel);
+        ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(parcel);
         parcel.recycle();
         return resultReceiver;
     }
 
+    private ServiceConnector<IAppClipsService> createServiceConnector(
+            @Application Context context) {
+        return new ServiceConnector.Impl<>(context, new Intent(context, AppClipsService.class),
+                Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY | Context.BIND_NOT_VISIBLE,
+                UserHandle.USER_SYSTEM, IAppClipsService.Stub::asInterface);
+    }
+
     /** This is a test only API for mocking response from {@link AppClipsActivity}. */
     @VisibleForTesting
     public ResultReceiver getResultReceiverForTest() {
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 8879501..5199bd4 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -84,6 +84,7 @@
         window.getDecorView();
         window.setLayout(
                 WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT);
+        getTheme().applyStyle(R.style.Theme_SystemUI_QuickSettings, false);
 
         setContentView(R.layout.brightness_mirror_container);
         FrameLayout frame = findViewById(R.id.brightness_mirror_container);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
index 7e0f504..a9b3d0a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
@@ -31,8 +31,7 @@
         val constraintAlpha = if (visible) 0f else 1f
         return ConstraintsChanges(
             qqsConstraintsChanges = {
-                setAlpha(R.id.statusIcons, constraintAlpha)
-                setAlpha(R.id.batteryRemainingIcon, constraintAlpha)
+                setAlpha(R.id.shade_header_system_icons, constraintAlpha)
             }
         )
     }
@@ -45,14 +44,15 @@
                     R.id.barrier,
                     ConstraintSet.START,
                     0,
-                    R.id.statusIcons,
+                    R.id.shade_header_system_icons,
                     R.id.privacy_container
                 )
-                connect(R.id.statusIcons, ConstraintSet.START, R.id.date, ConstraintSet.END)
+                connect(R.id.shade_header_system_icons, ConstraintSet.START, R.id.date,
+                    ConstraintSet.END)
                 connect(R.id.privacy_container, ConstraintSet.START, R.id.date, ConstraintSet.END)
-                constrainWidth(R.id.statusIcons, ViewGroup.LayoutParams.WRAP_CONTENT)
+                constrainWidth(R.id.shade_header_system_icons, ViewGroup.LayoutParams.WRAP_CONTENT)
                 constrainedWidth(R.id.date, true)
-                constrainedWidth(R.id.statusIcons, true)
+                constrainedWidth(R.id.shade_header_system_icons, true)
             }
         )
     }
@@ -84,7 +84,7 @@
                 setGuidelineEnd(centerEnd, offsetFromEdge)
                 connect(R.id.date, ConstraintSet.END, centerStart, ConstraintSet.START)
                 connect(
-                    R.id.statusIcons,
+                    R.id.shade_header_system_icons,
                     ConstraintSet.START,
                     centerEnd,
                     ConstraintSet.END
@@ -96,7 +96,7 @@
                     ConstraintSet.END
                 )
                 constrainedWidth(R.id.date, true)
-                constrainedWidth(R.id.statusIcons, true)
+                constrainedWidth(R.id.shade_header_system_icons, true)
             },
             qsConstraintsChanges = {
                 setGuidelineBegin(centerStart, offsetFromEdge)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 267b147..d97db3b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -17,8 +17,6 @@
 package com.android.systemui.shade;
 
 import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
-import static android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE;
-import static android.view.MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE;
 import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
 
@@ -30,6 +28,8 @@
 import static com.android.systemui.classifier.Classifier.GENERIC;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
 import static com.android.systemui.classifier.Classifier.UNLOCK;
+import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll;
+import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPENING;
@@ -90,7 +90,6 @@
 
 import com.android.app.animation.Interpolators;
 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.SystemBarUtils;
@@ -114,8 +113,11 @@
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.LaunchAnimator;
 import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.classifier.FalsingCollector;
+import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.DisplayId;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.doze.DozeLog;
@@ -125,14 +127,14 @@
 import com.android.systemui.flags.Flags;
 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.KeyguardViewConfigurator;
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 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.shared.model.WakefulnessModel;
 import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder;
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
@@ -156,6 +158,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.shade.transition.ShadeTransitionController;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.GestureRecorder;
@@ -206,7 +209,6 @@
 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.phone.dagger.CentralSurfacesComponent;
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController;
@@ -222,8 +224,6 @@
 import com.android.systemui.util.time.SystemClock;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 
-import kotlin.Unit;
-
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -234,9 +234,11 @@
 import javax.inject.Inject;
 import javax.inject.Provider;
 
+import kotlin.Unit;
+
 import kotlinx.coroutines.CoroutineDispatcher;
 
-@CentralSurfacesComponent.CentralSurfacesScope
+@SysUISingleton
 public final class NotificationPanelViewController implements ShadeSurface, Dumpable {
 
     public static final String TAG = NotificationPanelView.class.getSimpleName();
@@ -347,7 +349,6 @@
     private final NotificationGutsManager mGutsManager;
     private final AlternateBouncerInteractor mAlternateBouncerInteractor;
     private final QuickSettingsController mQsController;
-    private final InteractionJankMonitor mInteractionJankMonitor;
     private final TouchHandler mTouchHandler = new TouchHandler();
 
     private long mDownTime;
@@ -360,6 +361,7 @@
     /** The current squish amount for the predictive back animation */
     private float mCurrentBackProgress = 0.0f;
     private boolean mTracking;
+    private boolean mIsTrackingExpansionFromStatusBar;
     private boolean mHintAnimationRunning;
     private KeyguardBottomAreaView mKeyguardBottomArea;
     private boolean mExpanding;
@@ -599,6 +601,7 @@
 
     private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
     private final KeyguardInteractor mKeyguardInteractor;
+    private final KeyguardViewConfigurator mKeyguardViewConfigurator;
     private final @Nullable MultiShadeInteractor mMultiShadeInteractor;
     private final CoroutineDispatcher mMainDispatcher;
     private boolean mIsAnyMultiShadeExpanded;
@@ -724,7 +727,6 @@
             NotificationStackSizeCalculator notificationStackSizeCalculator,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
             ShadeTransitionController shadeTransitionController,
-            InteractionJankMonitor interactionJankMonitor,
             SystemClock systemClock,
             KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
             KeyguardBottomAreaInteractor keyguardBottomAreaInteractor,
@@ -741,8 +743,8 @@
             KeyguardLongPressViewModel keyguardLongPressViewModel,
             KeyguardInteractor keyguardInteractor,
             ActivityStarter activityStarter,
+            KeyguardViewConfigurator keyguardViewConfigurator,
             KeyguardFaceAuthInteractor keyguardFaceAuthInteractor) {
-        mInteractionJankMonitor = interactionJankMonitor;
         keyguardStateController.addCallback(new KeyguardStateController.Callback() {
             @Override
             public void onKeyguardFadingAwayChanged() {
@@ -763,6 +765,7 @@
         mLockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel;
         mKeyguardTransitionInteractor = keyguardTransitionInteractor;
         mKeyguardInteractor = keyguardInteractor;
+        mKeyguardViewConfigurator = keyguardViewConfigurator;
         mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
             @Override
             public void onViewAttachedToWindow(View v) {
@@ -1029,7 +1032,7 @@
                 new NsslHeightChangedListener());
         mNotificationStackScrollLayoutController.setOnEmptySpaceClickListener(
                 mOnEmptySpaceClickListener);
-        mQsController.initNotificationStackScrollLayoutController();
+        mQsController.init();
         mShadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged);
         mShadeHeadsUpTracker.addTrackingHeadsUpListener(
                 mNotificationStackScrollLayoutController::setTrackingHeadsUp);
@@ -1323,7 +1326,6 @@
         mKeyguardBottomArea.initFrom(oldBottomArea);
         mView.addView(mKeyguardBottomArea, index);
         initBottomArea();
-        mKeyguardIndicationController.setIndicationArea(mKeyguardBottomArea);
         mStatusBarStateListener.onDozeAmountChanged(mStatusBarStateController.getDozeAmount(),
                 mStatusBarStateController.getInterpolatedDozeAmount());
 
@@ -1365,6 +1367,9 @@
                         mKeyguardIndicationController.showTransientIndication(stringResourceId),
                 mVibratorHelper,
                 mActivityStarter);
+
+        // Rebind (for now), as a new bottom area and indication area may have been created
+        mKeyguardViewConfigurator.bindIndicationArea(mKeyguardBottomArea);
     }
 
     @VisibleForTesting
@@ -1402,14 +1407,15 @@
 
     private void setKeyguardBottomArea(KeyguardBottomAreaView keyguardBottomArea) {
         mKeyguardBottomArea = keyguardBottomArea;
-        mKeyguardIndicationController.setIndicationArea(mKeyguardBottomArea);
     }
 
-    void setOpenCloseListener(OpenCloseListener openCloseListener) {
+    @Override
+    public void setOpenCloseListener(OpenCloseListener openCloseListener) {
         mOpenCloseListener = openCloseListener;
     }
 
-    void setTrackingStartedListener(TrackingStartedListener trackingStartedListener) {
+    @Override
+    public void setTrackingStartedListener(TrackingStartedListener trackingStartedListener) {
         mTrackingStartedListener = trackingStartedListener;
     }
 
@@ -1819,6 +1825,7 @@
 
             // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state.
             setClosing(true);
+            mUpdateFlingOnLayout = false;
             if (delayed) {
                 mNextCollapseSpeedUpFactor = speedUpFactor;
                 this.mView.postDelayed(mFlingCollapseRunnable, 120);
@@ -2151,10 +2158,14 @@
     }
 
     int getFalsingThreshold() {
-        float factor = mCentralSurfaces.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
+        float factor = ShadeViewController.getFalsingThresholdFactor(getWakefulness());
         return (int) (mQsController.getFalsingThreshold() * factor);
     }
 
+    private WakefulnessModel getWakefulness() {
+        return mKeyguardInteractor.getWakefulnessModel().getValue();
+    }
+
     private void maybeAnimateBottomAreaAlpha() {
         mBottomAreaShadeAlphaAnimator.cancel();
         if (mBarState == StatusBarState.SHADE_LOCKED) {
@@ -2432,6 +2443,7 @@
         boolean isExpanded = !isFullyCollapsed() || mExpectingSynthesizedDown;
         if (mPanelExpanded != isExpanded) {
             mPanelExpanded = isExpanded;
+            updateSystemUiStateFlags();
             mShadeExpansionStateManager.onShadeExpansionFullyChanged(isExpanded);
             if (!isExpanded) {
                 mQsController.closeQsCustomizer();
@@ -2439,6 +2451,7 @@
         }
     }
 
+    @Override
     public boolean isPanelExpanded() {
         return mPanelExpanded;
     }
@@ -2466,7 +2479,8 @@
             alpha = getFadeoutAlpha();
         }
         if (mBarState == KEYGUARD && !mHintAnimationRunning
-                && !mKeyguardBypassController.getBypassEnabled()) {
+                && !mKeyguardBypassController.getBypassEnabled()
+                && !mQsController.getFullyExpanded()) {
             alpha *= mClockPositionResult.clockAlpha;
         }
         mNotificationStackScrollLayoutController.setAlpha(alpha);
@@ -2574,7 +2588,7 @@
                                         this);
                                 return;
                             }
-                            if (mCentralSurfaces.getNotificationShadeWindowView()
+                            if (mNotificationShadeWindowController.getWindowRootView()
                                     .isVisibleToUser()) {
                                 mView.getViewTreeObserver().removeOnGlobalLayoutListener(
                                         this);
@@ -2655,6 +2669,8 @@
     private void onTrackingStopped(boolean expand) {
         mFalsingCollector.onTrackingStopped();
         mTracking = false;
+        maybeStopTrackingExpansionFromStatusBar(expand);
+
         updateExpansionAndVisibility();
         if (expand) {
             mNotificationStackScrollLayoutController.setOverScrollAmount(0.0f, true /* onTop */,
@@ -2896,9 +2912,7 @@
         float scrimMinFraction;
         if (mSplitShadeEnabled) {
             boolean highHun = mHeadsUpStartHeight * 2.5
-                    >
-                    (mFeatureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)
-                    ? mSplitShadeFullTransitionDistance : mSplitShadeScrimTransitionDistance);
+                    > mSplitShadeFullTransitionDistance;
             // if HUN height is higher than 40% of predefined transition distance, it means HUN
             // is too high for regular transition. In that case we need to calculate transition
             // distance - here we take scrim transition distance as equal to shade transition
@@ -3029,12 +3043,6 @@
         mKeyguardStatusViewController.setStatusAccessibilityImportance(mode);
     }
 
-    //TODO(b/254875405): this should be removed.
-    @Override
-    public KeyguardBottomAreaView getKeyguardBottomAreaView() {
-        return mKeyguardBottomArea;
-    }
-
     @Override
     public void applyLaunchAnimationProgress(float linearProgress) {
         boolean hideIcons = LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS,
@@ -3375,11 +3383,13 @@
         ViewGroupFadeHelper.reset(mView);
     }
 
-    void addOnGlobalLayoutListener(ViewTreeObserver.OnGlobalLayoutListener listener) {
+    @Override
+    public void addOnGlobalLayoutListener(ViewTreeObserver.OnGlobalLayoutListener listener) {
         mView.getViewTreeObserver().addOnGlobalLayoutListener(listener);
     }
 
-    void removeOnGlobalLayoutListener(ViewTreeObserver.OnGlobalLayoutListener listener) {
+    @Override
+    public void removeOnGlobalLayoutListener(ViewTreeObserver.OnGlobalLayoutListener listener) {
         mView.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
     }
 
@@ -3438,9 +3448,8 @@
             Log.d(TAG, "Updating panel sysui state flags: fullyExpanded="
                     + isFullyExpanded() + " inQs=" + mQsController.getExpanded());
         }
-        boolean isPanelVisible = mCentralSurfaces != null && mCentralSurfaces.isPanelExpanded();
         mSysUiState
-                .setFlag(SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE, isPanelVisible)
+                .setFlag(SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE, mPanelExpanded)
                 .setFlag(SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED,
                         isFullyExpanded() && !mQsController.getExpanded())
                 .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED,
@@ -3600,8 +3609,10 @@
                 mShadeLog.logEndMotionEvent("endMotionEvent: flingExpands", forceCancel, expand);
             }
 
-            mDozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
-                    mCentralSurfaces.isWakeUpComingFromTouch());
+            mDozeLog.traceFling(
+                    expand,
+                    mTouchAboveFalsingThreshold,
+                    /* screenOnFromTouch=*/ getWakefulness().isDeviceInteractiveFromTapOrGesture());
             // Log collapse gesture if on lock screen.
             if (!expand && onKeyguard) {
                 float displayDensity = mCentralSurfaces.getDisplayDensity();
@@ -3848,8 +3859,8 @@
         return !isFullyCollapsed() && !mTracking && !mClosing;
     }
 
-    /** Collapses the shade instantly without animation. */
-    void instantCollapse() {
+    @Override
+    public void instantCollapse() {
         abortAnimations();
         setExpandedFraction(0f);
         if (mExpanding) {
@@ -4022,8 +4033,8 @@
         mFixedDuration = NO_FIXED_DURATION;
     }
 
-    /** */
-    boolean postToView(Runnable action) {
+    @Override
+    public boolean postToView(Runnable action) {
         return mView.post(action);
     }
 
@@ -4038,6 +4049,42 @@
     }
 
     @Override
+    public void startTrackingExpansionFromStatusBar() {
+        mIsTrackingExpansionFromStatusBar = true;
+        InteractionJankMonitorWrapper.begin(
+                mView, InteractionJankMonitorWrapper.CUJ_SHADE_EXPAND_FROM_STATUS_BAR);
+    }
+
+    /**
+     * Stops tracking an expansion that originated from the status bar (if we had started tracking
+     * it).
+     *
+     * @param expand the expand boolean passed to {@link #onTrackingStopped(boolean)}.
+     */
+    private void maybeStopTrackingExpansionFromStatusBar(boolean expand) {
+        if (!mIsTrackingExpansionFromStatusBar) {
+            return;
+        }
+        mIsTrackingExpansionFromStatusBar = false;
+
+        // Determine whether the shade actually expanded due to the status bar touch:
+        // - If the user just taps on the status bar, then #isExpanded is false but
+        // #onTrackingStopped is called with `true`.
+        // - If the user drags down on the status bar but doesn't drag down far enough, then
+        // #onTrackingStopped is called with `false` but #isExpanded is true.
+        // So, we need *both* #onTrackingStopped called with `true` *and* #isExpanded to be true in
+        // order to confirm that the shade successfully opened.
+        boolean shadeExpansionFromStatusBarSucceeded = expand && isExpanded();
+        if (shadeExpansionFromStatusBarSucceeded) {
+            InteractionJankMonitorWrapper.end(
+                    InteractionJankMonitorWrapper.CUJ_SHADE_EXPAND_FROM_STATUS_BAR);
+        } else {
+            InteractionJankMonitorWrapper.cancel(
+                    InteractionJankMonitorWrapper.CUJ_SHADE_EXPAND_FROM_STATUS_BAR);
+        }
+    }
+
+    @Override
     public void updateTouchableRegion() {
         //A layout will ensure that onComputeInternalInsets will be called and after that we can
         // resize the layout. Make sure that the window stays small for one frame until the
@@ -4668,6 +4715,9 @@
             final float x = event.getX(pointerIndex);
             final float y = event.getY(pointerIndex);
             boolean canCollapsePanel = canCollapsePanelOnTouch();
+            final boolean isTrackpadTwoOrThreeFingerSwipe = isTrackpadScroll(
+                    mTrackpadGestureFeaturesEnabled, event) || isTrackpadThreeFingerSwipe(
+                    mTrackpadGestureFeaturesEnabled, event);
 
             switch (event.getActionMasked()) {
                 case MotionEvent.ACTION_DOWN:
@@ -4702,7 +4752,7 @@
                     addMovement(event);
                     break;
                 case MotionEvent.ACTION_POINTER_UP:
-                    if (isTrackpadMotionEvent(event)) {
+                    if (isTrackpadTwoOrThreeFingerSwipe) {
                         break;
                     }
                     final int upPointer = event.getPointerId(event.getActionIndex());
@@ -4718,7 +4768,7 @@
                     mShadeLog.logMotionEventStatusBarState(event,
                             mStatusBarStateController.getState(),
                             "onInterceptTouchEvent: pointer down action");
-                    if (!isTrackpadMotionEvent(event)
+                    if (!isTrackpadTwoOrThreeFingerSwipe
                             && mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
                         mMotionAborted = true;
                         mVelocityTracker.clear();
@@ -4868,9 +4918,13 @@
                 return false;
             }
 
+            final boolean isTrackpadTwoOrThreeFingerSwipe = isTrackpadScroll(
+                    mTrackpadGestureFeaturesEnabled, event) || isTrackpadThreeFingerSwipe(
+                    mTrackpadGestureFeaturesEnabled, event);
+
             // On expanding, single mouse click expands the panel instead of dragging.
             if (isFullyCollapsed() && (event.isFromSource(InputDevice.SOURCE_MOUSE)
-                    && !isTrackpadMotionEvent(event))) {
+                    && !isTrackpadTwoOrThreeFingerSwipe)) {
                 if (event.getAction() == MotionEvent.ACTION_UP) {
                     expand(true /* animate */);
                 }
@@ -4932,7 +4986,7 @@
                     break;
 
                 case MotionEvent.ACTION_POINTER_UP:
-                    if (isTrackpadMotionEvent(event)) {
+                    if (isTrackpadTwoOrThreeFingerSwipe) {
                         break;
                     }
                     final int upPointer = event.getPointerId(event.getActionIndex());
@@ -4951,7 +5005,7 @@
                     mShadeLog.logMotionEventStatusBarState(event,
                             mStatusBarStateController.getState(),
                             "handleTouch: pointer down action");
-                    if (!isTrackpadMotionEvent(event)
+                    if (!isTrackpadTwoOrThreeFingerSwipe
                             && mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
                         mMotionAborted = true;
                         endMotionEvent(event, x, y, true /* forceCancel */);
@@ -5025,12 +5079,6 @@
             }
             return !mGestureWaitForTouchSlop || mTracking;
         }
-
-        private boolean isTrackpadMotionEvent(MotionEvent ev) {
-            return mTrackpadGestureFeaturesEnabled && (
-                    ev.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE
-                            || ev.getClassification() == CLASSIFICATION_TWO_FINGER_SWIPE);
-        }
     }
 
     private final class HeadsUpNotificationViewControllerImpl implements
@@ -5081,18 +5129,5 @@
             return super.performAccessibilityAction(host, action, args);
         }
     }
-
-    /** Listens for when touch tracking begins. */
-    interface TrackingStartedListener {
-        void onTrackingStarted();
-    }
-
-    /** Listens for when shade begins opening of finishes closing. */
-    interface OpenCloseListener {
-        /** Called when the shade finishes closing. */
-        void onClosingFinished();
-        /** Called when the shade starts opening. */
-        void onOpenStarted();
-    }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 0c800d4..8105a145 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -103,7 +103,7 @@
     private final KeyguardViewMediator mKeyguardViewMediator;
     private final KeyguardBypassController mKeyguardBypassController;
     private final AuthController mAuthController;
-    private ViewGroup mNotificationShadeView;
+    private ViewGroup mWindowRootView;
     private LayoutParams mLp;
     private boolean mHasTopUi;
     private boolean mHasTopUiChanged;
@@ -262,7 +262,7 @@
         mLp.privateFlags |= PRIVATE_FLAG_BEHAVIOR_CONTROLLED;
         mLp.insetsFlags.behavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
 
-        mWindowManager.addView(mNotificationShadeView, mLp);
+        mWindowManager.addView(mWindowRootView, mLp);
 
         mLpChanged.copyFrom(mLp);
         onThemeChanged();
@@ -274,13 +274,13 @@
     }
 
     @Override
-    public void setNotificationShadeView(ViewGroup view) {
-        mNotificationShadeView = view;
+    public void setWindowRootView(ViewGroup view) {
+        mWindowRootView = view;
     }
 
     @Override
-    public ViewGroup getNotificationShadeView() {
-        return mNotificationShadeView;
+    public ViewGroup getWindowRootView() {
+        return mWindowRootView;
     }
 
     @Override
@@ -289,7 +289,7 @@
     }
 
     private void setKeyguardDark(boolean dark) {
-        int vis = mNotificationShadeView.getSystemUiVisibility();
+        int vis = mWindowRootView.getSystemUiVisibility();
         if (dark) {
             vis = vis | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
             vis = vis | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
@@ -297,7 +297,7 @@
             vis = vis & ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
             vis = vis & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
         }
-        mNotificationShadeView.setSystemUiVisibility(vis);
+        mWindowRootView.setSystemUiVisibility(vis);
     }
 
     private void applyKeyguardFlags(NotificationShadeWindowState state) {
@@ -413,11 +413,11 @@
             visible = true;
             mLogger.d("Visibility forced to be true");
         }
-        if (mNotificationShadeView != null) {
+        if (mWindowRootView != null) {
             if (visible) {
-                mNotificationShadeView.setVisibility(View.VISIBLE);
+                mWindowRootView.setVisibility(View.VISIBLE);
             } else {
-                mNotificationShadeView.setVisibility(View.INVISIBLE);
+                mWindowRootView.setVisibility(View.INVISIBLE);
             }
         }
     }
@@ -439,10 +439,10 @@
 
     private void applyFitsSystemWindows(NotificationShadeWindowState state) {
         boolean fitsSystemWindows = !state.isKeyguardShowingAndNotOccluded();
-        if (mNotificationShadeView != null
-                && mNotificationShadeView.getFitsSystemWindows() != fitsSystemWindows) {
-            mNotificationShadeView.setFitsSystemWindows(fitsSystemWindows);
-            mNotificationShadeView.requestApplyInsets();
+        if (mWindowRootView != null
+                && mWindowRootView.getFitsSystemWindows() != fitsSystemWindows) {
+            mWindowRootView.setFitsSystemWindows(fitsSystemWindows);
+            mWindowRootView.requestApplyInsets();
         }
     }
 
@@ -482,7 +482,7 @@
         if (mDeferWindowLayoutParams == 0 && mLp != null && mLp.copyFrom(mLpChanged) != 0) {
             mLogger.logApplyingWindowLayoutParams(mLp);
             Trace.beginSection("updateViewLayout");
-            mWindowManager.updateViewLayout(mNotificationShadeView, mLp);
+            mWindowManager.updateViewLayout(mWindowRootView, mLp);
             Trace.endSection();
         }
     }
@@ -608,7 +608,7 @@
         try {
             final IWindowSession session = WindowManagerGlobal.getWindowSession();
             session.updateTapExcludeRegion(
-                    IWindow.Stub.asInterface(getNotificationShadeView().getWindowToken()),
+                    IWindow.Stub.asInterface(getWindowRootView().getWindowToken()),
                     region);
         } catch (RemoteException e) {
             Log.e(TAG, "could not update the tap exclusion region:" + e);
@@ -847,8 +847,8 @@
         pw.println("  mKeyguardPreferredRefreshRate=" + mKeyguardPreferredRefreshRate);
         pw.println("  mDeferWindowLayoutParams=" + mDeferWindowLayoutParams);
         pw.println(mCurrentState);
-        if (mNotificationShadeView != null && mNotificationShadeView.getViewRootImpl() != null) {
-            mNotificationShadeView.getViewRootImpl().dump("  ", pw);
+        if (mWindowRootView != null && mWindowRootView.getViewRootImpl() != null) {
+            mWindowRootView.getViewRootImpl().dump("  ", pw);
         }
         new DumpsysTableLogger(
                 TAG,
@@ -864,7 +864,7 @@
 
     @Override
     public void onThemeChanged() {
-        if (mNotificationShadeView == null) {
+        if (mWindowRootView == null) {
             return;
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
index d75190e..2b62b7d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
@@ -58,12 +58,14 @@
 import com.android.internal.view.FloatingActionMode;
 import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
 import com.android.systemui.R;
-import com.android.systemui.compose.ComposeFacade;
+import com.android.systemui.scene.ui.view.WindowRootView;
 
 /**
- * Combined keyguard and notification panel view. Also holding backdrop and scrims.
+ * Combined keyguard and notification panel view. Also holding backdrop and scrims. This view can
+ * serve as the root view of the main SysUI window, but because other views can also serve that
+ * purpose, users of this class cannot assume it is the root.
  */
-public class NotificationShadeWindowView extends FrameLayout {
+public class NotificationShadeWindowView extends WindowRootView {
     public static final String TAG = "NotificationShadeWindowView";
 
     private int mRightInset = 0;
@@ -146,18 +148,6 @@
     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
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 2f7644e..5c41d57 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -36,9 +36,14 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.AuthKeyguardMessageArea;
+import com.android.keyguard.KeyguardMessageAreaController;
 import com.android.keyguard.LockIconViewController;
 import com.android.keyguard.dagger.KeyguardBouncerComponent;
 import com.android.systemui.R;
+import com.android.systemui.back.domain.interactor.BackActionInteractor;
+import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
+import com.android.systemui.bouncer.ui.binder.KeyguardBouncerViewBinder;
+import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.compose.ComposeFacade;
 import com.android.systemui.dock.DockManager;
@@ -48,12 +53,12 @@
 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.log.BouncerLogger;
 import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
 import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor;
 import com.android.systemui.multishade.ui.view.MultiShadeView;
+import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
 import com.android.systemui.statusbar.DragDownHelper;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -108,6 +113,8 @@
     private NotificationStackScrollLayout mStackScrollLayout;
     private PhoneStatusBarViewController mStatusBarViewController;
     private final CentralSurfaces mService;
+    private final BackActionInteractor mBackActionInteractor;
+    private final PowerInteractor mPowerInteractor;
     private final NotificationShadeWindowController mNotificationShadeWindowController;
     private DragDownHelper mDragDownHelper;
     private boolean mExpandingBelowNotch;
@@ -141,6 +148,8 @@
             StatusBarWindowStateController statusBarWindowStateController,
             LockIconViewController lockIconViewController,
             CentralSurfaces centralSurfaces,
+            BackActionInteractor backActionInteractor,
+            PowerInteractor powerInteractor,
             NotificationShadeWindowController controller,
             Optional<UnfoldTransitionProgressProvider> unfoldTransitionProgressProvider,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
@@ -149,12 +158,15 @@
             PulsingGestureListener pulsingGestureListener,
             KeyguardBouncerViewModel keyguardBouncerViewModel,
             KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory,
+            KeyguardMessageAreaController.Factory messageAreaControllerFactory,
             KeyguardTransitionInteractor keyguardTransitionInteractor,
             PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
             FeatureFlags featureFlags,
             Provider<MultiShadeInteractor> multiShadeInteractorProvider,
             SystemClock clock,
-            Provider<MultiShadeMotionEventInteractor> multiShadeMotionEventInteractorProvider) {
+            Provider<MultiShadeMotionEventInteractor> multiShadeMotionEventInteractorProvider,
+            BouncerMessageInteractor bouncerMessageInteractor,
+            BouncerLogger bouncerLogger) {
         mLockscreenShadeTransitionController = transitionController;
         mFalsingCollector = falsingCollector;
         mStatusBarStateController = statusBarStateController;
@@ -167,8 +179,10 @@
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mStatusBarWindowStateController = statusBarWindowStateController;
         mLockIconViewController = lockIconViewController;
+        mBackActionInteractor = backActionInteractor;
         mLockIconViewController.init();
         mService = centralSurfaces;
+        mPowerInteractor = powerInteractor;
         mNotificationShadeWindowController = controller;
         mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
         mAmbientState = ambientState;
@@ -183,7 +197,11 @@
                 mView.findViewById(R.id.keyguard_bouncer_container),
                 keyguardBouncerViewModel,
                 primaryBouncerToGoneTransitionViewModel,
-                keyguardBouncerComponentFactory);
+                keyguardBouncerComponentFactory,
+                messageAreaControllerFactory,
+                bouncerMessageInteractor,
+                bouncerLogger,
+                featureFlags);
 
         collectFlow(mView, keyguardTransitionInteractor.getLockscreenToDreamingTransition(),
                 mLockscreenToDreamingTransition);
@@ -297,8 +315,7 @@
                         /* onGestureDetectedRunnable */
                         () -> {
                             mService.userActivity();
-                            mService.wakeUpIfDozing(
-                                    mClock.uptimeMillis(),
+                            mPowerInteractor.wakeUpIfDozing(
                                     "LOCK_ICON_TOUCH",
                                     PowerManager.WAKE_REASON_GESTURE);
                         }
@@ -433,7 +450,7 @@
                 switch (event.getKeyCode()) {
                     case KeyEvent.KEYCODE_BACK:
                         if (!down) {
-                            mService.onBackPressed();
+                            mBackActionInteractor.onBackRequested();
                         }
                         return true;
                     case KeyEvent.KEYCODE_MENU:
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
index 81fe3ca..fba0120 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
@@ -27,6 +27,7 @@
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.fragments.FragmentService
 import com.android.systemui.navigationbar.NavigationModeController
@@ -45,6 +46,7 @@
 @VisibleForTesting
 internal const val INSET_DEBOUNCE_MILLIS = 500L
 
+@SysUISingleton
 class NotificationsQSContainerController @Inject constructor(
         view: NotificationsQuickSettingsContainer,
         private val navigationModeController: NavigationModeController,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
index fd82e2f..ee4e98e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
@@ -18,19 +18,18 @@
 
 import android.hardware.display.AmbientDisplayConfiguration
 import android.os.PowerManager
-import android.os.SystemClock
 import android.provider.Settings
 import android.view.GestureDetector
 import android.view.MotionEvent
 import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dump.DumpManager
 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.power.domain.interactor.PowerInteractor
 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
 import com.android.systemui.tuner.TunerService.Tunable
 import java.io.PrintWriter
@@ -45,12 +44,11 @@
  * screen is still ON and not in the true AoD display state. When the device is in the true AoD
  * display state, wake-ups are handled by [com.android.systemui.doze.DozeSensors].
  */
-@CentralSurfacesComponent.CentralSurfacesScope
+@SysUISingleton
 class PulsingGestureListener @Inject constructor(
-        private val notificationShadeWindowView: NotificationShadeWindowView,
         private val falsingManager: FalsingManager,
         private val dockManager: DockManager,
-        private val centralSurfaces: CentralSurfaces,
+        private val powerInteractor: PowerInteractor,
         private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
         private val statusBarStateController: StatusBarStateController,
         private val shadeLogger: ShadeLogger,
@@ -88,11 +86,7 @@
             shadeLogger.logSingleTapUpFalsingState(proximityIsNotNear, isNotAFalseTap)
             if (proximityIsNotNear && isNotAFalseTap) {
                 shadeLogger.d("Single tap handled, requesting centralSurfaces.wakeUpIfDozing")
-                centralSurfaces.wakeUpIfDozing(
-                    SystemClock.uptimeMillis(),
-                    "PULSING_SINGLE_TAP",
-                    PowerManager.WAKE_REASON_TAP
-                )
+                powerInteractor.wakeUpIfDozing("PULSING_SINGLE_TAP", PowerManager.WAKE_REASON_TAP)
             }
             return true
         }
@@ -113,11 +107,7 @@
                 !falsingManager.isProximityNear &&
                 !falsingManager.isFalseDoubleTap
         ) {
-            centralSurfaces.wakeUpIfDozing(
-                    SystemClock.uptimeMillis(),
-                    "PULSING_DOUBLE_TAP",
-                    PowerManager.WAKE_REASON_TAP
-            )
+            powerInteractor.wakeUpIfDozing("PULSING_DOUBLE_TAP", PowerManager.WAKE_REASON_TAP)
             return true
         }
         return false
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
index 7d66a0b..025c4611 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
@@ -68,6 +68,7 @@
 import com.android.systemui.R;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.classifier.FalsingCollector;
+import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.fragments.FragmentHostManager;
@@ -78,6 +79,7 @@
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shade.data.repository.ShadeRepository;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.shade.transition.ShadeTransitionController;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -97,10 +99,10 @@
 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.CastController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.LargeScreenUtils;
+import com.android.systemui.util.kotlin.JavaAdapter;
 
 import dagger.Lazy;
 
@@ -111,7 +113,7 @@
 /** Handles QuickSettings touch handling, expansion and animation state
  * TODO (b/264460656) make this dumpable
  */
-@CentralSurfacesComponent.CentralSurfacesScope
+@SysUISingleton
 public class QuickSettingsController implements Dumpable {
     public static final String TAG = "QuickSettingsController";
 
@@ -153,6 +155,8 @@
     private final FeatureFlags mFeatureFlags;
     private final InteractionJankMonitor mInteractionJankMonitor;
     private final ShadeRepository mShadeRepository;
+    private final ShadeInteractor mShadeInteractor;
+    private final JavaAdapter mJavaAdapter;
     private final FalsingManager mFalsingManager;
     private final AccessibilityManager mAccessibilityManager;
     private final MetricsLogger mMetricsLogger;
@@ -342,6 +346,8 @@
             DumpManager dumpManager,
             KeyguardFaceAuthInteractor keyguardFaceAuthInteractor,
             ShadeRepository shadeRepository,
+            ShadeInteractor shadeInteractor,
+            JavaAdapter javaAdapter,
             CastController castController
     ) {
         mPanelViewControllerLazy = panelViewControllerLazy;
@@ -387,6 +393,8 @@
         mFeatureFlags = featureFlags;
         mInteractionJankMonitor = interactionJankMonitor;
         mShadeRepository = shadeRepository;
+        mShadeInteractor = shadeInteractor;
+        mJavaAdapter = javaAdapter;
 
         mLockscreenShadeTransitionController.addCallback(new LockscreenShadeTransitionCallback());
         dumpManager.registerDumpable(this);
@@ -459,7 +467,13 @@
     }
 
     // TODO (b/265054088): move this and others to a CoreStartable
-    void initNotificationStackScrollLayoutController() {
+    void init() {
+        initNotificationStackScrollLayoutController();
+        mJavaAdapter.alwaysCollectFlow(
+                mShadeInteractor.isExpandToQsEnabled(), this::setExpansionEnabledPolicy);
+    }
+
+    private void initNotificationStackScrollLayoutController() {
         mNotificationStackScrollLayoutController.setOverscrollTopChangedListener(
                 new NsslOverscrollTopChangedListener());
         mNotificationStackScrollLayoutController.setOnStackYChanged(this::onStackYChanged);
@@ -780,7 +794,6 @@
 
     /** update Qs height state */
     public void setExpansionHeight(float height) {
-        checkCorrectSplitShadeState(height);
         int maxHeight = getMaxExpansionHeight();
         height = Math.min(Math.max(
                 height, getMinExpansionHeight()), maxHeight);
@@ -802,14 +815,6 @@
         }
     }
 
-    /** TODO(b/269742565) Remove this logging */
-    private void checkCorrectSplitShadeState(float height) {
-        if (mSplitShadeEnabled && height == 0
-                && mPanelViewControllerLazy.get().isShadeFullyExpanded()) {
-            Log.wtfStack(TAG, "qsExpansion set to 0 while split shade is expanding or open");
-        }
-    }
-
     /** */
     public void setHeightOverrideToDesiredHeight() {
         if (isSizeChangeAnimationRunning() && isQsFragmentCreated()) {
@@ -892,7 +897,7 @@
     }
 
     /** */
-    public void setExpansionEnabledPolicy(boolean expansionEnabledPolicy) {
+    private void setExpansionEnabledPolicy(boolean expansionEnabledPolicy) {
         mExpansionEnabledPolicy = expansionEnabledPolicy;
         if (mQs != null) {
             mQs.setHeaderClickable(isExpansionEnabled());
@@ -1215,14 +1220,15 @@
         if (mIsFullWidth) {
             clipStatusView = qsVisible;
             float screenCornerRadius =
-                    !mSplitShadeEnabled || mRecordingController.isRecording()
-                            || mCastController.hasConnectedCastDevice()
+                    mRecordingController.isRecording() || mCastController.hasConnectedCastDevice()
                             ? 0 : mScreenCornerRadius;
             radius = (int) MathUtils.lerp(screenCornerRadius, mScrimCornerRadius,
                     Math.min(top / (float) mScrimCornerRadius, 1f));
 
-            float bottomRadius = mExpanded ? screenCornerRadius :
-                    calculateBottomCornerRadius(screenCornerRadius);
+            float bottomRadius = mSplitShadeEnabled ? screenCornerRadius : 0;
+            if (!mExpanded) {
+                bottomRadius = calculateBottomCornerRadius(bottomRadius);
+            }
             mScrimController.setNotificationBottomRadius(bottomRadius);
         }
         if (isQsFragmentCreated()) {
@@ -1888,7 +1894,7 @@
                 target = getMaxExpansionHeight();
                 break;
             case FLING_COLLAPSE:
-                if (mSplitShadeEnabled) { // TODO:(b/269742565) remove below log
+                if (mSplitShadeEnabled) {
                     Log.wtfStack(TAG, "FLING_COLLAPSE called in split shade");
                 }
                 setExpandImmediate(false);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
index d0a3cbb..317d885 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
@@ -38,23 +38,41 @@
     /** Collapse the shade instantly with no animation. */
     void instantCollapseShade();
 
-    /** See {@link #animateCollapsePanels(int, boolean, boolean, float)}. */
+    /** See {@link #animateCollapseShade(int, boolean, boolean, float)}. */
     void animateCollapseShade();
 
-    /** See {@link #animateCollapsePanels(int, boolean, boolean, float)}. */
+    /** See {@link #animateCollapseShade(int, boolean, boolean, float)}. */
     void animateCollapseShade(int flags);
 
-    /** See {@link #animateCollapsePanels(int, boolean, boolean, float)}. */
+    /** See {@link #animateCollapseShade(int, boolean, boolean, float)}. */
     void animateCollapseShadeForced();
 
-    /** See {@link #animateCollapsePanels(int, boolean, boolean, float)}. */
-    void animateCollapseShadeDelayed();
+    /** See {@link #animateCollapseShade(int, boolean, boolean, float)}. */
+    void animateCollapseShadeForcedDelayed();
 
     /**
      * Collapse the shade animated, showing the bouncer when on {@link StatusBarState#KEYGUARD} or
      * dismissing status bar when on {@link StatusBarState#SHADE}.
      */
-    void animateCollapsePanels(int flags, boolean force, boolean delayed, float speedUpFactor);
+    void animateCollapseShade(int flags, boolean force, boolean delayed, float speedUpFactor);
+
+    /** Expand the shade with an animation. */
+    void animateExpandShade();
+
+    /** Expand the shade with quick settings expanded with an animation. */
+    void animateExpandQs();
+
+    /** Posts a request to collapse the shade. */
+    void postAnimateCollapseShade();
+
+    /** Posts a request to force collapse the shade. */
+    void postAnimateForceCollapseShade();
+
+    /** Posts a request to expand the shade to quick settings. */
+    void postAnimateExpandQs();
+
+    /** Cancels any ongoing expansion touch handling and collapses the shade. */
+    void cancelExpansionAndCollapseShade();
 
     /**
      * If the shade is not fully expanded, collapse it animated.
@@ -115,6 +133,9 @@
      */
     void collapseShade(boolean animate);
 
+    /** Calls #collapseShade if already on the main thread. If not, posts a call to it. */
+    void collapseOnMainThread();
+
     /** Makes shade expanded but not visible. */
     void makeExpandedInvisible();
 
@@ -127,8 +148,11 @@
     /** Handle status bar touch event. */
     void onStatusBarTouch(MotionEvent event);
 
-    /** Called when the shade finishes collapsing. */
-    void onClosingFinished();
+    /** Called when a launch animation was cancelled. */
+    void onLaunchAnimationCancelled(boolean isLaunchForActivity);
+
+    /** Called when a launch animation ends. */
+    void onLaunchAnimationEnd(boolean launchIsFullScreen);
 
     /** Sets the listener for when the visibility of the shade changes. */
     void setVisibilityListener(ShadeVisibilityListener listener);
@@ -141,8 +165,7 @@
             NotificationShadeWindowViewController notificationShadeWindowViewController);
 
     /** */
-    void setNotificationPanelViewController(
-            NotificationPanelViewController notificationPanelViewController);
+    void setShadeViewController(ShadeViewController shadeViewController);
 
     /** Listens for shade visibility changes. */
     interface ShadeVisibilityListener {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
index d00dab6..b92afac 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
@@ -17,6 +17,7 @@
 package com.android.systemui.shade;
 
 import android.content.ComponentCallbacks2;
+import android.os.Looper;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ViewTreeObserver;
@@ -25,6 +26,7 @@
 
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationPresenter;
@@ -32,12 +34,14 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
 
 import dagger.Lazy;
 
 import java.util.ArrayList;
+import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
@@ -51,11 +55,13 @@
     private final int mDisplayId;
 
     private final CommandQueue mCommandQueue;
+    private final Executor mMainExecutor;
     private final KeyguardStateController mKeyguardStateController;
     private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final StatusBarStateController mStatusBarStateController;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private final StatusBarWindowController mStatusBarWindowController;
+    private final DeviceProvisionedController mDeviceProvisionedController;
 
     private final Lazy<AssistManager> mAssistManagerLazy;
     private final Lazy<NotificationGutsManager> mGutsManager;
@@ -64,7 +70,8 @@
 
     private boolean mExpandedVisible;
 
-    private NotificationPanelViewController mNotificationPanelViewController;
+    // TODO(b/237661616): Rename this variable to mShadeViewController.
+    private ShadeViewController mNotificationPanelViewController;
     private NotificationPresenter mPresenter;
     private NotificationShadeWindowViewController mNotificationShadeWindowViewController;
     private ShadeVisibilityListener mShadeVisibilityListener;
@@ -72,18 +79,22 @@
     @Inject
     public ShadeControllerImpl(
             CommandQueue commandQueue,
+            @Main Executor mainExecutor,
             KeyguardStateController keyguardStateController,
             StatusBarStateController statusBarStateController,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             StatusBarWindowController statusBarWindowController,
+            DeviceProvisionedController deviceProvisionedController,
             NotificationShadeWindowController notificationShadeWindowController,
             WindowManager windowManager,
             Lazy<AssistManager> assistManagerLazy,
             Lazy<NotificationGutsManager> gutsManager
     ) {
         mCommandQueue = commandQueue;
+        mMainExecutor = mainExecutor;
         mStatusBarStateController = statusBarStateController;
         mStatusBarWindowController = statusBarWindowController;
+        mDeviceProvisionedController = deviceProvisionedController;
         mGutsManager = gutsManager;
         mNotificationShadeWindowController = notificationShadeWindowController;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
@@ -107,21 +118,21 @@
 
     @Override
     public void animateCollapseShade(int flags) {
-        animateCollapsePanels(flags, false, false, 1.0f);
+        animateCollapseShade(flags, false, false, 1.0f);
     }
 
     @Override
     public void animateCollapseShadeForced() {
-        animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true, false, 1.0f);
+        animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE, true, false, 1.0f);
     }
 
     @Override
-    public void animateCollapseShadeDelayed() {
-        animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true, true, 1.0f);
+    public void animateCollapseShadeForcedDelayed() {
+        animateCollapseShade(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true, true, 1.0f);
     }
 
     @Override
-    public void animateCollapsePanels(int flags, boolean force, boolean delayed,
+    public void animateCollapseShade(int flags, boolean force, boolean delayed,
             float speedUpFactor) {
         if (!force && mStatusBarStateController.getState() != StatusBarState.SHADE) {
             runPostCollapseRunnables();
@@ -143,6 +154,25 @@
     }
 
     @Override
+    public void animateExpandShade() {
+        if (!mCommandQueue.panelsEnabled()) {
+            return;
+        }
+        mNotificationPanelViewController.expandToNotifications();
+    }
+
+    @Override
+    public void animateExpandQs() {
+        if (!mCommandQueue.panelsEnabled()) {
+            return;
+        }
+        // Settings are not available in setup
+        if (!mDeviceProvisionedController.isCurrentUserSetup()) return;
+
+        mNotificationPanelViewController.expandToQs();
+    }
+
+    @Override
     public boolean closeShadeIfOpen() {
         if (!mNotificationPanelViewController.isFullyCollapsed()) {
             mCommandQueue.animateCollapsePanels(
@@ -167,6 +197,20 @@
     public boolean isExpandingOrCollapsing() {
         return mNotificationPanelViewController.isExpandingOrCollapsing();
     }
+    @Override
+    public void postAnimateCollapseShade() {
+        mMainExecutor.execute(this::animateCollapseShade);
+    }
+
+    @Override
+    public void postAnimateForceCollapseShade() {
+        mMainExecutor.execute(this::animateCollapseShadeForced);
+    }
+
+    @Override
+    public void postAnimateExpandQs() {
+        mMainExecutor.execute(this::animateExpandQs);
+    }
 
     @Override
     public void postOnShadeExpanded(Runnable executable) {
@@ -202,7 +246,7 @@
     public boolean collapseShade() {
         if (!mNotificationPanelViewController.isFullyCollapsed()) {
             // close the shade if it was open
-            animateCollapseShadeDelayed();
+            animateCollapseShadeForcedDelayed();
             notifyVisibilityChanged(false);
 
             return true;
@@ -227,6 +271,26 @@
     }
 
     @Override
+    public void cancelExpansionAndCollapseShade() {
+        if (mNotificationPanelViewController.isTracking()) {
+            mNotificationShadeWindowViewController.cancelCurrentTouch();
+        }
+        if (mNotificationPanelViewController.isPanelExpanded()
+                && mStatusBarStateController.getState() == StatusBarState.SHADE) {
+            animateCollapseShade();
+        }
+    }
+
+    @Override
+    public void collapseOnMainThread() {
+        if (Looper.getMainLooper().isCurrentThread()) {
+            collapseShade();
+        } else {
+            mMainExecutor.execute(this::collapseShade);
+        }
+    }
+
+    @Override
     public void onStatusBarTouch(MotionEvent event) {
         if (event.getAction() == MotionEvent.ACTION_UP) {
             if (mExpandedVisible) {
@@ -235,8 +299,7 @@
         }
     }
 
-    @Override
-    public void onClosingFinished() {
+    private void onClosingFinished() {
         runPostCollapseRunnables();
         if (!mPresenter.isPresenterFullyCollapsed()) {
             // if we set it not to be focusable when collapsing, we have to undo it when we aborted
@@ -246,6 +309,27 @@
     }
 
     @Override
+    public void onLaunchAnimationCancelled(boolean isLaunchForActivity) {
+        if (mPresenter.isPresenterFullyCollapsed()
+                && !mPresenter.isCollapsing()
+                && isLaunchForActivity) {
+            onClosingFinished();
+        } else {
+            collapseShade(true /* animate */);
+        }
+    }
+
+    @Override
+    public void onLaunchAnimationEnd(boolean launchIsFullScreen) {
+        if (!mPresenter.isCollapsing()) {
+            onClosingFinished();
+        }
+        if (launchIsFullScreen) {
+            instantCollapseShade();
+        }
+    }
+
+    @Override
     public void instantCollapseShade() {
         mNotificationPanelViewController.instantCollapse();
         runPostCollapseRunnables();
@@ -343,12 +427,11 @@
     }
 
     @Override
-    public void setNotificationPanelViewController(
-            NotificationPanelViewController notificationPanelViewController) {
-        mNotificationPanelViewController = notificationPanelViewController;
+    public void setShadeViewController(ShadeViewController shadeViewController) {
+        mNotificationPanelViewController = shadeViewController;
         mNotificationPanelViewController.setTrackingStartedListener(this::runPostCollapseRunnables);
         mNotificationPanelViewController.setOpenCloseListener(
-                new NotificationPanelViewController.OpenCloseListener() {
+                new OpenCloseListener() {
                     @Override
                     public void onClosingFinished() {
                         ShadeControllerImpl.this.onClosingFinished();
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
index 3af75ce..8789a8b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
@@ -195,7 +195,9 @@
         set(value) {
             if (visible && field != value) {
                 field = value
+                iconContainer.setQsExpansionTransitioning(value > 0f && value < 1.0f)
                 updatePosition()
+                updateIgnoredSlots()
             }
         }
 
@@ -216,6 +218,8 @@
             view.onApplyWindowInsets(insets)
         }
 
+    private var singleCarrier = false
+
     private val demoModeReceiver =
         object : DemoMode {
             override fun demoCommands() = listOf(DemoMode.COMMAND_CLOCK)
@@ -479,17 +483,20 @@
     private fun updateListeners() {
         mShadeCarrierGroupController.setListening(visible)
         if (visible) {
-            updateSingleCarrier(mShadeCarrierGroupController.isSingleCarrier)
+            singleCarrier = mShadeCarrierGroupController.isSingleCarrier
+            updateIgnoredSlots()
             mShadeCarrierGroupController.setOnSingleCarrierChangedListener {
-                updateSingleCarrier(it)
+                singleCarrier = it
+                updateIgnoredSlots()
             }
         } else {
             mShadeCarrierGroupController.setOnSingleCarrierChangedListener(null)
         }
     }
 
-    private fun updateSingleCarrier(singleCarrier: Boolean) {
-        if (singleCarrier) {
+    private fun updateIgnoredSlots() {
+        // switching from QQS to QS state halfway through the transition
+        if (singleCarrier || qsExpandedFraction < 0.5) {
             iconContainer.removeIgnoredSlots(carrierIconSlots)
         } else {
             iconContainer.addIgnoredSlots(carrierIconSlots)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index 4fdd6e1..1c30bdd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -19,7 +19,7 @@
 import android.view.MotionEvent
 import com.android.systemui.log.dagger.ShadeLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.shade.ShadeViewController.Companion.FLING_COLLAPSE
 import com.android.systemui.shade.ShadeViewController.Companion.FLING_EXPAND
 import com.android.systemui.shade.ShadeViewController.Companion.FLING_HIDE
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index b7551cf..8ae9e5e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shade
 
+import android.annotation.SuppressLint
 import android.content.ContentResolver
 import android.os.Handler
 import android.view.LayoutInflater
@@ -28,13 +29,27 @@
 import com.android.systemui.battery.BatteryMeterViewController
 import com.android.systemui.biometrics.AuthRippleController
 import com.android.systemui.biometrics.AuthRippleView
+import com.android.systemui.compose.ComposeFacade
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.ui.view.KeyguardRootView
 import com.android.systemui.privacy.OngoingPrivacyChip
+import com.android.systemui.scene.shared.model.Scene
+import com.android.systemui.scene.shared.model.SceneContainerConfig
+import com.android.systemui.scene.shared.model.SceneContainerNames
+import com.android.systemui.scene.ui.view.SceneWindowRootView
+import com.android.systemui.scene.ui.view.WindowRootView
+import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.LightRevealScrim
+import com.android.systemui.statusbar.NotificationShelf
+import com.android.systemui.statusbar.NotificationShelfController
+import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent
+import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
 import com.android.systemui.statusbar.phone.StatusIconContainer
 import com.android.systemui.statusbar.phone.TapAgainView
 import com.android.systemui.statusbar.policy.BatteryController
@@ -46,6 +61,7 @@
 import dagger.multibindings.ClassKey
 import dagger.multibindings.IntoMap
 import javax.inject.Named
+import javax.inject.Provider
 
 /** Module for classes related to the notification shade. */
 @Module
@@ -56,22 +72,60 @@
     @ClassKey(AuthRippleController::class)
     abstract fun bindAuthRippleController(controller: AuthRippleController): CoreStartable
 
+    @Binds
+    @SysUISingleton
+    abstract fun bindsShadeViewController(
+        notificationPanelViewController: NotificationPanelViewController
+    ): ShadeViewController
+
     companion object {
         const val SHADE_HEADER = "large_screen_shade_header"
 
+        @SuppressLint("InflateParams") // Root views don't have parents.
+        @Provides
+        @SysUISingleton
+        fun providesWindowRootView(
+            layoutInflater: LayoutInflater,
+            featureFlags: FeatureFlags,
+            @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+            viewModelProvider: Provider<SceneContainerViewModel>,
+            @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+            containerConfigProvider: Provider<SceneContainerConfig>,
+            @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+            scenesProvider: Provider<Set<@JvmSuppressWildcards Scene>>,
+        ): WindowRootView {
+            return if (
+                featureFlags.isEnabled(Flags.SCENE_CONTAINER) && ComposeFacade.isComposeAvailable()
+            ) {
+                val sceneWindowRootView =
+                    layoutInflater.inflate(R.layout.scene_window_root, null) as SceneWindowRootView
+                sceneWindowRootView.init(
+                    viewModel = viewModelProvider.get(),
+                    containerConfig = containerConfigProvider.get(),
+                    scenes = scenesProvider.get(),
+                )
+                sceneWindowRootView
+            } else {
+                layoutInflater.inflate(R.layout.super_notification_shade, null)
+            }
+                as WindowRootView?
+                ?: throw IllegalStateException("Window root view could not be properly inflated")
+        }
+
         @Provides
         @SysUISingleton
         // TODO(b/277762009): Do something similar to
         //  {@link StatusBarWindowModule.InternalWindowView} so that only
         //  {@link NotificationShadeWindowViewController} can inject this view.
         fun providesNotificationShadeWindowView(
-            layoutInflater: LayoutInflater,
+            root: WindowRootView,
+            featureFlags: FeatureFlags,
         ): NotificationShadeWindowView {
-            return layoutInflater.inflate(R.layout.super_notification_shade, /* root= */ null)
-                as NotificationShadeWindowView?
-                ?: throw IllegalStateException(
-                    "R.layout.super_notification_shade could not be properly inflated"
-                )
+            if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+                return root.findViewById(R.id.legacy_window_root)
+            }
+            return root as NotificationShadeWindowView?
+                ?: throw IllegalStateException("root view not a NotificationShadeWindowView")
         }
 
         // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
@@ -83,6 +137,32 @@
             return notificationShadeWindowView.findViewById(R.id.notification_stack_scroller)
         }
 
+        @Provides
+        @SysUISingleton
+        fun providesNotificationShelfController(
+            featureFlags: FeatureFlags,
+            newImpl: Provider<NotificationShelfViewBinderWrapperControllerImpl>,
+            notificationShelfComponentBuilder: NotificationShelfComponent.Builder,
+            layoutInflater: LayoutInflater,
+            notificationStackScrollLayout: NotificationStackScrollLayout,
+        ): NotificationShelfController {
+            return if (featureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
+                newImpl.get()
+            } else {
+                val shelfView =
+                    layoutInflater.inflate(
+                        R.layout.status_bar_notification_shelf,
+                        notificationStackScrollLayout,
+                        false
+                    ) as NotificationShelf
+                val component =
+                    notificationShelfComponentBuilder.notificationShelf(shelfView).build()
+                val notificationShelfController = component.notificationShelfController
+                notificationShelfController.init()
+                notificationShelfController
+            }
+        }
+
         // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
         @Provides
         @SysUISingleton
@@ -92,6 +172,20 @@
             return notificationShadeWindowView.findViewById(R.id.notification_panel)
         }
 
+        /**
+         * Constructs a new, unattached [KeyguardBottomAreaView].
+         *
+         * Note that this is explicitly _not_ a singleton, as we want to be able to reinflate it
+         */
+        @Provides
+        fun providesKeyguardBottomAreaView(
+            npv: NotificationPanelView,
+            layoutInflater: LayoutInflater,
+        ): KeyguardBottomAreaView {
+            return layoutInflater.inflate(R.layout.keyguard_bottom_area, npv, false)
+                as KeyguardBottomAreaView
+        }
+
         @Provides
         @SysUISingleton
         fun providesLightRevealScrim(
@@ -100,6 +194,14 @@
             return notificationShadeWindowView.findViewById(R.id.light_reveal_scrim)
         }
 
+        @Provides
+        @SysUISingleton
+        fun providesKeyguardRootView(
+            notificationShadeWindowView: NotificationShadeWindowView,
+        ): KeyguardRootView {
+            return notificationShadeWindowView.findViewById(R.id.keyguard_root_view)
+        }
+
         // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
         @Provides
         @SysUISingleton
@@ -113,9 +215,15 @@
         @Provides
         @SysUISingleton
         fun providesLockIconView(
-            notificationShadeWindowView: NotificationShadeWindowView,
+            keyguardRootView: KeyguardRootView,
+            notificationPanelView: NotificationPanelView,
+            featureFlags: FeatureFlags
         ): LockIconView {
-            return notificationShadeWindowView.findViewById(R.id.lock_icon_view)
+            if (featureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON)) {
+                return keyguardRootView.findViewById(R.id.lock_icon_view)
+            } else {
+                return notificationPanelView.findViewById(R.id.lock_icon_view)
+            }
         }
 
         // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
@@ -130,6 +238,15 @@
         // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
         @Provides
         @SysUISingleton
+        fun providesNotificationsQuickSettingsContainer(
+            notificationShadeWindowView: NotificationShadeWindowView,
+        ): NotificationsQuickSettingsContainer {
+            return notificationShadeWindowView.findViewById(R.id.notification_container_parent)
+        }
+
+        // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
+        @Provides
+        @SysUISingleton
         @Named(SHADE_HEADER)
         fun providesShadeHeaderView(
             notificationShadeWindowView: NotificationShadeWindowView,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
index 5ac36bf..8d5c30b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
@@ -36,33 +36,12 @@
         headsUpManager: HeadsUpManagerPhone
     )
 
-    /**
-     * Animate QS collapse 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 fullyCollapse Do not stop when QS becomes QQS. Fling until QS isn't visible anymore.
-     */
-    fun animateCollapseQs(fullyCollapse: Boolean)
-
-    /** Returns whether the shade can be collapsed. */
-    fun canBeCollapsed(): Boolean
-
     /** Cancels any pending collapses. */
     fun cancelPendingCollapse()
 
     /** Cancels the views current animation. */
     fun cancelAnimation()
 
-    /**
-     * Close the keyguard user switcher if it is open and capable of closing.
-     *
-     * Has no effect if user switcher isn't supported, if the user switcher is already closed, or if
-     * the user switcher uses "simple" mode. The simple user switcher cannot be closed.
-     *
-     * @return true if the keyguard user switcher was open, and is now closed
-     */
-    fun closeUserSwitcherIfOpen(): Boolean
-
     /** Input focus transfer is about to happen. */
     fun startWaitingForExpandGesture()
 
@@ -116,9 +95,6 @@
     /** Sets the view's alpha to max. */
     fun resetAlpha()
 
-    /** Called when Back gesture has been committed (i.e. a back event has definitely occurred) */
-    fun onBackPressed()
-
     /** Sets progress of the predictive back animation. */
     fun onBackProgressed(progressFraction: Float)
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
index f75047c..9aa5eb0 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
@@ -17,11 +17,12 @@
 
 import android.view.MotionEvent
 import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.statusbar.RemoteInputController
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController
-import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
 import com.android.systemui.statusbar.phone.KeyguardStatusBarView
 import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController
 import java.util.function.Consumer
@@ -60,7 +61,7 @@
      * Returns whether the shade height is greater than zero or the shade is expecting a synthesized
      * down event.
      */
-    @get:Deprecated("use {@link #isExpanded()} instead") val isPanelExpanded: Boolean
+    val isPanelExpanded: Boolean
 
     /** Returns whether the shade is fully expanded in either QS or QQS. */
     val isShadeFullyExpanded: Boolean
@@ -77,6 +78,20 @@
     /** Collapses the shade with an animation duration in milliseconds. */
     fun collapseWithDuration(animationDuration: Int)
 
+    /** Collapses the shade instantly without animation. */
+    fun instantCollapse()
+
+    /**
+     * Animate QS collapse 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 fullyCollapse Do not stop when QS becomes QQS. Fling until QS isn't visible anymore.
+     */
+    fun animateCollapseQs(fullyCollapse: Boolean)
+
+    /** Returns whether the shade can be collapsed. */
+    fun canBeCollapsed(): Boolean
+
     /** Returns whether the shade is in the process of collapsing. */
     val isCollapsing: Boolean
 
@@ -89,6 +104,9 @@
     /** Returns whether the shade's top level view is enabled. */
     val isViewEnabled: Boolean
 
+    /** Sets a listener to be notified when the shade starts opening or finishes closing. */
+    fun setOpenCloseListener(openCloseListener: OpenCloseListener)
+
     /** Returns whether status bar icons should be hidden when the shade is expanded. */
     fun shouldHideStatusBarIconsWhenExpanded(): Boolean
 
@@ -98,6 +116,9 @@
      */
     fun blockExpansionForCurrentTouch()
 
+    /** Sets a listener to be notified when touch tracking begins. */
+    fun setTrackingStartedListener(trackingStartedListener: TrackingStartedListener)
+
     /**
      * Disables the shade header.
      *
@@ -120,19 +141,25 @@
     /** Returns the StatusBarState. */
     val barState: Int
 
-    /**
-     * Returns the bottom part of the keyguard, which contains quick affordances.
-     *
-     * TODO(b/275550429): this should be removed.
-     */
-    val keyguardBottomAreaView: KeyguardBottomAreaView?
-
     /** Returns the NSSL controller. */
     val notificationStackScrollLayoutController: NotificationStackScrollLayoutController
 
     /** Sets the amount of progress in the status bar launch animation. */
     fun applyLaunchAnimationProgress(linearProgress: Float)
 
+    /**
+     * Close the keyguard user switcher if it is open and capable of closing.
+     *
+     * Has no effect if user switcher isn't supported, if the user switcher is already closed, or if
+     * the user switcher uses "simple" mode. The simple user switcher cannot be closed.
+     *
+     * @return true if the keyguard user switcher was open, and is now closed
+     */
+    fun closeUserSwitcherIfOpen(): Boolean
+
+    /** Called when Back gesture has been committed (i.e. a back event has definitely occurred) */
+    fun onBackPressed()
+
     /** Sets whether the status bar launch animation is currently running. */
     fun setIsLaunchAnimationRunning(running: Boolean)
 
@@ -161,6 +188,15 @@
     /** Ensures that the touchable region is updated. */
     fun updateTouchableRegion()
 
+    /** Adds a global layout listener. */
+    fun addOnGlobalLayoutListener(listener: ViewTreeObserver.OnGlobalLayoutListener)
+
+    /** Removes a global layout listener. */
+    fun removeOnGlobalLayoutListener(listener: ViewTreeObserver.OnGlobalLayoutListener)
+
+    /** Posts the given runnable to the view. */
+    fun postToView(action: Runnable): Boolean
+
     // ******* Begin Keyguard Section *********
     /** Animate to expanded shade after a delay in ms. Used for lockscreen to shade transition. */
     fun transitionToExpandedShade(delay: Long)
@@ -214,6 +250,9 @@
     /** Sends an external (e.g. Status Bar) touch event to the Shade touch handler. */
     fun handleExternalTouch(event: MotionEvent): Boolean
 
+    /** Starts tracking a shade expansion gesture that originated from the status bar. */
+    fun startTrackingExpansionFromStatusBar()
+
     // ******* End Keyguard Section *********
 
     /** Returns the ShadeHeadsUpTracker. */
@@ -226,6 +265,16 @@
     val shadeNotificationPresenter: ShadeNotificationPresenter
 
     companion object {
+        /**
+         * Returns a multiplicative factor to use when determining the falsing threshold for touches
+         * on the shade. The factor will be larger when the device is waking up due to a touch or
+         * gesture.
+         */
+        @JvmStatic
+        fun getFalsingThresholdFactor(wakefulness: WakefulnessModel): Float {
+            return if (wakefulness.isDeviceInteractiveFromTapOrGesture()) 1.5f else 1.0f
+        }
+
         const val WAKEUP_ANIMATION_DELAY_MS = 250
         const val FLING_MAX_LENGTH_SECONDS = 0.6f
         const val FLING_SPEED_UP_FACTOR = 0.6f
@@ -307,3 +356,17 @@
     /** Return the fraction of the shade that's expanded, when in lockscreen. */
     val lockscreenShadeDragProgress: Float
 }
+
+/** Listens for when touch tracking begins. */
+interface TrackingStartedListener {
+    fun onTrackingStarted()
+}
+
+/** Listens for when shade begins opening or finishes closing. */
+interface OpenCloseListener {
+    /** Called when the shade finishes closing. */
+    fun onClosingFinished()
+
+    /** Called when the shade starts opening. */
+    fun onOpenStarted()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt
index d06634b..51a27cf 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt
@@ -21,9 +21,9 @@
 import com.android.systemui.log.ConstantStringsLogger
 import com.android.systemui.log.ConstantStringsLoggerImpl
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogMessage
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogMessage
 import javax.inject.Inject
 
 private const val TAG = "systemui.shadewindow"
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
new file mode 100644
index 0000000..6fde84a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.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.shade.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.user.domain.interactor.UserInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Business logic for shade interactions. */
+@SysUISingleton
+class ShadeInteractor
+@Inject
+constructor(
+    @Application scope: CoroutineScope,
+    disableFlagsRepository: DisableFlagsRepository,
+    keyguardRepository: KeyguardRepository,
+    userSetupRepository: UserSetupRepository,
+    deviceProvisionedController: DeviceProvisionedController,
+    userInteractor: UserInteractor,
+) {
+    /** Emits true if the shade is currently allowed and false otherwise. */
+    val isShadeEnabled: StateFlow<Boolean> =
+        disableFlagsRepository.disableFlags
+            .map { it.isShadeEnabled() }
+            .stateIn(scope, SharingStarted.Eagerly, initialValue = false)
+
+    /** Emits true if the shade can be expanded from QQS to QS and false otherwise. */
+    val isExpandToQsEnabled: Flow<Boolean> =
+        combine(
+            disableFlagsRepository.disableFlags,
+            isShadeEnabled,
+            keyguardRepository.isDozing,
+            userSetupRepository.isUserSetupFlow,
+        ) { disableFlags, isShadeEnabled, isDozing, isUserSetup ->
+            deviceProvisionedController.isDeviceProvisioned &&
+                // Disallow QS during setup if it's a simple user switcher. (The user intends to
+                // use the lock screen user switcher, QS is not needed.)
+                (isUserSetup || !userInteractor.isSimpleUserSwitcher) &&
+                isShadeEnabled &&
+                disableFlags.isQuickSettingsEnabled() &&
+                !isDozing
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
index 4e1c272..abb69f6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
@@ -16,24 +16,11 @@
 
 package com.android.systemui.shade.transition
 
-import android.content.res.Configuration
-import android.content.res.Resources
-import android.util.MathUtils.constrain
-import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.shade.PanelState
-import com.android.systemui.shade.STATE_OPENING
 import com.android.systemui.shade.ShadeExpansionChangeEvent
-import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.statusbar.policy.HeadsUpManager
-import com.android.systemui.util.LargeScreenUtils
 import java.io.PrintWriter
 import javax.inject.Inject
 
@@ -42,37 +29,19 @@
 class ScrimShadeTransitionController
 @Inject
 constructor(
-    configurationController: ConfigurationController,
     dumpManager: DumpManager,
     private val scrimController: ScrimController,
-    @Main private val resources: Resources,
-    private val statusBarStateController: SysuiStatusBarStateController,
-    private val headsUpManager: HeadsUpManager,
-    private val featureFlags: FeatureFlags,
 ) {
 
-    private var inSplitShade = false
-    private var splitShadeScrimTransitionDistance = 0
     private var lastExpansionFraction: Float? = null
     private var lastExpansionEvent: ShadeExpansionChangeEvent? = null
     private var currentPanelState: Int? = null
 
     init {
-        updateResources()
-        configurationController.addCallback(
-            object : ConfigurationController.ConfigurationListener {
-                override fun onConfigChanged(newConfig: Configuration?) {
-                    updateResources()
-                }
-            })
         dumpManager.registerDumpable(
-            ScrimShadeTransitionController::class.java.simpleName, this::dump)
-    }
-
-    private fun updateResources() {
-        inSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(resources)
-        splitShadeScrimTransitionDistance =
-            resources.getDimensionPixelSize(R.dimen.split_shade_scrim_transition_distance)
+            ScrimShadeTransitionController::class.java.simpleName,
+            this::dump
+        )
     }
 
     fun onPanelStateChanged(@PanelState state: Int) {
@@ -87,46 +56,25 @@
 
     private fun onStateChanged() {
         val expansionEvent = lastExpansionEvent ?: return
-        val panelState = currentPanelState
-        val expansionFraction = calculateScrimExpansionFraction(expansionEvent, panelState)
+        val expansionFraction = calculateScrimExpansionFraction(expansionEvent)
         scrimController.setRawPanelExpansionFraction(expansionFraction)
         lastExpansionFraction = expansionFraction
     }
 
-    private fun calculateScrimExpansionFraction(
-        expansionEvent: ShadeExpansionChangeEvent,
-        @PanelState panelState: Int?
-    ): Float {
-        return if (canUseCustomFraction(panelState)) {
-            constrain(expansionEvent.dragDownPxAmount / splitShadeScrimTransitionDistance, 0f, 1f)
-        } else {
-            expansionEvent.fraction
-        }
+    private fun calculateScrimExpansionFraction(expansionEvent: ShadeExpansionChangeEvent): Float {
+        return expansionEvent.fraction
     }
 
-    private fun canUseCustomFraction(panelState: Int?) =
-        inSplitShade && isScreenUnlocked() && panelState == STATE_OPENING &&
-                // in case of HUN we can't always use predefined distances to manage scrim
-                // transition because dragDownPxAmount can start from value bigger than
-                // splitShadeScrimTransitionDistance
-                !headsUpManager.isTrackingHeadsUp &&
-                !featureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)
-
-    private fun isScreenUnlocked() =
-        statusBarStateController.currentOrUpcomingState == StatusBarState.SHADE
-
     private fun dump(printWriter: PrintWriter, args: Array<String>) {
         printWriter.println(
             """
                 ScrimShadeTransitionController:
-                  Resources:
-                    inSplitShade: $inSplitShade
-                    isScreenUnlocked: ${isScreenUnlocked()}
-                    splitShadeScrimTransitionDistance: $splitShadeScrimTransitionDistance
                   State:
                     currentPanelState: $currentPanelState
                     lastExpansionFraction: $lastExpansionFraction
                     lastExpansionEvent: $lastExpansionEvent
-            """.trimIndent())
+            """
+                .trimIndent()
+        )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
index e008ec0..d3c19b7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
@@ -19,7 +19,7 @@
 import android.app.PendingIntent
 import com.android.systemui.log.dagger.NotifInteractionLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index 906c5ed..ec66e99 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -56,7 +56,6 @@
 import android.view.ViewGroup;
 import android.view.Window;
 import android.view.WindowManager;
-import android.view.WindowManager.KeyboardShortcutsReceiver;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.Button;
 import android.widget.EditText;
@@ -337,6 +336,12 @@
         mSpecialCharacterNames.put(KeyEvent.KEYCODE_MUHENKAN, "無変換");
         mSpecialCharacterNames.put(KeyEvent.KEYCODE_HENKAN, "変換");
         mSpecialCharacterNames.put(KeyEvent.KEYCODE_KATAKANA_HIRAGANA, "かな");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_ALT_LEFT, "Alt");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_ALT_RIGHT, "Alt");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_CTRL_LEFT, "Ctrl");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_CTRL_RIGHT, "Ctrl");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_SHIFT_LEFT, "Shift");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_SHIFT_RIGHT, "Shift");
 
         mModifierNames.put(KeyEvent.META_META_ON, "Meta");
         mModifierNames.put(KeyEvent.META_CTRL_ON, "Ctrl");
@@ -411,27 +416,45 @@
         mKeyCharacterMap = mBackupKeyCharacterMap;
     }
 
+    private boolean mAppShortcutsReceived;
+    private boolean mImeShortcutsReceived;
+
     @VisibleForTesting
     void showKeyboardShortcuts(int deviceId) {
         retrieveKeyCharacterMap(deviceId);
-        mWindowManager.requestAppKeyboardShortcuts(new KeyboardShortcutsReceiver() {
-            @Override
-            public void onKeyboardShortcutsReceived(
-                    final List<KeyboardShortcutGroup> result) {
-                // Add specific app shortcuts
-                if (result.isEmpty()) {
-                    mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, false);
-                } else {
-                    mSpecificAppGroup = reMapToKeyboardShortcutMultiMappingGroup(result);
-                    mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, true);
-                }
-                mFullShortsGroup.add(SHORTCUT_SYSTEM_INDEX, mSystemGroup);
-                mFullShortsGroup.add(SHORTCUT_INPUT_INDEX, mInputGroup);
-                mFullShortsGroup.add(SHORTCUT_OPENAPPS_INDEX, mOpenAppsGroup);
-                mFullShortsGroup.add(SHORTCUT_SPECIFICAPP_INDEX, mSpecificAppGroup);
-                showKeyboardShortcutSearchList(mFullShortsGroup);
+        mAppShortcutsReceived = false;
+        mImeShortcutsReceived = false;
+        mWindowManager.requestAppKeyboardShortcuts(result -> {
+            // Add specific app shortcuts
+            if (result.isEmpty()) {
+                mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, false);
+            } else {
+                mSpecificAppGroup.addAll(reMapToKeyboardShortcutMultiMappingGroup(result));
+                mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, true);
+            }
+            mAppShortcutsReceived = true;
+            if (mImeShortcutsReceived) {
+                mergeAndShowKeyboardShortcutsGroups();
             }
         }, deviceId);
+        mWindowManager.requestImeKeyboardShortcuts(result -> {
+            // Add specific Ime shortcuts
+            if (!result.isEmpty()) {
+                mInputGroup.addAll(reMapToKeyboardShortcutMultiMappingGroup(result));
+            }
+            mImeShortcutsReceived = true;
+            if (mAppShortcutsReceived) {
+                mergeAndShowKeyboardShortcutsGroups();
+            }
+        }, deviceId);
+    }
+
+    private void mergeAndShowKeyboardShortcutsGroups() {
+        mFullShortsGroup.add(SHORTCUT_SYSTEM_INDEX, mSystemGroup);
+        mFullShortsGroup.add(SHORTCUT_INPUT_INDEX, mInputGroup);
+        mFullShortsGroup.add(SHORTCUT_OPENAPPS_INDEX, mOpenAppsGroup);
+        mFullShortsGroup.add(SHORTCUT_SPECIFICAPP_INDEX, mSpecificAppGroup);
+        showKeyboardShortcutSearchList(mFullShortsGroup);
     }
 
     // The original data structure is only for 1-to-1 shortcut mapping, so remap the old
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
index 43fbc7c..a3fd82e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
@@ -56,7 +56,6 @@
 import android.view.ViewGroup;
 import android.view.Window;
 import android.view.WindowManager;
-import android.view.WindowManager.KeyboardShortcutsReceiver;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -129,6 +128,9 @@
     private KeyCharacterMap mKeyCharacterMap;
     private KeyCharacterMap mBackupKeyCharacterMap;
 
+    @Nullable private List<KeyboardShortcutGroup> mReceivedAppShortcutGroups = null;
+    @Nullable private List<KeyboardShortcutGroup> mReceivedImeShortcutGroups = null;
+
     @VisibleForTesting
     KeyboardShortcuts(Context context, WindowManager windowManager) {
         this.mContext = new ContextThemeWrapper(
@@ -324,6 +326,12 @@
         mSpecialCharacterNames.put(KeyEvent.KEYCODE_MUHENKAN, "無変換");
         mSpecialCharacterNames.put(KeyEvent.KEYCODE_HENKAN, "変換");
         mSpecialCharacterNames.put(KeyEvent.KEYCODE_KATAKANA_HIRAGANA, "かな");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_ALT_LEFT, "Alt");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_ALT_RIGHT, "Alt");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_CTRL_LEFT, "Ctrl");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_CTRL_RIGHT, "Ctrl");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_SHIFT_LEFT, "Shift");
+        mSpecialCharacterNames.put(KeyEvent.KEYCODE_SHIFT_RIGHT, "Shift");
 
         mModifierNames.put(KeyEvent.META_META_ON, "Meta");
         mModifierNames.put(KeyEvent.META_CTRL_ON, "Ctrl");
@@ -382,18 +390,36 @@
     @VisibleForTesting
     void showKeyboardShortcuts(int deviceId) {
         retrieveKeyCharacterMap(deviceId);
-        mWindowManager.requestAppKeyboardShortcuts(new KeyboardShortcutsReceiver() {
-            @Override
-            public void onKeyboardShortcutsReceived(
-                    final List<KeyboardShortcutGroup> result) {
-                result.add(getSystemShortcuts());
-                final KeyboardShortcutGroup appShortcuts = getDefaultApplicationShortcuts();
-                if (appShortcuts != null) {
-                    result.add(appShortcuts);
-                }
-                showKeyboardShortcutsDialog(result);
-            }
-        }, deviceId);
+        mReceivedAppShortcutGroups = null;
+        mReceivedImeShortcutGroups = null;
+        mWindowManager.requestAppKeyboardShortcuts(
+                result -> {
+                    mReceivedAppShortcutGroups = result;
+                    maybeMergeAndShowKeyboardShortcuts();
+                }, deviceId);
+        mWindowManager.requestImeKeyboardShortcuts(
+                result -> {
+                    mReceivedImeShortcutGroups = result;
+                    maybeMergeAndShowKeyboardShortcuts();
+                }, deviceId);
+    }
+
+    private void maybeMergeAndShowKeyboardShortcuts() {
+        if (mReceivedAppShortcutGroups == null || mReceivedImeShortcutGroups == null) {
+            return;
+        }
+        List<KeyboardShortcutGroup> shortcutGroups = mReceivedAppShortcutGroups;
+        shortcutGroups.addAll(mReceivedImeShortcutGroups);
+        mReceivedAppShortcutGroups = null;
+        mReceivedImeShortcutGroups = null;
+
+        final KeyboardShortcutGroup defaultAppShortcuts =
+                getDefaultApplicationShortcuts();
+        if (defaultAppShortcuts != null) {
+            shortcutGroups.add(defaultAppShortcuts);
+        }
+        shortcutGroups.add(getSystemShortcuts());
+        showKeyboardShortcutsDialog(shortcutGroups);
     }
 
     private void dismissKeyboardShortcuts() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index b847fb6..42ebaa3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -41,7 +41,7 @@
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_TRUST;
 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.log.LogLevel.ERROR;
+import static com.android.systemui.log.core.LogLevel.ERROR;
 import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY;
 
 import android.app.AlarmManager;
@@ -56,7 +56,6 @@
 import android.graphics.Color;
 import android.hardware.biometrics.BiometricSourceType;
 import android.hardware.face.FaceManager;
-import android.hardware.fingerprint.FingerprintManager;
 import android.os.BatteryManager;
 import android.os.Handler;
 import android.os.Looper;
@@ -86,6 +85,8 @@
 import com.android.systemui.R;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.biometrics.FaceHelpMessageDeferral;
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -95,8 +96,8 @@
 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.log.LogLevel;
+import com.android.systemui.keyguard.util.IndicationHelper;
+import com.android.systemui.log.core.LogLevel;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.settings.UserTracker;
@@ -148,6 +149,7 @@
     private final AuthController mAuthController;
     private final KeyguardLogger mKeyguardLogger;
     private final UserTracker mUserTracker;
+    private final BouncerMessageInteractor mBouncerMessageInteractor;
     private ViewGroup mIndicationArea;
     private KeyguardIndicationTextView mTopIndicationView;
     private KeyguardIndicationTextView mLockScreenIndicationView;
@@ -225,7 +227,8 @@
     // triggered while the device is asleep
     private final AlarmTimeout mHideTransientMessageHandler;
     private final AlarmTimeout mHideBiometricMessageHandler;
-    private FeatureFlags mFeatureFlags;
+    private final FeatureFlags mFeatureFlags;
+    private final IndicationHelper mIndicationHelper;
 
     /**
      * Creates a new KeyguardIndicationController and registers callbacks.
@@ -256,7 +259,9 @@
             AlternateBouncerInteractor alternateBouncerInteractor,
             AlarmManager alarmManager,
             UserTracker userTracker,
-            FeatureFlags flags
+            BouncerMessageInteractor bouncerMessageInteractor,
+            FeatureFlags flags,
+            IndicationHelper indicationHelper
     ) {
         mContext = context;
         mBroadcastDispatcher = broadcastDispatcher;
@@ -281,7 +286,9 @@
         mScreenLifecycle.addObserver(mScreenObserver);
         mAlternateBouncerInteractor = alternateBouncerInteractor;
         mUserTracker = userTracker;
+        mBouncerMessageInteractor = bouncerMessageInteractor;
         mFeatureFlags = flags;
+        mIndicationHelper = indicationHelper;
 
         mFaceAcquiredMessageDeferral = faceHelpMessageDeferral;
         mCoExFaceAcquisitionMsgIdsToShow = new HashSet<>();
@@ -1156,6 +1163,11 @@
                         msgId,
                         helpString);
             } else if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
+                if (biometricSourceType == FINGERPRINT && !fpAuthFailed) {
+                    mBouncerMessageInteractor.setFingerprintAcquisitionMessage(helpString);
+                } else if (faceAuthSoftError) {
+                    mBouncerMessageInteractor.setFaceAcquisitionMessage(helpString);
+                }
                 mStatusBarKeyguardViewManager.setKeyguardMessage(helpString,
                         mInitialTextColorState);
             } else if (mScreenLifecycle.getScreenState() == SCREEN_ON) {
@@ -1211,6 +1223,8 @@
             if (biometricSourceType == FACE) {
                 mFaceAcquiredMessageDeferral.reset();
             }
+            mBouncerMessageInteractor.setFaceAcquisitionMessage(null);
+            mBouncerMessageInteractor.setFingerprintAcquisitionMessage(null);
         }
 
         @Override
@@ -1231,18 +1245,20 @@
             } else if (biometricSourceType == FINGERPRINT) {
                 onFingerprintAuthError(msgId, errString);
             }
+            mBouncerMessageInteractor.setFaceAcquisitionMessage(null);
+            mBouncerMessageInteractor.setFingerprintAcquisitionMessage(null);
         }
 
         private void onFaceAuthError(int msgId, String errString) {
             CharSequence deferredFaceMessage = mFaceAcquiredMessageDeferral.getDeferredMessage();
             mFaceAcquiredMessageDeferral.reset();
-            if (shouldSuppressFaceError(msgId)) {
-                mKeyguardLogger.logBiometricMessage("suppressingFaceError", msgId, errString);
+            if (mIndicationHelper.shouldSuppressErrorMsg(FACE, msgId)) {
+                mKeyguardLogger.logBiometricMessage("KIC suppressingFaceError", msgId, errString);
                 return;
             }
             if (msgId == FaceManager.FACE_ERROR_TIMEOUT) {
                 handleFaceAuthTimeoutError(deferredFaceMessage);
-            } else if (isLockoutError(msgId)) {
+            } else if (mIndicationHelper.isFaceLockoutErrorMsg(msgId)) {
                 handleFaceLockoutError(errString);
             } else {
                 showErrorMessageNowOrLater(errString, null);
@@ -1250,8 +1266,8 @@
         }
 
         private void onFingerprintAuthError(int msgId, String errString) {
-            if (shouldSuppressFingerprintError(msgId)) {
-                mKeyguardLogger.logBiometricMessage("suppressingFingerprintError",
+            if (mIndicationHelper.shouldSuppressErrorMsg(FINGERPRINT, msgId)) {
+                mKeyguardLogger.logBiometricMessage("KIC suppressingFingerprintError",
                         msgId,
                         errString);
             } else {
@@ -1259,19 +1275,6 @@
             }
         }
 
-        private boolean shouldSuppressFingerprintError(int msgId) {
-            return ((isPrimaryAuthRequired() && !isLockoutError(msgId))
-                    || msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED
-                    || msgId == FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED
-                    || msgId == FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED);
-        }
-
-        private boolean shouldSuppressFaceError(int msgId) {
-            return ((isPrimaryAuthRequired() && msgId != FaceManager.FACE_ERROR_LOCKOUT_PERMANENT)
-                    || msgId == FaceManager.FACE_ERROR_CANCELED
-                    || msgId == FaceManager.FACE_ERROR_UNABLE_TO_PROCESS);
-        }
-
         @Override
         public void onTrustChanged(int userId) {
             if (!isCurrentUser(userId)) return;
@@ -1315,6 +1318,8 @@
                     showActionToUnlock();
                 }
             }
+            mBouncerMessageInteractor.setFaceAcquisitionMessage(null);
+            mBouncerMessageInteractor.setFingerprintAcquisitionMessage(null);
         }
 
         @Override
@@ -1393,11 +1398,6 @@
         return mContext.getString(followupMsgId);
     }
 
-    private static boolean isLockoutError(int msgId) {
-        return msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT
-                || msgId == FaceManager.FACE_ERROR_LOCKOUT;
-    }
-
     private void handleFaceAuthTimeoutError(@Nullable CharSequence deferredFaceMessage) {
         mKeyguardLogger.logBiometricMessage("deferred message after face auth timeout",
                 null, String.valueOf(deferredFaceMessage));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java
index dcd9dc3..4ec5f46 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java
@@ -52,7 +52,7 @@
         mActivatableNotificationViewController = activatableNotificationViewController;
         mKeyguardBypassController = keyguardBypassController;
         mStatusBarStateController = statusBarStateController;
-        mView.setSensitiveRevealAnimEndabled(featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM));
+        mView.setSensitiveRevealAnimEnabled(featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM));
         mOnAttachStateChangeListener = new View.OnAttachStateChangeListener() {
             @Override
             public void onViewAttachedToWindow(View v) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index c098f45..4710574 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -6,7 +6,6 @@
 import android.content.Context
 import android.content.res.Configuration
 import android.os.PowerManager
-import android.os.SystemClock
 import android.util.IndentingPrintWriter
 import android.util.MathUtils
 import android.view.MotionEvent
@@ -30,8 +29,10 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.qs.QS
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
@@ -76,6 +77,8 @@
     dumpManager: DumpManager,
     qsTransitionControllerFactory: LockscreenShadeQsTransitionController.Factory,
     private val shadeRepository: ShadeRepository,
+    private val shadeInteractor: ShadeInteractor,
+    private val powerInteractor: PowerInteractor,
 ) : Dumpable {
     private var pulseHeight: Float = 0f
     @get:VisibleForTesting
@@ -278,11 +281,7 @@
         // Bind the click listener of the shelf to go to the full shade
         notificationShelfController.setOnClickListener {
             if (statusBarStateController.state == StatusBarState.KEYGUARD) {
-                centralSurfaces.wakeUpIfDozing(
-                        SystemClock.uptimeMillis(),
-                        "SHADE_CLICK",
-                        PowerManager.WAKE_REASON_GESTURE,
-                )
+                powerInteractor.wakeUpIfDozing("SHADE_CLICK", PowerManager.WAKE_REASON_GESTURE)
                 goToLockedShade(it)
             }
         }
@@ -561,7 +560,7 @@
         animationHandler: ((Long) -> Unit)? = null,
         cancelAction: Runnable? = null
     ) {
-        if (centralSurfaces.isShadeDisabled) {
+        if (!shadeInteractor.isShadeEnabled.value) {
             cancelAction?.run()
             logger.logShadeDisabledOnGoToLockedShade()
             return
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java
index 1c9bc60..5dcf6d1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java
@@ -43,6 +43,11 @@
     void onUserSwitched(int newUserId);
 
     /**
+     * Called when a new row is created and bound to a notification.
+     */
+    void onBindRow(ExpandableNotificationRow row);
+
+    /**
      * @return true iff the device is in vr mode
      */
     boolean isDeviceInVrMode();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index c519115..da84afe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -27,7 +27,6 @@
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserManager;
 import android.service.notification.StatusBarNotification;
@@ -52,6 +51,7 @@
 import com.android.systemui.R;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.statusbar.dagger.CentralSurfacesDependenciesModule;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.RemoteInputControllerLogger;
@@ -60,19 +60,15 @@
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.policy.RemoteInputUriController;
 import com.android.systemui.statusbar.policy.RemoteInputView;
 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;
 import java.util.Objects;
-import java.util.Optional;
 import java.util.function.Consumer;
 
 /**
@@ -95,10 +91,8 @@
     private final NotificationLockscreenUserManager mLockscreenUserManager;
     private final SmartReplyController mSmartReplyController;
     private final NotificationVisibilityProvider mVisibilityProvider;
+    private final PowerInteractor mPowerInteractor;
     private final ActionClickLogger mLogger;
-
-    private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
-
     protected final Context mContext;
     protected final NotifPipelineFlags mNotifPipelineFlags;
     private final UserManager mUserManager;
@@ -122,10 +116,8 @@
         @Override
         public boolean onInteraction(
                 View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) {
-            mCentralSurfacesOptionalLazy.get().ifPresent(
-                    centralSurfaces -> centralSurfaces.wakeUpIfDozing(
-                            SystemClock.uptimeMillis(), "NOTIFICATION_CLICK",
-                            PowerManager.WAKE_REASON_GESTURE));
+            mPowerInteractor.wakeUpIfDozing(
+                    "NOTIFICATION_CLICK", PowerManager.WAKE_REASON_GESTURE);
 
             final NotificationEntry entry = getNotificationForParent(view.getParent());
             mLogger.logInitialClick(entry, pendingIntent);
@@ -259,7 +251,7 @@
             NotificationLockscreenUserManager lockscreenUserManager,
             SmartReplyController smartReplyController,
             NotificationVisibilityProvider visibilityProvider,
-            Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
+            PowerInteractor powerInteractor,
             StatusBarStateController statusBarStateController,
             RemoteInputUriController remoteInputUriController,
             RemoteInputControllerLogger remoteInputControllerLogger,
@@ -271,7 +263,7 @@
         mLockscreenUserManager = lockscreenUserManager;
         mSmartReplyController = smartReplyController;
         mVisibilityProvider = visibilityProvider;
-        mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
+        mPowerInteractor = powerInteractor;
         mLogger = logger;
         mBarService = IStatusBarService.Stub.asInterface(
                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
@@ -402,7 +394,7 @@
         while (p != null) {
             if (p instanceof View) {
                 View pv = (View) p;
-                if (pv.isRootNamespace()) {
+                if (pv.getId() == com.android.internal.R.id.status_bar_latest_event_content) {
                     riv = findRemoteInputView(pv);
                     row = (ExpandableNotificationRow) pv.getTag(R.id.row_tag_for_content_view);
                     break;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index 2ca0b00..47a4641 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -60,11 +60,11 @@
     default void attach() {}
 
     /** Sets the notification shade view. */
-    default void setNotificationShadeView(ViewGroup view) {}
+    default void setWindowRootView(ViewGroup view) {}
 
     /** Gets the notification shade view. */
     @Nullable
-    default ViewGroup getNotificationShadeView() {
+    default ViewGroup getWindowRootView() {
         return null;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 9d7f3be..25a1dc6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -40,8 +40,6 @@
 import com.android.internal.policy.SystemBarUtils;
 import com.android.systemui.R;
 import com.android.systemui.animation.ShadeInterpolation;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
 import com.android.systemui.statusbar.notification.NotificationUtils;
@@ -97,7 +95,7 @@
     private float mCornerAnimationDistance;
     private NotificationShelfController mController;
     private float mActualWidth = -1;
-    private boolean mSensitiveRevealAnimEndabled;
+    private boolean mSensitiveRevealAnimEnabled;
     private boolean mShelfRefactorFlagEnabled;
     private boolean mCanModifyColorOfNotifications;
     private boolean mCanInteract;
@@ -226,9 +224,7 @@
                 if (ambientState.isBouncerInTransit()) {
                     viewState.setAlpha(aboutToShowBouncerProgress(expansion));
                 } else {
-                    FeatureFlags flags = ambientState.getFeatureFlags();
-                    if (ambientState.isSmallScreen() || !flags.isEnabled(
-                            Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) {
+                    if (ambientState.isSmallScreen()) {
                         viewState.setAlpha(ShadeInterpolation.getContentAlpha(expansion));
                     } else {
                         LargeScreenShadeInterpolator interpolator =
@@ -265,21 +261,21 @@
                     viewState.hidden = true;
                 }
             }
-
-            final float stackEnd = ambientState.getStackY() + ambientState.getStackHeight();
-            if (mSensitiveRevealAnimEndabled && viewState.hidden) {
-                // if the shelf is hidden, position it at the end of the stack (plus the clip
-                // padding), such that when it appears animated, it will smoothly move in from the
-                // bottom, without jump cutting any notifications
-                viewState.setYTranslation(stackEnd + mPaddingBetweenElements);
-            } else {
-                viewState.setYTranslation(stackEnd - viewState.height);
-            }
         } else {
             viewState.hidden = true;
             viewState.location = ExpandableViewState.LOCATION_GONE;
             viewState.hasItemsInStableShelf = false;
         }
+
+        final float stackEnd = ambientState.getStackY() + ambientState.getStackHeight();
+        if (mSensitiveRevealAnimEnabled && viewState.hidden) {
+            // if the shelf is hidden, position it at the end of the stack (plus the clip
+            // padding), such that when it appears animated, it will smoothly move in from the
+            // bottom, without jump cutting any notifications
+            viewState.setYTranslation(stackEnd + mPaddingBetweenElements);
+        } else {
+            viewState.setYTranslation(stackEnd - viewState.height);
+        }
     }
 
     private int getSpeedBumpIndex() {
@@ -417,7 +413,7 @@
                     expandingAnimated, isLastChild, shelfClipStart);
 
             // TODO(b/172289889) scale mPaddingBetweenElements with expansion amount
-            if ((!mSensitiveRevealAnimEndabled && ((isLastChild && !child.isInShelf())
+            if ((!mSensitiveRevealAnimEnabled && ((isLastChild && !child.isInShelf())
                     || backgroundForceHidden)) || aboveShelf) {
                 notificationClipEnd = shelfStart + getIntrinsicHeight();
             } else {
@@ -466,7 +462,7 @@
                 // if the shelf is visible, but if the shelf is hidden, it causes incorrect curling.
                 // notificationClipEnd handles the discrepancy between a visible and hidden shelf,
                 // so we use that when on the keyguard (and while animating away) to reduce curling.
-                final float keyguardSafeShelfStart = !mSensitiveRevealAnimEndabled
+                final float keyguardSafeShelfStart = !mSensitiveRevealAnimEnabled
                         && mAmbientState.isOnKeyguard() ? notificationClipEnd : shelfStart;
                 updateCornerRoundnessOnScroll(anv, viewStart, keyguardSafeShelfStart);
             }
@@ -1068,8 +1064,8 @@
      * Set whether the sensitive reveal animation feature flag is enabled
      * @param enabled true if enabled
      */
-    public void setSensitiveRevealAnimEndabled(boolean enabled) {
-        mSensitiveRevealAnimEndabled = enabled;
+    public void setSensitiveRevealAnimEnabled(boolean enabled) {
+        mSensitiveRevealAnimEnabled = enabled;
     }
 
     public void setRefactorFlagEnabled(boolean enabled) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java
index 59afb18..9702bfc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java
@@ -18,19 +18,19 @@
 
 import android.view.View;
 
+import com.android.systemui.display.data.repository.DisplayMetricsRepository;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 
 /**
  * Calculates and moves the QS frame vertically.
  */
 public abstract class QsFrameTranslateController {
 
-    protected CentralSurfaces mCentralSurfaces;
+    protected DisplayMetricsRepository mDisplayMetricsRepository;
 
-    public QsFrameTranslateController(CentralSurfaces centralSurfaces) {
-        mCentralSurfaces = centralSurfaces;
+    public QsFrameTranslateController(DisplayMetricsRepository displayMetricsRepository) {
+        mDisplayMetricsRepository = displayMetricsRepository;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java
index 85b522c..e429b8b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java
@@ -19,9 +19,9 @@
 import android.view.View;
 
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.display.data.repository.DisplayMetricsRepository;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 
 import javax.inject.Inject;
 
@@ -34,8 +34,8 @@
 public class QsFrameTranslateImpl extends QsFrameTranslateController {
 
     @Inject
-    public QsFrameTranslateImpl(CentralSurfaces centralSurfaces) {
-        super(centralSurfaces);
+    public QsFrameTranslateImpl(DisplayMetricsRepository displayMetricsRepository) {
+        super(displayMetricsRepository);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/commandline/CommandParser.kt b/packages/SystemUI/src/com/android/systemui/statusbar/commandline/CommandParser.kt
new file mode 100644
index 0000000..de369c3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/commandline/CommandParser.kt
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.commandline
+
+/**
+ * [CommandParser] defines the collection of tokens which can be parsed from an incoming command
+ * list, and parses them into their respective containers. Supported tokens are of the following
+ * forms:
+ * ```
+ * Flag: boolean value, false by default. always optional.
+ * Param: named parameter, taking N args all of a given type. Currently only single arg parameters
+ *        are supported.
+ * SubCommand: named command created by adding a command to a parent. Supports all fields above, but
+ *             not other subcommands.
+ * ```
+ *
+ * Tokens are added via the factory methods for each token type. They can be made `required` by
+ * calling the [require] method for the appropriate type, as follows:
+ * ```
+ * val requiredParam = parser.require(parser.param(...))
+ * ```
+ *
+ * The reason for having an explicit require is so that generic type arguments can be handled
+ * properly. See [SingleArgParam] and [SingleArgParamOptional] for the difference between an
+ * optional parameter and a required one.
+ *
+ * Typical usage of a required parameter, however, will occur within the context of a
+ * [ParseableCommand], which defines a convenience `require()` method:
+ * ```
+ * class MyCommand : ParseableCommand {
+ *   val requiredParam = param(...).require()
+ * }
+ * ```
+ *
+ * This parser defines two modes of parsing, both of which validate for required parameters.
+ * 1. [parse] is a top-level parsing method. This parser will walk the given arg list and populate
+ *    all of the delegate classes based on their type. It will handle SubCommands, and after parsing
+ *    will check for any required-but-missing SubCommands or Params.
+ *
+ *    **This method requires that every received token is represented in its grammar.**
+ * 2. [parseAsSubCommand] is a second-level parsing method suitable for any [SubCommand]. This
+ *    method will handle _only_ flags and params. It will return parsing control to its parent
+ *    parser on the first unknown token rather than throwing.
+ */
+class CommandParser {
+    private val _flags = mutableListOf<Flag>()
+    val flags: List<Flag> = _flags
+    private val _params = mutableListOf<Param>()
+    val params: List<Param> = _params
+    private val _subCommands = mutableListOf<SubCommand>()
+    val subCommands: List<SubCommand> = _subCommands
+
+    private val tokenSet = mutableSetOf<String>()
+
+    /**
+     * Parse the arg list into the fields defined in the containing class.
+     *
+     * @return true if all required fields are present after parsing
+     * @throws ArgParseError on any failure to process args
+     */
+    fun parse(args: List<String>): Boolean {
+        if (args.isEmpty()) {
+            return false
+        }
+
+        val iterator = args.listIterator()
+        var tokenHandled: Boolean
+        while (iterator.hasNext()) {
+            val token = iterator.next()
+            tokenHandled = false
+
+            flags
+                .find { it.matches(token) }
+                ?.let {
+                    it.inner = true
+                    tokenHandled = true
+                }
+
+            if (tokenHandled) continue
+
+            params
+                .find { it.matches(token) }
+                ?.let {
+                    it.parseArgsFromIter(iterator)
+                    tokenHandled = true
+                }
+
+            if (tokenHandled) continue
+
+            subCommands
+                .find { it.matches(token) }
+                ?.let {
+                    it.parseSubCommandArgs(iterator)
+                    tokenHandled = true
+                }
+
+            if (!tokenHandled) {
+                throw ArgParseError("Unknown token: $token")
+            }
+        }
+
+        return validateRequiredParams()
+    }
+
+    /**
+     * Parse a subset of the commands that came in from the top-level [parse] method, for the
+     * subcommand that this parser represents. Note that subcommands may not contain other
+     * subcommands. But they may contain flags and params.
+     *
+     * @return true if all required fields are present after parsing
+     * @throws ArgParseError on any failure to process args
+     */
+    fun parseAsSubCommand(iter: ListIterator<String>): Boolean {
+        // arg[-1] is our subcommand name, so the rest of the args are either for this
+        // subcommand, OR for the top-level command to handle. Therefore, we bail on the first
+        // failure, but still check our own required params
+
+        // The mere presence of a subcommand (similar to a flag) is a valid subcommand
+        if (flags.isEmpty() && params.isEmpty()) {
+            return validateRequiredParams()
+        }
+
+        var tokenHandled: Boolean
+        while (iter.hasNext()) {
+            val token = iter.next()
+            tokenHandled = false
+
+            flags
+                .find { it.matches(token) }
+                ?.let {
+                    it.inner = true
+                    tokenHandled = true
+                }
+
+            if (tokenHandled) continue
+
+            params
+                .find { it.matches(token) }
+                ?.let {
+                    it.parseArgsFromIter(iter)
+                    tokenHandled = true
+                }
+
+            if (!tokenHandled) {
+                // Move the cursor position backwards since we've arrived at a token
+                // that we don't own
+                iter.previous()
+                break
+            }
+        }
+
+        return validateRequiredParams()
+    }
+
+    /**
+     * If [parse] or [parseAsSubCommand] does not produce a valid result, generate a list of errors
+     * based on missing elements
+     */
+    fun generateValidationErrorMessages(): List<String> {
+        val missingElements = mutableListOf<String>()
+
+        if (unhandledParams.isNotEmpty()) {
+            val names = unhandledParams.map { it.longName }
+            missingElements.add("No values passed for required params: $names")
+        }
+
+        if (unhandledSubCmds.isNotEmpty()) {
+            missingElements.addAll(unhandledSubCmds.map { it.longName })
+            val names = unhandledSubCmds.map { it.shortName }
+            missingElements.add("No values passed for required sub-commands: $names")
+        }
+
+        return missingElements
+    }
+
+    /** Check for any missing, required params, or any invalid subcommands */
+    private fun validateRequiredParams(): Boolean =
+        unhandledParams.isEmpty() && unhandledSubCmds.isEmpty() && unvalidatedSubCmds.isEmpty()
+
+    // If any required param (aka non-optional) hasn't handled a field, then return false
+    private val unhandledParams: List<Param>
+        get() = params.filter { (it is SingleArgParam<*>) && !it.handled }
+
+    private val unhandledSubCmds: List<SubCommand>
+        get() = subCommands.filter { (it is RequiredSubCommand<*> && !it.handled) }
+
+    private val unvalidatedSubCmds: List<SubCommand>
+        get() = subCommands.filter { !it.validationStatus }
+
+    private fun checkCliNames(short: String?, long: String): String? {
+        if (short != null && tokenSet.contains(short)) {
+            return short
+        }
+
+        if (tokenSet.contains(long)) {
+            return long
+        }
+
+        return null
+    }
+
+    private fun subCommandContainsSubCommands(cmd: ParseableCommand): Boolean =
+        cmd.parser.subCommands.isNotEmpty()
+
+    private fun registerNames(short: String?, long: String) {
+        if (short != null) {
+            tokenSet.add(short)
+        }
+        tokenSet.add(long)
+    }
+
+    /**
+     * Turns a [SingleArgParamOptional]<T> into a [SingleArgParam] by converting the [T?] into [T]
+     *
+     * @return a [SingleArgParam] property delegate
+     */
+    fun <T : Any> require(old: SingleArgParamOptional<T>): SingleArgParam<T> {
+        val newParam =
+            SingleArgParam(
+                longName = old.longName,
+                shortName = old.shortName,
+                description = old.description,
+                valueParser = old.valueParser,
+            )
+
+        replaceWithRequired(old, newParam)
+        return newParam
+    }
+
+    private fun <T : Any> replaceWithRequired(
+        old: SingleArgParamOptional<T>,
+        new: SingleArgParam<T>,
+    ) {
+        _params.remove(old)
+        _params.add(new)
+    }
+
+    /**
+     * Turns an [OptionalSubCommand] into a [RequiredSubCommand] by converting the [T?] in to [T]
+     *
+     * @return a [RequiredSubCommand] property delegate
+     */
+    fun <T : ParseableCommand> require(optional: OptionalSubCommand<T>): RequiredSubCommand<T> {
+        val newCmd = RequiredSubCommand(optional.cmd)
+        replaceWithRequired(optional, newCmd)
+        return newCmd
+    }
+
+    private fun <T : ParseableCommand> replaceWithRequired(
+        old: OptionalSubCommand<T>,
+        new: RequiredSubCommand<T>,
+    ) {
+        _subCommands.remove(old)
+        _subCommands.add(new)
+    }
+
+    internal fun flag(
+        longName: String,
+        shortName: String? = null,
+        description: String = "",
+    ): Flag {
+        checkCliNames(shortName, longName)?.let {
+            throw IllegalArgumentException("Detected reused flag name ($it)")
+        }
+        registerNames(shortName, longName)
+
+        val flag = Flag(shortName, longName, description)
+        _flags.add(flag)
+        return flag
+    }
+
+    internal fun <T : Any> param(
+        longName: String,
+        shortName: String? = null,
+        description: String = "",
+        valueParser: ValueParser<T>,
+    ): SingleArgParamOptional<T> {
+        checkCliNames(shortName, longName)?.let {
+            throw IllegalArgumentException("Detected reused param name ($it)")
+        }
+        registerNames(shortName, longName)
+
+        val param =
+            SingleArgParamOptional(
+                shortName = shortName,
+                longName = longName,
+                description = description,
+                valueParser = valueParser,
+            )
+        _params.add(param)
+        return param
+    }
+
+    internal fun <T : ParseableCommand> subCommand(
+        command: T,
+    ): OptionalSubCommand<T> {
+        checkCliNames(null, command.name)?.let {
+            throw IllegalArgumentException("Cannot re-use name for subcommand ($it)")
+        }
+
+        if (subCommandContainsSubCommands(command)) {
+            throw IllegalArgumentException(
+                "SubCommands may not contain other SubCommands. $command"
+            )
+        }
+
+        registerNames(null, command.name)
+
+        val subCmd = OptionalSubCommand(command)
+        _subCommands.add(subCmd)
+        return subCmd
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/commandline/Parameters.kt b/packages/SystemUI/src/com/android/systemui/statusbar/commandline/Parameters.kt
new file mode 100644
index 0000000..6ed5eed
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/commandline/Parameters.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.statusbar.commandline
+
+import android.util.IndentingPrintWriter
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+/**
+ * Definitions for all parameter types usable by [ParseableCommand]. Parameters are command line
+ * tokens that accept a fixed number of arguments and convert them to a parsed type.
+ *
+ * Example:
+ * ```
+ * my_command --single-arg-param arg
+ * ```
+ *
+ * In the example, `my_command` is the name of the command, `--single-arg-param` is the parameter,
+ * and `arg` is the value parsed by that parameter into its eventual type.
+ *
+ * Note on generics: The intended usage for parameters is to be able to return the parsed type from
+ * the given command as a `val` via property delegation. For example, let's say we have a command
+ * that has one optional and one required parameter:
+ * ```
+ * class MyCommand : ParseableCommand {
+ *   val requiredParam: Int by parser.param(...).required()
+ *   val optionalParam: Int? by parser.param(...)
+ * }
+ * ```
+ *
+ * In order to make the simple `param` method return the correct type, we need to do two things:
+ * 1. Break out the generic type into 2 pieces (TParsed and T)
+ * 2. Create two different underlying Parameter subclasses to handle the property delegation. One
+ *    handles `T?` and the other handles `T`. Note that in both cases, `TParsed` is always non-null
+ *    since the value parsed from the argument will throw an exception if missing or if it cannot be
+ *    parsed.
+ */
+
+/** A param type knows the number of arguments it expects */
+sealed interface Param : Describable {
+    val numArgs: Int
+
+    /**
+     * Consume [numArgs] items from the iterator and relay the result into its corresponding
+     * delegated type.
+     */
+    fun parseArgsFromIter(iterator: Iterator<String>)
+}
+
+/**
+ * Base class for required and optional SingleArgParam classes. For convenience, UnaryParam is
+ * defined as a [MultipleArgParam] where numArgs = 1. The benefit is that we can define the parsing
+ * in a single place, and yet on the client side we can unwrap the underlying list of params
+ * automatically.
+ */
+abstract class UnaryParamBase<out T, out TParsed : T>(val wrapped: MultipleArgParam<T, TParsed>) :
+    Param, ReadOnlyProperty<Any?, T> {
+    var handled = false
+
+    override fun describe(pw: IndentingPrintWriter) {
+        if (shortName != null) {
+            pw.print("$shortName, ")
+        }
+        pw.print(longName)
+        pw.println(" ${typeDescription()}")
+        if (description != null) {
+            pw.indented { pw.println(description) }
+        }
+    }
+
+    /**
+     * Try to describe the arg type. We can know if it's one of the base types what kind of input it
+     * takes. Otherwise just print "<arg>" and let the clients describe in the help text
+     */
+    private fun typeDescription() =
+        when (wrapped.valueParser) {
+            Type.Int -> "<int>"
+            Type.Float -> "<float>"
+            Type.String -> "<string>"
+            Type.Boolean -> "<boolean>"
+            else -> "<arg>"
+        }
+}
+
+/** Required single-arg parameter, delegating a non-null type to the client. */
+class SingleArgParam<out T : Any>(
+    override val longName: String,
+    override val shortName: String? = null,
+    override val description: String? = null,
+    val valueParser: ValueParser<T>,
+) :
+    UnaryParamBase<T, T>(
+        MultipleArgParam(
+            longName,
+            shortName,
+            1,
+            description,
+            valueParser,
+        )
+    ) {
+
+    override fun getValue(thisRef: Any?, property: KProperty<*>): T =
+        if (handled) {
+            wrapped.getValue(thisRef, property)[0]
+        } else {
+            throw IllegalStateException("Attempt to read property before parse() has executed")
+        }
+
+    override val numArgs: Int = 1
+
+    override fun parseArgsFromIter(iterator: Iterator<String>) {
+        wrapped.parseArgsFromIter(iterator)
+        handled = true
+    }
+}
+
+/** Optional single-argument parameter, delegating a nullable type to the client. */
+class SingleArgParamOptional<out T : Any>(
+    override val longName: String,
+    override val shortName: String? = null,
+    override val description: String? = null,
+    val valueParser: ValueParser<T>,
+) :
+    UnaryParamBase<T?, T>(
+        MultipleArgParam(
+            longName,
+            shortName,
+            1,
+            description,
+            valueParser,
+        )
+    ) {
+    override fun getValue(thisRef: Any?, property: KProperty<*>): T? =
+        wrapped.getValue(thisRef, property).getOrNull(0)
+
+    override val numArgs: Int = 1
+
+    override fun parseArgsFromIter(iterator: Iterator<String>) {
+        wrapped.parseArgsFromIter(iterator)
+        handled = true
+    }
+}
+
+/**
+ * Parses a list of args into the underlying [T] data type. The resultant value is an ordered list
+ * of type [TParsed].
+ *
+ * [T] and [TParsed] are split out here in the case where the entire param is optional. I.e., a
+ * MultipleArgParam<T?, T> indicates a command line argument that can be omitted. In that case, the
+ * inner list is List<T>?, NOT List<T?>. If the argument is provided, then the type is always going
+ * to be parsed into T rather than T?.
+ */
+class MultipleArgParam<out T, out TParsed : T>(
+    override val longName: String,
+    override val shortName: String? = null,
+    override val numArgs: Int = 1,
+    override val description: String? = null,
+    val valueParser: ValueParser<TParsed>,
+) : ReadOnlyProperty<Any?, List<TParsed>>, Param {
+    private val inner: MutableList<TParsed> = mutableListOf()
+
+    override fun getValue(thisRef: Any?, property: KProperty<*>): List<TParsed> = inner
+
+    /**
+     * Consumes [numArgs] values of the iterator and parses them into [TParsed].
+     *
+     * @throws ArgParseError on the first failure
+     */
+    override fun parseArgsFromIter(iterator: Iterator<String>) {
+        if (!iterator.hasNext()) {
+            throw ArgParseError("no argument provided for $shortName")
+        }
+        for (i in 0 until numArgs) {
+            valueParser
+                .parseValue(iterator.next())
+                .fold(onSuccess = { inner.add(it) }, onFailure = { throw it })
+        }
+    }
+}
+
+data class ArgParseError(override val message: String) : Exception(message)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/commandline/ParseableCommand.kt b/packages/SystemUI/src/com/android/systemui/statusbar/commandline/ParseableCommand.kt
new file mode 100644
index 0000000..ecd3fa6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/commandline/ParseableCommand.kt
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.commandline
+
+import android.util.IndentingPrintWriter
+import java.io.PrintWriter
+import java.lang.IllegalArgumentException
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+/**
+ * An implementation of [Command] that includes a [CommandParser] which can set all delegated
+ * properties.
+ *
+ * As the number of registrants to [CommandRegistry] grows, we should have a default mechanism for
+ * parsing common command line arguments. We are not expecting to build an arbitrarily-functional
+ * CLI, nor a GNU arg parse compliant interface here, we simply want to be able to empower clients
+ * to create simple CLI grammars such as:
+ * ```
+ * $ my_command [-f|--flag]
+ * $ my_command [-a|--arg] <params...>
+ * $ my_command [subcommand1] [subcommand2]
+ * $ my_command <positional_arg ...> # not-yet implemented
+ * ```
+ *
+ * Note that the flags `-h` and `--help` are reserved for the base class. It seems prudent to just
+ * avoid them in your implementation.
+ *
+ * Usage:
+ *
+ * The intended usage tries to be clever enough to enable good ergonomics, while not too clever as
+ * to be unmaintainable. Using the default parser is done using property delegates, and looks like:
+ * ```
+ * class MyCommand(
+ *     onExecute: (cmd: MyCommand, pw: PrintWriter) -> ()
+ * ) : ParseableCommand(name) {
+ *     val flag1 by flag(
+ *         shortName = "-f",
+ *         longName = "--flag",
+ *         required = false,
+ *     )
+ *     val param1: String by param(
+ *         shortName = "-a",
+ *         longName = "--args",
+ *         valueParser = Type.String
+ *     ).required()
+ *     val param2: Int by param(..., valueParser = Type.Int)
+ *     val subCommand by subCommand(...)
+ *
+ *     override fun execute(pw: PrintWriter) {
+ *         onExecute(this, pw)
+ *     }
+ *
+ *     companion object {
+ *        const val name = "my_command"
+ *     }
+ * }
+ *
+ * fun main() {
+ *     fun printArgs(cmd: MyCommand, pw: PrintWriter) {
+ *         pw.println("${cmd.flag1}")
+ *         pw.println("${cmd.param1}")
+ *         pw.println("${cmd.param2}")
+ *         pw.println("${cmd.subCommand}")
+ *     }
+ *
+ *     commandRegistry.registerCommand(MyCommand.companion.name) {
+ *         MyCommand() { (cmd, pw) ->
+ *             printArgs(cmd, pw)
+ *         }
+ *     }
+ * }
+ *
+ * ```
+ */
+abstract class ParseableCommand(val name: String, val description: String? = null) : Command {
+    val parser: CommandParser = CommandParser()
+
+    val help by flag(longName = "help", shortName = "h", description = "Print help and return")
+
+    /**
+     * After [execute(pw, args)] is called, this class goes through a parsing stage and sets all
+     * delegated properties. It is safe to read any delegated properties here.
+     *
+     * This method is never called for [SubCommand]s, since they are associated with a top-level
+     * command that handles [execute]
+     */
+    abstract fun execute(pw: PrintWriter)
+
+    /**
+     * Given a command string list, [execute] parses the incoming command and validates the input.
+     * If this command or any of its subcommands is passed `-h` or `--help`, then execute will only
+     * print the relevant help message and exit.
+     *
+     * If any error is thrown during parsing, we will catch and log the error. This process should
+     * _never_ take down its process. Override [onParseFailed] to handle an [ArgParseError].
+     *
+     * Important: none of the delegated fields can be read before this stage.
+     */
+    override fun execute(pw: PrintWriter, args: List<String>) {
+        val success: Boolean
+        try {
+            success = parser.parse(args)
+        } catch (e: ArgParseError) {
+            pw.println(e.message)
+            onParseFailed(e)
+            return
+        } catch (e: Exception) {
+            pw.println("Unknown exception encountered during parse")
+            pw.println(e)
+            return
+        }
+
+        // Now we've parsed the incoming command without error. There are two things to check:
+        // 1. If any help is requested, print the help message and return
+        // 2. Otherwise, make sure required params have been passed in, and execute
+
+        val helpSubCmds = subCmdsRequestingHelp()
+
+        // Top-level help encapsulates subcommands. Otherwise, if _any_ subcommand requests
+        // help then defer to them. Else, just execute
+        if (help) {
+            help(pw)
+        } else if (helpSubCmds.isNotEmpty()) {
+            helpSubCmds.forEach { it.help(pw) }
+        } else {
+            if (!success) {
+                parser.generateValidationErrorMessages().forEach { pw.println(it) }
+            } else {
+                execute(pw)
+            }
+        }
+    }
+
+    /**
+     * Returns a list of all commands that asked for help. If non-empty, parsing will stop to print
+     * help. It is not guaranteed that delegates are fulfilled if help is requested
+     */
+    private fun subCmdsRequestingHelp(): List<ParseableCommand> =
+        parser.subCommands.filter { it.cmd.help }.map { it.cmd }
+
+    /** Override to do something when parsing fails */
+    open fun onParseFailed(error: ArgParseError) {}
+
+    /** Override to print a usage clause. E.g. `usage: my-cmd <arg1> <arg2>` */
+    open fun usage(pw: IndentingPrintWriter) {}
+
+    /**
+     * Print out the list of tokens, their received types if any, and their description in a
+     * formatted string.
+     *
+     * Example:
+     * ```
+     * my-command:
+     *   MyCmd.description
+     *
+     * [optional] usage block
+     *
+     * Flags:
+     *   -f
+     *     description
+     *   --flag2
+     *     description
+     *
+     * Parameters:
+     *   Required:
+     *     -p1 [Param.Type]
+     *       description
+     *     --param2 [Param.Type]
+     *       description
+     *   Optional:
+     *     same as above
+     *
+     * SubCommands:
+     *   Required:
+     *     ...
+     *   Optional:
+     *     ...
+     * ```
+     */
+    override fun help(pw: PrintWriter) {
+        val ipw = IndentingPrintWriter(pw)
+        ipw.printBoxed(name)
+        ipw.println()
+
+        // Allow for a simple `usage` block for clients
+        ipw.indented { usage(ipw) }
+
+        if (description != null) {
+            ipw.indented { ipw.println(description) }
+            ipw.println()
+        }
+
+        val flags = parser.flags
+        if (flags.isNotEmpty()) {
+            ipw.println("FLAGS:")
+            ipw.indented {
+                flags.forEach {
+                    it.describe(ipw)
+                    ipw.println()
+                }
+            }
+        }
+
+        val (required, optional) = parser.params.partition { it is SingleArgParam<*> }
+        if (required.isNotEmpty()) {
+            ipw.println("REQUIRED PARAMS:")
+            required.describe(ipw)
+        }
+        if (optional.isNotEmpty()) {
+            ipw.println("OPTIONAL PARAMS:")
+            optional.describe(ipw)
+        }
+
+        val (reqSub, optSub) = parser.subCommands.partition { it is RequiredSubCommand<*> }
+        if (reqSub.isNotEmpty()) {
+            ipw.println("REQUIRED SUBCOMMANDS:")
+            reqSub.describe(ipw)
+        }
+        if (optSub.isNotEmpty()) {
+            ipw.println("OPTIONAL SUBCOMMANDS:")
+            optSub.describe(ipw)
+        }
+    }
+
+    fun flag(
+        longName: String,
+        shortName: String? = null,
+        description: String = "",
+    ): Flag {
+        if (!checkShortName(shortName)) {
+            throw IllegalArgumentException(
+                "Flag short name must be one character long, or null. Got ($shortName)"
+            )
+        }
+
+        if (!checkLongName(longName)) {
+            throw IllegalArgumentException("Flags must not start with '-'. Got $($longName)")
+        }
+
+        val short = shortName?.let { "-$shortName" }
+        val long = "--$longName"
+
+        return parser.flag(long, short, description)
+    }
+
+    fun <T : Any> param(
+        longName: String,
+        shortName: String? = null,
+        description: String = "",
+        valueParser: ValueParser<T>,
+    ): SingleArgParamOptional<T> {
+        if (!checkShortName(shortName)) {
+            throw IllegalArgumentException(
+                "Parameter short name must be one character long, or null. Got ($shortName)"
+            )
+        }
+
+        if (!checkLongName(longName)) {
+            throw IllegalArgumentException("Parameters must not start with '-'. Got $($longName)")
+        }
+
+        val short = shortName?.let { "-$shortName" }
+        val long = "--$longName"
+
+        return parser.param(long, short, description, valueParser)
+    }
+
+    fun <T : ParseableCommand> subCommand(
+        command: T,
+    ) = parser.subCommand(command)
+
+    /** For use in conjunction with [param], makes the parameter required */
+    fun <T : Any> SingleArgParamOptional<T>.required(): SingleArgParam<T> = parser.require(this)
+
+    /** For use in conjunction with [subCommand], makes the given [SubCommand] required */
+    fun <T : ParseableCommand> OptionalSubCommand<T>.required(): RequiredSubCommand<T> =
+        parser.require(this)
+
+    private fun checkShortName(short: String?): Boolean {
+        return short == null || short.length == 1
+    }
+
+    private fun checkLongName(long: String): Boolean {
+        return !long.startsWith("-")
+    }
+
+    companion object {
+        fun Iterable<Describable>.describe(pw: IndentingPrintWriter) {
+            pw.indented {
+                forEach {
+                    it.describe(pw)
+                    pw.println()
+                }
+            }
+        }
+    }
+}
+
+/**
+ * A flag is a boolean value passed over the command line. It can have a short form or long form.
+ * The value is [Boolean.true] if the flag is found, else false
+ */
+data class Flag(
+    override val shortName: String? = null,
+    override val longName: String,
+    override val description: String? = null,
+) : ReadOnlyProperty<Any?, Boolean>, Describable {
+    var inner: Boolean = false
+
+    override fun getValue(thisRef: Any?, property: KProperty<*>) = inner
+}
+
+/**
+ * Named CLI token. Can have a short or long name. Note: consider renaming to "primary" and
+ * "secondary" names since we don't actually care what the strings are
+ *
+ * Flags and params will have [shortName]s that are always prefixed with a single dash, while
+ * [longName]s are prefixed by a double dash. E.g., `my_command -f --flag`.
+ *
+ * Subcommands do not do any prefixing, and register their name as the [longName]
+ *
+ * Can be matched against an incoming token
+ */
+interface CliNamed {
+    val shortName: String?
+    val longName: String
+
+    fun matches(token: String) = shortName == token || longName == token
+}
+
+interface Describable : CliNamed {
+    val description: String?
+
+    fun describe(pw: IndentingPrintWriter) {
+        if (shortName != null) {
+            pw.print("$shortName, ")
+        }
+        pw.print(longName)
+        pw.println()
+        if (description != null) {
+            pw.indented { pw.println(description) }
+        }
+    }
+}
+
+/**
+ * Print [s] inside of a unicode character box, like so:
+ * ```
+ *  ╔═══════════╗
+ *  ║ my-string ║
+ *  ╚═══════════╝
+ * ```
+ */
+fun PrintWriter.printDoubleBoxed(s: String) {
+    val length = s.length
+    println("╔${"═".repeat(length + 2)}╗")
+    println("║ $s ║")
+    println("╚${"═".repeat(length + 2)}╝")
+}
+
+/**
+ * Print [s] inside of a unicode character box, like so:
+ * ```
+ *  ┌───────────┐
+ *  │ my-string │
+ *  └───────────┘
+ * ```
+ */
+fun PrintWriter.printBoxed(s: String) {
+    val length = s.length
+    println("┌${"─".repeat(length + 2)}┐")
+    println("│ $s │")
+    println("└${"─".repeat(length + 2)}┘")
+}
+
+fun IndentingPrintWriter.indented(block: () -> Unit) {
+    increaseIndent()
+    block()
+    decreaseIndent()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/commandline/SubCommand.kt b/packages/SystemUI/src/com/android/systemui/statusbar/commandline/SubCommand.kt
new file mode 100644
index 0000000..41bac86
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/commandline/SubCommand.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.statusbar.commandline
+
+import android.util.IndentingPrintWriter
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+/**
+ * Sub commands wrap [ParseableCommand]s and are attached to a parent [ParseableCommand]. As such
+ * they have their own parser which will parse the args as a subcommand. I.e., the subcommand's
+ * parser will consume the iterator created by the parent, reversing the index when it reaches an
+ * unknown token.
+ *
+ * In order to keep subcommands relatively simple and not have to do complicated validation, sub
+ * commands will return control to the parent parser as soon as they discover a token that they do
+ * not own. They will throw an [ArgParseError] if parsing fails or if they don't receive arguments
+ * for a required parameter.
+ */
+sealed interface SubCommand : Describable {
+    val cmd: ParseableCommand
+
+    /** Checks if all of the required elements were passed in to [parseSubCommandArgs] */
+    var validationStatus: Boolean
+
+    /**
+     * To keep parsing simple, [parseSubCommandArgs] requires a [ListIterator] so that it can rewind
+     * the iterator when it yields control upwards
+     */
+    fun parseSubCommandArgs(iterator: ListIterator<String>)
+}
+
+/**
+ * Note that the delegated type from the subcommand is `T: ParseableCommand?`. SubCommands are
+ * created via adding a fully-formed [ParseableCommand] to parent command.
+ *
+ * At this point in time, I don't recommend nesting subcommands.
+ */
+class OptionalSubCommand<T : ParseableCommand>(
+    override val cmd: T,
+) : SubCommand, ReadOnlyProperty<Any?, ParseableCommand?> {
+    override val shortName: String? = null
+    override val longName: String = cmd.name
+    override val description: String? = cmd.description
+    override var validationStatus = true
+
+    private var isPresent = false
+
+    /** Consume tokens from the iterator and pass them to the wrapped command */
+    override fun parseSubCommandArgs(iterator: ListIterator<String>) {
+        validationStatus = cmd.parser.parseAsSubCommand(iterator)
+        isPresent = true
+    }
+
+    override fun getValue(thisRef: Any?, property: KProperty<*>): T? =
+        if (isPresent) {
+            cmd
+        } else {
+            null
+        }
+
+    override fun describe(pw: IndentingPrintWriter) {
+        cmd.help(pw)
+    }
+}
+
+/**
+ * Non-optional subcommand impl. Top-level parser is expected to throw [ArgParseError] if this token
+ * is not present in the incoming command
+ */
+class RequiredSubCommand<T : ParseableCommand>(
+    override val cmd: T,
+) : SubCommand, ReadOnlyProperty<Any?, ParseableCommand> {
+    override val shortName: String? = null
+    override val longName: String = cmd.name
+    override val description: String? = cmd.description
+    override var validationStatus = true
+
+    /** Unhandled, required subcommands are an error */
+    var handled = false
+
+    override fun parseSubCommandArgs(iterator: ListIterator<String>) {
+        validationStatus = cmd.parser.parseAsSubCommand(iterator)
+        handled = true
+    }
+
+    override fun getValue(thisRef: Any?, property: KProperty<*>): ParseableCommand = cmd
+
+    override fun describe(pw: IndentingPrintWriter) {
+        cmd.help(pw)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/commandline/ValueParser.kt b/packages/SystemUI/src/com/android/systemui/statusbar/commandline/ValueParser.kt
new file mode 100644
index 0000000..01083d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/commandline/ValueParser.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.commandline
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+
+/**
+ * Utilities for parsing the [String] command line arguments. Arguments are related to the
+ * [Parameter] type, which declares the number of, and resulting type of, the arguments that it
+ * takes when parsing. For Example:
+ * ```
+ * my-command --param <str> --param2 <int>
+ * ```
+ *
+ * Defines 2 parameters, the first of which takes a string, and the second requires an int. Because
+ * fundamentally _everything_ is a string, we have to define a convenient way to get from the
+ * incoming `StringArg` to the resulting `T`-arg, where `T` is the type required by the client.
+ *
+ * Parsing is therefore a relatively straightforward operation: (String) -> T. However, since
+ * parsing can always fail, the type is actually (String) -> Result<T>. We will always want to fail
+ * on the first error and propagate it to the caller (typically this results in printing the `help`
+ * message of the command`).
+ *
+ * The identity parsing is trivial:
+ * ```
+ * (s: String) -> String = { s -> s }
+ * ```
+ *
+ * Basic mappings are actually even provided by Kotlin's stdlib:
+ * ```
+ * (s: String) -> Boolean = { s -> s.toBooleanOrNull() }
+ * (s: String) -> Int = { s -> s.toIntOrNull() }
+ * ...
+ * ```
+ *
+ * In order to properly encode errors, we will ascribe an error type to any `null` values, such that
+ * parsing looks like this:
+ * ```
+ * val mapping: (String) -> T? = {...} // for some T
+ * val parser: (String) -> Result<T> = { s ->
+ *   mapping(s)?.let {
+ *     Result.success(it)
+ *   } ?: Result.failure(/* some failure type */)
+ * }
+ * ```
+ *
+ * Composition
+ *
+ * The ability to compose value parsing enables us to provide a couple of reasonable default parsers
+ * and allow clients to seamlessly build upon that using map functions. Consider the case where we
+ * want to validate that a value is an [Int] between 0 and 100. We start with the generic [Int]
+ * parser, and a validator, of the type (Int) -> Result<Int>:
+ * ```
+ * val intParser = { s ->
+ *   s.toStringOrNull().?let {...} ?: ...
+ * }
+ *
+ * val validator = { i ->
+ *   if (i > 100 || i < 0) {
+ *     Result.failure(...)
+ *   } else {
+ *     Result.success(i)
+ *   }
+ * ```
+ *
+ * In order to combine these functions, we need to define a new [flatMap] function that can get us
+ * from a `Result<T>` to a `Result<R>`, and short-circuit on any error. We want to see this:
+ * ```
+ * val validatingParser = { s ->
+ *   intParser.invoke(s).flatMap { i ->
+ *     validator(i)
+ *   }
+ * }
+ * ```
+ *
+ * The flatMap is relatively simply defined, we can mimic the existing definition for [Result.map],
+ * though the implementation is uglier because of the `internal` definition for `value`
+ *
+ * ```
+ * inline fun <R, T> Result<T>.flatMap(transform: (value: T) -> Result<R>): Result<R> {
+ *   return when {
+ *     isSuccess -> transform(getOrThrow())
+ *     else -> Result.failure(exceptionOrNull()!!)
+ *   }
+ * }
+ * ```
+ */
+
+/**
+ * Given a [transform] that returns a [Result], apply the transform to this result, unwrapping the
+ * return value so that
+ *
+ * These [contract] and [callsInPlace] methods are copied from the [Result.map] definition
+ */
+@OptIn(ExperimentalContracts::class)
+inline fun <R, T> Result<T>.flatMap(transform: (value: T) -> Result<R>): Result<R> {
+    contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) }
+
+    return when {
+        // Should never throw, we just don't have access to [this.value]
+        isSuccess -> transform(getOrThrow())
+        // Exception should never be null here
+        else -> Result.failure(exceptionOrNull()!!)
+    }
+}
+
+/**
+ * ValueParser turns a [String] into a Result<A> by applying a transform. See the default
+ * implementations below for starting points. The intention here is to provide the base mappings and
+ * allow clients to attach their own transforms. They are expected to succeed or return null on
+ * failure. The failure is propagated to the command parser as a Result and will fail on any
+ * [Result.failure]
+ */
+fun interface ValueParser<out A> {
+    fun parseValue(value: String): Result<A>
+}
+
+/** Map a [ValueParser] of type A to one of type B, by applying the given [transform] */
+inline fun <A, B> ValueParser<A>.map(crossinline transform: (A) -> B?): ValueParser<B> {
+    return ValueParser<B> { value ->
+        this.parseValue(value).flatMap { a ->
+            transform(a)?.let { b -> Result.success(b) }
+                ?: Result.failure(ArgParseError("Failed to transform value $value"))
+        }
+    }
+}
+
+/**
+ * Base type parsers are provided by the lib, and can be simply composed upon by [ValueParser.map]
+ * functions on the parser
+ */
+
+/** String parsing always succeeds if the value exists */
+private val parseString: ValueParser<String> = ValueParser { value -> Result.success(value) }
+
+private val parseBoolean: ValueParser<Boolean> = ValueParser { value ->
+    value.toBooleanStrictOrNull()?.let { Result.success(it) }
+        ?: Result.failure(ArgParseError("Failed to parse $value as a boolean"))
+}
+
+private val parseInt: ValueParser<Int> = ValueParser { value ->
+    value.toIntOrNull()?.let { Result.success(it) }
+        ?: Result.failure(ArgParseError("Failed to parse $value as an int"))
+}
+
+private val parseFloat: ValueParser<Float> = ValueParser { value ->
+    value.toFloatOrNull()?.let { Result.success(it) }
+        ?: Result.failure(ArgParseError("Failed to parse $value as a float"))
+}
+
+/** Default parsers that can be use as-is, or [map]ped to another type */
+object Type {
+    val Boolean = parseBoolean
+    val Int = parseInt
+    val Float = parseFloat
+    val String = parseString
+}
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 2465c21..73f181b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -74,7 +74,7 @@
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.LogLevel;
+import com.android.systemui.log.core.LogLevel;
 import com.android.systemui.log.dagger.StatusBarNetworkControllerLog;
 import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
 import com.android.systemui.settings.UserTracker;
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 f6c9a5c..035fa04 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -28,6 +28,7 @@
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.AnimationFeatureFlags;
 import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -35,11 +36,13 @@
 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.power.domain.interactor.PowerInteractor;
 import com.android.systemui.settings.DisplayTracker;
+import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.shade.ShadeSurface;
 import com.android.systemui.shade.carrier.ShadeCarrierGroupController;
 import com.android.systemui.statusbar.ActionClickLogger;
 import com.android.systemui.statusbar.CommandQueue;
@@ -79,14 +82,14 @@
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.time.SystemClock;
 
-import java.util.Optional;
-import java.util.concurrent.Executor;
-
 import dagger.Binds;
 import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
 
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
 /**
  * This module provides instances needed to construct {@link CentralSurfacesImpl}. These are moved to
  * this separate from {@link CentralSurfacesModule} module so that components that wish to build
@@ -104,7 +107,7 @@
             NotificationLockscreenUserManager lockscreenUserManager,
             SmartReplyController smartReplyController,
             NotificationVisibilityProvider visibilityProvider,
-            Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
+            PowerInteractor powerInteractor,
             StatusBarStateController statusBarStateController,
             RemoteInputUriController remoteInputUriController,
             RemoteInputControllerLogger remoteInputControllerLogger,
@@ -117,7 +120,7 @@
                 lockscreenUserManager,
                 smartReplyController,
                 visibilityProvider,
-                centralSurfacesOptionalLazy,
+                powerInteractor,
                 statusBarStateController,
                 remoteInputUriController,
                 remoteInputControllerLogger,
@@ -272,6 +275,21 @@
         return ongoingCallController;
     }
 
+    /**
+     * {@link NotificationPanelViewController} implements two interfaces:
+     *  - {@link com.android.systemui.shade.ShadeViewController}, which can be used by any class
+     *    needing access to the shade.
+     *  - {@link ShadeSurface}, which should *only* be used by {@link CentralSurfacesImpl}.
+     *
+     * Since {@link ShadeSurface} should only be accessible by {@link CentralSurfacesImpl}, it's
+     * *only* bound in this CentralSurfaces dependencies module.
+     * The {@link com.android.systemui.shade.ShadeViewController} interface is bound in
+     * {@link com.android.systemui.shade.ShadeModule} so others can access it.
+     */
+    @Binds
+    @SysUISingleton
+    ShadeSurface provideShadeSurface(NotificationPanelViewController impl);
+
     /** */
     @Binds
     ShadeCarrierGroupController.SlotIndexResolver provideSlotIndexResolver(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/DisableFlagsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/DisableFlagsLogger.kt
index f04159c..dc86869 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/DisableFlagsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/DisableFlagsLogger.kt
@@ -71,15 +71,14 @@
     }
 
     /**
-     * Returns a string representing the, old, new, and new-after-modification disable flag states,
-     * as well as the differences between each of the states.
+     * Returns a string representing the new and new-after-modification disable flag states,
+     * as well as the differences between them (if there are any).
      *
-     * Example if [old], [new], and [newAfterLocalModification] are all different:
-     *   Old: EnaiHbcRso.qINgr | New: EnaihBcRso.qiNGR (changed: hB.iGR) | New after local
-     *   modification: EnaihBcRso.qInGR (changed: .n)
+     * Example if [new] and [newAfterLocalModification] are different:
+     *   New: EnaihBcRso.qiNGR | New after local modification: EnaihBCRso.qInGR (changed: C.In)
      *
-     * Example if [old] and [new] are the same:
-     *   EnaihBcRso.qiNGR (unchanged)
+     * Example if [new] and [newAfterLocalModification] are the same:
+     *   New: EnaihBcRso.qiNGR
      *
      * A capital character signifies the flag is set and a lowercase character signifies that the
      * flag isn't set. The flag states will be logged in the same order as the passed-in lists.
@@ -88,37 +87,17 @@
      * is no difference. the new-after-modification state also won't be included if there's no
      * difference from the new state.
      *
-     * @param old the disable state that had been previously sent. Null if we don't need to log the
-     *   previously sent state.
      * @param new the new disable state that has just been sent.
      * @param newAfterLocalModification the new disable states after a class has locally modified
      *   them. Null if the class does not locally modify.
      */
     fun getDisableFlagsString(
-        old: DisableState? = null,
         new: DisableState,
         newAfterLocalModification: DisableState? = null
     ): String {
         val builder = StringBuilder("Received new disable state: ")
 
-        // This if/else has slightly repetitive code but is easier to read.
-        if (old != null && old != new) {
-            builder.append("Old: ")
-            builder.append(getFlagsString(old))
-            builder.append(" | ")
-            builder.append("New: ")
-            builder.append(getFlagsString(new))
-            builder.append(" ")
-            builder.append(getDiffString(old, new))
-        } else if (old != null && old == new) {
-            // If old and new are the same, we only need to print one of them.
-            builder.append(getFlagsString(new))
-            builder.append(" ")
-            builder.append(getDiffString(old, new))
-        } else { // old == null
-            builder.append(getFlagsString(new))
-            // Don't get a diff string because we have no [old] to compare with.
-        }
+        builder.append(getFlagsString(new))
 
         if (newAfterLocalModification != null && new != newAfterLocalModification) {
             builder.append(" | New after local modification: ")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/dagger/DisableFlagsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/dagger/DisableFlagsModule.kt
new file mode 100644
index 0000000..3d5f8df
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/dagger/DisableFlagsModule.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.disableflags.dagger
+
+import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+/** Provides information related to disable flags. */
+@Module
+abstract class DisableFlagsModule {
+    @Binds
+    abstract fun bindDisableFlagsRepo(impl: DisableFlagsRepositoryImpl): DisableFlagsRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/model/DisableFlagsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/model/DisableFlagsModel.kt
new file mode 100644
index 0000000..2bb4765
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/model/DisableFlagsModel.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.statusbar.disableflags.data.model
+
+import android.app.StatusBarManager.DISABLE2_NONE
+import android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE
+import android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS
+import android.app.StatusBarManager.DISABLE_NONE
+import android.app.StatusBarManager.DISABLE_NOTIFICATION_ALERTS
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
+
+/**
+ * Model for the disable flags that come from [IStatusBar].
+ *
+ * For clients of the disable flags: do *not* refer to the disable integers directly. Instead,
+ * re-use or define a helper method that internally processes the flags. (We want to hide the
+ * bitwise logic here so no one else has to worry about it.)
+ */
+data class DisableFlagsModel(
+    private val disable1: Int = DISABLE_NONE,
+    private val disable2: Int = DISABLE2_NONE,
+) {
+    /** Returns true if notification alerts are allowed based on the flags. */
+    fun areNotificationAlertsEnabled(): Boolean {
+        return (disable1 and DISABLE_NOTIFICATION_ALERTS) == 0
+    }
+
+    /** Returns true if the shade is allowed based on the flags. */
+    fun isShadeEnabled(): Boolean {
+        return (disable2 and DISABLE2_NOTIFICATION_SHADE) == 0
+    }
+
+    /** Returns true if full quick settings are allowed based on the flags. */
+    fun isQuickSettingsEnabled(): Boolean {
+        return (disable2 and DISABLE2_QUICK_SETTINGS) == 0
+    }
+
+    /** Logs the change to the provided buffer. */
+    fun logChange(buffer: LogBuffer, disableFlagsLogger: DisableFlagsLogger) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                int1 = disable1
+                int2 = disable2
+            },
+            {
+                disableFlagsLogger.getDisableFlagsString(
+                    new = DisableFlagsLogger.DisableState(int1, int2),
+                )
+            }
+        )
+    }
+
+    private companion object {
+        const val TAG = "DisableFlagsModel"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt
new file mode 100644
index 0000000..13b74b4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.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.statusbar.disableflags.data.repository
+
+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.DisplayId
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.dagger.DisableFlagsRepositoryLog
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
+import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler
+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.distinctUntilChanged
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+/** Repository for the disable flags received from external systems. See [IStatusBar.disable]. */
+interface DisableFlagsRepository {
+    /** A model of the disable flags last received from [IStatusBar]. */
+    val disableFlags: StateFlow<DisableFlagsModel>
+}
+
+@SysUISingleton
+class DisableFlagsRepositoryImpl
+@Inject
+constructor(
+    commandQueue: CommandQueue,
+    @DisplayId private val thisDisplayId: Int,
+    @Application scope: CoroutineScope,
+    remoteInputQuickSettingsDisabler: RemoteInputQuickSettingsDisabler,
+    @DisableFlagsRepositoryLog private val logBuffer: LogBuffer,
+    private val disableFlagsLogger: DisableFlagsLogger,
+) : DisableFlagsRepository {
+    override val disableFlags: StateFlow<DisableFlagsModel> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : CommandQueue.Callbacks {
+                        override fun disable(
+                            displayId: Int,
+                            state1: Int,
+                            state2: Int,
+                            animate: Boolean,
+                        ) {
+                            if (displayId != thisDisplayId) {
+                                return
+                            }
+                            trySend(
+                                DisableFlagsModel(
+                                    state1,
+                                    // Ideally, [RemoteInputQuickSettingsDisabler] should instead
+                                    // expose a flow that gets `combine`d with this [disableFlags]
+                                    // flow in a [DisableFlagsInteractor] or
+                                    // [QuickSettingsInteractor]-type class. However, that's out of
+                                    // scope for the CentralSurfaces removal project.
+                                    remoteInputQuickSettingsDisabler.adjustDisableFlags(state2),
+                                )
+                            )
+                        }
+                    }
+                commandQueue.addCallback(callback)
+                awaitClose { commandQueue.removeCallback(callback) }
+            }
+            .distinctUntilChanged()
+            .onEach { it.logChange(logBuffer, disableFlagsLogger) }
+            // Use Eagerly because we always need to know about disable flags
+            .stateIn(scope, SharingStarted.Eagerly, DisableFlagsModel())
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt
index 3d6d489..84796f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt
@@ -22,6 +22,8 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.statusbar.window.StatusBarWindowController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.time.SystemClock
@@ -36,6 +38,13 @@
 
         @Provides
         @SysUISingleton
+        @SystemStatusAnimationSchedulerLog
+        fun provideSystemStatusAnimationSchedulerLogBuffer(factory: LogBufferFactory): LogBuffer {
+            return factory.create("SystemStatusAnimationSchedulerLog", 60)
+        }
+
+        @Provides
+        @SysUISingleton
         fun provideSystemStatusAnimationScheduler(
                 featureFlags: FeatureFlags,
                 coordinator: SystemEventCoordinator,
@@ -44,7 +53,8 @@
                 dumpManager: DumpManager,
                 systemClock: SystemClock,
                 @Application coroutineScope: CoroutineScope,
-                @Main executor: DelayableExecutor
+                @Main executor: DelayableExecutor,
+                logger: SystemStatusAnimationSchedulerLogger
         ): SystemStatusAnimationScheduler {
             return if (featureFlags.isEnabled(Flags.PLUG_IN_STATUS_BAR_CHIP)) {
                 SystemStatusAnimationSchedulerImpl(
@@ -53,7 +63,8 @@
                         statusBarWindowController,
                         dumpManager,
                         systemClock,
-                        coroutineScope
+                        coroutineScope,
+                        logger
                 )
             } else {
                 SystemStatusAnimationSchedulerLegacyImpl(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
index 56ea703..6fc715a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
@@ -18,7 +18,6 @@
 
 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
@@ -69,7 +68,8 @@
     private val statusBarWindowController: StatusBarWindowController,
     dumpManager: DumpManager,
     private val systemClock: SystemClock,
-    @Application private val coroutineScope: CoroutineScope
+    @Application private val coroutineScope: CoroutineScope,
+    private val logger: SystemStatusAnimationSchedulerLogger?
 ) : SystemStatusAnimationScheduler {
 
     companion object {
@@ -121,6 +121,10 @@
                     }
                 }
         }
+
+        coroutineScope.launch {
+            animationState.collect { logger?.logAnimationStateUpdate(it) }
+        }
     }
 
     @SystemAnimationState override fun getAnimationState(): Int = animationState.value
@@ -140,32 +144,17 @@
         ) {
             // 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")
-            }
-
+            logger?.logScheduleEvent(event)
             scheduleEvent(event)
         } else if (currentlyDisplayedEvent?.shouldUpdateFromEvent(event) == true) {
-            if (DEBUG) {
-                Log.d(
-                    TAG,
-                    "updating current event from: $event. animationState=${animationState.value}"
-                )
-            }
+            logger?.logUpdateEvent(event, animationState.value)
             currentlyDisplayedEvent?.updateFromEvent(event)
             if (event.forceVisible) hasPersistentDot = true
         } else if (scheduledEvent.value?.shouldUpdateFromEvent(event) == true) {
-            if (DEBUG) {
-                Log.d(
-                    TAG,
-                    "updating scheduled event from: $event. animationState=${animationState.value}"
-                )
-            }
+            logger?.logUpdateEvent(event, animationState.value)
             scheduledEvent.value?.updateFromEvent(event)
         } else {
-            if (DEBUG) {
-                Log.d(TAG, "ignoring event $event")
-            }
+            logger?.logIgnoreEvent(event)
         }
     }
 
@@ -356,6 +345,7 @@
     }
 
     private fun notifyTransitionToPersistentDot(): Animator? {
+        logger?.logTransitionToPersistentDotCallbackInvoked()
         val anims: List<Animator> =
             listeners.mapNotNull {
                 it.onSystemStatusAnimationTransitionToPersistentDot(
@@ -373,6 +363,7 @@
 
     private fun notifyHidePersistentDot(): Animator? {
         Assert.isMainThread()
+        logger?.logHidePersistentDotCallbackInvoked()
         val anims: List<Animator> = listeners.mapNotNull { it.onHidePersistentDot() }
 
         if (animationState.value == SHOWING_PERSISTENT_DOT) {
@@ -424,5 +415,4 @@
     }
 }
 
-private const val DEBUG = false
 private const val TAG = "SystemStatusAnimationSchedulerImpl"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLog.kt
new file mode 100644
index 0000000..4ac94a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLog.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.events
+
+import javax.inject.Qualifier
+
+/** Logs for the SystemStatusAnimationScheduler. */
+@Qualifier
+@MustBeDocumented
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class SystemStatusAnimationSchedulerLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLogger.kt
new file mode 100644
index 0000000..22b0b69
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLogger.kt
@@ -0,0 +1,92 @@
+package com.android.systemui.statusbar.events
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import javax.inject.Inject
+
+/** Logs for the SystemStatusAnimationScheduler. */
+@SysUISingleton
+class SystemStatusAnimationSchedulerLogger
+@Inject
+constructor(
+    @SystemStatusAnimationSchedulerLog private val logBuffer: LogBuffer,
+) {
+
+    fun logScheduleEvent(event: StatusEvent) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = event.javaClass.simpleName
+                int1 = event.priority
+                bool1 = event.forceVisible
+                bool2 = event.showAnimation
+            },
+            { "Scheduling event: $str1(forceVisible=$bool1, priority=$int1, showAnimation=$bool2)" }
+        )
+    }
+
+    fun logUpdateEvent(event: StatusEvent, @SystemAnimationState animationState: Int) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = event.javaClass.simpleName
+                int1 = event.priority
+                bool1 = event.forceVisible
+                bool2 = event.showAnimation
+                int2 = animationState
+            },
+            {
+                "Updating current event from: $str1(forceVisible=$bool1, priority=$int1, " +
+                    "showAnimation=$bool2), animationState=${animationState.name()}"
+            }
+        )
+    }
+
+    fun logIgnoreEvent(event: StatusEvent) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = event.javaClass.simpleName
+                int1 = event.priority
+                bool1 = event.forceVisible
+                bool2 = event.showAnimation
+            },
+            { "Ignore event: $str1(forceVisible=$bool1, priority=$int1, showAnimation=$bool2)" }
+        )
+    }
+
+    fun logHidePersistentDotCallbackInvoked() {
+        logBuffer.log(TAG, LogLevel.DEBUG, "Hide persistent dot callback invoked")
+    }
+
+    fun logTransitionToPersistentDotCallbackInvoked() {
+        logBuffer.log(TAG, LogLevel.DEBUG, "Transition to persistent dot callback invoked")
+    }
+
+    fun logAnimationStateUpdate(@SystemAnimationState animationState: Int) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { int1 = animationState },
+            { "AnimationState update: ${int1.name()}" }
+        )
+        animationState.name()
+    }
+
+    private fun @receiver:SystemAnimationState Int.name() =
+        when (this) {
+            IDLE -> "IDLE"
+            ANIMATION_QUEUED -> "ANIMATION_QUEUED"
+            ANIMATING_IN -> "ANIMATING_IN"
+            RUNNING_CHIP_ANIM -> "RUNNING_CHIP_ANIM"
+            ANIMATING_OUT -> "ANIMATING_OUT"
+            SHOWING_PERSISTENT_DOT -> "SHOWING_PERSISTENT_DOT"
+            else -> "UNKNOWN_ANIMATION_STATE"
+        }
+}
+
+private const val TAG = "SystemStatusAnimationSchedulerLog"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt
index a67c26c..96725fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt
@@ -19,7 +19,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.dagger.SwipeUpLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import javax.inject.Inject
 
 /** Log messages for [SwipeUpGestureHandler]. */
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 6c1dc8c..11b1053 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -139,6 +139,19 @@
 
             updateTextColorFromWallpaper()
             statusBarStateListener.onDozeAmountChanged(0f, statusBarStateController.dozeAmount)
+
+            if (regionSamplingEnabled && (!regionSamplers.containsKey(v))) {
+                var regionSampler = RegionSampler(
+                        v as View,
+                        uiExecutor,
+                        bgExecutor,
+                        regionSamplingEnabled,
+                        isLockscreen = true,
+                ) { updateTextColorFromRegionSampler() }
+                initializeTextColors(regionSampler)
+                regionSamplers[v] = regionSampler
+                regionSampler.startRegionSampler()
+            }
         }
 
         override fun onViewDetachedFromWindow(v: View) {
@@ -165,7 +178,16 @@
                     now.isBefore(Instant.ofEpochMilli(t.expiryTimeMillis))
         }
         if (weatherTarget != null) {
-            val weatherData = WeatherData.fromBundle(weatherTarget.baseAction.extras)
+            val clickIntent = weatherTarget.headerAction?.intent
+            val weatherData = WeatherData.fromBundle(weatherTarget.baseAction.extras, { v ->
+                if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                    activityStarter.startActivity(
+                        clickIntent,
+                        true, /* dismissShade */
+                        null,
+                        false)
+                }
+            })
             if (weatherData != null) {
                 keyguardUpdateMonitor.sendWeatherData(weatherData)
             }
@@ -173,23 +195,6 @@
 
         val filteredTargets = targets.filter(::filterSmartspaceTarget)
         plugin?.onTargetsAvailable(filteredTargets)
-        if (!isRegionSamplersCreated) {
-            for (v in smartspaceViews) {
-                if (regionSamplingEnabled) {
-                    var regionSampler = RegionSampler(
-                        v as View,
-                        uiExecutor,
-                        bgExecutor,
-                        regionSamplingEnabled,
-                        isLockscreen = true,
-                    ) { updateTextColorFromRegionSampler() }
-                    initializeTextColors(regionSampler)
-                    regionSamplers[v] = regionSampler
-                    regionSampler.startRegionSampler()
-                }
-            }
-            isRegionSamplersCreated = true
-        }
     }
 
     private val userTrackerCallback = object : UserTracker.Callback {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
index bab553e..d10fac6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
@@ -17,15 +17,14 @@
 
 import android.app.Notification;
 import android.os.PowerManager;
-import android.os.SystemClock;
 import android.service.notification.StatusBarNotification;
 import android.util.Log;
 import android.view.View;
 
 import com.android.systemui.DejankUtils;
+import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.wm.shell.bubbles.Bubbles;
 
 import java.util.Optional;
@@ -40,7 +39,7 @@
     private static final String TAG = "NotificationClicker";
 
     private final NotificationClickerLogger mLogger;
-    private final Optional<CentralSurfaces> mCentralSurfacesOptional;
+    private final PowerInteractor mPowerInteractor;
     private final Optional<Bubbles> mBubblesOptional;
     private final NotificationActivityStarter mNotificationActivityStarter;
 
@@ -54,11 +53,11 @@
 
     private NotificationClicker(
             NotificationClickerLogger logger,
-            Optional<CentralSurfaces> centralSurfacesOptional,
+            PowerInteractor powerInteractor,
             Optional<Bubbles> bubblesOptional,
             NotificationActivityStarter notificationActivityStarter) {
         mLogger = logger;
-        mCentralSurfacesOptional = centralSurfacesOptional;
+        mPowerInteractor = powerInteractor;
         mBubblesOptional = bubblesOptional;
         mNotificationActivityStarter = notificationActivityStarter;
     }
@@ -70,9 +69,7 @@
             return;
         }
 
-        mCentralSurfacesOptional.ifPresent(centralSurfaces -> centralSurfaces.wakeUpIfDozing(
-                SystemClock.uptimeMillis(), "NOTIFICATION_CLICK",
-                PowerManager.WAKE_REASON_GESTURE));
+        mPowerInteractor.wakeUpIfDozing("NOTIFICATION_CLICK", PowerManager.WAKE_REASON_GESTURE);
 
         final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
         final NotificationEntry entry = row.getEntry();
@@ -131,21 +128,22 @@
     /** Daggerized builder for NotificationClicker. */
     public static class Builder {
         private final NotificationClickerLogger mLogger;
+        private final PowerInteractor mPowerInteractor;
 
         @Inject
-        public Builder(NotificationClickerLogger logger) {
+        public Builder(NotificationClickerLogger logger, PowerInteractor powerInteractor) {
             mLogger = logger;
+            mPowerInteractor = powerInteractor;
         }
 
         /** Builds an instance. */
         public NotificationClicker build(
-                Optional<CentralSurfaces> centralSurfacesOptional,
                 Optional<Bubbles> bubblesOptional,
                 NotificationActivityStarter notificationActivityStarter
         ) {
             return new NotificationClicker(
                     mLogger,
-                    centralSurfacesOptional,
+                    mPowerInteractor,
                     bubblesOptional,
                     notificationActivityStarter);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
index a3a72d9..cea2b59 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.log.dagger.NotifInteractionLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import javax.inject.Inject
 
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 f7679ed..502e1d9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt
@@ -14,7 +14,7 @@
 package com.android.systemui.statusbar.notification
 
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.DEBUG
 import com.android.systemui.log.dagger.NotificationLockscreenLog
 import com.android.systemui.statusbar.StatusBarState
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/RemoteInputControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/RemoteInputControllerLogger.kt
index 487a5f8..7809eaa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/RemoteInputControllerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/RemoteInputControllerLogger.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.DEBUG
 import com.android.systemui.log.dagger.NotificationRemoteInputLog
 import javax.inject.Inject
 
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 d2c6d4b..5c72731 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
@@ -61,6 +61,7 @@
 import android.service.notification.NotificationListenerService.RankingMap;
 import android.service.notification.StatusBarNotification;
 import android.util.ArrayMap;
+import android.util.Log;
 import android.util.Pair;
 
 import androidx.annotation.NonNull;
@@ -274,10 +275,7 @@
         Assert.isMainThread();
         checkForReentrantCall();
 
-        // TODO (b/206842750): This method is called from (silent) clear all and non-clear all
-        // contexts and should be checking the NO_CLEAR flag, rather than depending on NSSL
-        // to pass in a properly filtered list of notifications
-
+        final int entryCount = entriesToDismiss.size();
         final List<NotificationEntry> entriesToLocallyDismiss = new ArrayList<>();
         for (int i = 0; i < entriesToDismiss.size(); i++) {
             NotificationEntry entry = entriesToDismiss.get(i).first;
@@ -286,28 +284,34 @@
             requireNonNull(stats);
             NotificationEntry storedEntry = mNotificationSet.get(entry.getKey());
             if (storedEntry == null) {
-                mLogger.logNonExistentNotifDismissed(entry);
+                mLogger.logDismissNonExistentNotif(entry, i, entryCount);
                 continue;
             }
             if (entry != storedEntry) {
                 throw mEulogizer.record(
                         new IllegalStateException("Invalid entry: "
                                 + "different stored and dismissed entries for " + logKey(entry)
+                                + " (" + i + "/" + entryCount + ")"
+                                + " dismissed=@" + Integer.toHexString(entry.hashCode())
                                 + " stored=@" + Integer.toHexString(storedEntry.hashCode())));
             }
 
             if (entry.getDismissState() == DISMISSED) {
+                mLogger.logDismissAlreadyDismissedNotif(entry, i, entryCount);
                 continue;
+            } else if (entry.getDismissState() == PARENT_DISMISSED) {
+                mLogger.logDismissAlreadyParentDismissedNotif(entry, i, entryCount);
             }
 
             updateDismissInterceptors(entry);
             if (isDismissIntercepted(entry)) {
-                mLogger.logNotifDismissedIntercepted(entry);
+                mLogger.logNotifDismissedIntercepted(entry, i, entryCount);
                 continue;
             }
 
             entriesToLocallyDismiss.add(entry);
             if (!entry.isCanceled()) {
+                int finalI = i;
                 // send message to system server if this notification hasn't already been cancelled
                 mBgExecutor.execute(() -> {
                     try {
@@ -320,7 +324,7 @@
                                 stats.notificationVisibility);
                     } catch (RemoteException e) {
                         // system process is dead if we're here.
-                        mLogger.logRemoteExceptionOnNotificationClear(entry, e);
+                        mLogger.logRemoteExceptionOnNotificationClear(entry, finalI, entryCount, e);
                     }
                 });
             }
@@ -357,14 +361,16 @@
         }
 
         final List<NotificationEntry> entries = new ArrayList<>(getAllNotifs());
+        final int initialEntryCount = entries.size();
         for (int i = entries.size() - 1; i >= 0; i--) {
             NotificationEntry entry = entries.get(i);
+
             if (!shouldDismissOnClearAll(entry, userId)) {
                 // system server won't be removing these notifications, but we still give dismiss
                 // interceptors the chance to filter the notification
                 updateDismissInterceptors(entry);
                 if (isDismissIntercepted(entry)) {
-                    mLogger.logNotifClearAllDismissalIntercepted(entry);
+                    mLogger.logNotifClearAllDismissalIntercepted(entry, i, initialEntryCount);
                 }
                 entries.remove(i);
             }
@@ -380,25 +386,46 @@
      */
     private void locallyDismissNotifications(List<NotificationEntry> entries) {
         final List<NotificationEntry> canceledEntries = new ArrayList<>();
-
+        final int entryCount = entries.size();
         for (int i = 0; i < entries.size(); i++) {
             NotificationEntry entry = entries.get(i);
 
+            final NotificationEntry storedEntry = mNotificationSet.get(entry.getKey());
+            if (storedEntry == null) {
+                mLogger.logLocallyDismissNonExistentNotif(entry, i, entryCount);
+            } else if (storedEntry != entry) {
+                mLogger.logLocallyDismissMismatchedEntry(entry, i, entryCount, storedEntry);
+            }
+
+            if (entry.getDismissState() == DISMISSED) {
+                mLogger.logLocallyDismissAlreadyDismissedNotif(entry, i, entryCount);
+            } else if (entry.getDismissState() == PARENT_DISMISSED) {
+                mLogger.logLocallyDismissAlreadyParentDismissedNotif(entry, i, entryCount);
+            }
+
             entry.setDismissState(DISMISSED);
-            mLogger.logNotifDismissed(entry);
+            mLogger.logLocallyDismissed(entry, i, entryCount);
 
             if (entry.isCanceled()) {
                 canceledEntries.add(entry);
-            } else {
-                // Mark any children as dismissed as system server will auto-dismiss them as well
-                if (entry.getSbn().getNotification().isGroupSummary()) {
-                    for (NotificationEntry otherEntry : mNotificationSet.values()) {
-                        if (shouldAutoDismissChildren(otherEntry, entry.getSbn().getGroupKey())) {
-                            otherEntry.setDismissState(PARENT_DISMISSED);
-                            mLogger.logChildDismissed(otherEntry);
-                            if (otherEntry.isCanceled()) {
-                                canceledEntries.add(otherEntry);
-                            }
+                continue;
+            }
+
+            // Mark any children as dismissed as system server will auto-dismiss them as well
+            if (entry.getSbn().getNotification().isGroupSummary()) {
+                for (NotificationEntry otherEntry : mNotificationSet.values()) {
+                    if (shouldAutoDismissChildren(otherEntry, entry.getSbn().getGroupKey())) {
+                        if (otherEntry.getDismissState() == DISMISSED) {
+                            mLogger.logLocallyDismissAlreadyDismissedChild(
+                                    otherEntry, entry, i, entryCount);
+                        } else if (otherEntry.getDismissState() == PARENT_DISMISSED) {
+                            mLogger.logLocallyDismissAlreadyParentDismissedChild(
+                                    otherEntry, entry, i, entryCount);
+                        }
+                        otherEntry.setDismissState(PARENT_DISMISSED);
+                        mLogger.logLocallyDismissedChild(otherEntry, entry, i, entryCount);
+                        if (otherEntry.isCanceled()) {
+                            canceledEntries.add(otherEntry);
                         }
                     }
                 }
@@ -408,7 +435,7 @@
         // Immediately remove any dismissed notifs that have already been canceled by system server
         // (probably due to being lifetime-extended up until this point).
         for (NotificationEntry canceledEntry : canceledEntries) {
-            mLogger.logDismissOnAlreadyCanceledEntry(canceledEntry);
+            mLogger.logLocallyDismissedAlreadyCanceledEntry(canceledEntry);
             tryRemoveNotification(canceledEntry);
         }
     }
@@ -517,10 +544,16 @@
      * @return True if the notification was removed, false otherwise.
      */
     private boolean tryRemoveNotification(NotificationEntry entry) {
-        if (mNotificationSet.get(entry.getKey()) != entry) {
+        final NotificationEntry storedEntry = mNotificationSet.get(entry.getKey());
+        if (storedEntry == null) {
+            Log.wtf(TAG, "TRY REMOVE non-existent notification " + logKey(entry));
+            return false;
+        } else if (storedEntry != entry) {
             throw mEulogizer.record(
-                    new IllegalStateException("No notification to remove with key "
-                            + logKey(entry)));
+                    new IllegalStateException("Mismatched stored and tryRemoved entries"
+                            + " for key " + logKey(entry) + ":"
+                            + " stored=@" + Integer.toHexString(storedEntry.hashCode())
+                            + " tryRemoved=@" + Integer.toHexString(entry.hashCode())));
         }
 
         if (!entry.isCanceled()) {
@@ -734,14 +767,16 @@
     }
 
     private void cancelLocalDismissal(NotificationEntry entry) {
-        if (entry.getDismissState() != NOT_DISMISSED) {
-            entry.setDismissState(NOT_DISMISSED);
-            if (entry.getSbn().getNotification().isGroupSummary()) {
-                for (NotificationEntry otherEntry : mNotificationSet.values()) {
-                    if (otherEntry.getSbn().getGroupKey().equals(entry.getSbn().getGroupKey())
-                            && otherEntry.getDismissState() == PARENT_DISMISSED) {
-                        otherEntry.setDismissState(NOT_DISMISSED);
-                    }
+        if (entry.getDismissState() == NOT_DISMISSED) {
+            mLogger.logCancelLocalDismissalNotDismissedNotif(entry);
+            return;
+        }
+        entry.setDismissState(NOT_DISMISSED);
+        if (entry.getSbn().getNotification().isGroupSummary()) {
+            for (NotificationEntry otherEntry : mNotificationSet.values()) {
+                if (otherEntry.getSbn().getGroupKey().equals(entry.getSbn().getGroupKey())
+                        && otherEntry.getDismissState() == PARENT_DISMISSED) {
+                    otherEntry.setDismissState(NOT_DISMISSED);
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
index 39d0833..0ab348d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.log.dagger.NotificationLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import javax.inject.Inject
 
 class GroupCoalescerLogger @Inject constructor(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
index 79c63e6..bd1141e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
@@ -2,7 +2,7 @@
 
 import com.android.systemui.log.dagger.NotificationLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.notification.row.NotificationGuts
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index e17ce5c..496fb83 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -4,7 +4,7 @@
 
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import javax.inject.Inject
 
 private const val TAG = "HeadsUpCoordinator"
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 2fa070c..07eb8a00 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
@@ -28,12 +28,9 @@
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.shared.model.TransitionStep
 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
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
@@ -50,30 +47,29 @@
 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
 import java.io.PrintWriter
 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.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.conflate
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.emitAll
-import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.transformLatest
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.yield
 
 /**
  * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section
- * headers on the lockscreen.
+ * headers on the lockscreen. If enabled, it will also track and hide seen notifications on the
+ * lockscreen.
  */
 @CoordinatorScope
 class KeyguardCoordinator
@@ -86,7 +82,6 @@
     private val keyguardRepository: KeyguardRepository,
     private val keyguardTransitionRepository: KeyguardTransitionRepository,
     private val logger: KeyguardCoordinatorLogger,
-    private val notifPipelineFlags: NotifPipelineFlags,
     @Application private val scope: CoroutineScope,
     private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider,
     private val secureSettings: SecureSettings,
@@ -95,6 +90,8 @@
 ) : Coordinator, Dumpable {
 
     private val unseenNotifications = mutableSetOf<NotificationEntry>()
+    private val unseenEntryAdded = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1)
+    private val unseenEntryRemoved = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1)
     private var unseenFilterEnabled = false
 
     override fun attach(pipeline: NotifPipeline) {
@@ -109,79 +106,130 @@
     private fun attachUnseenFilter(pipeline: NotifPipeline) {
         pipeline.addFinalizeFilter(unseenNotifFilter)
         pipeline.addCollectionListener(collectionListener)
-        scope.launch { trackUnseenNotificationsWhileUnlocked() }
-        scope.launch { invalidateWhenUnseenSettingChanges() }
+        scope.launch { trackUnseenFilterSettingChanges() }
         dumpManager.registerDumpable(this)
     }
 
-    private suspend fun trackUnseenNotificationsWhileUnlocked() {
-        // Whether or not we're actively tracking unseen notifications to mark them as seen when
-        // appropriate.
-        val isTrackingUnseen: Flow<Boolean> =
-            keyguardRepository.isKeyguardShowing
-                // transformLatest so that we can cancel listening to keyguard transitions once
-                // isKeyguardShowing changes (after a successful transition to the keyguard).
-                .transformLatest { isShowing ->
-                    if (isShowing) {
-                        // If the keyguard is showing, we're not tracking unseen.
-                        emit(false)
-                    } else {
-                        // If the keyguard stops showing, then start tracking unseen notifications.
-                        emit(true)
-                        // If the screen is turning off, stop tracking, but if that transition is
-                        // cancelled, then start again.
-                        emitAll(
-                            keyguardTransitionRepository.transitions.map { step ->
-                                !step.isScreenTurningOff
-                            }
-                        )
-                    }
-                }
-                // Prevent double emit of `false` caused by transition to AOD, followed by keyguard
-                // showing
+    private suspend fun trackSeenNotifications() {
+        // Whether or not keyguard is visible (or occluded).
+        val isKeyguardPresent: Flow<Boolean> =
+            keyguardTransitionRepository.transitions
+                .map { step -> step.to != KeyguardState.GONE }
                 .distinctUntilChanged()
                 .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) }
 
-        // Use collectLatest so that trackUnseenNotifications() is cancelled when the keyguard is
-        // showing again
-        var clearUnseenOnBeginTracking = false
-        isTrackingUnseen.collectLatest { trackingUnseen ->
-            if (!trackingUnseen) {
-                // Wait for the user to spend enough time on the lock screen before clearing unseen
-                // set when unlocked
-                awaitTimeSpentNotDozing(SEEN_TIMEOUT)
-                clearUnseenOnBeginTracking = true
-                logger.logSeenOnLockscreen()
+        // Separately track seen notifications while the device is locked, applying once the device
+        // is unlocked.
+        val notificationsSeenWhileLocked = mutableSetOf<NotificationEntry>()
+
+        // Use [collectLatest] to cancel any running jobs when [trackingUnseen] changes.
+        isKeyguardPresent.collectLatest { isKeyguardPresent: Boolean ->
+            if (isKeyguardPresent) {
+                // Keyguard is not gone, notifications need to be visible for a certain threshold
+                // before being marked as seen
+                trackSeenNotificationsWhileLocked(notificationsSeenWhileLocked)
             } else {
-                if (clearUnseenOnBeginTracking) {
-                    clearUnseenOnBeginTracking = false
-                    logger.logAllMarkedSeenOnUnlock()
-                    unseenNotifications.clear()
+                // Mark all seen-while-locked notifications as seen for real.
+                if (notificationsSeenWhileLocked.isNotEmpty()) {
+                    unseenNotifications.removeAll(notificationsSeenWhileLocked)
+                    logger.logAllMarkedSeenOnUnlock(
+                        seenCount = notificationsSeenWhileLocked.size,
+                        remainingUnseenCount = unseenNotifications.size
+                    )
+                    notificationsSeenWhileLocked.clear()
                 }
                 unseenNotifFilter.invalidateList("keyguard no longer showing")
-                trackUnseenNotifications()
+                // Keyguard is gone, notifications can be immediately marked as seen when they
+                // become visible.
+                trackSeenNotificationsWhileUnlocked()
             }
         }
     }
 
-    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)
+    /**
+     * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually
+     * been "seen" while the device is on the keyguard.
+     */
+    private suspend fun trackSeenNotificationsWhileLocked(
+        notificationsSeenWhileLocked: MutableSet<NotificationEntry>,
+    ) = coroutineScope {
+        // Remove removed notifications from the set
+        launch {
+            unseenEntryRemoved.collect { entry ->
+                if (notificationsSeenWhileLocked.remove(entry)) {
+                    logger.logRemoveSeenOnLockscreen(entry)
                 }
             }
-            // Suspend until the first emission
-            .first()
+        }
+        // Use collectLatest so that the timeout delay is cancelled if the device enters doze, and
+        // is restarted when doze ends.
+        keyguardRepository.isDozing.collectLatest { isDozing ->
+            if (!isDozing) {
+                trackSeenNotificationsWhileLockedAndNotDozing(notificationsSeenWhileLocked)
+            }
+        }
     }
 
-    // Track "unseen" notifications, marking them as seen when either shade is expanded or the
+    /**
+     * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually
+     * been "seen" while the device is on the keyguard and not dozing. Any new and existing unseen
+     * notifications are not marked as seen until they are visible for the [SEEN_TIMEOUT] duration.
+     */
+    private suspend fun trackSeenNotificationsWhileLockedAndNotDozing(
+        notificationsSeenWhileLocked: MutableSet<NotificationEntry>
+    ) = coroutineScope {
+        // All child tracking jobs will be cancelled automatically when this is cancelled.
+        val trackingJobsByEntry = mutableMapOf<NotificationEntry, Job>()
+
+        /**
+         * Wait for the user to spend enough time on the lock screen before removing notification
+         * from unseen set upon unlock.
+         */
+        suspend fun trackSeenDurationThreshold(entry: NotificationEntry) {
+            if (notificationsSeenWhileLocked.remove(entry)) {
+                logger.logResetSeenOnLockscreen(entry)
+            }
+            delay(SEEN_TIMEOUT)
+            notificationsSeenWhileLocked.add(entry)
+            trackingJobsByEntry.remove(entry)
+            logger.logSeenOnLockscreen(entry)
+        }
+
+        /** Stop any unseen tracking when a notification is removed. */
+        suspend fun stopTrackingRemovedNotifs(): Nothing =
+            unseenEntryRemoved.collect { entry ->
+                trackingJobsByEntry.remove(entry)?.let {
+                    it.cancel()
+                    logger.logStopTrackingLockscreenSeenDuration(entry)
+                }
+            }
+
+        /** Start tracking new notifications when they are posted. */
+        suspend fun trackNewUnseenNotifs(): Nothing = coroutineScope {
+            unseenEntryAdded.collect { entry ->
+                logger.logTrackingLockscreenSeenDuration(entry)
+                // If this is an update, reset the tracking.
+                trackingJobsByEntry[entry]?.let {
+                    it.cancel()
+                    logger.logResetSeenOnLockscreen(entry)
+                }
+                trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) }
+            }
+        }
+
+        // Start tracking for all notifications that are currently unseen.
+        logger.logTrackingLockscreenSeenDuration(unseenNotifications)
+        unseenNotifications.forEach { entry ->
+            trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) }
+        }
+
+        launch { trackNewUnseenNotifs() }
+        launch { stopTrackingRemovedNotifs() }
+    }
+
+    // Track "seen" notifications, marking them as such when either shade is expanded or the
     // notification becomes heads up.
-    private suspend fun trackUnseenNotifications() {
+    private suspend fun trackSeenNotificationsWhileUnlocked() {
         coroutineScope {
             launch { clearUnseenNotificationsWhenShadeIsExpanded() }
             launch { markHeadsUpNotificationsAsSeen() }
@@ -212,7 +260,7 @@
         }
     }
 
-    private suspend fun invalidateWhenUnseenSettingChanges() {
+    private suspend fun trackUnseenFilterSettingChanges() {
         secureSettings
             // emit whenever the setting has changed
             .observerFlow(
@@ -228,17 +276,23 @@
                     UserHandle.USER_CURRENT,
                 ) == 1
             }
+            // don't emit anything if nothing has changed
+            .distinctUntilChanged()
             // perform lookups on the bg thread pool
             .flowOn(bgDispatcher)
             // only track the most recent emission, if events are happening faster than they can be
             // consumed
             .conflate()
-            // update local field and invalidate if necessary
-            .collect { setting ->
+            .collectLatest { setting ->
+                // update local field and invalidate if necessary
                 if (setting != unseenFilterEnabled) {
                     unseenFilterEnabled = setting
                     unseenNotifFilter.invalidateList("unseen setting changed")
                 }
+                // if the setting is enabled, then start tracking and filtering unseen notifications
+                if (setting) {
+                    trackSeenNotifications()
+                }
             }
     }
 
@@ -250,6 +304,7 @@
                 ) {
                     logger.logUnseenAdded(entry.key)
                     unseenNotifications.add(entry)
+                    unseenEntryAdded.tryEmit(entry)
                 }
             }
 
@@ -259,12 +314,14 @@
                 ) {
                     logger.logUnseenUpdated(entry.key)
                     unseenNotifications.add(entry)
+                    unseenEntryAdded.tryEmit(entry)
                 }
             }
 
             override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
                 if (unseenNotifications.remove(entry)) {
                     logger.logUnseenRemoved(entry.key)
+                    unseenEntryRemoved.tryEmit(entry)
                 }
             }
         }
@@ -347,6 +404,3 @@
         private val SEEN_TIMEOUT = 5.seconds
     }
 }
-
-private val TransitionStep.isScreenTurningOff: Boolean
-    get() = transitionState == TransitionState.STARTED && to != KeyguardState.GONE
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorLogger.kt
index 1f8ec34..788659e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorLogger.kt
@@ -17,8 +17,9 @@
 package com.android.systemui.statusbar.notification.collection.coordinator
 
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.dagger.UnseenNotificationLog
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import javax.inject.Inject
 
 private const val TAG = "KeyguardCoordinator"
@@ -28,11 +29,14 @@
 constructor(
     @UnseenNotificationLog private val buffer: LogBuffer,
 ) {
-    fun logSeenOnLockscreen() =
+    fun logSeenOnLockscreen(entry: NotificationEntry) =
         buffer.log(
             TAG,
             LogLevel.DEBUG,
-            "Notifications on lockscreen will be marked as seen when unlocked."
+            messageInitializer = { str1 = entry.key },
+            messagePrinter = {
+                "Notification [$str1] on lockscreen will be marked as seen when unlocked."
+            },
         )
 
     fun logTrackingUnseen(trackingUnseen: Boolean) =
@@ -43,11 +47,21 @@
             messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." },
         )
 
-    fun logAllMarkedSeenOnUnlock() =
+    fun logAllMarkedSeenOnUnlock(
+        seenCount: Int,
+        remainingUnseenCount: Int,
+    ) =
         buffer.log(
             TAG,
             LogLevel.DEBUG,
-            "Notifications have been marked as seen now that device is unlocked."
+            messageInitializer = {
+                int1 = seenCount
+                int2 = remainingUnseenCount
+            },
+            messagePrinter = {
+                "$int1 Notifications have been marked as seen now that device is unlocked. " +
+                    "$int2 notifications remain unseen."
+            },
         )
 
     fun logShadeExpanded() =
@@ -96,4 +110,60 @@
             messageInitializer = { str1 = key },
             messagePrinter = { "Unseen notif has become heads up: $str1" },
         )
+
+    fun logTrackingLockscreenSeenDuration(unseenNotifications: Set<NotificationEntry>) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = {
+                str1 = unseenNotifications.joinToString { it.key }
+                int1 = unseenNotifications.size
+            },
+            messagePrinter = {
+                "Tracking $int1 unseen notifications for lockscreen seen duration threshold: $str1"
+            },
+        )
+    }
+
+    fun logTrackingLockscreenSeenDuration(entry: NotificationEntry) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { str1 = entry.key },
+            messagePrinter = {
+                "Tracking new notification for lockscreen seen duration threshold: $str1"
+            },
+        )
+    }
+
+    fun logStopTrackingLockscreenSeenDuration(entry: NotificationEntry) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { str1 = entry.key },
+            messagePrinter = {
+                "Stop tracking removed notification for lockscreen seen duration threshold: $str1"
+            },
+        )
+    }
+
+    fun logResetSeenOnLockscreen(entry: NotificationEntry) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { str1 = entry.key },
+            messagePrinter = {
+                "Reset tracking updated notification for lockscreen seen duration threshold: $str1"
+            },
+        )
+    }
+
+    fun logRemoveSeenOnLockscreen(entry: NotificationEntry) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { str1 = entry.key },
+            messagePrinter = { "Notification marked as seen on lockscreen removed: $str1" },
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
index 6271d38..bf65043 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.log.dagger.NotificationLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
index 1f4861a..0d9681f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.log.dagger.NotificationLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import javax.inject.Inject
 
 private const val TAG = "ShadeEventCoordinator"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index bdb206b..1896080 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -71,7 +71,6 @@
 
     private NotificationPresenter mPresenter;
     private NotificationListContainer mListContainer;
-    private BindRowCallback mBindRowCallback;
     private NotificationClicker mNotificationClicker;
     private FeatureFlags mFeatureFlags;
 
@@ -103,11 +102,9 @@
      * Sets up late-bound dependencies for this component.
      */
     public void setUpWithPresenter(NotificationPresenter presenter,
-            NotificationListContainer listContainer,
-            BindRowCallback bindRowCallback) {
+            NotificationListContainer listContainer) {
         mPresenter = presenter;
         mListContainer = listContainer;
-        mBindRowCallback = bindRowCallback;
 
         mIconManager.attach();
     }
@@ -143,7 +140,6 @@
                                         .expandableNotificationRow(row)
                                         .notificationEntry(entry)
                                         .onExpandClickListener(mPresenter)
-                                        .listContainer(mListContainer)
                                         .build();
                         ExpandableNotificationRowController rowController =
                                 component.getExpandableNotificationRowController();
@@ -179,7 +175,7 @@
         mNotificationRemoteInputManager.bindRow(row);
         entry.setRow(row);
         mNotifBindPipeline.manageRow(entry, row);
-        mBindRowCallback.onBindRow(row);
+        mPresenter.onBindRow(row);
         row.setInlineReplyAnimationFlagEnabled(
                 mFeatureFlags.isEnabled(NOTIFICATION_INLINE_REPLY_ANIMATION));
     }
@@ -235,12 +231,4 @@
             }
         });
     }
-
-    /** Callback for when a row is bound to an entry. */
-    public interface BindRowCallback {
-        /**
-         * Called when a new row is created and bound to a notification.
-         */
-        void onBindRow(ExpandableNotificationRow row);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
index f13ff68..a8409d0c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
@@ -18,9 +18,9 @@
 
 import com.android.systemui.log.dagger.NotificationLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.WARNING
 import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
index 20de785..014ac54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
@@ -22,11 +22,11 @@
 import android.service.notification.StatusBarNotification
 import com.android.systemui.log.dagger.NotificationLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
-import com.android.systemui.log.LogLevel.WTF
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.ERROR
+import com.android.systemui.log.core.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.WARNING
+import com.android.systemui.log.core.LogLevel.WTF
 import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason
 import com.android.systemui.statusbar.notification.collection.NotifCollection.FutureDismissal
@@ -108,27 +108,39 @@
         })
     }
 
-    fun logNotifDismissed(entry: NotificationEntry) {
+    fun logLocallyDismissed(entry: NotificationEntry, index: Int, count: Int) {
         buffer.log(TAG, INFO, {
             str1 = entry.logKey
+            int1 = index
+            int2 = count
         }, {
-            "DISMISSED $str1"
+            "LOCALLY DISMISSED $str1 ($int1/$int2)"
         })
     }
 
-    fun logNonExistentNotifDismissed(entry: NotificationEntry) {
+    fun logDismissNonExistentNotif(entry: NotificationEntry, index: Int, count: Int) {
         buffer.log(TAG, INFO, {
             str1 = entry.logKey
+            int1 = index
+            int2 = count
         }, {
-            "DISMISSED Non Existent $str1"
+            "DISMISS Non Existent $str1 ($int1/$int2)"
         })
     }
 
-    fun logChildDismissed(entry: NotificationEntry) {
+    fun logLocallyDismissedChild(
+            child: NotificationEntry,
+            parent: NotificationEntry,
+            parentIndex: Int,
+            parentCount: Int
+    ) {
         buffer.log(TAG, DEBUG, {
-            str1 = entry.logKey
+            str1 = child.logKey
+            str2 = parent.logKey
+            int1 = parentIndex
+            int2 = parentCount
         }, {
-            "CHILD DISMISSED (inferred): $str1"
+            "LOCALLY DISMISSED CHILD (inferred): $str1 of parent $str2 ($int1/$int2)"
         })
     }
 
@@ -140,27 +152,31 @@
         })
     }
 
-    fun logDismissOnAlreadyCanceledEntry(entry: NotificationEntry) {
+    fun logLocallyDismissedAlreadyCanceledEntry(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
             str1 = entry.logKey
         }, {
-            "Dismiss on $str1, which was already canceled. Trying to remove..."
+            "LOCALLY DISMISSED Already Canceled $str1. Trying to remove."
         })
     }
 
-    fun logNotifDismissedIntercepted(entry: NotificationEntry) {
+    fun logNotifDismissedIntercepted(entry: NotificationEntry, index: Int, count: Int) {
         buffer.log(TAG, INFO, {
             str1 = entry.logKey
+            int1 = index
+            int2 = count
         }, {
-            "DISMISS INTERCEPTED $str1"
+            "DISMISS INTERCEPTED $str1 ($int1/$int2)"
         })
     }
 
-    fun logNotifClearAllDismissalIntercepted(entry: NotificationEntry) {
+    fun logNotifClearAllDismissalIntercepted(entry: NotificationEntry, index: Int, count: Int) {
         buffer.log(TAG, INFO, {
             str1 = entry.logKey
+            int1 = index
+            int2 = count
         }, {
-            "CLEAR ALL DISMISSAL INTERCEPTED $str1"
+            "CLEAR ALL DISMISSAL INTERCEPTED $str1 ($int1/$int2)"
         })
     }
 
@@ -251,12 +267,19 @@
         })
     }
 
-    fun logRemoteExceptionOnNotificationClear(entry: NotificationEntry, e: RemoteException) {
+    fun logRemoteExceptionOnNotificationClear(
+            entry: NotificationEntry,
+            index: Int,
+            count: Int,
+            e: RemoteException
+    ) {
         buffer.log(TAG, WTF, {
             str1 = entry.logKey
+            int1 = index
+            int2 = count
             str2 = e.toString()
         }, {
-            "RemoteException while attempting to clear $str1:\n$str2"
+            "RemoteException while attempting to clear $str1 ($int1/$int2):\n$str2"
         })
     }
 
@@ -387,6 +410,126 @@
             "Mismatch: current $str2 is $str3 for: $str1"
         })
     }
+
+    fun logDismissAlreadyDismissedNotif(entry: NotificationEntry, index: Int, count: Int) {
+        buffer.log(TAG, DEBUG, {
+            str1 = entry.logKey
+            int1 = index
+            int2 = count
+        }, {
+            "DISMISS Already Dismissed $str1 ($int1/$int2)"
+        })
+    }
+
+    fun logDismissAlreadyParentDismissedNotif(
+            childEntry: NotificationEntry,
+            childIndex: Int,
+            childCount: Int
+    ) {
+        buffer.log(TAG, DEBUG, {
+            str1 = childEntry.logKey
+            int1 = childIndex
+            int2 = childCount
+            str2 = childEntry.parent?.summary?.logKey ?: "(null)"
+        }, {
+            "DISMISS Already Parent-Dismissed $str1 ($int1/$int2) with summary $str2"
+        })
+    }
+
+    fun logLocallyDismissNonExistentNotif(entry: NotificationEntry, index: Int, count: Int) {
+        buffer.log(TAG, INFO, {
+            str1 = entry.logKey
+            int1 = index
+            int2 = count
+        }, {
+            "LOCALLY DISMISS Non Existent $str1 ($int1/$int2)"
+        })
+    }
+
+    fun logLocallyDismissMismatchedEntry(
+            entry: NotificationEntry,
+            index: Int,
+            count: Int,
+            storedEntry: NotificationEntry
+    ) {
+        buffer.log(TAG, INFO, {
+            str1 = entry.logKey
+            int1 = index
+            int2 = count
+            str2 = Integer.toHexString(entry.hashCode())
+            str3 = Integer.toHexString(storedEntry.hashCode())
+        }, {
+            "LOCALLY DISMISS Mismatch $str1 ($int1/$int2): dismissing @$str2 but stored @$str3"
+        })
+    }
+
+    fun logLocallyDismissAlreadyDismissedNotif(
+            entry: NotificationEntry,
+            index: Int,
+            count: Int
+    ) {
+        buffer.log(TAG, INFO, {
+            str1 = entry.logKey
+            int1 = index
+            int2 = count
+        }, {
+            "LOCALLY DISMISS Already Dismissed $str1 ($int1/$int2)"
+        })
+    }
+
+    fun logLocallyDismissAlreadyParentDismissedNotif(
+            entry: NotificationEntry,
+            index: Int,
+            count: Int
+    ) {
+        buffer.log(TAG, INFO, {
+            str1 = entry.logKey
+            int1 = index
+            int2 = count
+        }, {
+            "LOCALLY DISMISS Already Dismissed $str1 ($int1/$int2)"
+        })
+    }
+
+    fun logLocallyDismissAlreadyDismissedChild(
+            childEntry: NotificationEntry,
+            parentEntry: NotificationEntry,
+            parentIndex: Int,
+            parentCount: Int
+    ) {
+        buffer.log(TAG, INFO, {
+            str1 = childEntry.logKey
+            str2 = parentEntry.logKey
+            int1 = parentIndex
+            int2 = parentCount
+        }, {
+            "LOCALLY DISMISS Already Dismissed Child $str1 of parent $str2 ($int1/$int2)"
+        })
+    }
+
+    fun logLocallyDismissAlreadyParentDismissedChild(
+            childEntry: NotificationEntry,
+            parentEntry: NotificationEntry,
+            parentIndex: Int,
+            parentCount: Int
+    ) {
+        buffer.log(TAG, INFO, {
+            str1 = childEntry.logKey
+            str2 = parentEntry.logKey
+            int1 = parentIndex
+            int2 = parentCount
+        }, {
+            "LOCALLY DISMISS Already Parent-Dismissed Child $str1 of parent $str2 ($int1/$int2)"
+        })
+    }
+
+    fun logCancelLocalDismissalNotDismissedNotif(entry: NotificationEntry) {
+        buffer.log(TAG, INFO, {
+            str1 = entry.logKey
+        }, {
+            "CANCEL LOCAL DISMISS Not Dismissed $str1"
+        })
+    }
 }
 
 private const val TAG = "NotifCollection"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
index 07fd349..e61f9bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.log.dagger.NotificationLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
 import com.android.systemui.util.Compile
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
index a880b71..082f308 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.log.dagger.NotificationLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import java.lang.RuntimeException
 import javax.inject.Inject
 
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 b100d44..31d4ab9 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
@@ -63,8 +63,12 @@
 import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerImpl;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule;
+import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
 import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm;
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModelModule;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 
 import dagger.Binds;
@@ -85,6 +89,8 @@
         ShadeEventsModule.class,
         NotifPipelineChoreographerModule.class,
         NotificationSectionHeadersModule.class,
+        NotificationListViewModelModule.class,
+        ActivatableNotificationViewModelModule.class,
 })
 public interface NotificationsModule {
     @Binds
@@ -159,6 +165,14 @@
         }
     }
 
+    /** Provides the container for the notification list. */
+    @Provides
+    @SysUISingleton
+    static NotificationListContainer provideListContainer(
+            NotificationStackScrollLayoutController nsslController) {
+        return nsslController.getNotificationListContainer();
+    }
+
     /**
      * Provide the active notification collection managing the notifications to render.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsInteractor.kt
new file mode 100644
index 0000000..8f7e269
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsInteractor.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.statusbar.notification.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import javax.inject.Inject
+
+/** Interactor for notifications in general. */
+@SysUISingleton
+class NotificationsInteractor
+@Inject
+constructor(
+    private val disableFlagsRepository: DisableFlagsRepository,
+) {
+    /** Returns true if notification alerts are allowed. */
+    fun areNotificationAlertsEnabled(): Boolean {
+        return disableFlagsRepository.disableFlags.value.areNotificationAlertsEnabled()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt
index a5278c3d..76e228b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt
@@ -20,10 +20,8 @@
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption
 import com.android.systemui.statusbar.NotificationPresenter
 import com.android.systemui.statusbar.notification.NotificationActivityStarter
-import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl
 import com.android.systemui.statusbar.notification.collection.render.NotifStackController
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
-import com.android.systemui.statusbar.phone.CentralSurfaces
 
 /**
  * The master controller for all notifications-related work
@@ -33,12 +31,10 @@
  */
 interface NotificationsController {
     fun initialize(
-        centralSurfaces: CentralSurfaces,
         presenter: NotificationPresenter,
         listContainer: NotificationListContainer,
         stackController: NotifStackController,
         notificationActivityStarter: NotificationActivityStarter,
-        bindRowCallback: NotificationRowBinderImpl.BindRowCallback
     )
 
     fun resetUserExpandedStates()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index 0ed4175..f7bd177 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -42,7 +42,6 @@
 import com.android.systemui.statusbar.notification.logging.NotificationMemoryMonitor
 import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
-import com.android.systemui.statusbar.phone.CentralSurfaces
 import com.android.wm.shell.bubbles.Bubbles
 import dagger.Lazy
 import java.util.Optional
@@ -78,12 +77,10 @@
 ) : NotificationsController {
 
     override fun initialize(
-        centralSurfaces: CentralSurfaces,
         presenter: NotificationPresenter,
         listContainer: NotificationListContainer,
         stackController: NotifStackController,
         notificationActivityStarter: NotificationActivityStarter,
-        bindRowCallback: NotificationRowBinderImpl.BindRowCallback
     ) {
         notificationListener.registerAsSystemService()
 
@@ -94,13 +91,8 @@
         })
 
         notificationRowBinder.setNotificationClicker(
-                clickerBuilder.build(
-                    Optional.ofNullable(centralSurfaces), bubblesOptional,
-                        notificationActivityStarter))
-        notificationRowBinder.setUpWithPresenter(
-                presenter,
-                listContainer,
-                bindRowCallback)
+            clickerBuilder.build(bubblesOptional, notificationActivityStarter))
+        notificationRowBinder.setUpWithPresenter(presenter, listContainer)
         headsUpViewBinder.setPresenter(presenter)
         notifBindPipelineInitializer.initialize()
         animatedImageNotificationManager.bind()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt
index 14856da..65ba6de 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt
@@ -21,10 +21,8 @@
 import com.android.systemui.statusbar.NotificationListener
 import com.android.systemui.statusbar.NotificationPresenter
 import com.android.systemui.statusbar.notification.NotificationActivityStarter
-import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl
 import com.android.systemui.statusbar.notification.collection.render.NotifStackController
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
-import com.android.systemui.statusbar.phone.CentralSurfaces
 import javax.inject.Inject
 
 /**
@@ -35,12 +33,10 @@
 ) : NotificationsController {
 
     override fun initialize(
-        centralSurfaces: CentralSurfaces,
         presenter: NotificationPresenter,
         listContainer: NotificationListContainer,
         stackController: NotifStackController,
         notificationActivityStarter: NotificationActivityStarter,
-        bindRowCallback: NotificationRowBinderImpl.BindRowCallback
     ) {
         // Always connect the listener even if notification-handling is disabled. Being a listener
         // grants special permissions and it's not clear if other things will break if we lose those
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
index 0b31265..c6d2861 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
@@ -2,7 +2,7 @@
 
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
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 5bac2a9..4a823a4 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
@@ -20,9 +20,9 @@
 
 import com.android.systemui.log.dagger.NotificationInterruptLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.WARNING
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import com.android.systemui.util.Compile
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
index fe03b2a..0e1f66f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.notification.logging
 
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationRenderLog
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
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 417d4ac..27510d4 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
@@ -321,8 +321,7 @@
     protected void setBackgroundTintColor(int color) {
         if (color != mCurrentBackgroundTint) {
             mCurrentBackgroundTint = color;
-            // TODO(282173943): re-enable this tinting optimization when Resources are thread-safe
-            if (false && color == mNormalColor) {
+            if (color == mNormalColor) {
                 // We don't need to tint a normal notification
                 color = 0;
             }
@@ -360,9 +359,9 @@
     }
 
     @Override
-    public long performRemoveAnimation(long duration, long delay,
-            float translationDirection, boolean isHeadsUpAnimation, float endLocation,
-            Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener) {
+    public long performRemoveAnimation(long duration, long delay, float translationDirection,
+            boolean isHeadsUpAnimation, Runnable onFinishedRunnable,
+            AnimatorListenerAdapter animationListener) {
         enableAppearDrawing(true);
         mIsHeadsUpAnimation = isHeadsUpAnimation;
         if (mDrawingAppearAnimation) {
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 9f397fe..b34c281 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
@@ -53,6 +53,7 @@
 import android.view.NotificationHeaderView;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewParent;
 import android.view.ViewStub;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -103,6 +104,8 @@
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
+import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.SwipeableView;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -170,6 +173,7 @@
     private PeopleNotificationIdentifier mPeopleNotificationIdentifier;
     private Optional<BubblesManager> mBubblesManagerOptional;
     private MetricsLogger mMetricsLogger;
+    private NotificationChildrenContainerLogger mChildrenContainerLogger;
     private NotificationDismissibilityProvider mDismissibilityProvider;
     private FeatureFlags mFeatureFlags;
     private int mIconTransformContentShift;
@@ -1627,6 +1631,51 @@
                 NotificationEntry child,
                 NotificationEntry newParent
         );
+
+        /**
+         * Called when an ExpandableNotificationRow transient view is removed from the
+         * NotificationChildrenContainer
+         */
+        void logRemoveTransientFromContainer(
+                NotificationEntry childEntry,
+                NotificationEntry containerEntry
+        );
+
+        /**
+         * Called when an ExpandableNotificationRow transient view is removed from the
+         * NotificationStackScrollLayout
+         */
+        void logRemoveTransientFromNssl(
+                NotificationEntry childEntry
+        );
+
+        /**
+         * Called when an ExpandableNotificationRow transient view is removed from a ViewGroup that
+         * is not NotificationChildrenContainer or NotificationStackScrollLayout
+         */
+        void logRemoveTransientFromViewGroup(
+                NotificationEntry childEntry,
+                ViewGroup containerView
+        );
+
+        /**
+         * Called when an ExpandableNotificationRow transient view is added to this
+         * ExpandableNotificationRow
+         */
+        void logAddTransientRow(
+                NotificationEntry childEntry,
+                NotificationEntry containerEntry,
+                int index
+        );
+
+        /**
+         * Called when an ExpandableNotificationRow transient view is removed from this
+         * ExpandableNotificationRow
+         */
+        void logRemoveTransientRow(
+                NotificationEntry childEntry,
+                NotificationEntry containerEntry
+        );
     }
 
     /**
@@ -1668,6 +1717,7 @@
             NotificationGutsManager gutsManager,
             NotificationDismissibilityProvider dismissibilityProvider,
             MetricsLogger metricsLogger,
+            NotificationChildrenContainerLogger childrenContainerLogger,
             SmartReplyConstants smartReplyConstants,
             SmartReplyController smartReplyController,
             FeatureFlags featureFlags,
@@ -1706,6 +1756,7 @@
         mBubblesManagerOptional = bubblesManagerOptional;
         mNotificationGutsManager = gutsManager;
         mMetricsLogger = metricsLogger;
+        mChildrenContainerLogger = childrenContainerLogger;
         mDismissibilityProvider = dismissibilityProvider;
         mFeatureFlags = featureFlags;
     }
@@ -1874,6 +1925,7 @@
             mChildrenContainer.setIsLowPriority(mIsLowPriority);
             mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this);
             mChildrenContainer.onNotificationUpdated();
+            mChildrenContainer.setLogger(mChildrenContainerLogger);
 
             mTranslateableViews.add(mChildrenContainer);
         });
@@ -2673,7 +2725,12 @@
     }
 
     public boolean isExpanded(boolean allowOnKeyguard) {
-        return (!mOnKeyguard || allowOnKeyguard)
+        if (DEBUG) {
+            if (!mShowingPublicInitialized && !allowOnKeyguard) {
+                Log.d(TAG, "mShowingPublic is not initialized.");
+            }
+        }
+        return !mShowingPublic && (!mOnKeyguard || allowOnKeyguard)
                 && (!hasUserChangedExpansion() && (isSystemExpanded() || isSystemChildExpanded())
                 || isUserExpanded());
     }
@@ -2918,7 +2975,6 @@
             long delay,
             float translationDirection,
             boolean isHeadsUpAnimation,
-            float endLocation,
             Runnable onFinishedRunnable,
             AnimatorListenerAdapter animationListener) {
         if (mMenuRow != null && mMenuRow.isMenuVisible()) {
@@ -2929,7 +2985,7 @@
                     public void onAnimationEnd(Animator animation) {
                         ExpandableNotificationRow.super.performRemoveAnimation(
                                 duration, delay, translationDirection, isHeadsUpAnimation,
-                                endLocation, onFinishedRunnable, animationListener);
+                                onFinishedRunnable, animationListener);
                     }
                 });
                 anim.start();
@@ -2937,7 +2993,7 @@
             }
         }
         return super.performRemoveAnimation(duration, delay, translationDirection,
-                isHeadsUpAnimation, endLocation, onFinishedRunnable, animationListener);
+                isHeadsUpAnimation, onFinishedRunnable, animationListener);
     }
 
     @Override
@@ -3620,6 +3676,8 @@
             pw.print(", mOnUserInteractionCallback null: " + (mOnUserInteractionCallback == null));
             pw.print(", removed: " + isRemoved());
             pw.print(", expandAnimationRunning: " + mExpandAnimationRunning);
+            pw.print(", mShowingPublic: " + mShowingPublic);
+            pw.print(", mShowingPublicInitialized: " + mShowingPublicInitialized);
             NotificationContentView showingLayout = getShowingLayout();
             pw.print(", privateShowing: " + (showingLayout == mPrivateLayout));
             pw.println();
@@ -3707,4 +3765,73 @@
     public boolean getShowSnooze() {
         return mShowSnooze;
     }
+
+    @Override
+    public void removeFromTransientContainer() {
+        final ViewGroup transientContainer = getTransientContainer();
+        final ViewParent parent = getParent();
+        // Only log when there is real removal of transient views
+        if (transientContainer == null || transientContainer != parent) {
+            super.removeFromTransientContainer();
+            return;
+        }
+        logRemoveFromTransientContainer(transientContainer);
+        super.removeFromTransientContainer();
+    }
+
+    /**
+     * Log calls to removeFromTransientContainer when the container is NotificationChildrenContainer
+     * or NotificationStackScrollLayout.
+     */
+    public void logRemoveFromTransientContainer(ViewGroup transientContainer) {
+        if (mLogger == null) {
+            return;
+        }
+        if (transientContainer instanceof NotificationChildrenContainer) {
+            mLogger.logRemoveTransientFromContainer(
+                    /* childEntry = */ getEntry(),
+                    /* containerEntry = */ ((NotificationChildrenContainer) transientContainer)
+                            .getContainingNotification().getEntry()
+            );
+        } else if (transientContainer instanceof NotificationStackScrollLayout) {
+            mLogger.logRemoveTransientFromNssl(
+                    /* childEntry = */ getEntry()
+            );
+        } else {
+            mLogger.logRemoveTransientFromViewGroup(
+                    /* childEntry = */ getEntry(),
+                    /* containerView = */ transientContainer
+            );
+        }
+    }
+
+    @Override
+    public void addTransientView(View view, int index) {
+        if (view instanceof ExpandableNotificationRow) {
+            logAddTransientRow((ExpandableNotificationRow) view, index);
+        }
+        super.addTransientView(view, index);
+    }
+
+    private void logAddTransientRow(ExpandableNotificationRow row, int index) {
+        if (mLogger == null) {
+            return;
+        }
+        mLogger.logAddTransientRow(row.getEntry(), getEntry(), index);
+    }
+
+    @Override
+    public void removeTransientView(View view) {
+        if (view instanceof ExpandableNotificationRow) {
+            logRemoveTransientRow((ExpandableNotificationRow) view);
+        }
+        super.removeTransientView(view);
+    }
+
+    private void logRemoveTransientRow(ExpandableNotificationRow row) {
+        if (mLogger == null) {
+            return;
+        }
+        mLogger.logRemoveTransientRow(row.getEntry(), getEntry());
+    }
 }
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 1acc9f9..a4e8c2e 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
@@ -50,6 +50,7 @@
 import com.android.systemui.statusbar.notification.row.dagger.AppName;
 import com.android.systemui.statusbar.notification.row.dagger.NotificationKey;
 import com.android.systemui.statusbar.notification.row.dagger.NotificationRowScope;
+import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -88,6 +89,7 @@
     private final ExpandableNotificationRow.OnExpandClickListener mOnExpandClickListener;
     private final StatusBarStateController mStatusBarStateController;
     private final MetricsLogger mMetricsLogger;
+    private final NotificationChildrenContainerLogger mChildrenContainerLogger;
     private final ExpandableNotificationRow.CoordinateOnClickListener mOnFeedbackClickListener;
     private final NotificationGutsManager mNotificationGutsManager;
     private final OnUserInteractionCallback mOnUserInteractionCallback;
@@ -125,6 +127,46 @@
                 ) {
                     mLogBufferLogger.logSkipAttachingKeepInParentChild(child, newParent);
                 }
+
+                @Override
+                public void logRemoveTransientFromContainer(
+                        NotificationEntry childEntry,
+                        NotificationEntry containerEntry
+                ) {
+                    mLogBufferLogger.logRemoveTransientFromContainer(childEntry, containerEntry);
+                }
+
+                @Override
+                public void logRemoveTransientFromNssl(
+                        NotificationEntry childEntry
+                ) {
+                    mLogBufferLogger.logRemoveTransientFromNssl(childEntry);
+                }
+
+                @Override
+                public void logRemoveTransientFromViewGroup(
+                        NotificationEntry childEntry,
+                        ViewGroup containerView
+                ) {
+                    mLogBufferLogger.logRemoveTransientFromViewGroup(childEntry, containerView);
+                }
+
+                @Override
+                public void logAddTransientRow(
+                        NotificationEntry childEntry,
+                        NotificationEntry containerEntry,
+                        int index
+                ) {
+                    mLogBufferLogger.logAddTransientRow(childEntry, containerEntry, index);
+                }
+
+                @Override
+                public void logRemoveTransientRow(
+                        NotificationEntry childEntry,
+                        NotificationEntry containerEntry
+                ) {
+                    mLogBufferLogger.logRemoveTransientRow(childEntry, containerEntry);
+                }
             };
 
 
@@ -135,6 +177,7 @@
             RemoteInputViewSubcomponent.Factory rivSubcomponentFactory,
             MetricsLogger metricsLogger,
             NotificationRowLogger logBufferLogger,
+            NotificationChildrenContainerLogger childrenContainerLogger,
             NotificationListContainer listContainer,
             SmartReplyConstants smartReplyConstants,
             SmartReplyController smartReplyController,
@@ -188,6 +231,7 @@
         mBubblesManagerOptional = bubblesManagerOptional;
         mDragController = dragController;
         mMetricsLogger = metricsLogger;
+        mChildrenContainerLogger = childrenContainerLogger;
         mLogBufferLogger = logBufferLogger;
         mSmartReplyConstants = smartReplyConstants;
         mSmartReplyController = smartReplyController;
@@ -222,6 +266,7 @@
                 mNotificationGutsManager,
                 mDismissibilityProvider,
                 mMetricsLogger,
+                mChildrenContainerLogger,
                 mSmartReplyConstants,
                 mSmartReplyController,
                 mFeatureFlags,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
index 7a2bee9..b950187 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
@@ -148,7 +148,7 @@
     private void dismissShade() {
         // Speed up dismissing the shade since the drag needs to be handled by
         // the shell layer underneath
-        mShadeController.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */,
+        mShadeController.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE, true /* force */,
                 false /* delayed */, 1.1f /* speedUpFactor */);
     }
 
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 f0e15c2..f986244 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
@@ -367,7 +367,6 @@
      *                             such that the  child appears to be going away to the top. 1
      *                             Should mean the opposite.
      * @param isHeadsUpAnimation Is this a headsUp animation.
-     * @param endLocation The location where the horizonal heads up disappear animation should end.
      * @param onFinishedRunnable A runnable which should be run when the animation is finished.
      * @param animationListener An animation listener to add to the animation.
      *
@@ -375,7 +374,7 @@
      * animation starts.
      */
     public abstract long performRemoveAnimation(long duration,
-            long delay, float translationDirection, boolean isHeadsUpAnimation, float endLocation,
+            long delay, float translationDirection, boolean isHeadsUpAnimation,
             Runnable onFinishedRunnable,
             AnimatorListenerAdapter animationListener);
 
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 0989df6..6bbeebf 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,11 @@
 
 package com.android.systemui.statusbar.notification.row;
 
-import static android.graphics.PorterDuff.Mode.SRC_ATOP;
-
 import android.annotation.ColorInt;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.graphics.ColorFilter;
-import android.graphics.PorterDuffColorFilter;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.util.IndentingPrintWriter;
@@ -161,20 +157,10 @@
      */
     public void updateColors() {
         Resources.Theme theme = mContext.getTheme();
-        final @ColorInt int textColor = getResources().getColor(R.color.notif_pill_text, theme);
-        final Drawable clearAllBg = theme.getDrawable(R.drawable.notif_footer_btn_background);
-        final Drawable manageBg = theme.getDrawable(R.drawable.notif_footer_btn_background);
-        // TODO(b/282173943): Remove redundant tinting once Resources are thread-safe
-        final @ColorInt int buttonBgColor =
-                Utils.getColorAttrDefaultColor(mContext, com.android.internal.R.attr.colorSurface);
-        final ColorFilter bgColorFilter = new PorterDuffColorFilter(buttonBgColor, SRC_ATOP);
-        if (buttonBgColor != 0) {
-            clearAllBg.setColorFilter(bgColorFilter);
-            manageBg.setColorFilter(bgColorFilter);
-        }
-        mClearAllButton.setBackground(clearAllBg);
+        int textColor = getResources().getColor(R.color.notif_pill_text, theme);
+        mClearAllButton.setBackground(theme.getDrawable(R.drawable.notif_footer_btn_background));
         mClearAllButton.setTextColor(textColor);
-        mManageButton.setBackground(manageBg);
+        mManageButton.setBackground(theme.getDrawable(R.drawable.notif_footer_btn_background));
         mManageButton.setTextColor(textColor);
         final @ColorInt int labelTextColor =
                 Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorPrimary);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
index 45be0b1..3856700 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.log.dagger.NotificationLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
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 b4bfded..13d1978 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
@@ -474,7 +474,6 @@
                             parentLayout,
                             remoteViewClickHandler);
                     validateView(v, entry, row.getResources());
-                    v.setIsRootNamespace(true);
                     applyCallback.setResultView(v);
                 } else {
                     newContentView.reapply(
@@ -511,7 +510,6 @@
                     return;
                 }
                 if (isNewView) {
-                    v.setIsRootNamespace(true);
                     applyCallback.setResultView(v);
                 } else if (existingWrapper != null) {
                     existingWrapper.onReinflated();
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 124df8c..20f4429 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
@@ -208,6 +208,23 @@
         mSmartReplyConstants = smartReplyConstants;
         mSmartReplyController = smartReplyController;
         mStatusBarService = statusBarService;
+        // We set root namespace so that we avoid searching children for id. Notification  might
+        // contain custom view and their ids may clash with ids already existing in shade or
+        // notification panel
+        setIsRootNamespace(true);
+    }
+
+    @Override
+    public View focusSearch(View focused, int direction) {
+        // This implementation is copied from ViewGroup but with removed special handling of
+        // setIsRootNamespace. This view is set as tree root using setIsRootNamespace and it
+        // causes focus to be stuck inside of it. We need to be root to avoid id conflicts
+        // but we don't want to behave like root when it comes to focusing.
+        if (mParent != null) {
+            return mParent.focusSearch(focused, direction);
+        }
+        Log.wtf(TAG, "NotificationContentView doesn't have parent");
+        return null;
     }
 
     public void reinflate() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowLogger.kt
index c3dd92a..4f5a04f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowLogger.kt
@@ -17,14 +17,21 @@
 
 package com.android.systemui.statusbar.notification.row
 
+import android.view.ViewGroup
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.log.dagger.NotificationRenderLog
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
-class NotificationRowLogger @Inject constructor(@NotificationLog private val buffer: LogBuffer) {
+class NotificationRowLogger
+@Inject
+constructor(
+    @NotificationLog private val buffer: LogBuffer,
+    @NotificationRenderLog private val notificationRenderBuffer: LogBuffer
+) {
     fun logKeepInParentChildDetached(child: NotificationEntry, oldParent: NotificationEntry?) {
         buffer.log(
             TAG,
@@ -48,6 +55,79 @@
             { "Skipping to attach $str1 to $str2, because it still flagged to keep in parent" }
         )
     }
+
+    fun logRemoveTransientFromContainer(
+        childEntry: NotificationEntry,
+        containerEntry: NotificationEntry
+    ) {
+        notificationRenderBuffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                str1 = childEntry.logKey
+                str2 = containerEntry.logKey
+            },
+            { "RemoveTransientRow from ChildrenContainer: childKey: $str1 -- containerKey: $str2" }
+        )
+    }
+
+    fun logRemoveTransientFromNssl(
+        childEntry: NotificationEntry,
+    ) {
+        notificationRenderBuffer.log(
+            TAG,
+            LogLevel.INFO,
+            { str1 = childEntry.logKey },
+            { "RemoveTransientRow from Nssl: childKey: $str1" }
+        )
+    }
+
+    fun logRemoveTransientFromViewGroup(
+        childEntry: NotificationEntry,
+        containerView: ViewGroup,
+    ) {
+        notificationRenderBuffer.log(
+            TAG,
+            LogLevel.WARNING,
+            {
+                str1 = childEntry.logKey
+                str2 = containerView.toString()
+            },
+            { "RemoveTransientRow from other ViewGroup: childKey: $str1 -- ViewGroup: $str2" }
+        )
+    }
+
+    fun logAddTransientRow(
+        childEntry: NotificationEntry,
+        containerEntry: NotificationEntry,
+        index: Int
+    ) {
+        notificationRenderBuffer.log(
+            TAG,
+            LogLevel.ERROR,
+            {
+                str1 = childEntry.logKey
+                str2 = containerEntry.logKey
+                int1 = index
+            },
+            { "addTransientRow to row: childKey: $str1 -- containerKey: $str2 -- index: $int1" }
+        )
+    }
+
+    fun logRemoveTransientRow(
+        childEntry: NotificationEntry,
+        containerEntry: NotificationEntry,
+    ) {
+        notificationRenderBuffer.log(
+            TAG,
+            LogLevel.ERROR,
+            {
+                str1 = childEntry.logKey
+                str2 = containerEntry.logKey
+            },
+            { "removeTransientRow from row: childKey: $str1 -- containerKey: $str2" }
+        )
+    }
 }
 
 private const val TAG = "NotifRow"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
index 684a276..02627fd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.log.dagger.NotificationLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
index b24cec1..0c686be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
@@ -235,7 +235,7 @@
 
     @Override
     public long performRemoveAnimation(long duration, long delay,
-            float translationDirection, boolean isHeadsUpAnimation, float endLocation,
+            float translationDirection, boolean isHeadsUpAnimation,
             Runnable onFinishedRunnable,
             AnimatorListenerAdapter animationListener) {
         // TODO: Use duration
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/ExpandableNotificationRowComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/ExpandableNotificationRowComponent.java
index 1a7417a..3588621 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/ExpandableNotificationRowComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/ExpandableNotificationRowComponent.java
@@ -25,7 +25,6 @@
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController;
-import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 
 import dagger.Binds;
@@ -59,8 +58,6 @@
         Builder notificationEntry(NotificationEntry entry);
         @BindsInstance
         Builder onExpandClickListener(ExpandableNotificationRow.OnExpandClickListener presenter);
-        @BindsInstance
-        Builder listContainer(NotificationListContainer listContainer);
         ExpandableNotificationRowComponent build();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt
index 014406f..4b89615 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt
@@ -17,26 +17,24 @@
 package com.android.systemui.statusbar.notification.shelf.domain.interactor
 
 import android.os.PowerManager
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.NotificationShelf
-import com.android.systemui.statusbar.phone.CentralSurfaces
-import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
-import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 
 /** Interactor for the [NotificationShelf] */
-@CentralSurfacesComponent.CentralSurfacesScope
+@SysUISingleton
 class NotificationShelfInteractor
 @Inject
 constructor(
     private val keyguardRepository: KeyguardRepository,
     private val deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository,
-    private val centralSurfaces: CentralSurfaces,
-    private val systemClock: SystemClock,
+    private val powerInteractor: PowerInteractor,
     private val keyguardTransitionController: LockscreenShadeTransitionController,
 ) {
     /** Is the shelf showing on the keyguard? */
@@ -55,11 +53,7 @@
 
     /** Transition keyguard to the locked shade, triggered by the shelf. */
     fun goToLockedShadeFromShelf() {
-        centralSurfaces.wakeUpIfDozing(
-            systemClock.uptimeMillis(),
-            "SHADE_CLICK",
-            PowerManager.WAKE_REASON_GESTURE,
-        )
+        powerInteractor.wakeUpIfDozing("SHADE_CLICK", PowerManager.WAKE_REASON_GESTURE)
         keyguardTransitionController.goToLockedShade(null)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
index 12956ab..23a58d2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
@@ -19,6 +19,7 @@
 import android.view.View
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.lifecycle.repeatWhenAttached
@@ -32,7 +33,6 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
 import com.android.systemui.statusbar.phone.NotificationIconContainer
-import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
 import javax.inject.Inject
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.launch
@@ -43,7 +43,7 @@
  * [NotificationShelfController] interface. Once the [LegacyNotificationShelfControllerImpl] is
  * removed, this class can go away and the ViewBinder can be used directly.
  */
-@CentralSurfacesScope
+@SysUISingleton
 class NotificationShelfViewBinderWrapperControllerImpl @Inject constructor() :
     NotificationShelfController {
 
@@ -81,7 +81,7 @@
         ActivatableNotificationViewBinder.bind(viewModel, shelf, falsingManager)
         shelf.apply {
             setRefactorFlagEnabled(true)
-            setSensitiveRevealAnimEndabled(featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM))
+            setSensitiveRevealAnimEnabled(featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM))
             // TODO(278765923): Replace with eventual NotificationIconContainerViewBinder#bind()
             notificationIconAreaController.setShelfIcons(shelfIcons)
             repeatWhenAttached {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
index fb19443..5ca8b53 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
@@ -16,16 +16,16 @@
 
 package com.android.systemui.statusbar.notification.shelf.ui.viewmodel
 
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.NotificationShelf
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel
 import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor
-import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
 
 /** ViewModel for [NotificationShelf]. */
-@CentralSurfacesScope
+@SysUISingleton
 class NotificationShelfViewModel
 @Inject
 constructor(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
index b8f28b5..04308b4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
@@ -69,11 +69,14 @@
         canvas.clipPath(clipPath)
     }
 
-
-    override fun performRemoveAnimation(duration: Long, delay: Long, translationDirection: Float,
-                                        isHeadsUpAnimation: Boolean, endLocation: Float,
-                                        onFinishedRunnable: Runnable?,
-                                        animationListener: AnimatorListenerAdapter?): Long {
+    override fun performRemoveAnimation(
+            duration: Long,
+            delay: Long,
+            translationDirection: Float,
+            isHeadsUpAnimation: Boolean,
+            onFinishedRunnable: Runnable?,
+            animationListener: AnimatorListenerAdapter?
+    ): Long {
         return 0
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index f8e374d..dad8064 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -132,6 +132,8 @@
     private boolean mContainingNotificationIsFaded = false;
     private RoundableState mRoundableState;
 
+    private NotificationChildrenContainerLogger mLogger;
+
     public NotificationChildrenContainer(Context context) {
         this(context, null);
     }
@@ -1507,6 +1509,33 @@
         return mNotificationHeaderWrapper;
     }
 
+    public void setLogger(NotificationChildrenContainerLogger logger) {
+        mLogger = logger;
+    }
+
+    @Override
+    public void addTransientView(View view, int index) {
+        if (mLogger != null && view instanceof ExpandableNotificationRow) {
+            mLogger.addTransientRow(
+                    ((ExpandableNotificationRow) view).getEntry(),
+                    getContainingNotification().getEntry(),
+                    index
+            );
+        }
+        super.addTransientView(view, index);
+    }
+
+    @Override
+    public void removeTransientView(View view) {
+        if (mLogger != null && view instanceof ExpandableNotificationRow) {
+            mLogger.removeTransientRow(
+                    ((ExpandableNotificationRow) view).getEntry(),
+                    getContainingNotification().getEntry()
+            );
+        }
+        super.removeTransientView(view);
+    }
+
     public String debugString() {
         return TAG + " { "
                 + "visibility: " + getVisibility()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerLogger.kt
new file mode 100644
index 0000000..4986b63
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerLogger.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.notification.stack
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.NotificationRenderLog
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
+import javax.inject.Inject
+
+class NotificationChildrenContainerLogger
+@Inject
+constructor(@NotificationRenderLog private val notificationRenderBuffer: LogBuffer) {
+    fun addTransientRow(
+        childEntry: NotificationEntry,
+        containerEntry: NotificationEntry,
+        index: Int
+    ) {
+        notificationRenderBuffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                str1 = childEntry.logKey
+                str2 = containerEntry.logKey
+                int1 = index
+            },
+            { "addTransientRow: childKey: $str1 -- containerKey: $str2 -- index: $int1" }
+        )
+    }
+
+    fun removeTransientRow(
+        childEntry: NotificationEntry,
+        containerEntry: NotificationEntry,
+    ) {
+        notificationRenderBuffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                str1 = childEntry.logKey
+                str2 = containerEntry.logKey
+            },
+            { "removeTransientRow: childKey: $str1 -- containerKey: $str2" }
+        )
+    }
+
+    companion object {
+        private const val TAG = "NotifChildrenContainer"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
index f953187..2da5582 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
@@ -19,7 +19,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.dagger.NotificationSectionLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import javax.inject.Inject
 
 private const val TAG = "NotifSections"
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 df6fe3d..479fbdb 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
@@ -104,13 +104,13 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
+import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.FooterView;
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -197,7 +197,6 @@
      */
     private Set<Integer> mDebugTextUsedYPositions;
     private final boolean mDebugRemoveAnimation;
-    private final boolean mSimplifiedAppearFraction;
     private final boolean mSensitiveRevealAnimEndabled;
     private boolean mAnimatedInsets;
 
@@ -316,7 +315,7 @@
         }
     };
     private NotificationStackScrollLogger mLogger;
-    private CentralSurfaces mCentralSurfaces;
+    private NotificationsController mNotificationsController;
     private ActivityStarter mActivityStarter;
     private final int[] mTempInt2 = new int[2];
     private boolean mGenerateChildOrderChangedEvent;
@@ -621,7 +620,6 @@
         FeatureFlags featureFlags = Dependency.get(FeatureFlags.class);
         mDebugLines = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES);
         mDebugRemoveAnimation = featureFlags.isEnabled(Flags.NSSL_DEBUG_REMOVE_ANIMATION);
-        mSimplifiedAppearFraction = featureFlags.isEnabled(Flags.SIMPLIFIED_APPEAR_FRACTION);
         mSensitiveRevealAnimEndabled = featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM);
         setAnimatedInsetsEnabled(featureFlags.isEnabled(Flags.ANIMATED_NOTIFICATION_SHADE_INSETS));
         mSectionsManager = Dependency.get(NotificationSectionsManager.class);
@@ -1638,14 +1636,6 @@
         return mAmbientState.getTrackedHeadsUpRow() != null;
     }
 
-    // TODO(b/246353296): remove it when Flags.SIMPLIFIED_APPEAR_FRACTION is removed
-    public float calculateAppearFractionOld(float height) {
-        float appearEndPosition = getAppearEndPosition();
-        float appearStartPosition = getAppearStartPosition();
-        return (height - appearStartPosition)
-                / (appearEndPosition - appearStartPosition);
-    }
-
     /**
      * @param height the height of the panel
      * @return Fraction of the appear animation that has been performed. Normally follows expansion
@@ -1653,33 +1643,24 @@
      * when HUN is swiped up.
      */
     @FloatRange(from = -1.0, to = 1.0)
-    public float simplifiedAppearFraction(float height) {
+    public float calculateAppearFraction(float height) {
         if (isHeadsUpTransition()) {
             // HUN is a special case because fraction can go negative if swiping up. And for now
             // it must go negative as other pieces responsible for proper translation up assume
             // negative value for HUN going up.
             // This can't use expansion fraction as that goes only from 0 to 1. Also when
             // appear fraction for HUN is 0, expansion fraction will be already around 0.2-0.3
-            // and that makes translation jump immediately. Let's use old implementation for now and
-            // see if we can figure out something better
-            return MathUtils.constrain(calculateAppearFractionOld(height), -1, 1);
+            // and that makes translation jump immediately.
+            float appearEndPosition = getAppearEndPosition();
+            float appearStartPosition = getAppearStartPosition();
+            float hunAppearFraction = (height - appearStartPosition)
+                    / (appearEndPosition - appearStartPosition);
+            return MathUtils.constrain(hunAppearFraction, -1, 1);
         } else {
             return mAmbientState.getExpansionFraction();
         }
     }
 
-    public float calculateAppearFraction(float height) {
-        if (mSimplifiedAppearFraction) {
-            return simplifiedAppearFraction(height);
-        } else if (mShouldUseSplitNotificationShade) {
-            // for split shade we want to always use the new way of calculating appear fraction
-            // because without it heads-up experience is very broken and it's less risky change
-            return simplifiedAppearFraction(height);
-        } else {
-            return calculateAppearFractionOld(height);
-        }
-    }
-
     public float getStackTranslation() {
         return mStackTranslation;
     }
@@ -2778,6 +2759,7 @@
         boolean animationGenerated = container != null && generateRemoveAnimation(child);
         if (animationGenerated) {
             if (!mSwipedOutViews.contains(child) || !isFullySwipedOut(child)) {
+                logAddTransientChild(child, container);
                 container.addTransientView(child, 0);
                 child.setTransientContainer(container);
             }
@@ -2793,6 +2775,46 @@
         focusNextViewIfFocused(child);
     }
 
+    private void logAddTransientChild(ExpandableView child, ViewGroup container) {
+        if (mLogger == null) {
+            return;
+        }
+        if (child instanceof ExpandableNotificationRow) {
+            if (container instanceof NotificationChildrenContainer) {
+                mLogger.addTransientChildNotificationToChildContainer(
+                        ((ExpandableNotificationRow) child).getEntry(),
+                        ((NotificationChildrenContainer) container)
+                                .getContainingNotification().getEntry()
+                );
+            } else if (container instanceof NotificationStackScrollLayout) {
+                mLogger.addTransientChildNotificationToNssl(
+                        ((ExpandableNotificationRow) child).getEntry()
+                );
+            } else {
+                mLogger.addTransientChildNotificationToViewGroup(
+                        ((ExpandableNotificationRow) child).getEntry(),
+                        container
+                );
+            }
+        }
+    }
+
+    @Override
+    public void addTransientView(View view, int index) {
+        if (mLogger != null && view instanceof ExpandableNotificationRow) {
+            mLogger.addTransientRow(((ExpandableNotificationRow) view).getEntry(), index);
+        }
+        super.addTransientView(view, index);
+    }
+
+    @Override
+    public void removeTransientView(View view) {
+        if (mLogger != null && view instanceof ExpandableNotificationRow) {
+            mLogger.removeTransientRow(((ExpandableNotificationRow) view).getEntry());
+        }
+        super.removeTransientView(view);
+    }
+
     /**
      * Has this view been fully swiped out such that it's not visible anymore.
      */
@@ -3052,7 +3074,9 @@
         if (!animationsEnabled) {
             mSwipedOutViews.clear();
             mChildrenToRemoveAnimated.clear();
-            clearTemporaryViewsInGroup(this);
+            clearTemporaryViewsInGroup(
+                    /* viewGroup = */ this,
+                    /* reason = */ "setAnimationsEnabled");
         }
     }
 
@@ -4008,7 +4032,7 @@
         mAmbientState.setExpansionChanging(false);
         if (!mIsExpanded) {
             resetScrollPosition();
-            mCentralSurfaces.resetUserExpandedStates();
+            mNotificationsController.resetUserExpandedStates();
             clearTemporaryViews();
             clearUserLockedViews();
             resetAllSwipeState();
@@ -4027,26 +4051,48 @@
 
     private void clearTemporaryViews() {
         // lets make sure nothing is transient anymore
-        clearTemporaryViewsInGroup(this);
+        clearTemporaryViewsInGroup(
+                /* viewGroup = */ this,
+                /* reason = */ "clearTemporaryViews"
+        );
         for (int i = 0; i < getChildCount(); i++) {
             ExpandableView child = getChildAtIndex(i);
             if (child instanceof ExpandableNotificationRow) {
                 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
-                clearTemporaryViewsInGroup(row.getChildrenContainer());
+                clearTemporaryViewsInGroup(
+                        /* viewGroup = */ row.getChildrenContainer(),
+                        /* reason = */ "clearTemporaryViewsInGroup(row.getChildrenContainer())"
+                );
             }
         }
     }
 
-    private void clearTemporaryViewsInGroup(ViewGroup viewGroup) {
+    private void clearTemporaryViewsInGroup(ViewGroup viewGroup, String reason) {
         while (viewGroup != null && viewGroup.getTransientViewCount() != 0) {
             final View transientView = viewGroup.getTransientView(0);
             viewGroup.removeTransientView(transientView);
             if (transientView instanceof ExpandableView) {
                 ((ExpandableView) transientView).setTransientContainer(null);
+                if (transientView instanceof ExpandableNotificationRow) {
+                    logTransientNotificationRowTraversalCleaned(
+                            (ExpandableNotificationRow) transientView,
+                            reason
+                    );
+                }
             }
         }
     }
 
+    private void logTransientNotificationRowTraversalCleaned(
+            ExpandableNotificationRow transientView,
+            String reason
+    ) {
+        if (mLogger == null) {
+            return;
+        }
+        mLogger.transientNotificationRowTraversalCleaned(transientView.getEntry(), reason);
+    }
+
     void onPanelTrackingStarted() {
         mPanelTracking = true;
         mAmbientState.setPanelTracking(true);
@@ -4602,8 +4648,8 @@
         return max + getStackTranslation();
     }
 
-    public void setCentralSurfaces(CentralSurfaces centralSurfaces) {
-        this.mCentralSurfaces = centralSurfaces;
+    public void setNotificationsController(NotificationsController notificationsController) {
+        this.mNotificationsController = notificationsController;
     }
 
     public void setActivityStarter(ActivityStarter activityStarter) {
@@ -5070,6 +5116,9 @@
             println(pw, "intrinsicPadding", mIntrinsicPadding);
             println(pw, "topPadding", mTopPadding);
             println(pw, "bottomPadding", mBottomPadding);
+            println(pw, "translationX", getTranslationX());
+            println(pw, "translationY", getTranslationY());
+            println(pw, "translationZ", getTranslationZ());
             mNotificationStackSizeCalculator.dump(pw, args);
         });
         pw.println();
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 11ba753..ef7375a 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
@@ -29,6 +29,7 @@
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_HIGH_PRIORITY;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.SelectedRows;
 import static com.android.systemui.statusbar.phone.NotificationIconAreaController.HIGH_PRIORITY;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -56,11 +57,18 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.ExpandHelper;
 import com.android.systemui.Gefingerpoken;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.classifier.FalsingCollector;
+import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.media.controls.ui.KeyguardMediaController;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
@@ -69,6 +77,7 @@
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener;
@@ -98,6 +107,7 @@
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
 import com.android.systemui.statusbar.notification.dagger.SilentHeader;
+import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -107,14 +117,12 @@
 import com.android.systemui.statusbar.notification.row.NotificationSnooze;
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 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.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.ScrimController;
-import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -137,13 +145,14 @@
 /**
  * Controller for {@link NotificationStackScrollLayout}.
  */
-@CentralSurfacesComponent.CentralSurfacesScope
+@SysUISingleton
 public class NotificationStackScrollLayoutController {
     private static final String TAG = "StackScrollerController";
     private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
 
     private final boolean mAllowLongPress;
     private final NotificationGutsManager mNotificationGutsManager;
+    private final NotificationsController mNotificationsController;
     private final NotificationVisibilityProvider mVisibilityProvider;
     private final HeadsUpManagerPhone mHeadsUpManager;
     private final NotificationRoundnessManager mNotificationRoundnessManager;
@@ -170,9 +179,9 @@
     private final KeyguardMediaController mKeyguardMediaController;
     private final SysuiStatusBarStateController mStatusBarStateController;
     private final KeyguardBypassController mKeyguardBypassController;
+    private final KeyguardInteractor mKeyguardInteractor;
+    private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
     private final NotificationLockscreenUserManager mLockscreenUserManager;
-    // TODO: CentralSurfaces should be encapsulated behind a Controller
-    private final CentralSurfaces mCentralSurfaces;
     private final SectionHeaderController mSilentHeaderController;
     private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
     private final InteractionJankMonitor mJankMonitor;
@@ -184,13 +193,17 @@
     private final GroupExpansionManager mGroupExpansionManager;
     private final NotifPipelineFlags mNotifPipelineFlags;
     private final SeenNotificationsProvider mSeenNotificationsProvider;
+    private final KeyguardTransitionRepository mKeyguardTransitionRepo;
 
     private NotificationStackScrollLayout mView;
     private NotificationSwipeHelper mSwipeHelper;
     @Nullable
     private Boolean mHistoryEnabled;
     private int mBarState;
+    private boolean mIsBouncerShowingFromCentralSurfaces;
     private HeadsUpAppearanceController mHeadsUpAppearanceController;
+    private boolean mIsInTransitionToAod = false;
+
     private final FeatureFlags mFeatureFlags;
     private final NotificationTargetsHelper mNotificationTargetsHelper;
     private final SecureSettings mSecureSettings;
@@ -430,7 +443,7 @@
                 @Override
                 public void onSnooze(StatusBarNotification sbn,
                                      NotificationSwipeActionHelper.SnoozeOption snoozeOption) {
-                    mCentralSurfaces.setNotificationSnoozed(sbn, snoozeOption);
+                    mNotificationsController.setNotificationSnoozed(sbn, snoozeOption);
                 }
 
                 @Override
@@ -561,7 +574,8 @@
 
                 @Override
                 public float getFalsingThresholdFactor() {
-                    return mCentralSurfaces.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
+                    return ShadeViewController.getFalsingThresholdFactor(
+                            mKeyguardInteractor.getWakefulnessModel().getValue());
                 }
 
                 @Override
@@ -615,6 +629,7 @@
             NotificationStackScrollLayout view,
             @Named(ALLOW_NOTIFICATION_LONG_PRESS_NAME) boolean allowLongPress,
             NotificationGutsManager notificationGutsManager,
+            NotificationsController notificationsController,
             NotificationVisibilityProvider visibilityProvider,
             HeadsUpManagerPhone headsUpManager,
             NotificationRoundnessManager notificationRoundnessManager,
@@ -625,6 +640,9 @@
             SysuiStatusBarStateController statusBarStateController,
             KeyguardMediaController keyguardMediaController,
             KeyguardBypassController keyguardBypassController,
+            KeyguardInteractor keyguardInteractor,
+            PrimaryBouncerInteractor primaryBouncerInteractor,
+            KeyguardTransitionRepository keyguardTransitionRepo,
             ZenModeController zenModeController,
             NotificationLockscreenUserManager lockscreenUserManager,
             Optional<NotificationListViewModel> nsslViewModel,
@@ -634,7 +652,6 @@
             FalsingManager falsingManager,
             @Main Resources resources,
             NotificationSwipeHelper.Builder notificationSwipeHelperBuilder,
-            CentralSurfaces centralSurfaces,
             ScrimController scrimController,
             GroupExpansionManager groupManager,
             @SilentHeader SectionHeaderController silentHeaderController,
@@ -658,10 +675,12 @@
             NotificationDismissibilityProvider dismissibilityProvider,
             ActivityStarter activityStarter) {
         mView = view;
+        mKeyguardTransitionRepo = keyguardTransitionRepo;
         mStackStateLogger = stackLogger;
         mLogger = logger;
         mAllowLongPress = allowLongPress;
         mNotificationGutsManager = notificationGutsManager;
+        mNotificationsController = notificationsController;
         mVisibilityProvider = visibilityProvider;
         mHeadsUpManager = headsUpManager;
         mNotificationRoundnessManager = notificationRoundnessManager;
@@ -672,6 +691,8 @@
         mStatusBarStateController = statusBarStateController;
         mKeyguardMediaController = keyguardMediaController;
         mKeyguardBypassController = keyguardBypassController;
+        mKeyguardInteractor = keyguardInteractor;
+        mPrimaryBouncerInteractor = primaryBouncerInteractor;
         mZenModeController = zenModeController;
         mLockscreenUserManager = lockscreenUserManager;
         mViewModel = nsslViewModel;
@@ -682,7 +703,6 @@
         mFalsingManager = falsingManager;
         mResources = resources;
         mNotificationSwipeHelperBuilder = notificationSwipeHelperBuilder;
-        mCentralSurfaces = centralSurfaces;
         mScrimController = scrimController;
         mJankMonitor = jankMonitor;
         mNotificationStackSizeCalculator = notificationStackSizeCalculator;
@@ -711,7 +731,7 @@
         mView.setController(this);
         mView.setLogger(mLogger);
         mView.setTouchHandler(new TouchHandler());
-        mView.setCentralSurfaces(mCentralSurfaces);
+        mView.setNotificationsController(mNotificationsController);
         mView.setActivityStarter(mActivityStarter);
         mView.setClearAllAnimationListener(this::onAnimationEnd);
         mView.setClearAllListener((selection) -> mUiEventLogger.log(
@@ -810,6 +830,9 @@
         mViewModel.ifPresent(
                 vm -> NotificationListViewBinder
                         .bind(mView, vm, mFalsingManager, mFeatureFlags, mNotifIconAreaController));
+
+        collectFlow(mView, mKeyguardTransitionRepo.getTransitions(),
+                this::onKeyguardTransitionChanged);
     }
 
     private boolean isInVisibleLocation(NotificationEntry entry) {
@@ -1198,6 +1221,14 @@
     }
 
     /**
+     * Sets whether the bouncer is currently showing. Should only be called from
+     * {@link CentralSurfaces}.
+     */
+    public void setBouncerShowingFromCentralSurfaces(boolean bouncerShowing) {
+        mIsBouncerShowingFromCentralSurfaces = bouncerShowing;
+    }
+
+    /**
      * Set the visibility of the view, and propagate it to specific children.
      *
      * @param visible either the view is visible or not.
@@ -1224,11 +1255,12 @@
 
         final boolean shouldShow = getVisibleNotificationCount() == 0
                 && !mView.isQsFullScreen()
-                // Hide empty shade view when in transition to Keyguard.
+                // Hide empty shade view when in transition to AOD.
                 // That avoids "No Notifications" to blink when transitioning to AOD.
                 // For more details, see: b/228790482
-                && !isInTransitionToKeyguard()
-                && !mCentralSurfaces.isBouncerShowing();
+                && !mIsInTransitionToAod
+                // Don't show any notification content if the bouncer is showing. See b/267060171.
+                && !isBouncerShowing();
 
         mView.updateEmptyShadeView(shouldShow, mZenModeController.areNotificationsHiddenInShade());
 
@@ -1236,6 +1268,24 @@
     }
 
     /**
+     * Returns whether the bouncer is currently showing.
+     *
+     * There's a possible timing difference between when CentralSurfaces marks the bouncer as not
+     * showing and when PrimaryBouncerInteractor marks the bouncer as not showing. (CentralSurfaces
+     * appears to mark the bouncer as showing for 10-200ms longer than PrimaryBouncerInteractor.)
+     *
+     * This timing difference could be load bearing, which is why we have a feature flag protecting
+     * where we fetch the value from. This flag is intended to be short-lived.
+     */
+    private boolean isBouncerShowing() {
+        if (mFeatureFlags.isEnabled(Flags.USE_REPOS_FOR_BOUNCER_SHOWING)) {
+            return mPrimaryBouncerInteractor.isBouncerShowing();
+        } else {
+            return mIsBouncerShowingFromCentralSurfaces;
+        }
+    }
+
+    /**
      * Update the importantForAccessibility of NotificationStackScrollLayout.
      * <p>
      * We want the NSSL to be unimportant for accessibility when there's no
@@ -1426,7 +1476,7 @@
         return mNotificationRoundnessManager;
     }
 
-    NotificationListContainer getNotificationListContainer() {
+    public NotificationListContainer getNotificationListContainer() {
         return mNotificationListContainer;
     }
 
@@ -1607,6 +1657,16 @@
         return shelf == null ? 0 : shelf.getIntrinsicHeight();
     }
 
+    @VisibleForTesting
+    void onKeyguardTransitionChanged(TransitionStep transitionStep) {
+        boolean isTransitionToAod = transitionStep.getFrom().equals(KeyguardState.GONE)
+                && transitionStep.getTo().equals(KeyguardState.AOD);
+        if (mIsInTransitionToAod != isTransitionToAod) {
+            mIsInTransitionToAod = isTransitionToAod;
+            updateShowEmptyShadeView();
+        }
+    }
+
     /**
      * Enum for UiEvent logged from this class
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutListContainerModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutListContainerModule.java
deleted file mode 100644
index 3dcaae2..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutListContainerModule.java
+++ /dev/null
@@ -1,32 +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.notification.stack;
-
-import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
-
-import dagger.Module;
-import dagger.Provides;
-
-@Module
-public abstract class NotificationStackScrollLayoutListContainerModule {
-    @Provides
-    @CentralSurfacesComponent.CentralSurfacesScope
-    static NotificationListContainer provideListContainer(
-            NotificationStackScrollLayoutController nsslController) {
-        return nsslController.getNotificationListContainer();
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
index 5b0ec1d..2c38b8d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
@@ -1,9 +1,12 @@
 package com.android.systemui.statusbar.notification.stack
 
-import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import android.view.ViewGroup
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.ERROR
+import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.log.dagger.NotificationRenderLog
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD
@@ -15,7 +18,8 @@
 import javax.inject.Inject
 
 class NotificationStackScrollLogger @Inject constructor(
-    @NotificationHeadsUpLog private val buffer: LogBuffer
+    @NotificationHeadsUpLog private val buffer: LogBuffer,
+    @NotificationRenderLog private val notificationRenderBuffer: LogBuffer
 ) {
     fun hunAnimationSkipped(entry: NotificationEntry, reason: String) {
         buffer.log(TAG, INFO, {
@@ -77,6 +81,79 @@
                     "isTouchBelowNotification: $bool2 motionEvent: $str1"
         })
     }
+
+    fun transientNotificationRowTraversalCleaned(entry: NotificationEntry, reason: String) {
+        notificationRenderBuffer.log(TAG, INFO, {
+            str1 = entry.logKey
+            str2 = reason
+        }, {
+            "transientNotificationRowTraversalCleaned: key: $str1 reason: $str2"
+        })
+    }
+
+    fun addTransientChildNotificationToChildContainer(
+            childEntry: NotificationEntry,
+            containerEntry: NotificationEntry,
+    ) {
+        notificationRenderBuffer.log(TAG, INFO, {
+            str1 = childEntry.logKey
+            str2 = containerEntry.logKey
+        }, {
+            "addTransientChildToContainer from onViewRemovedInternal: childKey: $str1 " +
+                    "-- containerKey: $str2"
+        })
+    }
+
+    fun addTransientChildNotificationToNssl(
+            childEntry: NotificationEntry,
+    ) {
+        notificationRenderBuffer.log(TAG, INFO, {
+            str1 = childEntry.logKey
+        }, {
+            "addTransientRowToNssl from onViewRemovedInternal: childKey: $str1"
+        })
+    }
+
+    fun addTransientChildNotificationToViewGroup(
+            childEntry: NotificationEntry,
+            container: ViewGroup
+    ) {
+        notificationRenderBuffer.log(TAG, ERROR, {
+            str1 = childEntry.logKey
+            str2 = container.toString()
+        }, {
+            "addTransientRowTo unhandled ViewGroup from onViewRemovedInternal: childKey: $str1 " +
+                    "-- ViewGroup: $str2"
+        })
+    }
+
+    fun addTransientRow(
+            childEntry: NotificationEntry,
+            index: Int
+    ) {
+        notificationRenderBuffer.log(
+                TAG,
+                INFO,
+                {
+                    str1 = childEntry.logKey
+                    int1 = index
+                },
+                { "addTransientRow to NSSL: childKey: $str1 -- index: $int1" }
+        )
+    }
+
+    fun removeTransientRow(
+            childEntry: NotificationEntry,
+    ) {
+        notificationRenderBuffer.log(
+                TAG,
+                INFO,
+                {
+                    str1 = childEntry.logKey
+                },
+                { "removeTransientRow from NSSL: childKey: $str1" }
+        )
+    }
 }
 
 private const val TAG = "NotificationStackScroll"
\ No newline at end of file
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 efb7926..b2d26d9 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
@@ -29,8 +29,6 @@
 import com.android.keyguard.BouncerPanelExpansionCalculator;
 import com.android.systemui.R;
 import com.android.systemui.animation.ShadeInterpolation;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
 import com.android.systemui.statusbar.EmptyShadeView;
 import com.android.systemui.statusbar.NotificationShelf;
@@ -189,9 +187,7 @@
 
     private float interpolateFooterAlpha(AmbientState ambientState) {
         float expansion = ambientState.getExpansionFraction();
-        FeatureFlags flags = ambientState.getFeatureFlags();
-        if (ambientState.isSmallScreen()
-                || !flags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) {
+        if (ambientState.isSmallScreen()) {
             return ShadeInterpolation.getContentAlpha(expansion);
         }
         LargeScreenShadeInterpolator interpolator = ambientState.getLargeScreenShadeInterpolator();
@@ -200,9 +196,7 @@
 
     private float interpolateNotificationContentAlpha(AmbientState ambientState) {
         float expansion = ambientState.getExpansionFraction();
-        FeatureFlags flags = ambientState.getFeatureFlags();
-        if (ambientState.isSmallScreen()
-                || !flags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) {
+        if (ambientState.isSmallScreen()) {
             return ShadeInterpolation.getContentAlpha(expansion);
         }
         LargeScreenShadeInterpolator interpolator = ambientState.getLargeScreenShadeInterpolator();
@@ -772,8 +766,9 @@
             boolean isTopEntry = topHeadsUpEntry == row;
             float unmodifiedEndLocation = childState.getYTranslation() + childState.height;
             if (mIsExpanded) {
-                if (row.mustStayOnScreen() && !childState.headsUpIsVisible
-                        && !row.showingPulsing() && !ambientState.isOnKeyguard()) {
+                if (shouldHunBeVisibleWhenScrolled(row.mustStayOnScreen(),
+                        childState.headsUpIsVisible, row.showingPulsing(),
+                        ambientState.isOnKeyguard(), row.getEntry().isStickyAndNotDemoted())) {
                     // Ensure that the heads up is always visible even when scrolled off
                     clampHunToTop(mQuickQsOffsetHeight, ambientState.getStackTranslation(),
                             row.getCollapsedHeight(), childState);
@@ -821,7 +816,15 @@
         }
     }
 
-    /**
+    @VisibleForTesting
+    boolean shouldHunBeVisibleWhenScrolled(boolean mustStayOnScreen, boolean headsUpIsVisible,
+            boolean showingPulsing, boolean isOnKeyguard, boolean headsUpOnKeyguard) {
+        return mustStayOnScreen && !headsUpIsVisible
+                        && !showingPulsing
+                        && (!isOnKeyguard || headsUpOnKeyguard);
+    }
+
+     /**
      * When shade is open and we are scrolled to the bottom of notifications,
      * clamp incoming HUN in its collapsed form, right below qs offset.
      * Transition pinned collapsed HUN to full height when scrolling back up.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
index f07dd00..2742a23 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
@@ -27,8 +27,6 @@
 import com.android.systemui.R;
 import com.android.systemui.shared.clocks.AnimatableClockView;
 import com.android.systemui.statusbar.NotificationShelf;
-import com.android.systemui.statusbar.StatusBarIconView;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
@@ -347,10 +345,12 @@
             final ExpandableView changingView = (ExpandableView) event.mChangingView;
             boolean loggable = false;
             boolean isHeadsUp = false;
+            boolean isGroupChild = false;
             String key = null;
             if (changingView instanceof ExpandableNotificationRow && mLogger != null) {
                 loggable = true;
                 isHeadsUp = ((ExpandableNotificationRow) changingView).isHeadsUp();
+                isGroupChild = changingView.isChildInGroup();
                 key = ((ExpandableNotificationRow) changingView).getEntry().getKey();
             }
             if (event.animationType ==
@@ -407,17 +407,25 @@
 
                 }
                 Runnable postAnimation = changingView::removeFromTransientContainer;
-                if (loggable && isHeadsUp) {
-                    mLogger.logHUNViewDisappearingWithRemoveEvent(key);
+                if (loggable) {
                     String finalKey = key;
-                    postAnimation = () -> {
-                        mLogger.disappearAnimationEnded(finalKey);
-                        changingView.removeFromTransientContainer();
-                    };
+                    if (isHeadsUp) {
+                        mLogger.logHUNViewDisappearingWithRemoveEvent(key);
+                        postAnimation = () -> {
+                            mLogger.disappearAnimationEnded(finalKey);
+                            changingView.removeFromTransientContainer();
+                        };
+                    } else if (isGroupChild) {
+                        mLogger.groupChildRemovalEventProcessed(key);
+                        postAnimation = () -> {
+                            mLogger.groupChildRemovalAnimationEnded(finalKey);
+                            changingView.removeFromTransientContainer();
+                        };
+                    }
                 }
                 changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR,
                         0 /* delay */, translationDirection, false /* isHeadsUpAppear */,
-                        0, postAnimation, null);
+                        postAnimation, null);
             } else if (event.animationType ==
                 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) {
                 if (mHostLayout.isFullySwipedOut(changingView)) {
@@ -464,28 +472,12 @@
                     mTmpState.initFrom(changingView);
                     endRunnable = changingView::removeFromTransientContainer;
                 }
-                float targetLocation = 0;
                 boolean needsAnimation = true;
                 if (changingView instanceof ExpandableNotificationRow) {
                     ExpandableNotificationRow row = (ExpandableNotificationRow) changingView;
                     if (row.isDismissed()) {
                         needsAnimation = false;
                     }
-
-                    NotificationEntry entry = row.getEntry();
-                    StatusBarIconView icon = entry.getIcons().getStatusBarIcon();
-                    final StatusBarIconView centeredIcon = entry.getIcons().getCenteredIcon();
-                    if (centeredIcon != null && centeredIcon.getParent() != null) {
-                        icon = centeredIcon;
-                    }
-                    if (icon.getParent() != null) {
-                        icon.getLocationOnScreen(mTmpLocation);
-                        float iconPosition = mTmpLocation[0] - icon.getTranslationX()
-                                + ViewState.getFinalTranslationX(icon)
-                                + icon.getWidth() * 0.25f;
-                        mHostLayout.getLocationOnScreen(mTmpLocation);
-                        targetLocation = iconPosition - mTmpLocation[0];
-                    }
                 }
 
                 if (needsAnimation) {
@@ -505,7 +497,7 @@
                     }
                     long removeAnimationDelay = changingView.performRemoveAnimation(
                             ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
-                            0, 0.0f, true /* isHeadsUpAppear */, targetLocation,
+                            0, 0.0f, true /* isHeadsUpAppear */,
                             postAnimation, getGlobalAnimationFinishedListener());
                     mAnimationProperties.delay += removeAnimationDelay;
                 } else if (endRunnable != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
index cca84b3..0b2c486 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
@@ -1,13 +1,15 @@
 package com.android.systemui.statusbar.notification.stack
 
-import com.android.systemui.log.dagger.NotificationHeadsUpLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.log.dagger.NotificationRenderLog
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
 class StackStateLogger @Inject constructor(
-    @NotificationHeadsUpLog private val buffer: LogBuffer
+    @NotificationHeadsUpLog private val buffer: LogBuffer,
+    @NotificationRenderLog private val notificationRenderBuffer: LogBuffer
 ) {
     fun logHUNViewDisappearing(key: String) {
         buffer.log(TAG, LogLevel.INFO, {
@@ -56,6 +58,21 @@
             "Heads up notification appear animation ended $str1 "
         })
     }
+
+    fun groupChildRemovalEventProcessed(key: String) {
+        notificationRenderBuffer.log(TAG, LogLevel.DEBUG, {
+            str1 = logKey(key)
+        }, {
+            "Group Child Notification removal event processed $str1 for ANIMATION_TYPE_REMOVE"
+        })
+    }
+    fun groupChildRemovalAnimationEnded(key: String) {
+        notificationRenderBuffer.log(TAG, LogLevel.INFO, {
+            str1 = logKey(key)
+        }, {
+            "Group child notification removal animation ended $str1 "
+        })
+    }
 }
 
 private const val TAG = "StackScroll"
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index aab1c2b..11f68e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
@@ -33,6 +34,7 @@
 object NotificationListViewModelModule {
     @JvmStatic
     @Provides
+    @SysUISingleton
     fun maybeProvideViewModel(
         featureFlags: FeatureFlags,
         shelfViewModel: Provider<NotificationShelfViewModel>,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index 1827a46..730ef57 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -45,6 +45,7 @@
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.shade.ShadeController
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
@@ -68,6 +69,7 @@
     private val keyguardViewMediatorLazy: Lazy<KeyguardViewMediator>,
     private val shadeControllerLazy: Lazy<ShadeController>,
     private val statusBarKeyguardViewManagerLazy: Lazy<StatusBarKeyguardViewManager>,
+    private val notifShadeWindowControllerLazy: Lazy<NotificationShadeWindowController>,
     private val activityLaunchAnimator: ActivityLaunchAnimator,
     private val context: Context,
     private val lockScreenUserManager: NotificationLockscreenUserManager,
@@ -387,6 +389,35 @@
     }
 
     /**
+     * Whether we should animate an activity launch.
+     *
+     * Note: This method must be called *before* dismissing the keyguard.
+     */
+    private fun shouldAnimateLaunch(
+        isActivityIntent: Boolean,
+        showOverLockscreen: Boolean,
+    ): Boolean {
+        // TODO(b/184121838): Support launch animations when occluded.
+        if (keyguardStateController.isOccluded) {
+            return false
+        }
+
+        // Always animate if we are not showing the keyguard or if we animate over the lockscreen
+        // (without unlocking it).
+        if (showOverLockscreen || !keyguardStateController.isShowing) {
+            return true
+        }
+
+        // We don't animate non-activity launches as they can break the animation.
+        // TODO(b/184121838): Support non activity launches on the lockscreen.
+        return isActivityIntent
+    }
+
+    override fun shouldAnimateLaunch(isActivityIntent: Boolean): Boolean {
+        return shouldAnimateLaunch(isActivityIntent, false)
+    }
+
+    /**
      * Encapsulates the activity logic for activity starter.
      *
      * Logic is duplicated in {@link CentralSurfacesImpl}
@@ -417,7 +448,7 @@
             val animate =
                 animationController != null &&
                     !willLaunchResolverActivity &&
-                    centralSurfaces?.shouldAnimateLaunch(true /* isActivityIntent */) == true
+                    shouldAnimateLaunch(isActivityIntent = true)
             val animController =
                 wrapAnimationController(
                     animationController = animationController,
@@ -536,7 +567,7 @@
             val animate =
                 !willLaunchResolverActivity &&
                     animationController != null &&
-                    centralSurfaces?.shouldAnimateLaunch(intent.isActivity) == true
+                    shouldAnimateLaunch(intent.isActivity)
 
             // If we animate, don't collapse the shade and defer the keyguard dismiss (in case we
             // run the animation on the keyguard). The animation will take care of (instantly)
@@ -593,7 +624,7 @@
                         Log.w(TAG, "Sending intent failed: $e")
                         if (!collapse) {
                             // executeRunnableDismissingKeyguard did not collapse for us already.
-                            centralSurfaces?.collapsePanelOnMainThread()
+                            shadeControllerLazy.get().collapseOnMainThread()
                         }
                         // TODO: Dismiss Keyguard.
                     }
@@ -635,7 +666,7 @@
 
             val animate =
                 animationController != null &&
-                    centralSurfaces?.shouldAnimateLaunch(
+                    shouldAnimateLaunch(
                         /* isActivityIntent= */ true,
                         showOverLockscreenWhenLocked
                     ) == true
@@ -713,7 +744,7 @@
             } else if (dismissShade) {
                 // The animation will take care of dismissing the shade at the end of the animation.
                 // If we don't animate, collapse it directly.
-                centralSurfaces?.collapseShade()
+                shadeControllerLazy.get().cancelExpansionAndCollapseShade()
             }
 
             // We should exit the dream to prevent the activity from starting below the
@@ -802,7 +833,7 @@
                                 shadeControllerLazy.get().isExpandedVisible &&
                                     !statusBarKeyguardViewManagerLazy.get().isBouncerShowing
                             ) {
-                                shadeControllerLazy.get().animateCollapseShadeDelayed()
+                                shadeControllerLazy.get().animateCollapseShadeForcedDelayed()
                             } else {
                                 // Do it after DismissAction has been processed to conserve the
                                 // needed ordering.
@@ -865,7 +896,9 @@
                 if (dismissShade) {
                     return StatusBarLaunchAnimatorController(
                         animationController,
-                        it,
+                        it.shadeViewController,
+                        shadeControllerLazy.get(),
+                        notifShadeWindowControllerLazy.get(),
                         isLaunchForActivity
                     )
                 }
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 f6d53b3..5eafa9e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
@@ -20,6 +20,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Resources;
+import android.database.ContentObserver;
 import android.hardware.display.ColorDisplayManager;
 import android.hardware.display.NightDisplayListener;
 import android.os.Handler;
@@ -28,6 +29,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.R;
+import com.android.systemui.dagger.NightDisplayListenerModule;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.AutoAddTracker;
@@ -82,7 +84,8 @@
     private final HotspotController mHotspotController;
     private final DataSaverController mDataSaverController;
     private final ManagedProfileController mManagedProfileController;
-    private final NightDisplayListener mNightDisplayListener;
+    private final NightDisplayListenerModule.Builder mNightDisplayListenerBuilder;
+    private NightDisplayListener mNightDisplayListener;
     private final CastController mCastController;
     private final DeviceControlsController mDeviceControlsController;
     private final WalletController mWalletController;
@@ -98,7 +101,7 @@
             HotspotController hotspotController,
             DataSaverController dataSaverController,
             ManagedProfileController managedProfileController,
-            NightDisplayListener nightDisplayListener,
+            NightDisplayListenerModule.Builder nightDisplayListenerBuilder,
             CastController castController,
             ReduceBrightColorsController reduceBrightColorsController,
             DeviceControlsController deviceControlsController,
@@ -114,7 +117,7 @@
         mHotspotController = hotspotController;
         mDataSaverController = dataSaverController;
         mManagedProfileController = managedProfileController;
-        mNightDisplayListener = nightDisplayListener;
+        mNightDisplayListenerBuilder = nightDisplayListenerBuilder;
         mCastController = castController;
         mReduceBrightColorsController = reduceBrightColorsController;
         mIsReduceBrightColorsAvailable = isReduceBrightColorsAvailable;
@@ -157,6 +160,10 @@
             mDataSaverController.addCallback(mDataSaverListener);
         }
         mManagedProfileController.addCallback(mProfileCallback);
+
+        mNightDisplayListener = mNightDisplayListenerBuilder
+                .setUser(mCurrentUser.getIdentifier())
+                .build();
         if (!mAutoTracker.isAdded(NIGHT)
                 && ColorDisplayManager.isNightDisplayAvailable(mContext)) {
             mNightDisplayListener.setCallback(mNightDisplayCallback);
@@ -193,7 +200,8 @@
         mHotspotController.removeCallback(mHotspotCallback);
         mDataSaverController.removeCallback(mDataSaverListener);
         mManagedProfileController.removeCallback(mProfileCallback);
-        if (ColorDisplayManager.isNightDisplayAvailable(mContext)) {
+        if (ColorDisplayManager.isNightDisplayAvailable(mContext)
+                && mNightDisplayListener != null) {
             mNightDisplayListener.setCallback(null);
         }
         if (mIsReduceBrightColorsAvailable) {
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 86bf7cb..478baf2f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -24,13 +24,11 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.os.Bundle;
-import android.os.PowerManager;
 import android.os.UserHandle;
-import android.service.notification.StatusBarNotification;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.view.RemoteAnimationAdapter;
 import android.view.View;
-import android.view.ViewGroup;
 import android.window.RemoteTransition;
 import android.window.SplashScreen;
 
@@ -39,19 +37,15 @@
 import androidx.lifecycle.LifecycleOwner;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.statusbar.RegisterStatusBarResult;
 import com.android.keyguard.AuthKeyguardMessageArea;
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.display.data.repository.DisplayMetricsRepository;
 import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
-import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.qs.QSPanelController;
-import com.android.systemui.shade.NotificationShadeWindowView;
-import com.android.systemui.shade.NotificationShadeWindowViewController;
 import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
-import com.android.systemui.statusbar.LightRevealScrim;
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.util.Compile;
@@ -69,14 +63,11 @@
     String TAG = "CentralSurfaces";
     boolean DEBUG = false;
     boolean SPEW = false;
-    boolean DUMPTRUCK = true; // extra dumpsys info
     boolean DEBUG_GESTURES = false;
     boolean DEBUG_MEDIA_FAKE_ARTWORK = false;
     boolean DEBUG_CAMERA_LIFT = false;
     boolean DEBUG_WINDOW_STATE = false;
     boolean DEBUG_WAKEUP_DELAY = Compile.IS_DEBUG;
-    // additional instrumentation for testing purposes; intended to be left on during development
-    boolean CHATTY = DEBUG;
     boolean SHOW_LOCKSCREEN_MEDIA_ARTWORK = true;
     String ACTION_FAKE_ARTWORK = "fake_artwork";
     int FADE_KEYGUARD_START_DELAY = 100;
@@ -194,14 +185,6 @@
         return contextForUser.getPackageManager();
     }
 
-    void animateExpandNotificationsPanel();
-
-    void animateExpandSettingsPanel(@Nullable String subpanel);
-
-    void collapsePanelOnMainThread();
-
-    void togglePanel();
-
     void start();
 
     boolean updateIsKeyguard();
@@ -212,15 +195,6 @@
     @Override
     Lifecycle getLifecycle();
 
-    /**
-     * Wakes up the device if the device was dozing.
-     */
-    void wakeUpIfDozing(long time, String why, @PowerManager.WakeReason int wakeReason);
-
-    NotificationShadeWindowView getNotificationShadeWindowView();
-
-    NotificationShadeWindowViewController getNotificationShadeWindowViewController();
-
     /** */
     ShadeViewController getShadeViewController();
 
@@ -229,46 +203,32 @@
 
     int getStatusBarHeight();
 
-    void updateQsExpansionEnabled();
-
-    boolean isShadeDisabled();
-
     boolean isLaunchingActivityOverLockscreen();
 
-    boolean isWakeUpComingFromTouch();
-
     void onKeyguardViewManagerStatesUpdated();
 
-    ViewGroup getNotificationScrollLayout();
-
     boolean isPulsing();
 
     boolean isOccluded();
 
-    //TODO: These can / should probably be moved to NotificationPresenter or ShadeController
-    void onLaunchAnimationCancelled(boolean isLaunchForActivity);
-
-    void onLaunchAnimationEnd(boolean launchIsFullScreen);
-
-    boolean shouldAnimateLaunch(boolean isActivityIntent, boolean showOverLockscreen);
-
-    boolean shouldAnimateLaunch(boolean isActivityIntent);
-
     boolean isDeviceInVrMode();
 
     NotificationPresenter getPresenter();
 
-    void postAnimateCollapsePanels();
-
-    void postAnimateForceCollapsePanels();
-
-    void postAnimateOpenPanels();
-
-    boolean isPanelExpanded();
-
+    /**
+     * Used to dispatch initial touch events before crossing the threshold to pull down the
+     * notification shade. After that, since the launcher window is set to slippery, input
+     * frameworks take care of routing the events to the notification shade.
+     */
     void onInputFocusTransfer(boolean start, boolean cancel, float velocity);
 
-    void animateCollapseQuickSettings();
+    /**
+     * Dispatches status bar motion event to the notification shade. This is different from
+     * {@link #onInputFocusTransfer(boolean, boolean, float)} as it doesn't rely on setting the
+     * launcher window slippery to allow the frameworks to route those events after passing the
+     * initial threshold.
+     */
+    default void onStatusBarTrackpadEvent(MotionEvent event) {}
 
     /** */
     boolean getCommandQueuePanelsEnabled();
@@ -289,16 +249,16 @@
     @Override
     void dump(PrintWriter pwOriginal, String[] args);
 
-    void createAndAddWindows(@Nullable RegisterStatusBarResult result);
-
+    /** @deprecated Use {@link DisplayMetricsRepository} instead. */
+    @Deprecated
     float getDisplayWidth();
 
+    /** @deprecated Use {@link DisplayMetricsRepository} instead. */
+    @Deprecated
     float getDisplayHeight();
 
     void readyForKeyguardDone();
 
-    void resetUserExpandedStates();
-
     void setLockscreenUser(int newUserId);
 
     void showKeyguard();
@@ -333,15 +293,11 @@
     /** Should the keyguard be hidden immediately in response to a back press/gesture. */
     boolean shouldKeyguardHideImmediately();
 
-    boolean onBackPressed();
-
     boolean onSpacePressed();
 
     void showBouncerWithDimissAndCancelIfKeyguard(OnDismissAction performAction,
             Runnable cancelAction);
 
-    LightRevealScrim getLightRevealScrim();
-
     // TODO: Figure out way to remove these.
     NavigationBarView getNavigationBarView();
 
@@ -353,10 +309,6 @@
 
     void setBouncerShowing(boolean bouncerShowing);
 
-    void setBouncerShowingOverDream(boolean bouncerShowingOverDream);
-
-    void collapseShade();
-
     int getWakefulnessState();
 
     boolean isScreenFullyOff();
@@ -389,9 +341,6 @@
 
     boolean isDeviceInteractive();
 
-    void setNotificationSnoozed(StatusBarNotification sbn,
-            NotificationSwipeActionHelper.SnoozeOption snoozeOption);
-
     void awakenDreams();
 
     void clearNotificationEffects();
@@ -429,14 +378,6 @@
 
     void resendMessage(Object msg);
 
-    int getDisabled1();
-
-    void setDisabled1(int disabled);
-
-    int getDisabled2();
-
-    void setDisabled2(int disabled);
-
     void setLastCameraLaunchSource(int source);
 
     void setLaunchCameraOnFinishedGoingToSleep(boolean launch);
@@ -449,14 +390,12 @@
 
     QSPanelController getQSPanelController();
 
-    boolean areNotificationAlertsDisabled();
-
+    /** @deprecated Use {@link DisplayMetricsRepository} instead. */
+    @Deprecated
     float getDisplayDensity();
 
     void extendDozePulse();
 
-    boolean shouldDelayWakeUpAnimation();
-
     public static class KeyboardShortcutsMessage {
         final int mDeviceId;
 
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 0ccc819..6431ef9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -111,6 +111,9 @@
     private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
 
+    private int mDisabled1;
+    private int mDisabled2;
+
     @Inject
     CentralSurfacesCommandQueueCallbacks(
             CentralSurfaces centralSurfaces,
@@ -208,7 +211,7 @@
 
     @Override
     public void animateCollapsePanels(int flags, boolean force) {
-        mShadeController.animateCollapsePanels(flags, force, false /* delayed */,
+        mShadeController.animateCollapseShade(flags, force, false /* delayed */,
                 1.0f /* speedUpFactor */);
     }
 
@@ -218,11 +221,7 @@
             Log.d(CentralSurfaces.TAG,
                     "animateExpand: mExpandedVisible=" + mShadeController.isExpandedVisible());
         }
-        if (!mCommandQueue.panelsEnabled()) {
-            return;
-        }
-
-        mShadeViewController.expandToNotifications();
+        mShadeController.animateExpandShade();
     }
 
     @Override
@@ -231,14 +230,7 @@
             Log.d(CentralSurfaces.TAG,
                     "animateExpand: mExpandedVisible=" + mShadeController.isExpandedVisible());
         }
-        if (!mCommandQueue.panelsEnabled()) {
-            return;
-        }
-
-        // Settings are not available in setup
-        if (!mDeviceProvisionedController.isCurrentUserSetup()) return;
-
-        mShadeViewController.expandToQs();
+        mShadeController.animateExpandQs();
     }
 
     @Override
@@ -255,31 +247,26 @@
     }
     /**
      * State is one or more of the DISABLE constants from StatusBarManager.
+     *
+     * @deprecated If you need to react to changes in disable flags, listen to
+     * {@link com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository}
+     * instead.
      */
     @Override
+    @Deprecated
     public void disable(int displayId, int state1, int state2, boolean animate) {
         if (displayId != mDisplayId) {
             return;
         }
 
-        int state2BeforeAdjustment = state2;
-        state2 = mRemoteInputQuickSettingsDisabler.adjustDisableFlags(state2);
-        Log.d(CentralSurfaces.TAG,
-                mDisableFlagsLogger.getDisableFlagsString(
-                        /* old= */ new DisableFlagsLogger.DisableState(
-                                mCentralSurfaces.getDisabled1(), mCentralSurfaces.getDisabled2()),
-                        /* new= */ new DisableFlagsLogger.DisableState(
-                                state1, state2BeforeAdjustment),
-                        /* newStateAfterLocalModification= */ new DisableFlagsLogger.DisableState(
-                                state1, state2)));
-
-        final int old1 = mCentralSurfaces.getDisabled1();
+        final int old1 = mDisabled1;
         final int diff1 = state1 ^ old1;
-        mCentralSurfaces.setDisabled1(state1);
+        mDisabled1 = state1;
 
-        final int old2 = mCentralSurfaces.getDisabled2();
+        state2 = mRemoteInputQuickSettingsDisabler.adjustDisableFlags(state2);
+        final int old2 = mDisabled2;
         final int diff2 = state2 ^ old2;
-        mCentralSurfaces.setDisabled2(state2);
+        mDisabled2 = state2;
 
         if ((diff1 & StatusBarManager.DISABLE_EXPAND) != 0) {
             if ((state1 & StatusBarManager.DISABLE_EXPAND) != 0) {
@@ -288,17 +275,12 @@
         }
 
         if ((diff1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) {
-            if (mCentralSurfaces.areNotificationAlertsDisabled()) {
+            if ((state1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) {
                 mHeadsUpManager.releaseAllImmediately();
             }
         }
 
-        if ((diff2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) != 0) {
-            mCentralSurfaces.updateQsExpansionEnabled();
-        }
-
         if ((diff2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0) {
-            mCentralSurfaces.updateQsExpansionEnabled();
             if ((state2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0) {
                 mShadeController.animateCollapseShade();
             }
@@ -556,10 +538,10 @@
 
     @Override
     public void togglePanel() {
-        if (mCentralSurfaces.isPanelExpanded()) {
+        if (mShadeViewController.isPanelExpanded()) {
             mShadeController.animateCollapseShade();
         } else {
-            animateExpandNotificationsPanel();
+            mShadeController.animateExpandShade();
         }
     }
 
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 648ece5..0d3dfae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -69,7 +69,6 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.Looper;
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -91,6 +90,7 @@
 import android.view.IRemoteAnimationRunner;
 import android.view.IWindowManager;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.view.ThreadedRenderer;
 import android.view.View;
 import android.view.ViewGroup;
@@ -135,7 +135,9 @@
 import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuController;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.assist.AssistManager;
+import com.android.systemui.back.domain.interactor.BackActionInteractor;
 import com.android.systemui.biometrics.AuthRippleController;
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.camera.CameraIntents;
 import com.android.systemui.charging.WiredChargingRippleController;
@@ -157,7 +159,6 @@
 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;
@@ -172,8 +173,8 @@
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.PluginManager;
 import com.android.systemui.plugins.qs.QS;
-import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.qs.QSFragment;
 import com.android.systemui.qs.QSPanelController;
 import com.android.systemui.recents.ScreenPinningRequest;
@@ -181,7 +182,6 @@
 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;
@@ -311,7 +311,7 @@
     private final DeviceStateManager mDeviceStateManager;
     private CentralSurfacesCommandQueueCallbacks mCommandQueueCallbacks;
     private float mTransitionToFullShadeProgress = 0f;
-    private NotificationListContainer mNotifListContainer;
+    private final NotificationListContainer mNotifListContainer;
     private boolean mIsShortcutListSearchEnabled;
 
     private final KeyguardStateController.Callback mKeyguardStateControllerCallback =
@@ -362,26 +362,6 @@
     }
 
     @Override
-    public int getDisabled1() {
-        return mDisabled1;
-    }
-
-    @Override
-    public void setDisabled1(int disabled) {
-        mDisabled1 = disabled;
-    }
-
-    @Override
-    public int getDisabled2() {
-        return mDisabled2;
-    }
-
-    @Override
-    public void setDisabled2(int disabled) {
-        mDisabled2 = disabled;
-    }
-
-    @Override
     public void setLastCameraLaunchSource(int source) {
         mLastCameraLaunchSource = source;
     }
@@ -411,23 +391,6 @@
         return mQSPanelController;
     }
 
-    /** */
-    @Override
-    public void animateExpandNotificationsPanel() {
-        mCommandQueueCallbacks.animateExpandNotificationsPanel();
-    }
-
-    /** */
-    @Override
-    public void animateExpandSettingsPanel(@Nullable String subpanel) {
-        mCommandQueueCallbacks.animateExpandSettingsPanel(subpanel);
-    }
-
-    /** */
-    @Override
-    public void togglePanel() {
-        mCommandQueueCallbacks.togglePanel();
-    }
     /**
      * The {@link StatusBarState} of the status bar.
      */
@@ -455,7 +418,7 @@
     private PhoneStatusBarTransitions mStatusBarTransitions;
     private final AuthRippleController mAuthRippleController;
     @WindowVisibleState private int mStatusBarWindowState = WINDOW_STATE_SHOWING;
-    protected final NotificationShadeWindowController mNotificationShadeWindowController;
+    private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final StatusBarInitializer mStatusBarInitializer;
     private final StatusBarWindowController mStatusBarWindowController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@@ -464,8 +427,6 @@
     private final LightRevealScrim mLightRevealScrim;
     private PowerButtonReveal mPowerButtonReveal;
 
-    private boolean mWakeUpComingFromTouch;
-
     /**
      * Whether we should delay the wakeup animation (which shows the notifications and moves the
      * clock view). This is typically done when waking up from a 'press to unlock' gesture on a
@@ -505,7 +466,6 @@
     private final AlternateBouncerInteractor mAlternateBouncerInteractor;
 
     private final PluginDependencyProvider mPluginDependencyProvider;
-    private final KeyguardDismissUtil mKeyguardDismissUtil;
     private final ExtensionController mExtensionController;
     private final UserInfoControllerImpl mUserInfoControllerImpl;
     private final DemoModeController mDemoModeController;
@@ -516,14 +476,12 @@
     private final Lazy<LightRevealScrimViewModel> mLightRevealScrimViewModelLazy;
 
     /** Controller for the Shade. */
-    @VisibleForTesting
-    ShadeSurface mShadeSurface;
+    private final ShadeSurface mShadeSurface;
     private final ShadeLogger mShadeLogger;
 
     // settings
     private QSPanelController mQSPanelController;
-    @VisibleForTesting
-    QuickSettingsController mQsController;
+    private final QuickSettingsController mQsController;
 
     KeyguardIndicationController mKeyguardIndicationController;
 
@@ -549,11 +507,6 @@
 
     private CentralSurfacesComponent mCentralSurfacesComponent;
 
-    // Flags for disabling the status bar
-    // Two variables because the first one evidently ran out of room for new flags.
-    private int mDisabled1 = 0;
-    private int mDisabled2 = 0;
-
     /**
      * This keeps track of whether we have (or haven't) registered the predictive back callback.
      * Since we can have visible -> visible transitions, we need to avoid
@@ -627,6 +580,7 @@
     private final ViewMediatorCallback mKeyguardViewMediatorCallback;
     private final ScrimController mScrimController;
     protected DozeScrimController mDozeScrimController;
+    private final BackActionInteractor mBackActionInteractor;
     private final Executor mUiBgExecutor;
 
     protected boolean mDozing;
@@ -655,7 +609,6 @@
     private final UserSwitcherController mUserSwitcherController;
     private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
     protected final BatteryController mBatteryController;
-    protected boolean mPanelExpanded;
     private UiModeManager mUiModeManager;
     private LogMaker mStatusBarStateLog;
     protected final NotificationIconAreaController mNotificationIconAreaController;
@@ -663,6 +616,7 @@
     private final SysuiColorExtractor mColorExtractor;
     private final ScreenLifecycle mScreenLifecycle;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
+    protected final PowerInteractor mPowerInteractor;
 
     private boolean mNoAnimationOnNextBarModeChange;
     private final SysuiStatusBarStateController mStatusBarStateController;
@@ -677,7 +631,8 @@
     private final Optional<StartingSurface> mStartingSurfaceOptional;
 
     private final ActivityIntentHelper mActivityIntentHelper;
-    private NotificationStackScrollLayoutController mStackScrollerController;
+
+    private final NotificationStackScrollLayoutController mStackScrollerController;
 
     private final ColorExtractor.OnColorsChangedListener mOnColorsChangedListener =
             (extractor, which) -> updateTheme();
@@ -685,17 +640,7 @@
     private final InteractionJankMonitor mJankMonitor;
 
     /** Existing callback that handles back gesture invoked for the Shade. */
-    private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
-        if (DEBUG) {
-            Log.d(TAG, "mOnBackInvokedCallback() called");
-        }
-        onBackPressed();
-    };
-
-    private boolean shouldBackBeHandled() {
-        return (mState != StatusBarState.KEYGUARD && mState != StatusBarState.SHADE_LOCKED
-                && !isBouncerShowingOverDream());
-    }
+    private final OnBackInvokedCallback mOnBackInvokedCallback;
 
     /**
      *  New callback that handles back gesture invoked, cancel, progress
@@ -705,12 +650,12 @@
     private final OnBackAnimationCallback mOnBackAnimationCallback = new OnBackAnimationCallback() {
         @Override
         public void onBackInvoked() {
-            onBackPressed();
+            mBackActionInteractor.onBackRequested();
         }
 
         @Override
         public void onBackProgressed(BackEvent event) {
-            if (shouldBackBeHandled()) {
+            if (mBackActionInteractor.shouldBackBeHandled()) {
                 if (mShadeSurface.canBeCollapsed()) {
                     float fraction = event.getProgress();
                     mShadeSurface.onBackProgressed(fraction);
@@ -756,14 +701,17 @@
             MetricsLogger metricsLogger,
             ShadeLogger shadeLogger,
             @UiBackground Executor uiBgExecutor,
+            ShadeSurface shadeSurface,
             NotificationMediaManager notificationMediaManager,
             NotificationLockscreenUserManager lockScreenUserManager,
             NotificationRemoteInputManager remoteInputManager,
+            QuickSettingsController quickSettingsController,
             UserSwitcherController userSwitcherController,
             BatteryController batteryController,
             SysuiColorExtractor colorExtractor,
             ScreenLifecycle screenLifecycle,
             WakefulnessLifecycle wakefulnessLifecycle,
+            PowerInteractor powerInteractor,
             SysuiStatusBarStateController statusBarStateController,
             Optional<Bubbles> bubblesOptional,
             Lazy<NoteTaskController> noteTaskControllerLazy,
@@ -773,12 +721,15 @@
             Lazy<AssistManager> assistManagerLazy,
             ConfigurationController configurationController,
             NotificationShadeWindowController notificationShadeWindowController,
+            NotificationShelfController notificationShelfController,
+            NotificationStackScrollLayoutController notificationStackScrollLayoutController,
             DozeParameters dozeParameters,
             ScrimController scrimController,
             Lazy<LockscreenWallpaper> lockscreenWallpaperLazy,
             Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
             AuthRippleController authRippleController,
             DozeServiceHost dozeServiceHost,
+            BackActionInteractor backActionInteractor,
             PowerManager powerManager,
             ScreenPinningRequest screenPinningRequest,
             DozeScrimController dozeScrimController,
@@ -792,7 +743,6 @@
             InitController initController,
             @Named(TIME_TICK_HANDLER_NAME) Handler timeTickHandler,
             PluginDependencyProvider pluginDependencyProvider,
-            KeyguardDismissUtil keyguardDismissUtil,
             ExtensionController extensionController,
             UserInfoControllerImpl userInfoControllerImpl,
             PhoneStatusBarPolicy phoneStatusBarPolicy,
@@ -839,6 +789,7 @@
         mKeyguardBypassController = keyguardBypassController;
         mKeyguardStateController = keyguardStateController;
         mHeadsUpManager = headsUpManagerPhone;
+        mBackActionInteractor = backActionInteractor;
         mKeyguardIndicationController = keyguardIndicationController;
         mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
         mFalsingCollector = falsingCollector;
@@ -853,14 +804,17 @@
         mMetricsLogger = metricsLogger;
         mShadeLogger = shadeLogger;
         mUiBgExecutor = uiBgExecutor;
+        mShadeSurface = shadeSurface;
         mMediaManager = notificationMediaManager;
         mLockscreenUserManager = lockScreenUserManager;
         mRemoteInputManager = remoteInputManager;
+        mQsController = quickSettingsController;
         mUserSwitcherController = userSwitcherController;
         mBatteryController = batteryController;
         mColorExtractor = colorExtractor;
         mScreenLifecycle = screenLifecycle;
         mWakefulnessLifecycle = wakefulnessLifecycle;
+        mPowerInteractor = powerInteractor;
         mStatusBarStateController = statusBarStateController;
         mBubblesOptional = bubblesOptional;
         mNoteTaskControllerLazy = noteTaskControllerLazy;
@@ -870,6 +824,10 @@
         mAssistManagerLazy = assistManagerLazy;
         mConfigurationController = configurationController;
         mNotificationShadeWindowController = notificationShadeWindowController;
+        mNotificationShelfController = notificationShelfController;
+        mStackScrollerController = notificationStackScrollLayoutController;
+        mStackScroller = mStackScrollerController.getView();
+        mNotifListContainer = mStackScrollerController.getNotificationListContainer();
         mDozeServiceHost = dozeServiceHost;
         mPowerManager = powerManager;
         mDozeParameters = dozeParameters;
@@ -889,7 +847,6 @@
         mKeyguardViewMediatorCallback = viewMediatorCallback;
         mInitController = initController;
         mPluginDependencyProvider = pluginDependencyProvider;
-        mKeyguardDismissUtil = keyguardDismissUtil;
         mExtensionController = extensionController;
         mUserInfoControllerImpl = userInfoControllerImpl;
         mIconPolicy = phoneStatusBarPolicy;
@@ -958,6 +915,12 @@
         }
         // Based on teamfood flag, enable predictive back animation for the Shade.
         mAnimateBack = mFeatureFlags.isEnabled(Flags.WM_SHADE_ANIMATE_BACK_GESTURE);
+        mOnBackInvokedCallback = () -> {
+            if (DEBUG) {
+                Log.d(TAG, "mOnBackInvokedCallback() called");
+            }
+            mBackActionInteractor.onBackRequested();
+        };
     }
 
     private void initBubbles(Bubbles bubbles) {
@@ -1151,7 +1114,8 @@
                     @Override
                     public void onPluginConnected(OverlayPlugin plugin, Context pluginContext) {
                         mMainExecutor.execute(
-                                () -> plugin.setup(getNotificationShadeWindowView(),
+                                () -> plugin.setup(
+                                        mNotificationShadeWindowController.getWindowRootView(),
                                         getNavigationBarView(),
                                         new Callback(plugin), mDozeParameters));
                     }
@@ -1551,22 +1515,15 @@
 
     @VisibleForTesting
     void onShadeExpansionFullyChanged(Boolean isExpanded) {
-        if (mPanelExpanded != isExpanded) {
-            mPanelExpanded = isExpanded;
-            if (getShadeViewController() != null) {
-                // Needed to update SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE
-                getShadeViewController().updateSystemUiStateFlags();
+        if (isExpanded && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) {
+            if (DEBUG) {
+                Log.v(TAG, "clearing notification effects from Height");
             }
-            if (isExpanded && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) {
-                if (DEBUG) {
-                    Log.v(TAG, "clearing notification effects from Height");
-                }
-                clearNotificationEffects();
-            }
+            clearNotificationEffects();
+        }
 
-            if (!isExpanded) {
-                mRemoteInputManager.onPanelCollapsed();
-            }
+        if (!isExpanded) {
+            mRemoteInputManager.onPanelCollapsed();
         }
     }
 
@@ -1604,12 +1561,10 @@
         mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter);
         mShadeController.setNotificationPresenter(mPresenter);
         mNotificationsController.initialize(
-                this,
                 mPresenter,
                 mNotifListContainer,
                 mStackScrollerController.getNotifStackController(),
-                mNotificationActivityStarter,
-                mCentralSurfacesComponent.getBindRowCallback());
+                mNotificationActivityStarter);
     }
 
     /**
@@ -1621,22 +1576,6 @@
         mCommandQueue.disable(mDisplayId, state1, state2, false /* animate */);
     }
 
-    /**
-     * Ask the display to wake up if currently dozing, else do nothing
-     *
-     * @param time when to wake up
-     * @param why the reason for the wake up
-     */
-    @Override
-    public void wakeUpIfDozing(long time, String why, @PowerManager.WakeReason int wakeReason) {
-        if (mDozing && mScreenOffAnimationController.allowWakeUpIfDozing()) {
-            mPowerManager.wakeUp(
-                    time, wakeReason, "com.android.systemui:" + why);
-            mWakeUpComingFromTouch = true;
-            mFalsingCollector.onScreenOnFromTouch();
-        }
-    }
-
     // TODO(b/117478341): This was left such that CarStatusBar can override this method.
     // Try to remove this.
     protected void createNavigationBar(@Nullable RegisterStatusBarResult result) {
@@ -1665,27 +1604,20 @@
                 CollapsedStatusBarFragment.class,
                 mCentralSurfacesComponent::createCollapsedStatusBarFragment);
 
+        ViewGroup windowRootView = mCentralSurfacesComponent.getWindowRootView();
         mNotificationShadeWindowView = mCentralSurfacesComponent.getNotificationShadeWindowView();
         mNotificationShadeWindowViewController = mCentralSurfacesComponent
                 .getNotificationShadeWindowViewController();
         // TODO(b/277762009): Inject [NotificationShadeWindowView] directly into the controller.
         //  (Right now, there's a circular dependency.)
-        mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
+        mNotificationShadeWindowController.setWindowRootView(windowRootView);
         mNotificationShadeWindowViewController.setupExpandedStatusBar();
-        NotificationPanelViewController npvc =
-                mCentralSurfacesComponent.getNotificationPanelViewController();
-        mShadeSurface = npvc;
-        mShadeController.setNotificationPanelViewController(npvc);
+        mShadeController.setShadeViewController(mShadeSurface);
         mShadeController.setNotificationShadeWindowViewController(
                 mNotificationShadeWindowViewController);
-        mStackScrollerController =
-                mCentralSurfacesComponent.getNotificationStackScrollLayoutController();
-        mQsController = mCentralSurfacesComponent.getQuickSettingsController();
-        mStackScroller = mStackScrollerController.getView();
-        mNotifListContainer = mCentralSurfacesComponent.getNotificationListContainer();
+        mBackActionInteractor.setup(mQsController, mShadeSurface);
         mPresenter = mCentralSurfacesComponent.getNotificationPresenter();
         mNotificationActivityStarter = mCentralSurfacesComponent.getNotificationActivityStarter();
-        mNotificationShelfController = mCentralSurfacesComponent.getNotificationShelfController();
 
         mHeadsUpManager.addListener(mCentralSurfacesComponent.getStatusBarHeadsUpChangeListener());
 
@@ -1746,21 +1678,10 @@
 
         mLightBarController.setBiometricUnlockController(mBiometricUnlockController);
         mMediaManager.setBiometricUnlockController(mBiometricUnlockController);
-        mKeyguardDismissUtil.setDismissHandler(this::executeWhenUnlocked);
         Trace.endSection();
     }
 
     @Override
-    public NotificationShadeWindowView getNotificationShadeWindowView() {
-        return mNotificationShadeWindowView;
-    }
-
-    @Override
-    public NotificationShadeWindowViewController getNotificationShadeWindowViewController() {
-        return mNotificationShadeWindowViewController;
-    }
-
-    @Override
     public ShadeViewController getShadeViewController() {
         return mShadeSurface;
     }
@@ -1775,28 +1696,6 @@
         return mStatusBarWindowController.getStatusBarHeight();
     }
 
-    /**
-     * Disable QS if device not provisioned.
-     * If the user switcher is simple then disable QS during setup because
-     * the user intends to use the lock screen user switcher, QS in not needed.
-     */
-    @Override
-    public void updateQsExpansionEnabled() {
-        final boolean expandEnabled = mDeviceProvisionedController.isDeviceProvisioned()
-                && (mUserSetup || mUserSwitcherController == null
-                        || !mUserSwitcherController.isSimpleUserSwitcher())
-                && !isShadeDisabled()
-                && ((mDisabled2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) == 0)
-                && !mDozing;
-        mQsController.setExpansionEnabledPolicy(expandEnabled);
-        Log.d(TAG, "updateQsExpansionEnabled - QS Expand enabled: " + expandEnabled);
-    }
-
-    @Override
-    public boolean isShadeDisabled() {
-        return (mDisabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0;
-    }
-
     private void updateReportRejectedTouchVisibility() {
         if (mReportRejectedTouch == null) {
             return;
@@ -1805,11 +1704,6 @@
                 && mFalsingCollector.isReportingEnabled() ? View.VISIBLE : View.INVISIBLE);
     }
 
-    @Override
-    public boolean areNotificationAlertsDisabled() {
-        return (mDisabled1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0;
-    }
-
     /**
      * Whether we are currently animating an activity launch above the lockscreen (occluding
      * activity).
@@ -1819,11 +1713,6 @@
         return mIsLaunchingActivityOverLockscreen;
     }
 
-    @Override
-    public boolean isWakeUpComingFromTouch() {
-        return mWakeUpComingFromTouch;
-    }
-
     /**
      * To be called when there's a state change in StatusBarKeyguardViewManager.
      */
@@ -1833,11 +1722,6 @@
     }
 
     @Override
-    public ViewGroup getNotificationScrollLayout() {
-        return mStackScroller;
-    }
-
-    @Override
     public boolean isPulsing() {
         return mDozeServiceHost.isPulsing();
     }
@@ -1853,58 +1737,6 @@
         return mKeyguardStateController.isOccluded();
     }
 
-    /** A launch animation was cancelled. */
-    //TODO: These can / should probably be moved to NotificationPresenter or ShadeController
-    @Override
-    public void onLaunchAnimationCancelled(boolean isLaunchForActivity) {
-        if (mPresenter.isPresenterFullyCollapsed() && !mPresenter.isCollapsing()
-                && isLaunchForActivity) {
-            mShadeController.onClosingFinished();
-        } else {
-            mShadeController.collapseShade(true /* animate */);
-        }
-    }
-
-    /** A launch animation ended. */
-    @Override
-    public void onLaunchAnimationEnd(boolean launchIsFullScreen) {
-        if (!mPresenter.isCollapsing()) {
-            mShadeController.onClosingFinished();
-        }
-        if (launchIsFullScreen) {
-            mShadeController.instantCollapseShade();
-        }
-    }
-
-    /**
-     * Whether we should animate an activity launch.
-     *
-     * Note: This method must be called *before* dismissing the keyguard.
-     */
-    @Override
-    public boolean shouldAnimateLaunch(boolean isActivityIntent, boolean showOverLockscreen) {
-        // TODO(b/184121838): Support launch animations when occluded.
-        if (isOccluded()) {
-            return false;
-        }
-
-        // Always animate if we are not showing the keyguard or if we animate over the lockscreen
-        // (without unlocking it).
-        if (showOverLockscreen || !mKeyguardStateController.isShowing()) {
-            return true;
-        }
-
-        // We don't animate non-activity launches as they can break the animation.
-        // TODO(b/184121838): Support non activity launches on the lockscreen.
-        return isActivityIntent;
-    }
-
-    /** Whether we should animate an activity launch. */
-    @Override
-    public boolean shouldAnimateLaunch(boolean isActivityIntent) {
-        return shouldAnimateLaunch(isActivityIntent, false /* showOverLockscreen */);
-    }
-
     @Override
     public boolean isDeviceInVrMode() {
         return mPresenter.isDeviceInVrMode();
@@ -1957,30 +1789,9 @@
                     SystemClock.uptimeMillis(),
                     PowerManager.WAKE_REASON_APPLICATION,
                     "com.android.systemui:full_screen_intent");
-            mWakeUpComingFromTouch = false;
         }
     }
 
-    @Override
-    public void postAnimateCollapsePanels() {
-        mMainExecutor.execute(mShadeController::animateCollapseShade);
-    }
-
-    @Override
-    public void postAnimateForceCollapsePanels() {
-        mMainExecutor.execute(mShadeController::animateCollapseShadeForced);
-    }
-
-    @Override
-    public void postAnimateOpenPanels() {
-        mMessageRouter.sendMessage(MSG_OPEN_SETTINGS_PANEL);
-    }
-
-    @Override
-    public boolean isPanelExpanded() {
-        return mPanelExpanded;
-    }
-
     /**
      * Called when another window is about to transfer it's input focus.
      */
@@ -1998,11 +1809,8 @@
     }
 
     @Override
-    public void animateCollapseQuickSettings() {
-        if (mState == StatusBarState.SHADE) {
-            mShadeSurface.collapse(
-                    true, false /* delayed */, 1.0f /* speedUpFactor */);
-        }
+    public void onStatusBarTrackpadEvent(MotionEvent event) {
+        mShadeSurface.handleExternalTouch(event);
     }
 
     private void onExpandedInvisible() {
@@ -2256,8 +2064,7 @@
                 + CameraIntents.getOverrideCameraPackage(mContext));
     }
 
-    @Override
-    public void createAndAddWindows(@Nullable RegisterStatusBarResult result) {
+    private void createAndAddWindows(@Nullable RegisterStatusBarResult result) {
         makeStatusBarView(result);
         mNotificationShadeWindowController.attach();
         mStatusBarWindowController.attach();
@@ -2274,16 +2081,19 @@
     }
 
     @Override
+    @Deprecated
     public float getDisplayDensity() {
         return mDisplayMetrics.density;
     }
 
     @Override
+    @Deprecated
     public float getDisplayWidth() {
         return mDisplayMetrics.widthPixels;
     }
 
     @Override
+    @Deprecated
     public float getDisplayHeight() {
         return mDisplayMetrics.heightPixels;
     }
@@ -2341,7 +2151,7 @@
                     mNotificationShadeWindowController.setNotTouchable(false);
                 }
                 finishBarAnimations();
-                resetUserExpandedStates();
+                mNotificationsController.resetUserExpandedStates();
             }
             Trace.endSection();
         }
@@ -2360,20 +2170,6 @@
         }
     };
 
-    @Override
-    public void resetUserExpandedStates() {
-        mNotificationsController.resetUserExpandedStates();
-    }
-
-    private void executeWhenUnlocked(OnDismissAction action, boolean requiresShadeOpen,
-            boolean afterKeyguardGone) {
-        if (mKeyguardStateController.isShowing() && requiresShadeOpen) {
-            mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
-        }
-        mActivityStarter.dismissKeyguardThenExecute(action, null /* cancelAction */,
-                afterKeyguardGone /* afterKeyguardGone */);
-    }
-
     /**
      * Notify the shade controller that the current user changed
      *
@@ -2381,10 +2177,6 @@
      */
     @Override
     public void setLockscreenUser(int newUserId) {
-        if (mLockscreenWallpaper != null && !mWallpaperManager.isLockscreenLiveWallpaperEnabled()) {
-            mLockscreenWallpaper.setCurrentUser(newUserId);
-        }
-        mScrimController.setCurrentUser(newUserId);
         if (mWallpaperSupported) {
             mWallpaperChangedReceiver.onReceive(mContext, null);
         }
@@ -2860,7 +2652,6 @@
                 || (mDozing && mDozeParameters.shouldControlScreenOff() && keyguardVisibleOrWillBe);
 
         mShadeSurface.setDozing(mDozing, animate);
-        updateQsExpansionEnabled();
         Trace.endSection();
     }
 
@@ -2889,7 +2680,7 @@
             case KeyEvent.KEYCODE_BACK:
                 if (mState == StatusBarState.KEYGUARD
                         && mStatusBarKeyguardViewManager.dispatchBackKeyEventPreIme()) {
-                    return onBackPressed();
+                    return mBackActionInteractor.onBackRequested();
                 }
         }
         return false;
@@ -2929,34 +2720,6 @@
     }
 
     @Override
-    public boolean onBackPressed() {
-        if (mStatusBarKeyguardViewManager.canHandleBackPressed()) {
-            mStatusBarKeyguardViewManager.onBackPressed();
-            return true;
-        }
-        if (mQsController.isCustomizing()) {
-            mQsController.closeQsCustomizer();
-            return true;
-        }
-        if (mQsController.getExpanded()) {
-            mShadeSurface.animateCollapseQs(false);
-            return true;
-        }
-        if (mShadeSurface.closeUserSwitcherIfOpen()) {
-            return true;
-        }
-        if (shouldBackBeHandled()) {
-            if (mShadeSurface.canBeCollapsed()) {
-                // this is the Shade dismiss animation, so make sure QQS closes when it ends.
-                mShadeSurface.onBackPressed();
-                mShadeController.animateCollapseShade();
-            }
-            return true;
-        }
-        return false;
-    }
-
-    @Override
     public boolean onSpacePressed() {
         if (mDeviceInteractive && mState != StatusBarState.SHADE) {
             mShadeController.animateCollapseShadeForced();
@@ -2999,19 +2762,6 @@
     }
 
     /**
-     * Collapse the panel directly if we are on the main thread, post the collapsing on the main
-     * thread if we are not.
-     */
-    @Override
-    public void collapsePanelOnMainThread() {
-        if (Looper.getMainLooper().isCurrentThread()) {
-            mShadeController.collapseShade();
-        } else {
-            mContext.getMainExecutor().execute(mShadeController::collapseShade);
-        }
-    }
-
-    /**
      * Updates the light reveal effect to reflect the reason we're waking or sleeping (for example,
      * from the power button).
      * @param wakingUp Whether we're updating because we're waking up (true) or going to sleep
@@ -3047,11 +2797,6 @@
         }
     }
 
-    @Override
-    public LightRevealScrim getLightRevealScrim() {
-        return mLightRevealScrim;
-    }
-
     // TODO: Figure out way to remove these.
     @Override
     public NavigationBarView getNavigationBarView() {
@@ -3074,9 +2819,8 @@
     }
 
     protected ViewRootImpl getViewRootImpl()  {
-        NotificationShadeWindowView nswv = getNotificationShadeWindowView();
-        if (nswv != null) return nswv.getViewRootImpl();
-
+        View root = mNotificationShadeWindowController.getWindowRootView();
+        if (root != null) return root.getViewRootImpl();
         return null;
     }
     /**
@@ -3087,12 +2831,12 @@
         mBouncerShowing = bouncerShowing;
         mKeyguardBypassController.setBouncerShowing(bouncerShowing);
         mPulseExpansionHandler.setBouncerShowing(bouncerShowing);
+        mStackScrollerController.setBouncerShowingFromCentralSurfaces(bouncerShowing);
         setBouncerShowingForStatusBarComponents(bouncerShowing);
         mStatusBarHideIconsForBouncerManager.setBouncerShowingAndTriggerUpdate(bouncerShowing);
         mCommandQueue.recomputeDisableFlags(mDisplayId, true /* animate */);
         if (mBouncerShowing) {
-            wakeUpIfDozing(SystemClock.uptimeMillis(), "BOUNCER_VISIBLE",
-                    PowerManager.WAKE_REASON_GESTURE);
+            mPowerInteractor.wakeUpIfDozing("BOUNCER_VISIBLE", PowerManager.WAKE_REASON_GESTURE);
         }
         updateScrimController();
         if (!mBouncerShowing) {
@@ -3101,17 +2845,6 @@
     }
 
     /**
-     * Sets whether the bouncer over dream is showing. Note that the bouncer over dream is handled
-     * independently of the rest of the notification panel. As a result, setting this state via
-     * {@link #setBouncerShowing(boolean)} leads to unintended side effects from states modified
-     * behind the dream.
-     */
-    @Override
-    public void setBouncerShowingOverDream(boolean bouncerShowingOverDream) {
-        mBouncerShowingOverDream = bouncerShowingOverDream;
-    }
-
-    /**
      * Propagate the bouncer state to status bar components.
      *
      * Separate from {@link #setBouncerShowing} because we sometimes re-create the status bar and
@@ -3128,19 +2861,6 @@
         mShadeSurface.setBouncerShowing(bouncerShowing);
     }
 
-    /**
-     * Collapses the notification shade if it is tracking or expanded.
-     */
-    @Override
-    public void collapseShade() {
-        if (mShadeSurface.isTracking()) {
-            mNotificationShadeWindowViewController.cancelCurrentTouch();
-        }
-        if (mPanelExpanded && mState == StatusBarState.SHADE) {
-            mShadeController.animateCollapseShade();
-        }
-    }
-
     @VisibleForTesting
     final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
         @Override
@@ -3149,7 +2869,6 @@
             releaseGestureWakeLock();
             mLaunchCameraWhenFinishedWaking = false;
             mDeviceInteractive = false;
-            mWakeUpComingFromTouch = false;
             updateVisibleToUser();
 
             updateNotificationPanelTouchState();
@@ -3505,7 +3224,7 @@
         mScrimController.setExpansionAffectsAlpha(!unlocking);
 
         if (mAlternateBouncerInteractor.isVisibleState()) {
-            if ((!isOccluded() || isPanelExpanded())
+            if ((!isOccluded() || mShadeSurface.isPanelExpanded())
                     && (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED
                     || mTransitionToFullShadeProgress > 0f)) {
                 mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED_SHADE);
@@ -3572,7 +3291,7 @@
     protected IStatusBarService mBarService;
 
     // all notifications
-    protected NotificationStackScrollLayout mStackScroller;
+    private final NotificationStackScrollLayout mStackScroller;
 
     protected AccessibilityManager mAccessibilityManager;
 
@@ -3602,7 +3321,7 @@
     protected Display mDisplay;
     private int mDisplayId;
 
-    protected NotificationShelfController mNotificationShelfController;
+    private final NotificationShelfController mNotificationShelfController;
 
     private final Lazy<AssistManager> mAssistManagerLazy;
 
@@ -3635,12 +3354,6 @@
     };
 
     @Override
-    public void setNotificationSnoozed(StatusBarNotification sbn, SnoozeOption snoozeOption) {
-        mNotificationsController.setNotificationSnoozed(sbn, snoozeOption);
-    }
-
-
-    @Override
     public void awakenDreams() {
         mUiBgExecutor.execute(() -> {
             try {
@@ -3841,10 +3554,10 @@
 
             if (userSetup != mUserSetup) {
                 mUserSetup = userSetup;
-                if (!mUserSetup) {
-                    animateCollapseQuickSettings();
+                if (!mUserSetup && mState == StatusBarState.SHADE) {
+                    mShadeSurface.collapse(true /* animate */, false  /* delayed */,
+                            1.0f  /* speedUpFactor */);
                 }
-                updateQsExpansionEnabled();
             }
         }
     };
@@ -3979,7 +3692,6 @@
                     // reset them again when we're waking up
                     mShadeSurface.resetViews(dozingAnimated && isDozing);
 
-                    updateQsExpansionEnabled();
                     mKeyguardViewMediator.setDozing(mDozing);
 
                     updateDozingState();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardDismissUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardDismissUtil.java
index 27b68f2..1c90c0d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardDismissUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardDismissUtil.java
@@ -16,48 +16,41 @@
 
 package com.android.systemui.statusbar.phone;
 
-import android.util.Log;
-
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import javax.inject.Inject;
 
 /**
- * Executes actions that require the screen to be unlocked. Delegates the actual handling to an
- * implementation passed via {@link #setDismissHandler}.
+ * Executes actions that require the screen to be unlocked.
  */
 @SysUISingleton
 public class KeyguardDismissUtil implements KeyguardDismissHandler {
-    private static final String TAG = "KeyguardDismissUtil";
+    private final KeyguardStateController mKeyguardStateController;
 
-    private volatile KeyguardDismissHandler mDismissHandler;
+    private final SysuiStatusBarStateController mStatusBarStateController;
+
+    private final ActivityStarter mActivityStarter;
 
     @Inject
-    public KeyguardDismissUtil() {
+    public KeyguardDismissUtil(KeyguardStateController keyguardStateController,
+            SysuiStatusBarStateController statusBarStateController,
+            ActivityStarter activityStarter) {
+        mKeyguardStateController = keyguardStateController;
+        mStatusBarStateController = statusBarStateController;
+        mActivityStarter = activityStarter;
     }
 
-    /** Sets the actual {@link KeyguardDismissHandler} implementation. */
-    public void setDismissHandler(KeyguardDismissHandler dismissHandler) {
-        mDismissHandler = dismissHandler;
-    }
-
-    /**
-     * Executes an action that requires the screen to be unlocked.
-     *
-     * <p>Must be called after {@link #setDismissHandler}.
-     *
-     * @param requiresShadeOpen does the shade need to be forced open when hiding the keyguard?
-     */
     @Override
     public void executeWhenUnlocked(OnDismissAction action, boolean requiresShadeOpen,
             boolean afterKeyguardGone) {
-        KeyguardDismissHandler dismissHandler = mDismissHandler;
-        if (dismissHandler == null) {
-            Log.wtf(TAG, "KeyguardDismissHandler not set.");
-            action.onDismiss();
-            return;
+        if (mKeyguardStateController.isShowing() && requiresShadeOpen) {
+            mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
         }
-        dismissHandler.executeWhenUnlocked(action, requiresShadeOpen, afterKeyguardGone);
+        mActivityStarter.dismissKeyguardThenExecute(action, null /* cancelAction */,
+                afterKeyguardGone /* afterKeyguardGone */);
     }
 }
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 4d716c2..680f19a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -45,7 +45,7 @@
 import com.android.systemui.R;
 import com.android.systemui.battery.BatteryMeterViewController;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.log.LogLevel;
+import com.android.systemui.log.core.LogLevel;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.ShadeViewStateProvider;
 import com.android.systemui.statusbar.CommandQueue;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
index 5c357d7..686efb7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
@@ -21,7 +21,7 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent
 import com.android.systemui.log.dagger.LSShadeTransitionLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
index c07b5e0..b2c39f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
@@ -40,12 +40,17 @@
 
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.CoreStartable;
 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.settings.UserTracker;
 import com.android.systemui.statusbar.NotificationMediaManager;
+import com.android.systemui.user.data.model.SelectedUserModel;
+import com.android.systemui.user.data.model.SelectionStatus;
+import com.android.systemui.user.data.repository.UserRepository;
+import com.android.systemui.util.kotlin.JavaAdapter;
 
 import libcore.io.IoUtils;
 
@@ -59,7 +64,7 @@
  */
 @SysUISingleton
 public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implements Runnable,
-        Dumpable {
+        Dumpable, CoreStartable {
 
     private static final String TAG = "LockscreenWallpaper";
 
@@ -72,6 +77,8 @@
     private final WallpaperManager mWallpaperManager;
     private final KeyguardUpdateMonitor mUpdateMonitor;
     private final Handler mH;
+    private final JavaAdapter mJavaAdapter;
+    private final UserRepository mUserRepository;
 
     private boolean mCached;
     private Bitmap mCache;
@@ -88,6 +95,8 @@
             DumpManager dumpManager,
             NotificationMediaManager mediaManager,
             @Main Handler mainHandler,
+            JavaAdapter javaAdapter,
+            UserRepository userRepository,
             UserTracker userTracker) {
         dumpManager.registerDumpable(getClass().getSimpleName(), this);
         mWallpaperManager = wallpaperManager;
@@ -95,6 +104,8 @@
         mUpdateMonitor = keyguardUpdateMonitor;
         mMediaManager = mediaManager;
         mH = mainHandler;
+        mJavaAdapter = javaAdapter;
+        mUserRepository = userRepository;
 
         if (iWallpaperManager != null && !mWallpaperManager.isLockscreenLiveWallpaperEnabled()) {
             // Service is disabled on some devices like Automotive
@@ -106,6 +117,14 @@
         }
     }
 
+    @Override
+    public void start() {
+        if (!isLockscreenLiveWallpaperEnabled()) {
+            mJavaAdapter.alwaysCollectFlow(
+                    mUserRepository.getSelectedUser(), this::setSelectedUser);
+        }
+    }
+
     public Bitmap getBitmap() {
         assertLockscreenLiveWallpaperNotEnabled();
 
@@ -169,9 +188,15 @@
         }
     }
 
-    public void setCurrentUser(int user) {
+    private void setSelectedUser(SelectedUserModel selectedUserModel) {
         assertLockscreenLiveWallpaperNotEnabled();
 
+        if (selectedUserModel.getSelectionStatus().equals(SelectionStatus.SELECTION_IN_PROGRESS)) {
+            // Wait until the selection has finished before updating.
+            return;
+        }
+
+        int user = selectedUserModel.getUserInfo().id;
         if (user != mCurrentUserId) {
             if (mSelectedUser == null || user != mSelectedUser.getIdentifier()) {
                 mCached = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
index 313410a..2fd244e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
@@ -240,7 +240,7 @@
     private void reloadDimens(Context context) {
         Resources res = context.getResources();
         mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size);
-        mIconHPadding = res.getDimensionPixelSize(R.dimen.status_bar_icon_padding);
+        mIconHPadding = res.getDimensionPixelSize(R.dimen.status_bar_icon_horizontal_margin);
         mAodIconAppearTranslation = res.getDimensionPixelSize(
                 R.dimen.shelf_appear_translation);
     }
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 e6b76ad..3b5aaea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -48,6 +48,7 @@
 import com.android.systemui.dagger.qualifiers.DisplayId;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
+import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor;
 import com.android.systemui.privacy.PrivacyItem;
 import com.android.systemui.privacy.PrivacyItemController;
 import com.android.systemui.privacy.PrivacyType;
@@ -74,6 +75,7 @@
 import com.android.systemui.statusbar.policy.UserInfoController;
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.util.RingerModeTracker;
+import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.time.DateFormatUtil;
 
 import java.io.PrintWriter;
@@ -121,9 +123,12 @@
     private final String mSlotCamera;
     private final String mSlotSensorsOff;
     private final String mSlotScreenRecord;
+    private final String mSlotConnectedDisplay;
     private final int mDisplayId;
     private final SharedPreferences mSharedPreferences;
     private final DateFormatUtil mDateFormatUtil;
+    private final JavaAdapter mJavaAdapter;
+    private final ConnectedDisplayInteractor mConnectedDisplayInteractor;
     private final TelecomManager mTelecomManager;
 
     private final Handler mHandler;
@@ -182,9 +187,13 @@
             @Main SharedPreferences sharedPreferences, DateFormatUtil dateFormatUtil,
             RingerModeTracker ringerModeTracker,
             PrivacyItemController privacyItemController,
-            PrivacyLogger privacyLogger) {
+            PrivacyLogger privacyLogger,
+            ConnectedDisplayInteractor connectedDisplayInteractor,
+            JavaAdapter javaAdapter
+    ) {
         mIconController = iconController;
         mCommandQueue = commandQueue;
+        mConnectedDisplayInteractor = connectedDisplayInteractor;
         mBroadcastDispatcher = broadcastDispatcher;
         mHandler = new Handler(looper);
         mResources = resources;
@@ -211,8 +220,11 @@
         mTelecomManager = telecomManager;
         mRingerModeTracker = ringerModeTracker;
         mPrivacyLogger = privacyLogger;
+        mJavaAdapter = javaAdapter;
 
         mSlotCast = resources.getString(com.android.internal.R.string.status_bar_cast);
+        mSlotConnectedDisplay = resources.getString(
+                com.android.internal.R.string.status_bar_connected_display);
         mSlotHotspot = resources.getString(com.android.internal.R.string.status_bar_hotspot);
         mSlotBluetooth = resources.getString(com.android.internal.R.string.status_bar_bluetooth);
         mSlotTty = resources.getString(com.android.internal.R.string.status_bar_tty);
@@ -285,6 +297,10 @@
         mIconController.setIcon(mSlotCast, R.drawable.stat_sys_cast, null);
         mIconController.setIconVisibility(mSlotCast, false);
 
+        // connected display
+        mIconController.setIcon(mSlotConnectedDisplay, R.drawable.stat_sys_connected_display, null);
+        mIconController.setIconVisibility(mSlotConnectedDisplay, false);
+
         // hotspot
         mIconController.setIcon(mSlotHotspot, R.drawable.stat_sys_hotspot,
                 mResources.getString(R.string.accessibility_status_bar_hotspot));
@@ -342,6 +358,8 @@
         mSensorPrivacyController.addCallback(mSensorPrivacyListener);
         mLocationController.addCallback(this);
         mRecordingController.addCallback(this);
+        mJavaAdapter.alwaysCollectFlow(mConnectedDisplayInteractor.getConnectedDisplayState(),
+                this::onConnectedDisplayAvailabilityChanged);
 
         mCommandQueue.addCallback(this);
     }
@@ -800,4 +818,14 @@
         if (DEBUG) Log.d(TAG, "screenrecord: hiding icon");
         mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false));
     }
+
+    private void onConnectedDisplayAvailabilityChanged(ConnectedDisplayInteractor.State state) {
+        boolean visible = state != ConnectedDisplayInteractor.State.DISCONNECTED;
+
+        if (DEBUG) {
+            Log.d(TAG, "connected_display: " + (visible ? "showing" : "hiding") + " icon");
+        }
+
+        mIconController.setIconVisibility(mSlotConnectedDisplay, visible);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index e6cb68f..f0fc143 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -176,6 +176,7 @@
                     shadeLogger.logMotionEvent(event, "top edge touch ignored")
                     return true
                 }
+                centralSurfaces.shadeViewController.startTrackingExpansionFromStatusBar()
             }
             return centralSurfaces.shadeViewController.handleExternalTouch(event)
         }
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 25ecf1a..c16e13c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -59,7 +59,7 @@
 import com.android.systemui.flags.Flags;
 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.bouncer.shared.constants.KeyguardBouncerConstants;
 import com.android.systemui.keyguard.shared.model.ScrimAlpha;
 import com.android.systemui.keyguard.shared.model.TransitionState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -881,24 +881,11 @@
                     mBehindAlpha = 1;
                     mNotificationsAlpha = behindFraction * mDefaultScrimAlpha;
                 } else {
-                    if (mFeatureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) {
-                        mBehindAlpha = mLargeScreenShadeInterpolator.getBehindScrimAlpha(
-                                mPanelExpansionFraction * mDefaultScrimAlpha);
-                        mNotificationsAlpha =
-                                mLargeScreenShadeInterpolator.getNotificationScrimAlpha(
-                                        mPanelExpansionFraction);
-                    } else {
-                        // Behind scrim will finish fading in at 30% expansion.
-                        float behindFraction = MathUtils
-                                .constrainedMap(0f, 1f, 0f, 0.3f, mPanelExpansionFraction);
-                        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);
-                    }
+                    mBehindAlpha = mLargeScreenShadeInterpolator.getBehindScrimAlpha(
+                            mPanelExpansionFraction * mDefaultScrimAlpha);
+                    mNotificationsAlpha =
+                            mLargeScreenShadeInterpolator.getNotificationScrimAlpha(
+                                    mPanelExpansionFraction);
                 }
                 mBehindTint = mState.getBehindTint();
                 mInFrontAlpha = 0;
@@ -1489,10 +1476,6 @@
         }
     }
 
-    public void setCurrentUser(int currentUser) {
-        // Don't care in the base class.
-    }
-
     private void updateThemeColors() {
         if (mScrimBehind == null) return;
         int background = Utils.getColorAttr(mScrimBehind.getContext(),
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 a8a834f..b14fe90 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -173,7 +173,7 @@
      */
     class DarkIconManager extends IconManager {
         private final DarkIconDispatcher mDarkIconDispatcher;
-        private int mIconHPadding;
+        private final int mIconHorizontalMargin;
 
         public DarkIconManager(
                 LinearLayout linearLayout,
@@ -189,8 +189,8 @@
                     wifiUiAdapter,
                     mobileUiAdapter,
                     mobileContextProvider);
-            mIconHPadding = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.status_bar_icon_padding);
+            mIconHorizontalMargin = mContext.getResources().getDimensionPixelSize(
+                    R.dimen.status_bar_icon_horizontal_margin);
             mDarkIconDispatcher = darkIconDispatcher;
         }
 
@@ -205,7 +205,7 @@
         protected LayoutParams onCreateLayoutParams() {
             LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                     ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
-            lp.setMargins(mIconHPadding, 0, mIconHPadding, 0);
+            lp.setMargins(mIconHorizontalMargin, 0, mIconHorizontalMargin, 0);
             return lp;
         }
 
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 1bf63be..cb2a78d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -18,7 +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.bouncer.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;
@@ -53,16 +53,16 @@
 import com.android.keyguard.KeyguardViewController;
 import com.android.keyguard.ViewMediatorCallback;
 import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
+import com.android.systemui.bouncer.ui.BouncerView;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
-import com.android.systemui.keyguard.data.BouncerView;
-import com.android.systemui.keyguard.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;
@@ -144,6 +144,7 @@
     // Local cache of expansion events, to avoid duplicates
     private float mFraction = -1f;
     private boolean mTracking = false;
+    private boolean mBouncerShowingOverDream;
 
     private final PrimaryBouncerExpansionCallback mExpansionCallback =
             new PrimaryBouncerExpansionCallback() {
@@ -182,8 +183,8 @@
 
             @Override
             public void onVisibilityChanged(boolean isVisible) {
-                mCentralSurfaces.setBouncerShowingOverDream(
-                        isVisible && mDreamOverlayStateController.isOverlayActive());
+                mBouncerShowingOverDream =
+                        isVisible && mDreamOverlayStateController.isOverlayActive();
 
                 if (!isVisible) {
                     mCentralSurfaces.setPrimaryBouncerHiddenFraction(EXPANSION_HIDDEN);
@@ -702,7 +703,7 @@
 
     @Override
     public void reset(boolean hideBouncerWhenShowing) {
-        if (mKeyguardStateController.isShowing()) {
+        if (mKeyguardStateController.isShowing() && !bouncerIsAnimatingAway()) {
             final boolean isOccluded = mKeyguardStateController.isOccluded();
             // Hide quick settings.
             mShadeViewController.resetViews(/* animate= */ !isOccluded);
@@ -750,7 +751,7 @@
 
     @Override
     public void onStartedWakingUp() {
-        mCentralSurfaces.getNotificationShadeWindowView().getWindowInsetsController()
+        mNotificationShadeWindowController.getWindowRootView().getWindowInsetsController()
                 .setAnimationsDisabled(false);
         NavigationBarView navBarView = mCentralSurfaces.getNavigationBarView();
         if (navBarView != null) {
@@ -764,7 +765,7 @@
 
     @Override
     public void onStartedGoingToSleep() {
-        mCentralSurfaces.getNotificationShadeWindowView().getWindowInsetsController()
+        mNotificationShadeWindowController.getWindowRootView().getWindowInsetsController()
                 .setAnimationsDisabled(true);
         NavigationBarView navBarView = mCentralSurfaces.getNavigationBarView();
         if (navBarView != null) {
@@ -823,6 +824,11 @@
     }
 
     @Override
+    public boolean isBouncerShowingOverDream() {
+        return mBouncerShowingOverDream;
+    }
+
+    @Override
     public void setOccluded(boolean occluded, boolean animate) {
         final boolean wasOccluded = mKeyguardStateController.isOccluded();
         final boolean isOccluding = !wasOccluded && occluded;
@@ -1114,7 +1120,7 @@
             if (view != null) {
                 view.setVisibility(View.VISIBLE);
             }
-            mCentralSurfaces.getNotificationShadeWindowView().getWindowInsetsController()
+            mNotificationShadeWindowController.getWindowRootView().getWindowInsetsController()
                     .show(navigationBars());
         }
     };
@@ -1192,7 +1198,7 @@
                 }
             } else {
                 mNotificationContainer.removeCallbacks(mMakeNavigationBarVisibleRunnable);
-                mCentralSurfaces.getNotificationShadeWindowView().getWindowInsetsController()
+                mNotificationShadeWindowController.getWindowRootView().getWindowInsetsController()
                         .hide(navigationBars());
             }
         }
@@ -1312,7 +1318,7 @@
 
     @Override
     public ViewRootImpl getViewRootImpl() {
-        ViewGroup viewGroup = mNotificationShadeWindowController.getNotificationShadeView();
+        ViewGroup viewGroup = mNotificationShadeWindowController.getWindowRootView();
         if (viewGroup != null) {
             return viewGroup.getViewRootImpl();
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
index 9f69db9..b67ec58 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
@@ -3,6 +3,9 @@
 import android.view.View
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.animation.LaunchAnimator
+import com.android.systemui.shade.ShadeController
+import com.android.systemui.shade.ShadeViewController
+import com.android.systemui.statusbar.NotificationShadeWindowController
 
 /**
  * A [ActivityLaunchAnimator.Controller] that takes care of collapsing the status bar at the right
@@ -10,36 +13,38 @@
  */
 class StatusBarLaunchAnimatorController(
     private val delegate: ActivityLaunchAnimator.Controller,
-    private val centralSurfaces: CentralSurfaces,
+    private val shadeViewController: ShadeViewController,
+    private val shadeController: ShadeController,
+    private val notificationShadeWindowController: NotificationShadeWindowController,
     private val isLaunchForActivity: Boolean = true
 ) : ActivityLaunchAnimator.Controller by delegate {
     // Always sync the opening window with the shade, given that we draw a hole punch in the shade
     // of the same size and position as the opening app to make it visible.
     override val openingWindowSyncView: View?
-        get() = centralSurfaces.notificationShadeWindowView
+        get() = notificationShadeWindowController.windowRootView
 
     override fun onIntentStarted(willAnimate: Boolean) {
         delegate.onIntentStarted(willAnimate)
         if (willAnimate) {
-            centralSurfaces.shadeViewController.setIsLaunchAnimationRunning(true)
+            shadeViewController.setIsLaunchAnimationRunning(true)
         } else {
-            centralSurfaces.collapsePanelOnMainThread()
+            shadeController.collapseOnMainThread()
         }
     }
 
     override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
         delegate.onLaunchAnimationStart(isExpandingFullyAbove)
-        centralSurfaces.shadeViewController.setIsLaunchAnimationRunning(true)
+        shadeViewController.setIsLaunchAnimationRunning(true)
         if (!isExpandingFullyAbove) {
-            centralSurfaces.shadeViewController.collapseWithDuration(
+            shadeViewController.collapseWithDuration(
                 ActivityLaunchAnimator.TIMINGS.totalDuration.toInt())
         }
     }
 
     override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
         delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
-        centralSurfaces.shadeViewController.setIsLaunchAnimationRunning(false)
-        centralSurfaces.onLaunchAnimationEnd(isExpandingFullyAbove)
+        shadeViewController.setIsLaunchAnimationRunning(false)
+        shadeController.onLaunchAnimationEnd(isExpandingFullyAbove)
     }
 
     override fun onLaunchAnimationProgress(
@@ -48,12 +53,12 @@
         linearProgress: Float
     ) {
         delegate.onLaunchAnimationProgress(state, progress, linearProgress)
-        centralSurfaces.shadeViewController.applyLaunchAnimationProgress(linearProgress)
+        shadeViewController.applyLaunchAnimationProgress(linearProgress)
     }
 
     override fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean?) {
         delegate.onLaunchAnimationCancelled()
-        centralSurfaces.shadeViewController.setIsLaunchAnimationRunning(false)
-        centralSurfaces.onLaunchAnimationCancelled(isLaunchForActivity)
+        shadeViewController.setIsLaunchAnimationRunning(false)
+        shadeController.onLaunchAnimationCancelled(isLaunchForActivity)
     }
 }
\ No newline at end of file
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 7bbb03b..f79a081 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -63,6 +63,7 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -121,7 +122,8 @@
 
     private final CentralSurfaces mCentralSurfaces;
     private final NotificationPresenter mPresenter;
-    private final ShadeViewController mNotificationPanel;
+    private final ShadeViewController mShadeViewController;
+    private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final ActivityLaunchAnimator mActivityLaunchAnimator;
     private final NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
     private final UserTracker mUserTracker;
@@ -156,7 +158,8 @@
             OnUserInteractionCallback onUserInteractionCallback,
             CentralSurfaces centralSurfaces,
             NotificationPresenter presenter,
-            ShadeViewController panel,
+            ShadeViewController shadeViewController,
+            NotificationShadeWindowController notificationShadeWindowController,
             ActivityLaunchAnimator activityLaunchAnimator,
             NotificationLaunchAnimatorControllerProvider notificationAnimationProvider,
             LaunchFullScreenIntentProvider launchFullScreenIntentProvider,
@@ -182,6 +185,7 @@
         mLockPatternUtils = lockPatternUtils;
         mStatusBarRemoteInputCallback = remoteInputCallback;
         mActivityIntentHelper = activityIntentHelper;
+        mNotificationShadeWindowController = notificationShadeWindowController;
         mFeatureFlags = featureFlags;
         mMetricsLogger = metricsLogger;
         mLogger = logger;
@@ -189,7 +193,7 @@
         // TODO: use KeyguardStateController#isOccluded to remove this dependency
         mCentralSurfaces = centralSurfaces;
         mPresenter = presenter;
-        mNotificationPanel = panel;
+        mShadeViewController = shadeViewController;
         mActivityLaunchAnimator = activityLaunchAnimator;
         mNotificationAnimationProvider = notificationAnimationProvider;
         mUserTracker = userTracker;
@@ -233,7 +237,7 @@
                 && mActivityIntentHelper.wouldPendingLaunchResolverActivity(intent,
                 mLockscreenUserManager.getCurrentUserId());
         final boolean animate = !willLaunchResolverActivity
-                && mCentralSurfaces.shouldAnimateLaunch(isActivityIntent);
+                && mActivityStarter.shouldAnimateLaunch(isActivityIntent);
         boolean showOverLockscreen = mKeyguardStateController.isShowing() && intent != null
                 && mActivityIntentHelper.wouldPendingShowOverLockscreen(intent,
                 mLockscreenUserManager.getCurrentUserId());
@@ -284,7 +288,7 @@
         }
 
         // Always defer the keyguard dismiss when animating.
-        return animate || !mNotificationPanel.isFullyCollapsed();
+        return animate || !mShadeViewController.isFullyCollapsed();
     }
 
     private void handleNotificationClickAfterPanelCollapsed(
@@ -319,7 +323,7 @@
                     removeHunAfterClick(row);
                     // Show work challenge, do not run PendingIntent and
                     // remove notification
-                    collapseOnMainThread();
+                    mShadeController.collapseOnMainThread();
                     return;
                 }
             }
@@ -436,7 +440,9 @@
             ActivityLaunchAnimator.Controller animationController =
                     new StatusBarLaunchAnimatorController(
                             mNotificationAnimationProvider.getAnimatorController(row, null),
-                            mCentralSurfaces,
+                            mShadeViewController,
+                            mShadeController,
+                            mNotificationShadeWindowController,
                             isActivityIntent);
             mActivityLaunchAnimator.startPendingIntentWithAnimation(
                     animationController,
@@ -467,7 +473,7 @@
     @Override
     public void startNotificationGutsIntent(final Intent intent, final int appUid,
             ExpandableNotificationRow row) {
-        boolean animate = mCentralSurfaces.shouldAnimateLaunch(true /* isActivityIntent */);
+        boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
         ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
             @Override
             public boolean onDismiss() {
@@ -475,7 +481,10 @@
                     ActivityLaunchAnimator.Controller animationController =
                             new StatusBarLaunchAnimatorController(
                                     mNotificationAnimationProvider.getAnimatorController(row),
-                                    mCentralSurfaces, true /* isActivityIntent */);
+                                    mShadeViewController,
+                                    mShadeController,
+                                    mNotificationShadeWindowController,
+                                    true /* isActivityIntent */);
 
                     mActivityLaunchAnimator.startIntentWithAnimation(
                             animationController, animate, intent.getPackage(),
@@ -500,7 +509,7 @@
 
     @Override
     public void startHistoryIntent(View view, boolean showHistory) {
-        boolean animate = mCentralSurfaces.shouldAnimateLaunch(true /* isActivityIntent */);
+        boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
         ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
             @Override
             public boolean onDismiss() {
@@ -520,9 +529,12 @@
                             );
                     ActivityLaunchAnimator.Controller animationController =
                             viewController == null ? null
-                                : new StatusBarLaunchAnimatorController(viewController,
-                                        mCentralSurfaces,
-                                    true /* isActivityIntent */);
+                                : new StatusBarLaunchAnimatorController(
+                                        viewController,
+                                        mShadeViewController,
+                                        mShadeController,
+                                        mNotificationShadeWindowController,
+                                        true /* isActivityIntent */);
 
                     mActivityLaunchAnimator.startIntentWithAnimation(animationController, animate,
                             intent.getPackage(),
@@ -621,11 +633,4 @@
         return true;
     }
 
-    private void collapseOnMainThread() {
-        if (Looper.getMainLooper().isCurrentThread()) {
-            mShadeController.collapseShade();
-        } else {
-            mMainThreadHandler.post(mShadeController::collapseShade);
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
index 12f023b..d07378e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
@@ -19,10 +19,10 @@
 import android.app.PendingIntent
 import com.android.systemui.log.dagger.NotifInteractionLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.ERROR
+import com.android.systemui.log.core.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.WARNING
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
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 dfaee4c..35285b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -22,7 +22,6 @@
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.os.SystemClock;
 import android.service.notification.StatusBarNotification;
 import android.service.vr.IVrManager;
 import android.service.vr.IVrStateCallbacks;
@@ -36,11 +35,11 @@
 import com.android.systemui.R;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
+import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.QuickSettingsController;
 import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
@@ -53,8 +52,8 @@
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
 import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource;
+import com.android.systemui.statusbar.notification.domain.interactor.NotificationsInteractor;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptSuppressor;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -69,9 +68,7 @@
 import javax.inject.Inject;
 
 @CentralSurfacesComponent.CentralSurfacesScope
-class StatusBarNotificationPresenter implements NotificationPresenter,
-        NotificationRowBinderImpl.BindRowCallback,
-        CommandQueue.Callbacks {
+class StatusBarNotificationPresenter implements NotificationPresenter, CommandQueue.Callbacks {
     private static final String TAG = "StatusBarNotificationPresenter";
 
     private final ActivityStarter mActivityStarter;
@@ -86,9 +83,10 @@
     private final HeadsUpManagerPhone mHeadsUpManager;
     private final AboveShelfObserver mAboveShelfObserver;
     private final DozeScrimController mDozeScrimController;
-    private final KeyguardIndicationController mKeyguardIndicationController;
     private final CentralSurfaces mCentralSurfaces;
+    private final NotificationsInteractor mNotificationsInteractor;
     private final LockscreenShadeTransitionController mShadeTransitionController;
+    private final PowerInteractor mPowerInteractor;
     private final CommandQueue mCommandQueue;
 
     private final AccessibilityManager mAccessibilityManager;
@@ -115,9 +113,10 @@
             NotificationShadeWindowController notificationShadeWindowController,
             DynamicPrivacyController dynamicPrivacyController,
             KeyguardStateController keyguardStateController,
-            KeyguardIndicationController keyguardIndicationController,
             CentralSurfaces centralSurfaces,
+            NotificationsInteractor notificationsInteractor,
             LockscreenShadeTransitionController shadeTransitionController,
+            PowerInteractor powerInteractor,
             CommandQueue commandQueue,
             NotificationLockscreenUserManager lockscreenUserManager,
             SysuiStatusBarStateController sysuiStatusBarStateController,
@@ -137,10 +136,11 @@
         mQsController = quickSettingsController;
         mHeadsUpManager = headsUp;
         mDynamicPrivacyController = dynamicPrivacyController;
-        mKeyguardIndicationController = keyguardIndicationController;
         // TODO: use KeyguardStateController#isOccluded to remove this dependency
         mCentralSurfaces = centralSurfaces;
+        mNotificationsInteractor = notificationsInteractor;
         mShadeTransitionController = shadeTransitionController;
+        mPowerInteractor = powerInteractor;
         mCommandQueue = commandQueue;
         mLockscreenUserManager = lockscreenUserManager;
         mStatusBarStateController = sysuiStatusBarStateController;
@@ -173,7 +173,6 @@
                 mNotificationPanel.getShadeNotificationPresenter().createRemoteInputDelegate());
 
         initController.addPostInitTask(() -> {
-            mKeyguardIndicationController.init();
             mNotifShadeEventSource.setShadeEmptiedCallback(this::maybeClosePanelForShadeEmptied);
             mNotifShadeEventSource.setNotifRemovedByUserCallback(this::maybeEndAmbientPulse);
             notificationInterruptStateProvider.addSuppressor(mInterruptSuppressor);
@@ -242,9 +241,7 @@
     public void onExpandClicked(NotificationEntry clickedEntry, View clickedView,
             boolean nowExpanded) {
         mHeadsUpManager.setExpanded(clickedEntry, nowExpanded);
-        mCentralSurfaces.wakeUpIfDozing(
-                SystemClock.uptimeMillis(), "NOTIFICATION_CLICK",
-                PowerManager.WAKE_REASON_GESTURE);
+        mPowerInteractor.wakeUpIfDozing("NOTIFICATION_CLICK", PowerManager.WAKE_REASON_GESTURE);
         if (nowExpanded) {
             if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
                 mShadeTransitionController.goToLockedShade(clickedEntry.getRow());
@@ -341,7 +338,7 @@
 
         @Override
         public boolean suppressInterruptions(NotificationEntry entry) {
-            return mCentralSurfaces.areNotificationAlertsDisabled();
+            return !mNotificationsInteractor.areNotificationAlertsEnabled();
         }
     };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterModule.java
index 26c483c..d94e434 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterModule.java
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.phone;
 
 import com.android.systemui.statusbar.NotificationPresenter;
-import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
 
 import dagger.Binds;
 import dagger.Module;
@@ -26,8 +25,4 @@
 public abstract class StatusBarNotificationPresenterModule {
     @Binds
     abstract NotificationPresenter bindPresenter(StatusBarNotificationPresenter impl);
-
-    @Binds
-    abstract NotificationRowBinderImpl.BindRowCallback bindBindRowCallback(
-            StatusBarNotificationPresenter impl);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java
index 26c1767..604b1f5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java
@@ -64,6 +64,7 @@
     private boolean mNeedsUnderflow;
     // Individual StatusBarIconViews draw their etc dots centered in this width
     private int mIconDotFrameWidth;
+    private boolean mQsExpansionTransitioning;
     private boolean mShouldRestrictIcons = true;
     // Used to count which states want to be visible during layout
     private ArrayList<StatusIconState> mLayoutStates = new ArrayList<>();
@@ -87,6 +88,10 @@
         super.onFinishInflate();
     }
 
+    public void setQsExpansionTransitioning(boolean expansionTransitioning) {
+        mQsExpansionTransitioning = expansionTransitioning;
+    }
+
     public void setShouldRestrictIcons(boolean should) {
         mShouldRestrictIcons = should;
     }
@@ -145,8 +150,8 @@
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         mMeasureViews.clear();
-        int mode = MeasureSpec.getMode(widthMeasureSpec);
-        final int width = MeasureSpec.getSize(widthMeasureSpec);
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
         final int count = getChildCount();
         // Collect all of the views which want to be laid out
         for (int i = 0; i < count; i++) {
@@ -163,7 +168,7 @@
         boolean trackWidth = true;
 
         // Measure all children so that they report the correct width
-        int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED);
+        int childWidthSpec = MeasureSpec.makeMeasureSpec(specWidth, MeasureSpec.UNSPECIFIED);
         mNeedsUnderflow = mShouldRestrictIcons && visibleCount > MAX_ICONS;
         for (int i = 0; i < visibleCount; i++) {
             // Walking backwards
@@ -182,18 +187,35 @@
                 totalWidth += getViewTotalMeasuredWidth(child) + spacing;
             }
         }
+        setMeasuredDimension(
+                getMeasuredWidth(widthMode, specWidth, totalWidth),
+                getMeasuredHeight(heightMeasureSpec, mMeasureViews));
+    }
 
-        if (mode == MeasureSpec.EXACTLY) {
-            if (!mNeedsUnderflow && totalWidth > width) {
-                mNeedsUnderflow = true;
-            }
-            setMeasuredDimension(width, MeasureSpec.getSize(heightMeasureSpec));
+    private int getMeasuredHeight(int heightMeasureSpec, List<View> measuredChildren) {
+        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
+            return MeasureSpec.getSize(heightMeasureSpec);
         } else {
-            if (mode == MeasureSpec.AT_MOST && totalWidth > width) {
-                mNeedsUnderflow = true;
-                totalWidth = width;
+            int highest = 0;
+            for (View child : measuredChildren) {
+                highest = Math.max(child.getMeasuredHeight(), highest);
             }
-            setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec));
+            return highest + getPaddingTop() + getPaddingBottom();
+        }
+    }
+
+    private int getMeasuredWidth(int widthMode, int specWidth, int totalWidth) {
+        if (widthMode == MeasureSpec.EXACTLY) {
+            if (!mNeedsUnderflow && totalWidth > specWidth) {
+                mNeedsUnderflow = true;
+            }
+            return specWidth;
+        } else {
+            if (widthMode == MeasureSpec.AT_MOST && totalWidth > specWidth) {
+                mNeedsUnderflow = true;
+                totalWidth = specWidth;
+            }
+            return totalWidth;
         }
     }
 
@@ -280,34 +302,6 @@
     }
 
     /**
-     * Sets the list of ignored icon slots clearing the current list.
-     * @param slots names of the icons to ignore
-     */
-    public void setIgnoredSlots(List<String> slots) {
-        mIgnoredSlots.clear();
-        addIgnoredSlots(slots);
-    }
-
-    /**
-     * Returns the view corresponding to a particular slot.
-     *
-     * Use it solely to manipulate how it is presented.
-     * @param slot name of the slot to find. Names are defined in
-     *            {@link com.android.internal.R.config_statusBarIcons}
-     * @return a view for the slot if this container has it, else {@code null}
-     */
-    public View getViewForSlot(String slot) {
-        for (int i = 0; i < getChildCount(); i++) {
-            View child = getChildAt(i);
-            if (child instanceof StatusIconDisplayable
-                    && ((StatusIconDisplayable) child).getSlot().equals(slot)) {
-                return child;
-            }
-        }
-        return null;
-    }
-
-    /**
      * Layout is happening from end -> start
      */
     private void calculateIconTranslations() {
@@ -397,6 +391,7 @@
             StatusIconState vs = getViewStateFromChild(child);
             if (vs != null) {
                 vs.applyToView(child);
+                vs.qsExpansionTransitioning = mQsExpansionTransitioning;
             }
         }
     }
@@ -431,6 +426,7 @@
         /// StatusBarIconView.STATE_*
         public int visibleState = STATE_ICON;
         public boolean justAdded = true;
+        public boolean qsExpansionTransitioning = false;
 
         // How far we are from the end of the view actually is the most relevant for animation
         float distanceToViewEnd = -1;
@@ -473,12 +469,13 @@
             }
 
             icon.setVisibleState(visibleState, animateVisibility);
-            if (animationProperties != null) {
+            if (animationProperties != null && !qsExpansionTransitioning) {
                 animateTo(view, animationProperties);
             } else {
                 super.applyToView(view);
             }
 
+            qsExpansionTransitioning = false;
             justAdded = false;
             distanceToViewEnd = currentDistanceToEnd;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index 592d1aa..96a4d90 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.statusbar.CircleReveal
 import com.android.systemui.statusbar.LightRevealScrim
+import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.StatusBarStateControllerImpl
 import com.android.systemui.statusbar.notification.AnimatableProperty
@@ -60,6 +61,7 @@
     private val keyguardStateController: KeyguardStateController,
     private val dozeParameters: dagger.Lazy<DozeParameters>,
     private val globalSettings: GlobalSettings,
+    private val notifShadeWindowControllerLazy: dagger.Lazy<NotificationShadeWindowController>,
     private val interactionJankMonitor: InteractionJankMonitor,
     private val powerManager: PowerManager,
     private val handler: Handler = Handler()
@@ -114,7 +116,7 @@
 
             override fun onAnimationStart(animation: Animator?) {
                 interactionJankMonitor.begin(
-                    mCentralSurfaces.notificationShadeWindowView, CUJ_SCREEN_OFF)
+                        notifShadeWindowControllerLazy.get().windowRootView, CUJ_SCREEN_OFF)
             }
         })
     }
@@ -218,7 +220,7 @@
                 .setCustomInterpolator(View.ALPHA, Interpolators.FAST_OUT_SLOW_IN),
             true /* animate */)
         interactionJankMonitor.begin(
-            mCentralSurfaces.notificationShadeWindowView,
+                notifShadeWindowControllerLazy.get().windowRootView,
             CUJ_SCREEN_OFF_SHOW_AOD
         )
     }
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 273e783..4ae460a 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
@@ -20,18 +20,12 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.scene.ui.view.WindowRootView;
 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.notification.NotificationActivityStarter;
-import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
-import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutListContainerModule;
 import com.android.systemui.statusbar.phone.CentralSurfacesCommandQueueCallbacks;
 import com.android.systemui.statusbar.phone.CentralSurfacesImpl;
 import com.android.systemui.statusbar.phone.StatusBarHeadsUpChangeListener;
@@ -57,7 +51,6 @@
  * outside the component. Should more items be moved *into* this component to avoid so many getters?
  */
 @Subcomponent(modules = {
-        NotificationStackScrollLayoutListContainerModule.class,
         StatusBarViewModule.class,
         StatusBarNotificationActivityStarterModule.class,
         StatusBarNotificationPresenterModule.class,
@@ -80,31 +73,20 @@
     @Scope
     @interface CentralSurfacesScope {}
 
+    /** Creates the root view of the main SysUI window}. */
+    WindowRootView getWindowRootView();
+
     /**
-     * Creates a {@link NotificationShadeWindowView}.
+     * Creates or returns a {@link NotificationShadeWindowView}.
      */
     NotificationShadeWindowView getNotificationShadeWindowView();
 
-    /** */
-    NotificationShelfController getNotificationShelfController();
-
-    /** */
-    NotificationStackScrollLayoutController getNotificationStackScrollLayoutController();
-
     /**
      * Creates a NotificationShadeWindowViewController.
      */
     NotificationShadeWindowViewController getNotificationShadeWindowViewController();
 
     /**
-     * Creates a NotificationPanelViewController.
-     */
-    NotificationPanelViewController getNotificationPanelViewController();
-
-    /** Creates a QuickSettingsController. */
-    QuickSettingsController getQuickSettingsController();
-
-    /**
      * Creates a StatusBarHeadsUpChangeListener.
      */
     StatusBarHeadsUpChangeListener getStatusBarHeadsUpChangeListener();
@@ -129,8 +111,4 @@
     NotificationActivityStarter getNotificationActivityStarter();
 
     NotificationPresenter getNotificationPresenter();
-
-    NotificationRowBinderImpl.BindRowCallback getBindRowCallback();
-
-    NotificationListContainer getNotificationListContainer();
 }
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 4ccbc5a..ebdde78 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
@@ -16,39 +16,20 @@
 
 package com.android.systemui.statusbar.phone.dagger;
 
-import android.view.LayoutInflater;
-
 import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.R;
 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.shade.NotificationPanelView;
-import com.android.systemui.shade.NotificationPanelViewController;
-import com.android.systemui.shade.NotificationShadeWindowView;
-import com.android.systemui.shade.NotificationsQuickSettingsContainer;
 import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.LegacyNotificationShelfControllerImpl;
-import com.android.systemui.statusbar.NotificationShelf;
-import com.android.systemui.statusbar.NotificationShelfController;
 import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
-import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent;
-import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule;
-import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl;
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModelModule;
-import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider;
 import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusBarLocationPublisher;
-import com.android.systemui.statusbar.phone.SystemBarAttributesListener;
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragmentLogger;
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
@@ -60,81 +41,25 @@
 import com.android.systemui.util.CarrierConfigTracker;
 import com.android.systemui.util.settings.SecureSettings;
 
-import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
-import dagger.multibindings.IntoSet;
 
 import java.util.concurrent.Executor;
 
 import javax.inject.Named;
-import javax.inject.Provider;
 
-@Module(subcomponents = StatusBarFragmentComponent.class,
-        includes = {
-                ActivatableNotificationViewModelModule.class,
-                NotificationListViewModelModule.class,
-        })
+/**
+ * A module for {@link CentralSurfacesComponent.CentralSurfacesScope} components.
+ *
+ * @deprecated CentralSurfacesScope will be removed shortly (b/277762009). Classes should be
+ *   annotated with @SysUISingleton instead.
+ */
+@Module(subcomponents = StatusBarFragmentComponent.class)
+@Deprecated
 public abstract class StatusBarViewModule {
 
     public static final String STATUS_BAR_FRAGMENT = "status_bar_fragment";
 
-    /** */
-    @Provides
-    @CentralSurfacesComponent.CentralSurfacesScope
-    public static NotificationShelf providesNotificationShelf(LayoutInflater layoutInflater,
-            NotificationStackScrollLayout notificationStackScrollLayout) {
-        NotificationShelf view = (NotificationShelf) layoutInflater.inflate(
-                R.layout.status_bar_notification_shelf, notificationStackScrollLayout, false);
-
-        if (view == null) {
-            throw new IllegalStateException(
-                    "R.layout.status_bar_notification_shelf could not be properly inflated");
-        }
-        return view;
-    }
-
-    /** */
-    @Provides
-    @CentralSurfacesComponent.CentralSurfacesScope
-    public static NotificationShelfController providesStatusBarWindowView(
-            FeatureFlags featureFlags,
-            Provider<NotificationShelfViewBinderWrapperControllerImpl> newImpl,
-            NotificationShelfComponent.Builder notificationShelfComponentBuilder,
-            NotificationShelf notificationShelf) {
-        if (featureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
-            return newImpl.get();
-        } else {
-            NotificationShelfComponent component = notificationShelfComponentBuilder
-                    .notificationShelf(notificationShelf)
-                    .build();
-            LegacyNotificationShelfControllerImpl notificationShelfController =
-                    component.getNotificationShelfController();
-            notificationShelfController.init();
-
-            return notificationShelfController;
-        }
-    }
-
-    /** */
-    @Binds
-    @CentralSurfacesComponent.CentralSurfacesScope
-    abstract ShadeViewController bindsShadeViewController(
-            NotificationPanelViewController notificationPanelViewController);
-
-    /** */
-    @Provides
-    @CentralSurfacesComponent.CentralSurfacesScope
-    public static NotificationsQuickSettingsContainer getNotificationsQuickSettingsContainer(
-            NotificationShadeWindowView notificationShadeWindowView) {
-        return notificationShadeWindowView.findViewById(R.id.notification_container_parent);
-    }
-
-    @Binds
-    @IntoSet
-    abstract StatusBarBoundsProvider.BoundsChangeListener sysBarAttrsListenerAsBoundsListener(
-            SystemBarAttributesListener systemBarAttributesListener);
-
     /**
      * Creates a new {@link CollapsedStatusBarFragment}.
      *
@@ -199,17 +124,4 @@
                 statusBarWindowStateController,
                 keyguardUpdateMonitor);
     }
-
-    /**
-     * Constructs a new, unattached {@link KeyguardBottomAreaView}.
-     *
-     * Note that this is explicitly _not_ a singleton, as we want to be able to reinflate it
-     */
-    @Provides
-    public static KeyguardBottomAreaView providesKeyguardBottomAreaView(
-            NotificationPanelView npv, LayoutInflater layoutInflater) {
-        return (KeyguardBottomAreaView) layoutInflater.inflate(R
-                .layout.keyguard_bottom_area, npv, false);
-    }
-
 }
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 0d057f3..2efdf8a 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
@@ -14,9 +14,6 @@
 
 package com.android.systemui.statusbar.phone.fragment;
 
-import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.IDLE;
-import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.SHOWING_PERSISTENT_DOT;
-
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.Fragment;
@@ -39,6 +36,7 @@
 import androidx.core.animation.Animator;
 
 import com.android.app.animation.Interpolators;
+import com.android.app.animation.InterpolatorsAndroidX;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
@@ -77,6 +75,8 @@
 import com.android.systemui.util.CarrierConfigTracker.DefaultDataSubscriptionChangedListener;
 import com.android.systemui.util.settings.SecureSettings;
 
+import kotlin.Unit;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -99,12 +99,16 @@
     private static final String EXTRA_PANEL_STATE = "panel_state";
     public static final String STATUS_BAR_ICON_MANAGER_TAG = "status_bar_icon_manager";
     public static final int FADE_IN_DURATION = 320;
+    public static final int FADE_OUT_DURATION = 160;
     public static final int FADE_IN_DELAY = 50;
+    private static final int SOURCE_SYSTEM_EVENT_ANIMATOR = 1;
+    private static final int SOURCE_OTHER = 2;
     private StatusBarFragmentComponent mStatusBarFragmentComponent;
     private PhoneStatusBarView mStatusBar;
     private final StatusBarStateController mStatusBarStateController;
     private final KeyguardStateController mKeyguardStateController;
     private final ShadeViewController mShadeViewController;
+    private MultiSourceMinAlphaController mEndSideAlphaController;
     private LinearLayout mEndSideContent;
     private View mClockView;
     private View mOngoingCallChip;
@@ -149,7 +153,7 @@
         }
     };
     private OperatorNameViewController mOperatorNameViewController;
-    private StatusBarSystemEventAnimator mSystemEventAnimator;
+    private StatusBarSystemEventDefaultAnimator mSystemEventAnimator;
 
     private final CarrierConfigChangedListener mCarrierConfigCallback =
             new CarrierConfigChangedListener() {
@@ -297,14 +301,14 @@
         updateBlockedIcons();
         mStatusBarIconController.addIconGroup(mDarkIconManager);
         mEndSideContent = mStatusBar.findViewById(R.id.status_bar_end_side_content);
+        mEndSideAlphaController = new MultiSourceMinAlphaController(mEndSideContent);
         mClockView = mStatusBar.findViewById(R.id.clock);
         mOngoingCallChip = mStatusBar.findViewById(R.id.ongoing_call_chip);
         showEndSideContent(false);
         showClock(false);
         initOperatorName();
         initNotificationIconArea();
-        mSystemEventAnimator =
-                new StatusBarSystemEventAnimator(mEndSideContent, getResources());
+        mSystemEventAnimator = getSystemEventAnimator();
         mCarrierConfigTracker.addCallback(mCarrierConfigCallback);
         mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener);
 
@@ -593,18 +597,27 @@
     }
 
     private void hideEndSideContent(boolean animate) {
-        animateHide(mEndSideContent, animate);
+        if (!animate) {
+            mEndSideAlphaController.setAlpha(/*alpha*/ 0f, SOURCE_OTHER);
+        } else {
+            mEndSideAlphaController.animateToAlpha(/*alpha*/ 0f, SOURCE_OTHER, FADE_OUT_DURATION,
+                    InterpolatorsAndroidX.ALPHA_OUT, /*startDelay*/ 0);
+        }
     }
 
     private void showEndSideContent(boolean animate) {
-        // Only show the system icon area if we are not currently animating
-        int state = mAnimationScheduler.getAnimationState();
-        if (state == IDLE || state == SHOWING_PERSISTENT_DOT) {
-            animateShow(mEndSideContent, animate);
+        if (!animate) {
+            mEndSideAlphaController.setAlpha(1f, SOURCE_OTHER);
+            return;
+        }
+        if (mKeyguardStateController.isKeyguardFadingAway()) {
+            mEndSideAlphaController.animateToAlpha(/*alpha*/ 1f, SOURCE_OTHER,
+                    mKeyguardStateController.getKeyguardFadingAwayDuration(),
+                    InterpolatorsAndroidX.LINEAR_OUT_SLOW_IN,
+                    mKeyguardStateController.getKeyguardFadingAwayDelay());
         } else {
-            // We are in the middle of a system status event animation, which will animate the
-            // alpha (but not the visibility). Allow the view to become visible again
-            mEndSideContent.setVisibility(View.VISIBLE);
+            mEndSideAlphaController.animateToAlpha(/*alpha*/ 1f, SOURCE_OTHER, FADE_IN_DURATION,
+                    InterpolatorsAndroidX.ALPHA_IN, FADE_IN_DELAY);
         }
     }
 
@@ -671,7 +684,7 @@
 
         v.animate()
                 .alpha(0f)
-                .setDuration(160)
+                .setDuration(FADE_OUT_DURATION)
                 .setStartDelay(0)
                 .setInterpolator(Interpolators.ALPHA_OUT)
                 .withEndAction(() -> v.setVisibility(state));
@@ -754,6 +767,16 @@
         return mSystemEventAnimator.onSystemEventAnimationFinish(hasPersistentDot);
     }
 
+    private StatusBarSystemEventDefaultAnimator getSystemEventAnimator() {
+        return new StatusBarSystemEventDefaultAnimator(getResources(), (alpha) -> {
+            mEndSideAlphaController.setAlpha(alpha, SOURCE_SYSTEM_EVENT_ANIMATOR);
+            return Unit.INSTANCE;
+        }, (translationX) -> {
+            mEndSideContent.setTranslationX(translationX);
+            return Unit.INSTANCE;
+        }, /*isAnimationRunning*/ false);
+    }
+
     private void updateStatusBarLocation(int left, int right) {
         int leftMargin = left - mStatusBar.getLeft();
         int rightMargin = mStatusBar.getRight() - right;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
index 8c19fb4..7cdb9c0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.log.dagger.CollapsedSbFragmentLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import javax.inject.Inject
 
@@ -33,7 +33,6 @@
      * modifications that were made to the flags locally.
      *
      * @param new see [DisableFlagsLogger.getDisableFlagsString]
-     * @param newAfterLocalModification see [DisableFlagsLogger.getDisableFlagsString]
      */
     fun logDisableFlagChange(
         new: DisableFlagsLogger.DisableState,
@@ -47,8 +46,7 @@
                 },
                 {
                     disableFlagsLogger.getDisableFlagsString(
-                        old = null,
-                        new = DisableFlagsLogger.DisableState(int1, int2),
+                        DisableFlagsLogger.DisableState(int1, int2),
                     )
                 }
         )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaController.kt
new file mode 100644
index 0000000..c8836e4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaController.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.fragment
+
+import android.view.View
+import androidx.core.animation.Interpolator
+import androidx.core.animation.ValueAnimator
+import com.android.app.animation.InterpolatorsAndroidX
+
+/**
+ * A controller that keeps track of multiple sources applying alpha value changes to a view. It will
+ * always apply the minimum alpha value of all sources.
+ */
+internal class MultiSourceMinAlphaController
+@JvmOverloads
+constructor(private val view: View, private val initialAlpha: Float = 1f) {
+
+    private val alphas = mutableMapOf<Int, Float>()
+    private val animators = mutableMapOf<Int, ValueAnimator>()
+
+    /**
+     * Sets the alpha of the provided source and applies it to the view (if no other source has set
+     * a lower alpha currently). If an animator of the same source is still running (i.e.
+     * [animateToAlpha] was called before), that animator is cancelled.
+     */
+    fun setAlpha(alpha: Float, sourceId: Int) {
+        animators[sourceId]?.cancel()
+        updateAlpha(alpha, sourceId)
+    }
+
+    /** Animates to the alpha of the provided source. */
+    fun animateToAlpha(
+        alpha: Float,
+        sourceId: Int,
+        duration: Long,
+        interpolator: Interpolator = InterpolatorsAndroidX.ALPHA_IN,
+        startDelay: Long = 0
+    ) {
+        animators[sourceId]?.cancel()
+        val animator = ValueAnimator.ofFloat(getMinAlpha(), alpha)
+        animator.duration = duration
+        animator.startDelay = startDelay
+        animator.interpolator = interpolator
+        animator.addUpdateListener { updateAlpha(animator.animatedValue as Float, sourceId) }
+        animator.start()
+        animators[sourceId] = animator
+    }
+
+    fun reset() {
+        alphas.clear()
+        animators.forEach { it.value.cancel() }
+        animators.clear()
+        applyAlphaToView()
+    }
+
+    private fun updateAlpha(alpha: Float, sourceId: Int) {
+        alphas[sourceId] = alpha
+        applyAlphaToView()
+    }
+
+    private fun applyAlphaToView() {
+        val minAlpha = getMinAlpha()
+        view.visibility = if (minAlpha != 0f) View.VISIBLE else View.INVISIBLE
+        view.alpha = minAlpha
+    }
+
+    private fun getMinAlpha() = alphas.minOfOrNull { it.value } ?: initialAlpha
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
index 8f9f019..eb4d963 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
@@ -27,20 +27,23 @@
 import com.android.systemui.statusbar.phone.PhoneStatusBarView;
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
 import com.android.systemui.statusbar.phone.StatusBarBoundsProvider;
+import com.android.systemui.statusbar.phone.SystemBarAttributesListener;
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
 import com.android.systemui.statusbar.policy.Clock;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
 
+import dagger.Binds;
+import dagger.Module;
+import dagger.Provides;
+import dagger.multibindings.IntoSet;
+import dagger.multibindings.Multibinds;
+
 import java.util.Optional;
 import java.util.Set;
 
 import javax.inject.Named;
 
-import dagger.Module;
-import dagger.Provides;
-import dagger.multibindings.Multibinds;
-
 /** Dagger module for {@link StatusBarFragmentComponent}. */
 @Module
 public interface StatusBarFragmentModule {
@@ -152,4 +155,10 @@
     /** */
     @Multibinds
     Set<StatusBarBoundsProvider.BoundsChangeListener> boundsChangeListeners();
+
+    /** */
+    @Binds
+    @IntoSet
+    StatusBarBoundsProvider.BoundsChangeListener sysBarAttrsListenerAsBoundsListener(
+            SystemBarAttributesListener systemBarAttributesListener);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt
index b3a1c40..051e88f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt
@@ -23,7 +23,7 @@
 import com.android.settingslib.mobile.MobileMappings
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.pipeline.dagger.MobileInputLog
 import javax.inject.Inject
 
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 a47f95d..54948a4 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
@@ -57,7 +57,6 @@
 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.combine
@@ -205,9 +204,6 @@
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), null)
 
-    private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
-        MutableSharedFlow(extraBufferCapacity = 1)
-
     override val defaultDataSubId: StateFlow<Int> =
         broadcastDispatcher
             .broadcastFlow(
@@ -223,7 +219,6 @@
                 initialValue = INVALID_SUBSCRIPTION_ID,
             )
             .onStart { emit(subscriptionManagerProxy.getDefaultDataSubscriptionId()) }
-            .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
             .stateIn(scope, SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
 
     private val carrierConfigChangedEvent =
@@ -232,7 +227,7 @@
             .onEach { logger.logActionCarrierConfigChanged() }
 
     override val defaultDataSubRatConfig: StateFlow<Config> =
-        merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
+        merge(defaultDataSubId, carrierConfigChangedEvent)
             .onStart { emit(Unit) }
             .mapLatest { Config.readConfig(context) }
             .distinctUntilChanged()
@@ -255,15 +250,8 @@
             .distinctUntilChanged()
             .onEach { logger.logDefaultMobileIconGroup(it) }
 
-    override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository {
-        if (!isValidSubId(subId)) {
-            throw IllegalArgumentException(
-                "subscriptionId $subId is not in the list of valid subscriptions"
-            )
-        }
-
-        return getOrCreateRepoForSubId(subId)
-    }
+    override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository =
+        getOrCreateRepoForSubId(subId)
 
     private fun getOrCreateRepoForSubId(subId: Int) =
         subIdRepositoryCache[subId]
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt
index 7e0c145..cea6654 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.pipeline.dagger.MobileViewLog
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
 import java.io.PrintWriter
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt
index 507549b..f4c5723 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.pipeline.dagger.VerboseMobileViewLog
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger.Companion.getIdForLogging
 import com.android.systemui.statusbar.pipeline.mobile.ui.model.SignalIconModel
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
index cac0ae3..8a4d14e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityInputLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityInputLogger.kt
@@ -20,7 +20,7 @@
 import android.net.NetworkCapabilities
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.pipeline.dagger.SharedConnectivityInputLog
 import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel
 import javax.inject.Inject
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
index 328d901..4b9de85 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/LoggerHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/LoggerHelper.kt
@@ -19,7 +19,7 @@
 import android.net.Network
 import android.net.NetworkCapabilities
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 
 /** Helper object for logs that are shared between wifi and mobile. */
 object LoggerHelper {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DefaultConnectionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DefaultConnectionModel.kt
index 058eda4..9a504c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DefaultConnectionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DefaultConnectionModel.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.pipeline.shared.data.model
 
 import android.net.NetworkCapabilities
-import com.android.systemui.log.LogMessage
+import com.android.systemui.log.core.LogMessage
 
 /**
  * A model for all of the current default connections(s).
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 a4fbc2c..a57be66 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
@@ -96,7 +96,7 @@
             networkId = DEMO_NET_ID,
             isValidated = validated ?: true,
             level = level ?: 0,
-            ssid = ssid,
+            ssid = ssid ?: DEMO_NET_SSID,
 
             // These fields below aren't supported in demo mode, since they aren't needed to satisfy
             // the interface.
@@ -115,5 +115,6 @@
 
     companion object {
         private const val DEMO_NET_ID = 1234
+        private const val DEMO_NET_SSID = "Demo SSID"
     }
 }
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
index 4a9ceac..f244376 100644
--- 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
@@ -20,7 +20,7 @@
 import android.net.NetworkCapabilities
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.pipeline.dagger.WifiInputLog
 import com.android.systemui.statusbar.pipeline.shared.LoggerHelper
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerLogger.kt
index 6ba2a81..096ad1f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerLogger.kt
@@ -22,7 +22,7 @@
 import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_UNLOCKED
 import com.android.internal.R
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.VERBOSE
+import com.android.systemui.log.core.LogLevel.VERBOSE
 import com.android.systemui.log.dagger.DeviceStateAutoRotationLog
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
index 06ed1fd..175473f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -18,8 +18,8 @@
 
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.VERBOSE
+import com.android.systemui.log.core.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.VERBOSE
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
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 a82646a..710588c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.policy;
 
+import static android.hardware.biometrics.BiometricSourceType.FACE;
+
 import android.annotation.NonNull;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -199,6 +201,11 @@
         Trace.endSection();
     }
 
+    private void notifyKeyguardFaceAuthEnabledChanged() {
+        // Copy the list to allow removal during callback.
+        new ArrayList<>(mCallbacks).forEach(Callback::onFaceAuthEnabledChanged);
+    }
+
     private void notifyUnlockedChanged() {
         Trace.beginSection("KeyguardStateController#notifyUnlockedChanged");
         // Copy the list to allow removal during callback.
@@ -419,6 +426,16 @@
         }
 
         @Override
+        public void onBiometricEnrollmentStateChanged(BiometricSourceType biometricSourceType) {
+            if (biometricSourceType == FACE) {
+                // We only care about enrollment state here. Keyguard face auth enabled is just
+                // same as face auth enrolled
+                update(false);
+                notifyKeyguardFaceAuthEnabledChanged();
+            }
+        }
+
+        @Override
         public void onStartedWakingUp() {
             update(false /* updateAlways */);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
index 066ac04..a9d2029 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
@@ -18,7 +18,7 @@
 
 import android.view.View
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 
 /** A logger for temporary view changes -- see [TemporaryViewDisplayController]. */
 open class TemporaryViewLogger<T : TemporaryViewInfo>(
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 d55751b..6706873 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.temporarydisplay.TemporaryViewLogger
 import com.android.systemui.temporarydisplay.dagger.ChipbarLog
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
index dfe748a..c109eb4 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
@@ -18,9 +18,9 @@
 
 import com.android.systemui.log.dagger.ToastLog
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogMessage
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogMessage
 import javax.inject.Inject
 
 private const val TAG = "ToastLog"
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/model/SelectedUserModel.kt b/packages/SystemUI/src/com/android/systemui/user/data/model/SelectedUserModel.kt
new file mode 100644
index 0000000..cefd466
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/data/model/SelectedUserModel.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.user.data.model
+
+import android.content.pm.UserInfo
+
+/** A model for the currently selected user. */
+data class SelectedUserModel(
+    /** Information about the user. */
+    val userInfo: UserInfo,
+    /** The current status of the selection. */
+    val selectionStatus: SelectionStatus,
+)
+
+/** The current status of the selection. */
+enum class SelectionStatus {
+    /** This user has started being selected but the selection hasn't completed. */
+    SELECTION_IN_PROGRESS,
+    /** The selection of this user has completed. */
+    SELECTION_COMPLETE,
+}
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 3de75ca..954765c 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
@@ -33,6 +33,8 @@
 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.SelectedUserModel
+import com.android.systemui.user.data.model.SelectionStatus
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
@@ -69,6 +71,9 @@
     /** List of all users on the device. */
     val userInfos: Flow<List<UserInfo>>
 
+    /** Information about the currently-selected user, including [UserInfo] and other details. */
+    val selectedUser: StateFlow<SelectedUserModel>
+
     /** [UserInfo] of the currently-selected user. */
     val selectedUserInfo: Flow<UserInfo>
 
@@ -146,9 +151,6 @@
     private val _userInfos = MutableStateFlow<List<UserInfo>?>(null)
     override val userInfos: Flow<List<UserInfo>> = _userInfos.filterNotNull()
 
-    private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
-    override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
-
     override var mainUserId: Int = UserHandle.USER_NULL
         private set
     override var lastSelectedNonGuestUserId: Int = UserHandle.USER_NULL
@@ -174,12 +176,57 @@
     override var isRefreshUsersPaused: Boolean = false
 
     init {
-        observeSelectedUser()
         if (featureFlags.isEnabled(FACE_AUTH_REFACTOR)) {
             observeUserSwitching()
         }
     }
 
+    override val selectedUser: StateFlow<SelectedUserModel> = run {
+        // Some callbacks don't modify the selection status, so maintain the current value.
+        var currentSelectionStatus = SelectionStatus.SELECTION_COMPLETE
+        conflatedCallbackFlow {
+                fun send(selectionStatus: SelectionStatus) {
+                    currentSelectionStatus = selectionStatus
+                    trySendWithFailureLogging(
+                        SelectedUserModel(tracker.userInfo, selectionStatus),
+                        TAG,
+                    )
+                }
+
+                val callback =
+                    object : UserTracker.Callback {
+                        override fun onUserChanging(newUser: Int, userContext: Context) {
+                            send(SelectionStatus.SELECTION_IN_PROGRESS)
+                        }
+
+                        override fun onUserChanged(newUser: Int, userContext: Context) {
+                            send(SelectionStatus.SELECTION_COMPLETE)
+                        }
+
+                        override fun onProfilesChanged(profiles: List<UserInfo>) {
+                            send(currentSelectionStatus)
+                        }
+                    }
+
+                tracker.addCallback(callback, mainDispatcher.asExecutor())
+                send(currentSelectionStatus)
+
+                awaitClose { tracker.removeCallback(callback) }
+            }
+            .onEach {
+                if (!it.userInfo.isGuest) {
+                    lastSelectedNonGuestUserId = it.userInfo.id
+                }
+            }
+            .stateIn(
+                applicationScope,
+                SharingStarted.Eagerly,
+                initialValue = SelectedUserModel(tracker.userInfo, currentSelectionStatus)
+            )
+    }
+
+    override val selectedUserInfo: Flow<UserInfo> = selectedUser.map { it.userInfo }
+
     override fun refreshUsers() {
         applicationScope.launch {
             val result = withContext(backgroundDispatcher) { manager.aliveUsers }
@@ -201,7 +248,7 @@
     }
 
     override fun getSelectedUserInfo(): UserInfo {
-        return checkNotNull(_selectedUserInfo.value)
+        return selectedUser.value.userInfo
     }
 
     override fun isSimpleUserSwitcher(): Boolean {
@@ -234,38 +281,6 @@
             .launchIn(applicationScope)
     }
 
-    private fun observeSelectedUser() {
-        conflatedCallbackFlow {
-                fun send() {
-                    trySendWithFailureLogging(tracker.userInfo, TAG)
-                }
-
-                val callback =
-                    object : UserTracker.Callback {
-                        override fun onUserChanging(newUser: Int, userContext: Context) {
-                            send()
-                        }
-
-                        override fun onProfilesChanged(profiles: List<UserInfo>) {
-                            send()
-                        }
-                    }
-
-                tracker.addCallback(callback, mainDispatcher.asExecutor())
-                send()
-
-                awaitClose { tracker.removeCallback(callback) }
-            }
-            .onEach {
-                if (!it.isGuest) {
-                    lastSelectedNonGuestUserId = it.id
-                }
-
-                _selectedUserInfo.value = it
-            }
-            .launchIn(applicationScope)
-    }
-
     private suspend fun getSettings(): UserSwitcherSettingsModel {
         return withContext(backgroundDispatcher) {
             val isSimpleUserSwitcher =
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
index d6b3b22..12d7b4d 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
@@ -19,12 +19,44 @@
 import android.view.View
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.lifecycle.repeatWhenAttached
 import java.util.function.Consumer
+import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+/** A class allowing Java classes to collect on Kotlin flows. */
+@SysUISingleton
+class JavaAdapter
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+) {
+    /**
+     * Collect information for the given [flow], calling [consumer] for each emitted event.
+     *
+     * Important: This will immediately start collection and *never* stop it. This should only be
+     * used by classes that *need* to always be collecting a value and processing it. Whenever
+     * possible, please use [collectFlow] instead; that method will stop the collection when a view
+     * has disappeared, which will ensure that we don't perform unnecessary work.
+     *
+     * Do *not* call this method in a class's constructor. Instead, call it in
+     * [com.android.systemui.CoreStartable.start] or similar method.
+     */
+    fun <T> alwaysCollectFlow(
+        flow: Flow<T>,
+        consumer: Consumer<T>,
+    ): Job {
+        return scope.launch { flow.collect { consumer.accept(it) } }
+    }
+}
 
 /**
  * Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/PackageManagerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/PackageManagerExt.kt
index 891ee0c..e32ed5f 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/PackageManagerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/PackageManagerExt.kt
@@ -16,12 +16,19 @@
 
 package com.android.systemui.util.kotlin
 
+import android.annotation.UserHandleAware
 import android.annotation.WorkerThread
 import android.content.pm.ComponentInfo
 import android.content.pm.PackageManager
 import com.android.systemui.util.Assert
 
+/**
+ * Determines whether a component is actually enabled (not just its default value).
+ *
+ * @throws IllegalArgumentException if the component is not found
+ */
 @WorkerThread
+@UserHandleAware
 fun PackageManager.isComponentActuallyEnabled(componentInfo: ComponentInfo): Boolean {
     Assert.isNotMainThread()
     return when (getComponentEnabledSetting(componentInfo.componentName)) {
diff --git a/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLockLogger.kt b/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLockLogger.kt
index 0926800..d69b10f 100644
--- a/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLockLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLockLogger.kt
@@ -18,7 +18,7 @@
 
 import android.os.PowerManager
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import javax.inject.Inject
 
 class WakeLockLogger @Inject constructor(@WakeLockLog private val buffer: LogBuffer) {
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index 080be6d..12da17f 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -192,12 +192,6 @@
             android:excludeFromRecents="true" />
 
         <activity
-            android:name="com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity"
-            android:exported="false"
-            android:permission="com.android.systemui.permission.SELF"
-            android:excludeFromRecents="true" />
-
-        <activity
             android:name="com.android.systemui.notetask.LaunchNotesRoleSettingsTrampolineActivity"
             android:exported="false"
             android:permission="com.android.systemui.permission.SELF"
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java
index f5cd0ca..319a02d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java
@@ -50,6 +50,7 @@
 
 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;
@@ -59,6 +60,7 @@
 @RunWithLooper
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
+@Ignore("b/286245842")
 public class AdminSecondaryLockScreenControllerTest extends SysuiTestCase {
 
     private static final int TARGET_USER_ID = KeyguardUpdateMonitor.getCurrentUser();
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index e9a9f3a..2318988 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -21,21 +21,22 @@
 import android.widget.TextView
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 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.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
+import com.android.systemui.log.LogBuffer
 import com.android.systemui.plugins.ClockAnimations
 import com.android.systemui.plugins.ClockController
 import com.android.systemui.plugins.ClockEvents
-import com.android.systemui.plugins.ClockFaceController
 import com.android.systemui.plugins.ClockFaceConfig
+import com.android.systemui.plugins.ClockFaceController
 import com.android.systemui.plugins.ClockFaceEvents
 import com.android.systemui.plugins.ClockTickRate
-import com.android.systemui.log.LogBuffer
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -54,6 +55,7 @@
 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
@@ -118,8 +120,11 @@
                 commandQueue = commandQueue,
                 featureFlags = featureFlags,
                 bouncerRepository = bouncerRepository,
+                configurationRepository = FakeConfigurationRepository(),
             ),
-            KeyguardTransitionInteractor(transitionRepository, TestScope().backgroundScope),
+            KeyguardTransitionInteractorFactory.create(
+                    scope = TestScope().backgroundScope,
+            ).keyguardTransitionInteractor,
             broadcastDispatcher,
             batteryController,
             keyguardUpdateMonitor,
@@ -156,9 +161,8 @@
 
     @Test
     fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) {
-        // TODO(b/266103601): delete this test and add more coverage for updateColors()
-        // verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
-        // verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
+         verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
+         verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
 
         val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
         verify(configurationController).addCallback(capture(captor))
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
index 061340e..65c677e 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
@@ -16,6 +16,7 @@
 
 package com.android.keyguard;
 
+import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
 
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
@@ -192,6 +193,7 @@
         assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1);
         assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE);
         assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0);
+        assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE);
     }
 
     @Test
@@ -201,6 +203,7 @@
         assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1);
         assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE);
         assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0);
+        assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE);
     }
 
     @Test
@@ -216,6 +219,7 @@
         // only big clock is removed at switch
         assertThat(mLargeClockFrame.getParent()).isNull();
         assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0);
+        assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE);
     }
 
     @Test
@@ -227,6 +231,7 @@
         // only big clock is removed at switch
         assertThat(mLargeClockFrame.getParent()).isNull();
         assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0);
+        assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index 71a57c7..2eea9eb 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -18,10 +18,12 @@
 
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import android.view.View
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
 import androidx.test.filters.SmallTest
 import com.android.internal.util.LatencyTracker
 import com.android.internal.widget.LockPatternUtils
-import com.android.internal.widget.LockPatternView
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
@@ -29,6 +31,10 @@
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.statusbar.policy.DevicePostureController
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED
+import com.android.systemui.util.mockito.any
+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
@@ -45,7 +51,7 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class KeyguardPatternViewControllerTest : SysuiTestCase() {
-  @Mock private lateinit var mKeyguardPatternView: KeyguardPatternView
+   private lateinit var mKeyguardPatternView: KeyguardPatternView
 
   @Mock private lateinit var mKeyguardUpdateMonitor: KeyguardUpdateMonitor
 
@@ -63,54 +69,70 @@
   @Mock
   private lateinit var mKeyguardMessageAreaControllerFactory: KeyguardMessageAreaController.Factory
 
-  @Mock private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea
-
   @Mock
   private lateinit var mKeyguardMessageAreaController:
       KeyguardMessageAreaController<BouncerKeyguardMessageArea>
 
-  @Mock private lateinit var mLockPatternView: LockPatternView
-
-  @Mock private lateinit var mPostureController: DevicePostureController
+    @Mock private lateinit var mPostureController: DevicePostureController
 
   private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController
   private lateinit var fakeFeatureFlags: FakeFeatureFlags
 
-  @Before
-  fun setup() {
-    MockitoAnnotations.initMocks(this)
-    `when`(mKeyguardPatternView.isAttachedToWindow).thenReturn(true)
-    `when`(
-            mKeyguardPatternView.requireViewById<BouncerKeyguardMessageArea>(
-                R.id.bouncer_message_area))
-        .thenReturn(mKeyguardMessageArea)
-    `when`(mKeyguardPatternView.findViewById<LockPatternView>(R.id.lockPatternView))
-        .thenReturn(mLockPatternView)
-    `when`(mKeyguardMessageAreaControllerFactory.create(mKeyguardMessageArea))
-        .thenReturn(mKeyguardMessageAreaController)
-    `when`(mKeyguardPatternView.resources).thenReturn(context.resources)
-    fakeFeatureFlags = FakeFeatureFlags()
-    fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, false)
-    mKeyguardPatternViewController =
-        KeyguardPatternViewController(
-            mKeyguardPatternView,
-            mKeyguardUpdateMonitor,
-            mSecurityMode,
-            mLockPatternUtils,
-            mKeyguardSecurityCallback,
-            mLatencyTracker,
-            mFalsingCollector,
-            mEmergencyButtonController,
-            mKeyguardMessageAreaControllerFactory,
-            mPostureController,
-            fakeFeatureFlags)
-  }
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        whenever(mKeyguardMessageAreaControllerFactory.create(any()))
+            .thenReturn(mKeyguardMessageAreaController)
+        fakeFeatureFlags = FakeFeatureFlags()
+        fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, false)
+        mKeyguardPatternView = View.inflate(mContext, R.layout.keyguard_pattern_view, null)
+                as KeyguardPatternView
+
+
+        mKeyguardPatternViewController =
+            KeyguardPatternViewController(
+                mKeyguardPatternView,
+                mKeyguardUpdateMonitor,
+                mSecurityMode,
+                mLockPatternUtils,
+                mKeyguardSecurityCallback,
+                mLatencyTracker,
+                mFalsingCollector,
+                mEmergencyButtonController,
+                mKeyguardMessageAreaControllerFactory,
+                mPostureController,
+                fakeFeatureFlags
+            )
+        mKeyguardPatternView.onAttachedToWindow()
+    }
+
+    @Test
+    fun tabletopPostureIsDetectedFromStart() {
+        overrideResource(R.dimen.half_opened_bouncer_height_ratio, 0.5f)
+        whenever(mPostureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED)
+
+        mKeyguardPatternViewController.onViewAttached()
+
+        assertThat(getPatternTopGuideline()).isEqualTo(getExpectedTopGuideline())
+    }
+
+    private fun getPatternTopGuideline(): Float {
+        val cs = ConstraintSet()
+        val container =
+            mKeyguardPatternView.findViewById(R.id.pattern_container) as ConstraintLayout
+        cs.clone(container)
+        return cs.getConstraint(R.id.pattern_top_guideline).layout.guidePercent
+    }
+
+    private fun getExpectedTopGuideline(): Float {
+        return mContext.resources.getFloat(R.dimen.half_opened_bouncer_height_ratio)
+    }
 
   @Test
   fun withFeatureFlagOn_oldMessage_isHidden() {
     fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
 
-    mKeyguardPatternViewController.init()
+    mKeyguardPatternViewController.onViewAttached()
 
     verify<KeyguardMessageAreaController<*>>(mKeyguardMessageAreaController).disable()
   }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index 2f20f76..58b1edc 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -18,7 +18,7 @@
 
 import static com.android.keyguard.KeyguardSecurityContainer.MODE_DEFAULT;
 import static com.android.keyguard.KeyguardSecurityContainer.MODE_ONE_HANDED;
-import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE;
+import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -62,6 +62,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.SideFpsController;
 import com.android.systemui.biometrics.SideFpsUiRequestSource;
+import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
 import com.android.systemui.classifier.FalsingA11yDelegate;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.flags.FakeFeatureFlags;
@@ -216,7 +217,8 @@
                 mUserSwitcherController, mFeatureFlags, mGlobalSettings,
                 mSessionTracker, Optional.of(mSideFpsController), mFalsingA11yDelegate,
                 mTelephonyManager, mViewMediatorCallback, mAudioManager,
-                mock(KeyguardFaceAuthInteractor.class));
+                mock(KeyguardFaceAuthInteractor.class),
+                mock(BouncerMessageInteractor.class));
     }
 
     @Test
@@ -393,6 +395,7 @@
 
         // WHEN a request is made from the SimPin screens to show the next security method
         when(mKeyguardSecurityModel.getSecurityMode(TARGET_USER_ID)).thenReturn(SecurityMode.None);
+        when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(true);
         mKeyguardSecurityContainerController.showNextSecurityScreenOrFinish(
                 /* authenticated= */true,
                 TARGET_USER_ID,
@@ -421,6 +424,28 @@
     }
 
     @Test
+    public void showNextSecurityScreenOrFinish_SimPin_Swipe() {
+        // GIVEN the current security method is SimPin
+        when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false);
+        when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID)).thenReturn(false);
+        mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.SimPin);
+
+        // WHEN a request is made from the SimPin screens to show the next security method
+        when(mKeyguardSecurityModel.getSecurityMode(TARGET_USER_ID)).thenReturn(SecurityMode.None);
+        // WHEN security method is SWIPE
+        when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false);
+        mKeyguardSecurityContainerController.showNextSecurityScreenOrFinish(
+                /* authenticated= */true,
+                TARGET_USER_ID,
+                /* bypassSecondaryLockScreen= */true,
+                SecurityMode.SimPin);
+
+        // THEN the next security method of None will dismiss keyguard.
+        verify(mViewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt());
+    }
+
+
+    @Test
     public void onSwipeUp_whenFaceDetectionIsNotRunning_initiatesFaceAuth() {
         KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
                 getRegisteredSwipeListener();
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 3cb4c0c..5abab62 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -17,6 +17,7 @@
 package com.android.keyguard;
 
 import static android.app.StatusBarManager.SESSION_KEYGUARD;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT;
@@ -38,6 +39,7 @@
 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.flags.Flags.FP_LISTEN_OCCLUDING_APPS;
 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;
@@ -64,6 +66,8 @@
 import static org.mockito.Mockito.when;
 
 import android.app.Activity;
+import android.app.ActivityTaskManager;
+import android.app.IActivityTaskManager;
 import android.app.admin.DevicePolicyManager;
 import android.app.trust.IStrongAuthTracker;
 import android.app.trust.TrustManager;
@@ -79,6 +83,7 @@
 import android.content.pm.UserInfo;
 import android.database.ContentObserver;
 import android.hardware.SensorPrivacyManager;
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.BiometricSourceType;
@@ -115,6 +120,7 @@
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
 
@@ -134,9 +140,12 @@
 import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.log.SessionTracker;
 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;
@@ -173,6 +182,8 @@
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
 public class KeyguardUpdateMonitorTest extends SysuiTestCase {
+    private static final String PKG_ALLOWING_FP_LISTEN_ON_OCCLUDING_ACTIVITY =
+            "test_app_fp_listen_on_occluding_activity";
     private static final String TEST_CARRIER = "TEST_CARRIER";
     private static final String TEST_CARRIER_2 = "TEST_CARRIER_2";
     private static final int TEST_CARRIER_ID = 1;
@@ -262,6 +273,10 @@
     private UsbPortStatus mUsbPortStatus;
     @Mock
     private Uri mURI;
+    @Mock
+    private TaskStackChangeListeners mTaskStackChangeListeners;
+    @Mock
+    private IActivityTaskManager mActivityTaskManager;
 
     private List<FaceSensorPropertiesInternal> mFaceSensorProperties;
     private List<FingerprintSensorPropertiesInternal> mFingerprintSensorProperties;
@@ -278,6 +293,7 @@
     private final Executor mBackgroundExecutor = Runnable::run;
     private final Executor mMainExecutor = Runnable::run;
     private TestableLooper mTestableLooper;
+    private FakeFeatureFlags mFeatureFlags;
     private Handler mHandler;
     private TestableKeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private MockitoSession mMockitoSession;
@@ -324,8 +340,14 @@
                 mDumpManager
         );
 
+        mContext.getOrCreateTestableResources().addOverride(com.android.systemui
+                        .R.array.config_fingerprint_listen_on_occluding_activity_packages,
+                new String[]{ PKG_ALLOWING_FP_LISTEN_ON_OCCLUDING_ACTIVITY });
+
         mTestableLooper = TestableLooper.get(this);
         allowTestableLooperAsMainThread();
+        mFeatureFlags = new FakeFeatureFlags();
+        mFeatureFlags.set(FP_LISTEN_OCCLUDING_APPS, false);
 
         when(mSecureSettings.getUriFor(anyString())).thenReturn(mURI);
 
@@ -1555,6 +1577,50 @@
     }
 
     @Test
+    public void listenForFingerprint_whenOccludingAppPkgOnAllowlist()
+            throws RemoteException {
+        mFeatureFlags.set(FP_LISTEN_OCCLUDING_APPS, true);
+
+        // GIVEN keyguard isn't visible (app occluding)
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
+        mKeyguardUpdateMonitor.setKeyguardShowing(true, true);
+        when(mStrongAuthTracker.hasUserAuthenticatedSinceBoot()).thenReturn(true);
+
+        // GIVEN the top activity is from a package that allows fingerprint listening over its
+        // occluding activities
+        setTopStandardActivity(PKG_ALLOWING_FP_LISTEN_ON_OCCLUDING_ACTIVITY);
+        onTaskStackChanged();
+
+        // THEN we SHOULD listen for non-UDFPS fingerprint
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isEqualTo(true);
+
+        // THEN we should listen for udfps (hiding mechanism to actually auth is
+        // controlled by UdfpsKeyguardViewController)
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isEqualTo(true);
+    }
+
+    @Test
+    public void doNotListenForFingerprint_whenOccludingAppPkgNotOnAllowlist()
+            throws RemoteException {
+        mFeatureFlags.set(FP_LISTEN_OCCLUDING_APPS, true);
+
+        // GIVEN keyguard isn't visible (app occluding)
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
+        mKeyguardUpdateMonitor.setKeyguardShowing(true, true);
+        when(mStrongAuthTracker.hasUserAuthenticatedSinceBoot()).thenReturn(true);
+
+        // GIVEN top activity is not in the allowlist for listening to fp over occluding activities
+        setTopStandardActivity("notInAllowList");
+
+        // THEN we should not listen for non-UDFPS fingerprint
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isEqualTo(false);
+
+        // THEN we should listen for udfps (hiding mechanism to actually auth is
+        // controlled by UdfpsKeyguardViewController)
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isEqualTo(true);
+    }
+
+    @Test
     public void testOccludingAppFingerprintListeningState() {
         // GIVEN keyguard isn't visible (app occluding)
         mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
@@ -2945,6 +3011,24 @@
                 TelephonyManager.SIM_STATE_NOT_READY);
     }
 
+    @Test
+    public void onAuthEnrollmentChangesCallbacksAreNotified() {
+        KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class);
+        ArgumentCaptor<AuthController.Callback> authCallback = ArgumentCaptor.forClass(
+                AuthController.Callback.class);
+        verify(mAuthController).addCallback(authCallback.capture());
+
+        mKeyguardUpdateMonitor.registerCallback(callback);
+
+        authCallback.getValue().onEnrollmentsChanged(TYPE_FINGERPRINT);
+        mTestableLooper.processAllMessages();
+        verify(callback).onBiometricEnrollmentStateChanged(BiometricSourceType.FINGERPRINT);
+
+        authCallback.getValue().onEnrollmentsChanged(BiometricAuthenticator.TYPE_FACE);
+        mTestableLooper.processAllMessages();
+        verify(callback).onBiometricEnrollmentStateChanged(BiometricSourceType.FACE);
+    }
+
     private void verifyFingerprintAuthenticateNeverCalled() {
         verify(mFingerprintManager, never()).authenticate(any(), any(), any(), any(), any());
         verify(mFingerprintManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
@@ -3255,6 +3339,23 @@
                 BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE);
     }
 
+    private void setTopStandardActivity(String pkgName) throws RemoteException {
+        final ActivityTaskManager.RootTaskInfo taskInfo = new ActivityTaskManager.RootTaskInfo();
+        taskInfo.visible = true;
+        taskInfo.topActivity = TextUtils.isEmpty(pkgName)
+                ? null : new ComponentName(pkgName, "testClass");
+        when(mActivityTaskManager.getRootTaskInfo(anyInt(), eq(ACTIVITY_TYPE_STANDARD)))
+                .thenReturn(taskInfo);
+    }
+
+    private void onTaskStackChanged() {
+        ArgumentCaptor<TaskStackChangeListener> taskStackChangeListenerCaptor =
+                ArgumentCaptor.forClass(TaskStackChangeListener.class);
+        verify(mTaskStackChangeListeners).registerTaskStackListener(
+                taskStackChangeListenerCaptor.capture());
+        taskStackChangeListenerCaptor.getValue().onTaskStackChangedBackground();
+    }
+
     private class TestableKeyguardUpdateMonitor extends KeyguardUpdateMonitor {
         AtomicBoolean mSimStateChanged = new AtomicBoolean(false);
         AtomicInteger mCachedSimState = new AtomicInteger(-1);
@@ -3272,7 +3373,8 @@
                     mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager,
                     mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager,
                     mFaceWakeUpTriggersConfig, mDevicePostureController,
-                    Optional.of(mInteractiveToAuthProvider));
+                    Optional.of(mInteractiveToAuthProvider), mFeatureFlags,
+                    mTaskStackChangeListeners, mActivityTaskManager);
             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 403bd8c..75106e7 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
@@ -18,6 +18,8 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1;
+import static com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR;
+import static com.android.systemui.flags.Flags.MIGRATE_LOCK_ICON;
 
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
@@ -40,18 +42,15 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.biometrics.AuthRippleController;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 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.flags.FakeFeatureFlags;
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
-import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
 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;
@@ -92,13 +91,11 @@
     protected @Mock ConfigurationController mConfigurationController;
     protected @Mock VibratorHelper mVibrator;
     protected @Mock AuthRippleController mAuthRippleController;
-    protected @Mock FeatureFlags mFeatureFlags;
     protected @Mock KeyguardTransitionRepository mTransitionRepository;
-    protected @Mock CommandQueue mCommandQueue;
     protected FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock());
+    protected FakeFeatureFlags mFeatureFlags;
     protected @Mock PrimaryBouncerInteractor mPrimaryBouncerInteractor;
 
-
     protected LockIconViewController mUnderTest;
 
     // Capture listeners so that they can be used to send events
@@ -147,6 +144,9 @@
         when(mStatusBarStateController.isDozing()).thenReturn(false);
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
 
+        mFeatureFlags = new FakeFeatureFlags();
+        mFeatureFlags.set(FACE_AUTH_REFACTOR, false);
+        mFeatureFlags.set(MIGRATE_LOCK_ICON, false);
         mUnderTest = new LockIconViewController(
                 mLockIconView,
                 mStatusBarStateController,
@@ -164,12 +164,7 @@
                 mResources,
                 new KeyguardTransitionInteractor(mTransitionRepository,
                         TestScopeProvider.getTestScope().getBackgroundScope()),
-                new KeyguardInteractor(
-                        new FakeKeyguardRepository(),
-                        mCommandQueue,
-                        mFeatureFlags,
-                        new FakeKeyguardBouncerRepository()
-                ),
+                KeyguardInteractorFactory.create(mFeatureFlags).getKeyguardInteractor(),
                 mFeatureFlags,
                 mPrimaryBouncerInteractor
         );
@@ -231,7 +226,7 @@
     }
 
     protected void init(boolean useMigrationFlag) {
-        when(mFeatureFlags.isEnabled(DOZING_MIGRATION_1)).thenReturn(useMigrationFlag);
+        mFeatureFlags.set(DOZING_MIGRATION_1, useMigrationFlag);
         mUnderTest.init();
 
         verify(mLockIconView, atLeast(1)).addOnAttachStateChangeListener(mAttachCaptor.capture());
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/AnalogClockControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/clock/AnalogClockControllerTest.java
deleted file mode 100644
index ef3af8a..0000000
--- a/packages/SystemUI/tests/src/com/android/keyguard/clock/AnalogClockControllerTest.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.res.Resources;
-import android.graphics.Color;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper.RunWithLooper;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.colorextraction.SysuiColorExtractor;
-
-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)
-@RunWithLooper
-public final class AnalogClockControllerTest extends SysuiTestCase {
-
-    private AnalogClockController mClockController;
-    @Mock SysuiColorExtractor mMockColorExtractor;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-
-        Resources res = getContext().getResources();
-        LayoutInflater layoutInflater = LayoutInflater.from(getContext());
-        mClockController = new AnalogClockController(res, layoutInflater,
-                mMockColorExtractor);
-    }
-
-    @Test
-    public void setDarkAmount_AOD() {
-        ViewGroup smallClockFrame = (ViewGroup) mClockController.getView();
-        View smallClock = smallClockFrame.getChildAt(0);
-        // WHEN dark amount is set to AOD
-        mClockController.setDarkAmount(1f);
-        // THEN small clock should be shown.
-        assertThat(smallClock.getVisibility()).isEqualTo(View.VISIBLE);
-    }
-
-    @Test
-    public void setColorPalette_setDigitalClock() {
-        ViewGroup smallClock = (ViewGroup) mClockController.getView();
-        // WHEN color palette is set
-        mClockController.setColorPalette(true, new int[]{Color.RED});
-        // THEN child of small clock should have text color set.
-        TextView digitalClock = (TextView) smallClock.getChildAt(0);
-        assertThat(digitalClock.getCurrentTextColor()).isEqualTo(Color.RED);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/BubbleClockControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/clock/BubbleClockControllerTest.java
deleted file mode 100644
index b56986e..0000000
--- a/packages/SystemUI/tests/src/com/android/keyguard/clock/BubbleClockControllerTest.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.res.Resources;
-import android.graphics.Color;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper.RunWithLooper;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.colorextraction.SysuiColorExtractor;
-
-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)
-@RunWithLooper
-public final class BubbleClockControllerTest extends SysuiTestCase {
-
-    private BubbleClockController mClockController;
-    @Mock SysuiColorExtractor mMockColorExtractor;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-
-        Resources res = getContext().getResources();
-        LayoutInflater layoutInflater = LayoutInflater.from(getContext());
-        mClockController = new BubbleClockController(res, layoutInflater, mMockColorExtractor);
-    }
-
-    @Test
-    public void setDarkAmount_AOD() {
-        ViewGroup smallClockFrame = (ViewGroup) mClockController.getView();
-        View smallClock = smallClockFrame.getChildAt(0);
-        // WHEN dark amount is set to AOD
-        mClockController.setDarkAmount(1f);
-        // THEN small clock should not be shown.
-        assertThat(smallClock.getVisibility()).isEqualTo(View.VISIBLE);
-    }
-
-    @Test
-    public void setColorPalette_setDigitalClock() {
-        ViewGroup smallClock = (ViewGroup) mClockController.getView();
-        // WHEN text color is set
-        mClockController.setColorPalette(true, new int[]{Color.RED});
-        // THEN child of small clock should have text color set.
-        TextView digitalClock = (TextView) smallClock.getChildAt(0);
-        assertThat(digitalClock.getCurrentTextColor()).isEqualTo(Color.RED);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockInfoTest.java b/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockInfoTest.java
deleted file mode 100644
index 4c0890a..0000000
--- a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockInfoTest.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.verify;
-
-import android.graphics.Bitmap;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper.RunWithLooper;
-
-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.MockitoAnnotations;
-
-import java.util.function.Supplier;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@RunWithLooper
-public final class ClockInfoTest extends SysuiTestCase {
-
-    @Mock
-    private Supplier<Bitmap> mMockSupplier;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-    }
-
-    @Test
-    public void testGetName() {
-        final String name = "name";
-        ClockInfo info = ClockInfo.builder().setName(name).build();
-        assertThat(info.getName()).isEqualTo(name);
-    }
-
-    @Test
-    public void testGetTitle() {
-        final String title = "title";
-        ClockInfo info = ClockInfo.builder().setTitle(() -> title).build();
-        assertThat(info.getTitle()).isEqualTo(title);
-    }
-
-    @Test
-    public void testGetId() {
-        final String id = "id";
-        ClockInfo info = ClockInfo.builder().setId(id).build();
-        assertThat(info.getId()).isEqualTo(id);
-    }
-
-    @Test
-    public void testGetThumbnail() {
-        ClockInfo info = ClockInfo.builder().setThumbnail(mMockSupplier).build();
-        info.getThumbnail();
-        verify(mMockSupplier).get();
-    }
-
-    @Test
-    public void testGetPreview() {
-        ClockInfo info = ClockInfo.builder().setPreview(mMockSupplier).build();
-        info.getPreview();
-        verify(mMockSupplier).get();
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java
deleted file mode 100644
index 7a5b772..0000000
--- a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.ContentResolver;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper.RunWithLooper;
-import android.view.LayoutInflater;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.dock.DockManager;
-import com.android.systemui.dock.DockManagerFake;
-import com.android.systemui.plugins.ClockPlugin;
-import com.android.systemui.plugins.PluginManager;
-import com.android.systemui.settings.UserTracker;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import org.junit.After;
-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;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-// Need to run tests on main looper to allow for onClockChanged operation to happen synchronously.
-@RunWithLooper(setAsMainLooper = true)
-public final class ClockManagerTest extends SysuiTestCase {
-
-    private static final String BUBBLE_CLOCK = BubbleClockController.class.getName();
-    private static final Class<?> BUBBLE_CLOCK_CLASS = BubbleClockController.class;
-    private static final int MAIN_USER_ID = 0;
-    private static final int SECONDARY_USER_ID = 11;
-    private static final Uri SETTINGS_URI = null;
-
-    private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
-    private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
-    private ClockManager mClockManager;
-    private ContentObserver mContentObserver;
-    private DockManagerFake mFakeDockManager;
-    private ArgumentCaptor<UserTracker.Callback> mUserTrackerCallbackCaptor;
-    @Mock PluginManager mMockPluginManager;
-    @Mock SysuiColorExtractor mMockColorExtractor;
-    @Mock ContentResolver mMockContentResolver;
-    @Mock UserTracker mUserTracker;
-    @Mock SettingsWrapper mMockSettingsWrapper;
-    @Mock ClockManager.ClockChangedListener mMockListener1;
-    @Mock ClockManager.ClockChangedListener mMockListener2;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-
-        LayoutInflater inflater = LayoutInflater.from(getContext());
-
-        mFakeDockManager = new DockManagerFake();
-
-        when(mUserTracker.getUserId()).thenReturn(MAIN_USER_ID);
-        mUserTrackerCallbackCaptor = ArgumentCaptor.forClass(UserTracker.Callback.class);
-
-        mClockManager = new ClockManager(getContext(), inflater,
-                mMockPluginManager, mMockColorExtractor, mMockContentResolver,
-                mUserTracker, mMainExecutor, mMockSettingsWrapper, mFakeDockManager);
-
-        mClockManager.addBuiltinClock(() -> new BubbleClockController(
-                getContext().getResources(), inflater, mMockColorExtractor));
-        mClockManager.addOnClockChangedListener(mMockListener1);
-        mClockManager.addOnClockChangedListener(mMockListener2);
-        verify(mUserTracker).addCallback(mUserTrackerCallbackCaptor.capture(), any());
-        reset(mMockListener1, mMockListener2);
-
-        mContentObserver = mClockManager.getContentObserver();
-    }
-
-    @After
-    public void tearDown() {
-        mClockManager.removeOnClockChangedListener(mMockListener1);
-        mClockManager.removeOnClockChangedListener(mMockListener2);
-    }
-
-    @Test
-    public void dockEvent() {
-        mFakeDockManager.setDockEvent(DockManager.STATE_DOCKED);
-        assertThat(mClockManager.isDocked()).isTrue();
-    }
-
-    @Test
-    public void undockEvent() {
-        mFakeDockManager.setDockEvent(DockManager.STATE_NONE);
-        assertThat(mClockManager.isDocked()).isFalse();
-    }
-
-    @Test
-    public void getCurrentClock_default() {
-        // GIVEN that settings doesn't contain any values
-        when(mMockSettingsWrapper.getLockScreenCustomClockFace(anyInt())).thenReturn(null);
-        when(mMockSettingsWrapper.getDockedClockFace(anyInt())).thenReturn(null);
-        // WHEN settings change event is fired
-        mContentObserver.onChange(false, Arrays.asList(SETTINGS_URI), 0, MAIN_USER_ID);
-        // THEN the result is null, indicated the default clock face should be used.
-        assertThat(mClockManager.getCurrentClock()).isNull();
-    }
-
-    @Test
-    public void getCurrentClock_customClock() {
-        // GIVEN that settings is set to the bubble clock face
-        when(mMockSettingsWrapper.getLockScreenCustomClockFace(anyInt())).thenReturn(BUBBLE_CLOCK);
-        // WHEN settings change event is fired
-        mContentObserver.onChange(false, Arrays.asList(SETTINGS_URI), 0, MAIN_USER_ID);
-        // THEN the plugin is the bubble clock face.
-        assertThat(mClockManager.getCurrentClock()).isInstanceOf(BUBBLE_CLOCK_CLASS);
-    }
-
-    @Test
-    public void onClockChanged_customClock() {
-        // GIVEN that settings is set to the bubble clock face
-        when(mMockSettingsWrapper.getLockScreenCustomClockFace(anyInt())).thenReturn(BUBBLE_CLOCK);
-        // WHEN settings change event is fired
-        mContentObserver.onChange(false, Arrays.asList(SETTINGS_URI), 0, MAIN_USER_ID);
-        // THEN the plugin is the bubble clock face.
-        ArgumentCaptor<ClockPlugin> captor = ArgumentCaptor.forClass(ClockPlugin.class);
-        verify(mMockListener1).onClockChanged(captor.capture());
-        assertThat(captor.getValue()).isInstanceOf(BUBBLE_CLOCK_CLASS);
-    }
-
-    @Test
-    public void onClockChanged_uniqueInstances() {
-        // GIVEN that settings is set to the bubble clock face
-        when(mMockSettingsWrapper.getLockScreenCustomClockFace(anyInt())).thenReturn(BUBBLE_CLOCK);
-        // WHEN settings change event is fired
-        mContentObserver.onChange(false, Arrays.asList(SETTINGS_URI), 0, MAIN_USER_ID);
-        // THEN the listeners receive separate instances of the Bubble clock plugin.
-        ArgumentCaptor<ClockPlugin> captor1 = ArgumentCaptor.forClass(ClockPlugin.class);
-        ArgumentCaptor<ClockPlugin> captor2 = ArgumentCaptor.forClass(ClockPlugin.class);
-        verify(mMockListener1).onClockChanged(captor1.capture());
-        verify(mMockListener2).onClockChanged(captor2.capture());
-        assertThat(captor1.getValue()).isInstanceOf(BUBBLE_CLOCK_CLASS);
-        assertThat(captor2.getValue()).isInstanceOf(BUBBLE_CLOCK_CLASS);
-        assertThat(captor1.getValue()).isNotSameInstanceAs(captor2.getValue());
-    }
-
-    @Test
-    public void getCurrentClock_badSettingsValue() {
-        // GIVEN that settings contains a value that doesn't correspond to a
-        // custom clock face.
-        when(mMockSettingsWrapper.getLockScreenCustomClockFace(anyInt())).thenReturn("bad value");
-        // WHEN settings change event is fired
-        mContentObserver.onChange(false, Arrays.asList(SETTINGS_URI), 0, MAIN_USER_ID);
-        // THEN the result is null.
-        assertThat(mClockManager.getCurrentClock()).isNull();
-    }
-
-    @Test
-    public void getCurrentClock_dockedDefault() {
-        // WHEN dock event is fired
-        mFakeDockManager.setDockEvent(DockManager.STATE_DOCKED);
-        // THEN the result is null, indicating the default clock face.
-        assertThat(mClockManager.getCurrentClock()).isNull();
-    }
-
-    @Test
-    public void getCurrentClock_dockedCustomClock() {
-        // GIVEN settings is set to the bubble clock face
-        when(mMockSettingsWrapper.getDockedClockFace(anyInt())).thenReturn(BUBBLE_CLOCK);
-        // WHEN dock event fires
-        mFakeDockManager.setDockEvent(DockManager.STATE_DOCKED);
-        // THEN the plugin is the bubble clock face.
-        assertThat(mClockManager.getCurrentClock()).isInstanceOf(BUBBLE_CLOCK_CLASS);
-    }
-
-    @Test
-    public void getCurrentClock_badDockedSettingsValue() {
-        // GIVEN settings contains a value that doesn't correspond to an available clock face.
-        when(mMockSettingsWrapper.getDockedClockFace(anyInt())).thenReturn("bad value");
-        // WHEN dock event fires
-        mFakeDockManager.setDockEvent(DockManager.STATE_DOCKED);
-        // THEN the result is null.
-        assertThat(mClockManager.getCurrentClock()).isNull();
-    }
-
-    @Test
-    public void getCurrentClock_badDockedSettingsFallback() {
-        // GIVEN settings contains a value that doesn't correspond to an available clock face, but
-        // locked screen settings is set to bubble clock.
-        when(mMockSettingsWrapper.getDockedClockFace(anyInt())).thenReturn("bad value");
-        when(mMockSettingsWrapper.getLockScreenCustomClockFace(anyInt())).thenReturn(BUBBLE_CLOCK);
-        // WHEN dock event is fired
-        mFakeDockManager.setDockEvent(DockManager.STATE_DOCKED);
-        // THEN the plugin is the bubble clock face.
-        assertThat(mClockManager.getCurrentClock()).isInstanceOf(BUBBLE_CLOCK_CLASS);
-    }
-
-    @Test
-    public void onUserChanged_defaultClock() {
-        // WHEN the user changes
-        switchUser(SECONDARY_USER_ID);
-        // THEN the plugin is null for the default clock face
-        assertThat(mClockManager.getCurrentClock()).isNull();
-    }
-
-    @Test
-    public void onUserChanged_customClock() {
-        // GIVEN that a second user has selected the bubble clock face
-        when(mMockSettingsWrapper.getLockScreenCustomClockFace(SECONDARY_USER_ID)).thenReturn(
-                BUBBLE_CLOCK);
-        // WHEN the user changes
-        switchUser(SECONDARY_USER_ID);
-        // THEN the plugin is the bubble clock face.
-        assertThat(mClockManager.getCurrentClock()).isInstanceOf(BUBBLE_CLOCK_CLASS);
-    }
-
-    @Test
-    public void onUserChanged_docked() {
-        // GIVEN device is docked
-        mFakeDockManager.setDockEvent(DockManager.STATE_DOCKED);
-        // AND the second user as selected the bubble clock for the dock
-        when(mMockSettingsWrapper.getDockedClockFace(SECONDARY_USER_ID)).thenReturn(BUBBLE_CLOCK);
-        // WHEN the user changes
-        switchUser(SECONDARY_USER_ID);
-        // THEN the plugin is the bubble clock face.
-        assertThat(mClockManager.getCurrentClock()).isInstanceOf(BUBBLE_CLOCK_CLASS);
-    }
-
-    private void switchUser(int newUser) {
-        when(mUserTracker.getUserId()).thenReturn(newUser);
-        mUserTrackerCallbackCaptor.getValue().onUserChanged(newUser, mContext);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockOptionsProviderTest.java b/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockOptionsProviderTest.java
deleted file mode 100644
index d2832fb9..0000000
--- a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockOptionsProviderTest.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.verify;
-
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper.RunWithLooper;
-
-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.MockitoAnnotations;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.Supplier;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@RunWithLooper
-public final class ClockOptionsProviderTest extends SysuiTestCase {
-
-    private static final String CONTENT_SCHEME = "content";
-    private static final String AUTHORITY = "com.android.keyguard.clock";
-    private static final String LIST_OPTIONS = "list_options";
-    private static final String PREVIEW = "preview";
-    private static final String THUMBNAIL = "thumbnail";
-    private static final String MIME_TYPE_LIST_OPTIONS = "vnd.android.cursor.dir/clock_faces";
-    private static final String MIME_TYPE_PNG = "image/png";
-    private static final String NAME_COLUMN = "name";
-    private static final String TITLE_COLUMN = "title";
-    private static final String ID_COLUMN = "id";
-    private static final String PREVIEW_COLUMN = "preview";
-    private static final String THUMBNAIL_COLUMN = "thumbnail";
-
-    private ClockOptionsProvider mProvider;
-    private Supplier<List<ClockInfo>> mMockSupplier;
-    private List<ClockInfo> mClocks;
-    private Uri mListOptionsUri;
-    @Mock
-    private Supplier<Bitmap> mMockBitmapSupplier;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mClocks = new ArrayList<>();
-        mProvider = new ClockOptionsProvider(() -> mClocks);
-        mListOptionsUri = new Uri.Builder()
-                .scheme(CONTENT_SCHEME)
-                .authority(AUTHORITY)
-                .appendPath(LIST_OPTIONS)
-                .build();
-    }
-
-    @Test
-    public void testGetType_listOptions() {
-        Uri uri = new Uri.Builder()
-                .scheme(CONTENT_SCHEME)
-                .authority(AUTHORITY)
-                .appendPath(LIST_OPTIONS)
-                .build();
-        assertThat(mProvider.getType(uri)).isEqualTo(MIME_TYPE_LIST_OPTIONS);
-    }
-
-    @Test
-    public void testGetType_preview() {
-        Uri uri = new Uri.Builder()
-                .scheme(CONTENT_SCHEME)
-                .authority(AUTHORITY)
-                .appendPath(PREVIEW)
-                .appendPath("id")
-                .build();
-        assertThat(mProvider.getType(uri)).isEqualTo(MIME_TYPE_PNG);
-    }
-
-    @Test
-    public void testGetType_thumbnail() {
-        Uri uri = new Uri.Builder()
-                .scheme(CONTENT_SCHEME)
-                .authority(AUTHORITY)
-                .appendPath(THUMBNAIL)
-                .appendPath("id")
-                .build();
-        assertThat(mProvider.getType(uri)).isEqualTo(MIME_TYPE_PNG);
-    }
-
-    @Test
-    public void testQuery_noClocks() {
-        Cursor cursor = mProvider.query(mListOptionsUri, null, null, null);
-        assertThat(cursor.getCount()).isEqualTo(0);
-    }
-
-    @Test
-    public void testQuery_listOptions() {
-        mClocks.add(ClockInfo.builder()
-                .setName("name_a")
-                .setTitle(() -> "title_a")
-                .setId("id_a")
-                .build());
-        mClocks.add(ClockInfo.builder()
-                .setName("name_b")
-                .setTitle(() -> "title_b")
-                .setId("id_b")
-                .build());
-        Cursor cursor = mProvider.query(mListOptionsUri, null, null, null);
-        assertThat(cursor.getCount()).isEqualTo(2);
-        cursor.moveToFirst();
-        assertThat(cursor.getString(
-                cursor.getColumnIndex(NAME_COLUMN))).isEqualTo("name_a");
-        assertThat(cursor.getString(
-                cursor.getColumnIndex(TITLE_COLUMN))).isEqualTo("title_a");
-        assertThat(cursor.getString(
-                cursor.getColumnIndex(ID_COLUMN))).isEqualTo("id_a");
-        assertThat(cursor.getString(
-                cursor.getColumnIndex(PREVIEW_COLUMN)))
-                .isEqualTo("content://com.android.keyguard.clock/preview/id_a");
-        assertThat(cursor.getString(
-                cursor.getColumnIndex(THUMBNAIL_COLUMN)))
-                .isEqualTo("content://com.android.keyguard.clock/thumbnail/id_a");
-        cursor.moveToNext();
-        assertThat(cursor.getString(
-                cursor.getColumnIndex(NAME_COLUMN))).isEqualTo("name_b");
-        assertThat(cursor.getString(
-                cursor.getColumnIndex(TITLE_COLUMN))).isEqualTo("title_b");
-        assertThat(cursor.getString(
-                cursor.getColumnIndex(ID_COLUMN))).isEqualTo("id_b");
-        assertThat(cursor.getString(
-                cursor.getColumnIndex(PREVIEW_COLUMN)))
-                .isEqualTo("content://com.android.keyguard.clock/preview/id_b");
-        assertThat(cursor.getString(
-                cursor.getColumnIndex(THUMBNAIL_COLUMN)))
-                .isEqualTo("content://com.android.keyguard.clock/thumbnail/id_b");
-    }
-
-    @Test
-    public void testOpenFile_preview() throws Exception {
-        mClocks.add(ClockInfo.builder()
-                .setId("id")
-                .setPreview(mMockBitmapSupplier)
-                .build());
-        Uri uri = new Uri.Builder()
-                .scheme(CONTENT_SCHEME)
-                .authority(AUTHORITY)
-                .appendPath(PREVIEW)
-                .appendPath("id")
-                .build();
-        mProvider.openFile(uri, "r").close();
-        verify(mMockBitmapSupplier).get();
-    }
-
-    @Test
-    public void testOpenFile_thumbnail() throws Exception {
-        mClocks.add(ClockInfo.builder()
-                .setId("id")
-                .setThumbnail(mMockBitmapSupplier)
-                .build());
-        Uri uri = new Uri.Builder()
-                .scheme(CONTENT_SCHEME)
-                .authority(AUTHORITY)
-                .appendPath(THUMBNAIL)
-                .appendPath("id")
-                .build();
-        mProvider.openFile(uri, "r").close();
-        verify(mMockBitmapSupplier).get();
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockPaletteTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockPaletteTest.kt
deleted file mode 100644
index 347b26d..0000000
--- a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockPaletteTest.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock
-
-import android.graphics.Color
-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
-
-@RunWith(AndroidTestingRunner::class)
-@SmallTest
-class ClockPaletteTest : SysuiTestCase() {
-
-    private lateinit var clockPalette: ClockPalette
-    private lateinit var colors: IntArray
-
-    @Before
-    fun setUp() {
-        clockPalette = ClockPalette()
-        // colors used are reds from light to dark.
-        val hsv: FloatArray = FloatArray(3)
-        Color.colorToHSV(Color.RED, hsv)
-        colors = IntArray(10)
-        val step: Float = (0f - hsv[2]) / colors.size
-        for (i in 0 until colors.size) {
-            hsv[2] += step
-            colors[i] = Color.HSVToColor(hsv)
-        }
-    }
-
-    @Test
-    fun testDark() {
-        // GIVEN on AOD
-        clockPalette.setDarkAmount(1f)
-        // AND GIVEN that wallpaper doesn't support dark text
-        clockPalette.setColorPalette(false, colors)
-        // THEN the secondary color should be lighter than the primary color
-        assertThat(value(clockPalette.getPrimaryColor()))
-                .isGreaterThan(value(clockPalette.getSecondaryColor()))
-    }
-
-    @Test
-    fun testDarkText() {
-        // GIVEN on lock screen
-        clockPalette.setDarkAmount(0f)
-        // AND GIVEN that wallpaper supports dark text
-        clockPalette.setColorPalette(true, colors)
-        // THEN the secondary color should be darker the primary color
-        assertThat(value(clockPalette.getPrimaryColor()))
-                .isLessThan(value(clockPalette.getSecondaryColor()))
-    }
-
-    @Test
-    fun testLightText() {
-        // GIVEN on lock screen
-        clockPalette.setDarkAmount(0f)
-        // AND GIVEN that wallpaper doesn't support dark text
-        clockPalette.setColorPalette(false, colors)
-        // THEN the secondary color should be darker than the primary color
-        assertThat(value(clockPalette.getPrimaryColor()))
-                .isGreaterThan(value(clockPalette.getSecondaryColor()))
-    }
-
-    @Test
-    fun testNullColors() {
-        // GIVEN on AOD
-        clockPalette.setDarkAmount(1f)
-        // AND GIVEN that wallpaper colors are null
-        clockPalette.setColorPalette(false, null)
-        // THEN the primary color should be whilte
-        assertThat(clockPalette.getPrimaryColor()).isEqualTo(Color.WHITE)
-    }
-
-    private fun value(color: Int): Float {
-        val hsv: FloatArray = FloatArray(3)
-        Color.colorToHSV(color, hsv)
-        return hsv[2]
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/CrossFadeDarkControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/clock/CrossFadeDarkControllerTest.java
deleted file mode 100644
index fd7657f..0000000
--- a/packages/SystemUI/tests/src/com/android/keyguard/clock/CrossFadeDarkControllerTest.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper.RunWithLooper;
-import android.view.View;
-import android.widget.TextView;
-
-import com.android.systemui.SysuiTestCase;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@RunWithLooper
-public final class CrossFadeDarkControllerTest extends SysuiTestCase {
-
-    private View mViewFadeIn;
-    private View mViewFadeOut;
-    private CrossFadeDarkController mDarkController;
-
-    @Before
-    public void setUp() {
-        mViewFadeIn = new TextView(getContext());
-        mViewFadeIn.setVisibility(View.VISIBLE);
-        mViewFadeIn.setAlpha(1f);
-        mViewFadeOut = new TextView(getContext());
-        mViewFadeOut.setVisibility(View.VISIBLE);
-        mViewFadeOut.setAlpha(1f);
-
-        mDarkController = new CrossFadeDarkController(mViewFadeIn, mViewFadeOut);
-    }
-
-    @Test
-    public void setDarkAmount_fadeIn() {
-        // WHEN dark amount corresponds to AOD
-        mDarkController.setDarkAmount(1f);
-        // THEN fade in view should be faded in and fade out view faded out.
-        assertThat(mViewFadeIn.getAlpha()).isEqualTo(1f);
-        assertThat(mViewFadeIn.getVisibility()).isEqualTo(View.VISIBLE);
-        assertThat(mViewFadeOut.getAlpha()).isEqualTo(0f);
-        assertThat(mViewFadeOut.getVisibility()).isEqualTo(View.GONE);
-    }
-
-    @Test
-    public void setDarkAmount_fadeOut() {
-        // WHEN dark amount corresponds to lock screen
-        mDarkController.setDarkAmount(0f);
-        // THEN fade out view should bed faded out and fade in view faded in.
-        assertThat(mViewFadeIn.getAlpha()).isEqualTo(0f);
-        assertThat(mViewFadeIn.getVisibility()).isEqualTo(View.GONE);
-        assertThat(mViewFadeOut.getAlpha()).isEqualTo(1f);
-        assertThat(mViewFadeOut.getVisibility()).isEqualTo(View.VISIBLE);
-    }
-
-    @Test
-    public void setDarkAmount_partialFadeIn() {
-        // WHEN dark amount corresponds to a partial transition
-        mDarkController.setDarkAmount(0.9f);
-        // THEN views should have intermediate alpha value.
-        assertThat(mViewFadeIn.getAlpha()).isGreaterThan(0f);
-        assertThat(mViewFadeIn.getAlpha()).isLessThan(1f);
-        assertThat(mViewFadeIn.getVisibility()).isEqualTo(View.VISIBLE);
-    }
-
-    @Test
-    public void setDarkAmount_partialFadeOut() {
-        // WHEN dark amount corresponds to a partial transition
-        mDarkController.setDarkAmount(0.1f);
-        // THEN views should have intermediate alpha value.
-        assertThat(mViewFadeOut.getAlpha()).isGreaterThan(0f);
-        assertThat(mViewFadeOut.getAlpha()).isLessThan(1f);
-        assertThat(mViewFadeOut.getVisibility()).isEqualTo(View.VISIBLE);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/SettingsWrapperTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/clock/SettingsWrapperTest.kt
deleted file mode 100644
index 573581d..0000000
--- a/packages/SystemUI/tests/src/com/android/keyguard/clock/SettingsWrapperTest.kt
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock
-
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.google.common.truth.Truth.assertThat
-import org.json.JSONObject
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-private const val PACKAGE = "com.android.keyguard.clock.Clock"
-private const val CLOCK_FIELD = "clock"
-private const val TIMESTAMP_FIELD = "_applied_timestamp"
-private const val USER_ID = 0
-
-@RunWith(AndroidTestingRunner::class)
-@SmallTest
-class SettingsWrapperTest : SysuiTestCase() {
-
-    private lateinit var wrapper: SettingsWrapper
-    private lateinit var migration: SettingsWrapper.Migration
-
-    @Before
-    fun setUp() {
-        migration = mock(SettingsWrapper.Migration::class.java)
-        wrapper = SettingsWrapper(getContext().contentResolver, migration)
-    }
-
-    @Test
-    fun testDecodeUnnecessary() {
-        // GIVEN a settings value that doesn't need to be decoded
-        val value = PACKAGE
-        // WHEN the value is decoded
-        val decoded = wrapper.decode(value, USER_ID)
-        // THEN the same value is returned, because decoding isn't necessary.
-        // TODO(b/135674383): Null should be returned when the migration code in removed.
-        assertThat(decoded).isEqualTo(value)
-        // AND the value is migrated to JSON format
-        verify(migration).migrate(value, USER_ID)
-    }
-
-    @Test
-    fun testDecodeJSON() {
-        // GIVEN a settings value that is encoded in JSON
-        val json: JSONObject = JSONObject()
-        json.put(CLOCK_FIELD, PACKAGE)
-        json.put(TIMESTAMP_FIELD, System.currentTimeMillis())
-        val value = json.toString()
-        // WHEN the value is decoded
-        val decoded = wrapper.decode(value, USER_ID)
-        // THEN the clock field should have been extracted
-        assertThat(decoded).isEqualTo(PACKAGE)
-    }
-
-    @Test
-    fun testDecodeJSONWithoutClockField() {
-        // GIVEN a settings value that doesn't contain the CLOCK_FIELD
-        val json: JSONObject = JSONObject()
-        json.put(TIMESTAMP_FIELD, System.currentTimeMillis())
-        val value = json.toString()
-        // WHEN the value is decoded
-        val decoded = wrapper.decode(value, USER_ID)
-        // THEN null is returned
-        assertThat(decoded).isNull()
-        // AND the value is not migrated to JSON format
-        verify(migration, never()).migrate(value, USER_ID)
-    }
-
-    @Test
-    fun testDecodeNullJSON() {
-        assertThat(wrapper.decode(null, USER_ID)).isNull()
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/SmallClockPositionTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/clock/SmallClockPositionTest.kt
deleted file mode 100644
index 3a27e35..0000000
--- a/packages/SystemUI/tests/src/com/android/keyguard/clock/SmallClockPositionTest.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock
-
-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
-
-@RunWith(AndroidTestingRunner::class)
-@SmallTest
-class SmallClockPositionTest : SysuiTestCase() {
-
-    private val statusBarHeight = 100
-    private val lockPadding = 15
-    private val lockHeight = 35
-    private val burnInY = 20
-
-    private lateinit var position: SmallClockPosition
-
-    @Before
-    fun setUp() {
-        position = SmallClockPosition(statusBarHeight, lockPadding, lockHeight, burnInY)
-    }
-
-    @Test
-    fun loadResources() {
-        // Cover constructor taking Resources object.
-        position = SmallClockPosition(context)
-        position.setDarkAmount(1f)
-        assertThat(position.preferredY).isGreaterThan(0)
-    }
-
-    @Test
-    fun darkPosition() {
-        // GIVEN on AOD
-        position.setDarkAmount(1f)
-        // THEN Y is sum of statusBarHeight, lockPadding, lockHeight, lockPadding, burnInY
-        assertThat(position.preferredY).isEqualTo(185)
-    }
-
-    @Test
-    fun lockPosition() {
-        // GIVEN on lock screen
-        position.setDarkAmount(0f)
-        // THEN Y position is statusBarHeight + lockPadding + lockHeight + lockPadding
-        // (100 + 15 + 35 + 15 = 165)
-        assertThat(position.preferredY).isEqualTo(165)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/ViewPreviewerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/clock/ViewPreviewerTest.kt
deleted file mode 100644
index 5ece6ef..0000000
--- a/packages/SystemUI/tests/src/com/android/keyguard/clock/ViewPreviewerTest.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.clock
-
-import android.content.Context
-import com.google.common.truth.Truth.assertThat
-
-import android.graphics.Canvas
-import android.graphics.Color
-import android.testing.AndroidTestingRunner
-import android.view.View
-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 ViewPreviewerTest : SysuiTestCase() {
-
-    private lateinit var previewer: ViewPreviewer
-    private lateinit var view: View
-
-    @Before
-    fun setUp() {
-        previewer = ViewPreviewer()
-        view = TestView(context)
-    }
-
-    @Test
-    fun testCreatePreview() {
-        val width = 100
-        val height = 100
-        // WHEN a preview image is created
-        val bitmap = previewer.createPreview(view, width, height)!!
-        // THEN the bitmap has the expected width and height
-        assertThat(bitmap.height).isEqualTo(height)
-        assertThat(bitmap.width).isEqualTo(width)
-        assertThat(bitmap.getPixel(0, 0)).isEqualTo(Color.RED)
-    }
-
-    class TestView(context: Context) : View(context) {
-        override fun onDraw(canvas: Canvas?) {
-            super.onDraw(canvas)
-            canvas?.drawColor(Color.RED)
-        }
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java
index a0fdc8f..24a5e80 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java
@@ -48,8 +48,7 @@
     @Test
     public void testInitDependency() throws ExecutionException, InterruptedException {
         Dependency.clearDependencies();
-        SystemUIInitializer initializer =
-                SystemUIInitializerFactory.createFromConfigNoAssert(mContext);
+        SystemUIInitializer initializer = new SystemUIInitializerImpl(mContext);
         initializer.init(true);
         Dependency dependency = initializer.getSysUIComponent().createDependency();
         dependency.start();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt
index 8207fa6..d500b5a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt
@@ -74,7 +74,8 @@
 
         val decorationSupport = DisplayDecorationSupport()
         decorationSupport.format = PixelFormat.R_8
-        decorHwcLayer = Mockito.spy(ScreenDecorHwcLayer(mContext, decorationSupport))
+        decorHwcLayer =
+            Mockito.spy(ScreenDecorHwcLayer(mContext, decorationSupport, /* debug */ false))
         whenever(decorHwcLayer.width).thenReturn(displayWidth)
         whenever(decorHwcLayer.height).thenReturn(displayHeight)
         whenever(decorHwcLayer.context).thenReturn(mockContext)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index 4cf5a4b..796e665 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -43,7 +43,6 @@
 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.times;
 import static org.mockito.Mockito.verify;
@@ -63,6 +62,7 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
+import android.util.PathParser;
 import android.util.Size;
 import android.view.Display;
 import android.view.DisplayCutout;
@@ -83,6 +83,7 @@
 import com.android.systemui.decor.CornerDecorProvider;
 import com.android.systemui.decor.CutoutDecorProviderFactory;
 import com.android.systemui.decor.CutoutDecorProviderImpl;
+import com.android.systemui.decor.DebugRoundedCornerModel;
 import com.android.systemui.decor.DecorProvider;
 import com.android.systemui.decor.DecorProviderFactory;
 import com.android.systemui.decor.FaceScanningOverlayProviderImpl;
@@ -95,8 +96,8 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.commandline.CommandRegistry;
 import com.android.systemui.statusbar.events.PrivacyDotViewController;
-import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.concurrency.FakeThreadFactory;
 import com.android.systemui.util.settings.FakeSettings;
@@ -139,7 +140,7 @@
     @Mock
     private Display mDisplay;
     @Mock
-    private TunerService mTunerService;
+    private CommandRegistry mCommandRegistry;
     @Mock
     private UserTracker mUserTracker;
     @Mock
@@ -233,8 +234,9 @@
                 mExecutor,
                 new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer"))));
 
-        mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings,
-                mTunerService, mUserTracker, mDisplayTracker, mDotViewController, mThreadFactory,
+        mScreenDecorations = spy(new ScreenDecorations(mContext, mSecureSettings,
+                mCommandRegistry, mUserTracker, mDisplayTracker, mDotViewController,
+                mThreadFactory,
                 mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
                 new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")),
                 mAuthController) {
@@ -251,12 +253,6 @@
             }
 
             @Override
-            public void onTuningChanged(String key, String newValue) {
-                super.onTuningChanged(key, newValue);
-                mExecutor.runAllReady();
-            }
-
-            @Override
             protected void updateOverlayWindowVisibilityIfViewExists(@Nullable View view) {
                 super.updateOverlayWindowVisibilityIfViewExists(view);
                 mExecutor.runAllReady();
@@ -268,9 +264,10 @@
             }
         });
         mScreenDecorations.mDisplayInfo = mDisplayInfo;
+        // Make sure tests are never run starting in debug mode
+        mScreenDecorations.setDebug(false);
         doReturn(1f).when(mScreenDecorations).getPhysicalPixelDisplaySizeRatio();
         doNothing().when(mScreenDecorations).updateOverlayProviderViews(any());
-        reset(mTunerService);
 
         try {
             mPrivacyDotShowingListener = mScreenDecorations.mPrivacyDotShowingListener.getClass()
@@ -464,8 +461,6 @@
         mScreenDecorations.start();
         // No views added.
         verifyOverlaysExistAndAdded(false, false, false, false, null);
-        // No Tuners tuned.
-        verify(mTunerService, never()).addTunable(any(), any());
         // No dot controller init
         verify(mDotViewController, never()).initialize(any(), any(), any(), any());
     }
@@ -497,8 +492,6 @@
         // Face scanning doesn't exist
         verifyFaceScanningViewExists(false);
 
-        // One tunable.
-        verify(mTunerService, times(1)).addTunable(any(), any());
         // Dot controller init
         verify(mDotViewController, times(1)).initialize(
                 isA(View.class), isA(View.class), isA(View.class), isA(View.class));
@@ -528,8 +521,6 @@
         // Face scanning doesn't exist
         verifyFaceScanningViewExists(false);
 
-        // One tunable.
-        verify(mTunerService, times(1)).addTunable(any(), any());
         // No dot controller init
         verify(mDotViewController, never()).initialize(any(), any(), any(), any());
     }
@@ -560,8 +551,6 @@
         // Face scanning doesn't exist
         verifyFaceScanningViewExists(false);
 
-        // One tunable.
-        verify(mTunerService, times(1)).addTunable(any(), any());
         // Dot controller init
         verify(mDotViewController, times(1)).initialize(
                 isA(View.class), isA(View.class), isA(View.class), isA(View.class));
@@ -1073,18 +1062,89 @@
     }
 
     @Test
+    public void testDebugRoundedCorners_noDeviceCornersSet() {
+        setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                null /* roundedTopDrawable */, null /* roundedBottomDrawable */,
+                0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */);
+
+        mScreenDecorations.start();
+        // No rounded corners exist at this point
+        verifyOverlaysExistAndAdded(false, false, false, false, View.VISIBLE);
+
+        // Path from rounded.xml, scaled by 10x to produce 80x80 corners
+        Path debugPath = PathParser.createPathFromPathData("M8,0H0v8C0,3.6,3.6,0,8,0z");
+        // WHEN debug corners are added to the delegate
+        DebugRoundedCornerModel debugCorner = new DebugRoundedCornerModel(
+                debugPath,
+                80,
+                80,
+                10f,
+                10f
+        );
+        mScreenDecorations.mDebugRoundedCornerDelegate
+                .applyNewDebugCorners(debugCorner, debugCorner);
+
+        // AND debug mode is entered
+        mScreenDecorations.setDebug(true);
+        mExecutor.runAllReady();
+
+        // THEN the debug corners provide decor
+        List<DecorProvider> providers = mScreenDecorations.getProviders(false);
+        assertEquals(4, providers.size());
+
+        // Top and bottom overlays contain the debug rounded corners
+        verifyOverlaysExistAndAdded(false, true, false, true, View.VISIBLE);
+    }
+
+    @Test
+    public void testDebugRoundedCornersRemoved_noDeviceCornersSet() {
+        // GIVEN a device with no rounded corners defined
+        setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
+                null /* roundedTopDrawable */, null /* roundedBottomDrawable */,
+                0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */);
+
+        mScreenDecorations.start();
+        // No rounded corners exist at this point
+        verifyOverlaysExistAndAdded(false, false, false, false, View.VISIBLE);
+
+        // Path from rounded.xml, scaled by 10x to produce 80x80 corners
+        Path debugPath = PathParser.createPathFromPathData("M8,0H0v8C0,3.6,3.6,0,8,0z");
+        // WHEN debug corners are added to the delegate
+        DebugRoundedCornerModel debugCorner = new DebugRoundedCornerModel(
+                debugPath,
+                80,
+                80,
+                10f,
+                10f
+        );
+        mScreenDecorations.mDebugRoundedCornerDelegate
+                .applyNewDebugCorners(debugCorner, debugCorner);
+
+        // AND debug mode is entered
+        mScreenDecorations.setDebug(true);
+        mExecutor.runAllReady();
+
+        // Top and bottom overlays contain the debug rounded corners
+        verifyOverlaysExistAndAdded(false, true, false, true, View.VISIBLE);
+
+        // WHEN debug is exited
+        mScreenDecorations.setDebug(false);
+        mExecutor.runAllReady();
+
+        // THEN the decor is removed
+        verifyOverlaysExistAndAdded(false, false, false, false, View.VISIBLE);
+        assertThat(mScreenDecorations.mDebugRoundedCornerDelegate.getHasBottom()).isFalse();
+        assertThat(mScreenDecorations.mDebugRoundedCornerDelegate.getHasTop()).isFalse();
+    }
+
+    @Test
     public void testRegistration_From_NoOverlay_To_HasOverlays() {
         doReturn(false).when(mScreenDecorations).hasOverlays();
         mScreenDecorations.start();
-        verify(mTunerService, times(0)).addTunable(any(), any());
-        verify(mTunerService, times(1)).removeTunable(any());
         assertThat(mScreenDecorations.mIsRegistered, is(false));
-        reset(mTunerService);
 
         doReturn(true).when(mScreenDecorations).hasOverlays();
         mScreenDecorations.onConfigurationChanged(new Configuration());
-        verify(mTunerService, times(1)).addTunable(any(), any());
-        verify(mTunerService, times(0)).removeTunable(any());
         assertThat(mScreenDecorations.mIsRegistered, is(true));
     }
 
@@ -1093,14 +1153,9 @@
         doReturn(true).when(mScreenDecorations).hasOverlays();
 
         mScreenDecorations.start();
-        verify(mTunerService, times(1)).addTunable(any(), any());
-        verify(mTunerService, times(0)).removeTunable(any());
         assertThat(mScreenDecorations.mIsRegistered, is(true));
-        reset(mTunerService);
 
         mScreenDecorations.onConfigurationChanged(new Configuration());
-        verify(mTunerService, times(0)).addTunable(any(), any());
-        verify(mTunerService, times(0)).removeTunable(any());
         assertThat(mScreenDecorations.mIsRegistered, is(true));
     }
 
@@ -1109,15 +1164,10 @@
         doReturn(true).when(mScreenDecorations).hasOverlays();
 
         mScreenDecorations.start();
-        verify(mTunerService, times(1)).addTunable(any(), any());
-        verify(mTunerService, times(0)).removeTunable(any());
         assertThat(mScreenDecorations.mIsRegistered, is(true));
-        reset(mTunerService);
 
         doReturn(false).when(mScreenDecorations).hasOverlays();
         mScreenDecorations.onConfigurationChanged(new Configuration());
-        verify(mTunerService, times(0)).addTunable(any(), any());
-        verify(mTunerService, times(1)).removeTunable(any());
         assertThat(mScreenDecorations.mIsRegistered, is(false));
     }
 
@@ -1180,8 +1230,9 @@
         mFaceScanningProviders.add(mFaceScanningDecorProvider);
         when(mFaceScanningProviderFactory.getProviders()).thenReturn(mFaceScanningProviders);
         when(mFaceScanningProviderFactory.getHasProviders()).thenReturn(true);
-        ScreenDecorations screenDecorations = new ScreenDecorations(mContext, mExecutor,
-                mSecureSettings, mTunerService, mUserTracker, mDisplayTracker, mDotViewController,
+        ScreenDecorations screenDecorations = new ScreenDecorations(mContext,
+                mSecureSettings, mCommandRegistry, mUserTracker, mDisplayTracker,
+                mDotViewController,
                 mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
                 new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mAuthController);
         screenDecorations.start();
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 f64db78f..caf230d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java
@@ -16,8 +16,10 @@
 
 package com.android.systemui.accessibility;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
@@ -185,6 +187,19 @@
         verify(mMagnificationSettingsController).closeMagnificationSettings();
     }
 
+    @Test
+    public void onUserMagnificationScaleChanged() throws RemoteException {
+        final int testUserId = 1;
+        final float testScale = 3.0f;
+        mIWindowMagnificationConnection.onUserMagnificationScaleChanged(
+                testUserId, TEST_DISPLAY, testScale);
+        waitForIdleSync();
+
+        assertTrue(mWindowMagnification.mUsersScales.contains(testUserId));
+        assertEquals(mWindowMagnification.mUsersScales.get(testUserId).get(TEST_DISPLAY),
+                (Float) testScale);
+    }
+
     private class FakeControllerSupplier extends
             DisplayIdIndexSupplier<WindowMagnificationController> {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java
index 665246b..62a176c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java
@@ -73,9 +73,9 @@
 
     @Test
     public void testShowSettingsPanel() {
-        mMagnificationSettingsController.showMagnificationSettings();
+        mMagnificationSettingsController.toggleSettingsPanelVisibility();
 
-        verify(mWindowMagnificationSettings).showSettingPanel();
+        verify(mWindowMagnificationSettings).toggleSettingsPanelVisibility();
     }
 
     @Test
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 9c36af3..31c09b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.accessibility;
 
+import static android.content.pm.PackageManager.FEATURE_WINDOW_MAGNIFICATION;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
@@ -96,6 +97,7 @@
 import com.google.common.util.concurrent.AtomicDouble;
 
 import org.junit.After;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -147,8 +149,15 @@
     private View.OnTouchListener mTouchListener;
     private MotionEventHelper mMotionEventHelper = new MotionEventHelper();
 
+    /**
+     *  return whether window magnification is supported for current test context.
+     */
+    private boolean isWindowModeSupported() {
+        return getContext().getPackageManager().hasSystemFeature(FEATURE_WINDOW_MAGNIFICATION);
+    }
+
     @Before
-    public void setUp() throws RemoteException {
+    public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         mContext = Mockito.spy(getContext());
         mHandler = new FakeHandler(TestableLooper.get(this).getLooper());
@@ -202,6 +211,9 @@
             return null;
         }).when(mSpyView).setOnTouchListener(
                 any(View.OnTouchListener.class));
+
+        // skip test if window magnification is not supported to prevent fail results. (b/279820875)
+        Assume.assumeTrue(isWindowModeSupported());
     }
 
     @After
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java
index ce96708..275723b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java
@@ -16,10 +16,10 @@
 
 package com.android.systemui.accessibility;
 
+import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
-import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -28,8 +28,10 @@
 import static junit.framework.Assert.assertNotNull;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -37,6 +39,7 @@
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.database.ContentObserver;
+import android.graphics.Rect;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
@@ -49,8 +52,10 @@
 import android.widget.CompoundButton;
 import android.widget.LinearLayout;
 
+import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.accessibility.common.MagnificationConstants;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
@@ -60,12 +65,13 @@
 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)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@TestableLooper.RunWithLooper
 public class WindowMagnificationSettingsTest extends SysuiTestCase {
 
     private static final int MAGNIFICATION_SIZE_SMALL = 1;
@@ -85,6 +91,11 @@
     private WindowMagnificationSettings mWindowMagnificationSettings;
     private MotionEventHelper mMotionEventHelper = new MotionEventHelper();
 
+    private ArgumentCaptor<Float> mSecureSettingsScaleCaptor;
+    private ArgumentCaptor<String> mSecureSettingsNameCaptor;
+    private ArgumentCaptor<Integer> mSecureSettingsUserHandleCaptor;
+    private ArgumentCaptor<Float> mCallbackMagnifierScaleCaptor;
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -100,6 +111,10 @@
                 mSecureSettings);
 
         mSettingView = mWindowMagnificationSettings.getSettingView();
+        mSecureSettingsScaleCaptor = ArgumentCaptor.forClass(Float.class);
+        mSecureSettingsNameCaptor = ArgumentCaptor.forClass(String.class);
+        mSecureSettingsUserHandleCaptor = ArgumentCaptor.forClass(Integer.class);
+        mCallbackMagnifierScaleCaptor = ArgumentCaptor.forClass(Float.class);
     }
 
     @After
@@ -275,6 +290,39 @@
     }
 
     @Test
+    public void onScreenSizeChanged_resetPositionToRightBottomCorner() {
+        setupMagnificationCapabilityAndMode(
+                /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL,
+                /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
+        mWindowMagnificationSettings.showSettingPanel();
+
+        // move the panel to the center of draggable window bounds
+        mWindowMagnificationSettings.mParams.x =
+                mWindowMagnificationSettings.mDraggableWindowBounds.centerX();
+        mWindowMagnificationSettings.mParams.y =
+                mWindowMagnificationSettings.mDraggableWindowBounds.centerY();
+        mWindowMagnificationSettings.updateButtonViewLayoutIfNeeded();
+
+        final Rect testWindowBounds = new Rect(
+                mWindowManager.getCurrentWindowMetrics().getBounds());
+        testWindowBounds.set(testWindowBounds.left, testWindowBounds.top,
+                testWindowBounds.right + 200, testWindowBounds.bottom + 50);
+        mWindowManager.setWindowBounds(testWindowBounds);
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            mWindowMagnificationSettings.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+        });
+
+        // the panel position should be reset to the bottom-right corner
+        assertEquals(
+                mWindowMagnificationSettings.mParams.x,
+                mWindowMagnificationSettings.mDraggableWindowBounds.right);
+        assertEquals(
+                mWindowMagnificationSettings.mParams.y,
+                mWindowMagnificationSettings.mDraggableWindowBounds.bottom);
+    }
+
+    @Test
     public void showSettingsPanel_observerRegistered() {
         setupMagnificationCapabilityAndMode(
                 /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL,
@@ -289,6 +337,20 @@
     }
 
     @Test
+    public void showSettingsPanel_observerForMagnificationScaleRegistered() {
+        setupMagnificationCapabilityAndMode(
+                /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL,
+                /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
+
+        mWindowMagnificationSettings.showSettingPanel();
+
+        verify(mSecureSettings).registerContentObserverForUser(
+                eq(ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE),
+                any(ContentObserver.class),
+                eq(UserHandle.USER_CURRENT));
+    }
+
+    @Test
     public void hideSettingsPanel_observerUnregistered() {
         setupMagnificationCapabilityAndMode(
                 /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL,
@@ -297,7 +359,162 @@
         mWindowMagnificationSettings.showSettingPanel();
         mWindowMagnificationSettings.hideSettingPanel();
 
-        verify(mSecureSettings).unregisterContentObserver(any(ContentObserver.class));
+        verify(mSecureSettings, times(2)).unregisterContentObserver(any(ContentObserver.class));
+    }
+
+    @Test
+    public void seekbarProgress_justInflated_maxValueAndProgressSetCorrectly() {
+        setupScaleInSecureSettings(0f);
+        assertThat(mWindowMagnificationSettings.mZoomSeekbar.getProgress()).isEqualTo(0);
+        assertThat(mWindowMagnificationSettings.mZoomSeekbar.getMax()).isEqualTo(70);
+    }
+
+    @Test
+    public void seekbarProgress_minMagnification_seekbarProgressIsCorrect() {
+        setupScaleInSecureSettings(0f);
+        setupMagnificationCapabilityAndMode(
+                /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW,
+                /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
+
+        mWindowMagnificationSettings.showSettingPanel();
+
+        // Seekbar index from 0 to 70. 1.0f scale (A11Y_SCALE_MIN_VALUE) would correspond to 0.
+        assertThat(mWindowMagnificationSettings.mZoomSeekbar.getProgress()).isEqualTo(0);
+    }
+
+    @Test
+    public void seekbarProgress_belowMinMagnification_seekbarProgressIsZero() {
+        setupScaleInSecureSettings(0f);
+        setupMagnificationCapabilityAndMode(
+                /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW,
+                /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
+
+        mWindowMagnificationSettings.showSettingPanel();
+
+        assertThat(mWindowMagnificationSettings.mZoomSeekbar.getProgress()).isEqualTo(0);
+    }
+
+    @Test
+    public void seekbarProgress_magnificationBefore_seekbarProgressIsHalf() {
+        setupScaleInSecureSettings(4f);
+        setupMagnificationCapabilityAndMode(
+                /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW,
+                /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
+
+        mWindowMagnificationSettings.showSettingPanel();
+
+        // float scale : from 1.0f to 8.0f, seekbar index from 0 to 70.
+        // 4.0f would correspond to 30.
+        assertThat(mWindowMagnificationSettings.mZoomSeekbar.getProgress()).isEqualTo(30);
+    }
+
+    @Test
+    public void seekbarProgress_maxMagnificationBefore_seekbarProgressIsMax() {
+        setupScaleInSecureSettings(8f);
+        setupMagnificationCapabilityAndMode(
+                /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW,
+                /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
+
+        mWindowMagnificationSettings.showSettingPanel();
+
+        // 8.0f is max magnification {@link MagnificationScaleProvider#MAX_SCALE}.
+        // Max zoom seek bar is 70.
+        assertThat(mWindowMagnificationSettings.mZoomSeekbar.getProgress()).isEqualTo(70);
+    }
+
+    @Test
+    public void seekbarProgress_aboveMaxMagnificationBefore_seekbarProgressIsMax() {
+        setupScaleInSecureSettings(9f);
+        setupMagnificationCapabilityAndMode(
+                /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW,
+                /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
+
+        mWindowMagnificationSettings.showSettingPanel();
+
+        // Max zoom seek bar is 70.
+        assertThat(mWindowMagnificationSettings.mZoomSeekbar.getProgress()).isEqualTo(70);
+    }
+
+    @Test
+    public void seekbarProgress_progressChangedRoughlyHalf_scaleAndCallbackUpdated() {
+        setupMagnificationCapabilityAndMode(
+                /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW,
+                /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
+        mWindowMagnificationSettings.showSettingPanel();
+
+        mWindowMagnificationSettings.mZoomSeekbar.setProgress(30);
+
+        verifyScaleUpdatedInSecureSettings(4f);
+        verifyCallbackOnMagnifierScale(4f);
+    }
+
+    @Test
+    public void seekbarProgress_minProgress_callbackUpdated() {
+        setupMagnificationCapabilityAndMode(
+                /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW,
+                /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
+        mWindowMagnificationSettings.showSettingPanel();
+        // Set progress to non-zero first so onProgressChanged can be triggered upon setting to 0.
+        mWindowMagnificationSettings.mZoomSeekbar.setProgress(30);
+
+        mWindowMagnificationSettings.mZoomSeekbar.setProgress(0);
+
+        // For now, secure settings will not be updated for values < 1.3f. Follow up on this later.
+        verify(mWindowMagnificationSettingsCallback, times(2))
+                .onMagnifierScale(mCallbackMagnifierScaleCaptor.capture());
+        var capturedArgs = mCallbackMagnifierScaleCaptor.getAllValues();
+        assertThat(capturedArgs).hasSize(2);
+        assertThat(capturedArgs.get(1)).isWithin(0.01f).of(1f);
+    }
+
+    @Test
+    public void seekbarProgress_maxProgress_scaleAndCallbackUpdated() {
+        setupMagnificationCapabilityAndMode(
+                /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW,
+                /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
+        mWindowMagnificationSettings.showSettingPanel();
+
+        mWindowMagnificationSettings.mZoomSeekbar.setProgress(70);
+
+        verifyScaleUpdatedInSecureSettings(8f);
+        verifyCallbackOnMagnifierScale(8f);
+    }
+
+    @Test
+    public void seekbarProgress_scaleUpdatedAfterSettingPanelOpened_progressAlsoUpdated() {
+        setupMagnificationCapabilityAndMode(
+                /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL,
+                /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
+        var contentObserverCaptor = ArgumentCaptor.forClass(ContentObserver.class);
+        mWindowMagnificationSettings.showSettingPanel();
+        verify(mSecureSettings).registerContentObserverForUser(
+                eq(ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE),
+                contentObserverCaptor.capture(),
+                eq(UserHandle.USER_CURRENT));
+
+        // Simulate outside changes.
+        setupScaleInSecureSettings(4f);
+        // Simulate callback due to outside change.
+        contentObserverCaptor.getValue().onChange(/* selfChange= */ false);
+
+        assertThat(mWindowMagnificationSettings.mZoomSeekbar.getProgress()).isEqualTo(30);
+    }
+
+    private void verifyScaleUpdatedInSecureSettings(float scale) {
+        verify(mSecureSettings).putFloatForUser(
+                mSecureSettingsNameCaptor.capture(),
+                mSecureSettingsScaleCaptor.capture(),
+                mSecureSettingsUserHandleCaptor.capture());
+        assertThat(mSecureSettingsScaleCaptor.getValue()).isWithin(0.01f).of(scale);
+        assertThat(mSecureSettingsNameCaptor.getValue())
+                .isEqualTo(Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE);
+        assertThat(mSecureSettingsUserHandleCaptor.getValue()).isEqualTo(UserHandle.USER_CURRENT);
+    }
+
+    private void verifyCallbackOnMagnifierScale(float scale) {
+        verify(mWindowMagnificationSettingsCallback)
+                .onMagnifierScale(mCallbackMagnifierScaleCaptor.capture());
+        assertThat(mCallbackMagnifierScaleCaptor.getValue()).isWithin(0.01f).of(scale);
     }
 
     private <T extends View> T getInternalView(@IdRes int idRes) {
@@ -308,12 +525,19 @@
 
     private void setupMagnificationCapabilityAndMode(int capability, int mode) {
         when(mSecureSettings.getIntForUser(
-                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
-                ACCESSIBILITY_MAGNIFICATION_MODE_NONE,
-                UserHandle.USER_CURRENT)).thenReturn(capability);
+                eq(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY),
+                anyInt(),
+                eq(UserHandle.USER_CURRENT))).thenReturn(capability);
         when(mSecureSettings.getIntForUser(
-                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
-                ACCESSIBILITY_MAGNIFICATION_MODE_NONE,
-                UserHandle.USER_CURRENT)).thenReturn(mode);
+                eq(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE),
+                anyInt(),
+                eq(UserHandle.USER_CURRENT))).thenReturn(mode);
+    }
+
+    private void setupScaleInSecureSettings(float scale) {
+        when(mSecureSettings.getFloatForUser(
+                ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
+                MagnificationConstants.SCALE_MIN_VALUE,
+                UserHandle.USER_CURRENT)).thenReturn(scale);
     }
 }
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 38ecec0..db58074 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java
@@ -110,7 +110,7 @@
             mWindowMagnification.mMagnificationSettingsControllerCallback
                     .onSettingsPanelVisibilityChanged(TEST_DISPLAY, /* shown= */ true);
             return null;
-        }).when(mMagnificationSettingsController).showMagnificationSettings();
+        }).when(mMagnificationSettingsController).toggleSettingsPanelVisibility();
         doAnswer(invocation -> {
             mWindowMagnification.mMagnificationSettingsControllerCallback
                     .onSettingsPanelVisibilityChanged(TEST_DISPLAY, /* shown= */ false);
@@ -198,7 +198,7 @@
         mWindowMagnification.mWindowMagnifierCallback.onClickSettingsButton(TEST_DISPLAY);
         waitForIdleSync();
 
-        verify(mMagnificationSettingsController).showMagnificationSettings();
+        verify(mMagnificationSettingsController).toggleSettingsPanelVisibility();
         verify(mA11yLogger).log(
                 eq(MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_PANEL_OPENED));
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java
index f6ca938..fd258e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java
@@ -29,7 +29,8 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.util.settings.SecureSettings;
-import com.android.wm.shell.bubbles.DismissView;
+import com.android.wm.shell.bubbles.DismissViewUtils;
+import com.android.wm.shell.common.bubbles.DismissView;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -63,6 +64,7 @@
         final MenuView stubMenuView = new MenuView(mContext, stubMenuViewModel,
                 stubMenuViewAppearance);
         mDismissView = spy(new DismissView(mContext));
+        DismissViewUtils.setup(mDismissView);
         mDismissAnimationController = new DismissAnimationController(mDismissView, stubMenuView);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
index d4efbe4..98be49f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
@@ -40,7 +40,8 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.accessibility.MotionEventHelper;
 import com.android.systemui.util.settings.SecureSettings;
-import com.android.wm.shell.bubbles.DismissView;
+import com.android.wm.shell.bubbles.DismissViewUtils;
+import com.android.wm.shell.common.bubbles.DismissView;
 
 import org.junit.After;
 import org.junit.Before;
@@ -88,6 +89,7 @@
         mStubMenuView.setTranslationY(0);
         mMenuAnimationController = spy(new MenuAnimationController(mStubMenuView));
         mDismissView = spy(new DismissView(mContext));
+        DismissViewUtils.setup(mDismissView);
         mDismissAnimationController =
                 spy(new DismissAnimationController(mDismissView, mStubMenuView));
         mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController,
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
index f10c21b..d5e6881 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt
@@ -25,6 +25,8 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView
+import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView.OnSeekBarWithIconButtonsChangeListener
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.whenever
@@ -38,6 +40,7 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
+import org.mockito.Mock
 import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
@@ -63,8 +66,9 @@
             .getResources()
             .getStringArray(com.android.settingslib.R.array.entryvalues_font_size)
 
+    @Mock private lateinit var userTracker: UserTracker
     @Captor
-    private lateinit var seekBarChangeCaptor: ArgumentCaptor<SeekBar.OnSeekBarChangeListener>
+    private lateinit var seekBarChangeCaptor: ArgumentCaptor<OnSeekBarWithIconButtonsChangeListener>
 
     @Before
     fun setUp() {
@@ -72,7 +76,7 @@
         val mainHandler = Handler(TestableLooper.get(this).getLooper())
         systemSettings = FakeSettings()
         // Guarantee that the systemSettings always starts with the default font scale.
-        systemSettings.putFloat(Settings.System.FONT_SCALE, 1.0f)
+        systemSettings.putFloatForUser(Settings.System.FONT_SCALE, 1.0f, userTracker.userId)
         secureSettings = FakeSettings()
         systemClock = FakeSystemClock()
         backgroundDelayableExecutor = FakeExecutor(systemClock)
@@ -82,6 +86,7 @@
                 systemSettings,
                 secureSettings,
                 systemClock,
+                userTracker,
                 mainHandler,
                 backgroundDelayableExecutor
             )
@@ -93,7 +98,12 @@
 
         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)
+        val currentScale =
+            systemSettings.getFloatForUser(
+                Settings.System.FONT_SCALE,
+                /* def= */ 1.0f,
+                userTracker.userId
+            )
 
         assertThat(currentScale).isEqualTo(fontSizeValueArray[progress].toFloat())
 
@@ -119,7 +129,12 @@
         backgroundDelayableExecutor.advanceClockToNext()
         backgroundDelayableExecutor.runAllReady()
 
-        val currentScale = systemSettings.getFloat(Settings.System.FONT_SCALE, /* def= */ 1.0f)
+        val currentScale =
+            systemSettings.getFloatForUser(
+                Settings.System.FONT_SCALE,
+                /* def= */ 1.0f,
+                userTracker.userId
+            )
         assertThat(seekBar.getProgress()).isEqualTo(1)
         assertThat(currentScale).isEqualTo(fontSizeValueArray[1].toFloat())
 
@@ -145,7 +160,12 @@
         backgroundDelayableExecutor.advanceClockToNext()
         backgroundDelayableExecutor.runAllReady()
 
-        val currentScale = systemSettings.getFloat(Settings.System.FONT_SCALE, /* def= */ 1.0f)
+        val currentScale =
+            systemSettings.getFloatForUser(
+                Settings.System.FONT_SCALE,
+                /* def= */ 1.0f,
+                userTracker.userId
+            )
         assertThat(seekBar.getProgress()).isEqualTo(fontSizeValueArray.size - 2)
         assertThat(currentScale)
             .isEqualTo(fontSizeValueArray[fontSizeValueArray.size - 2].toFloat())
@@ -157,18 +177,24 @@
     fun progressChanged_keyWasNotSetBefore_fontScalingHasBeenChangedIsOn() {
         fontScalingDialog.show()
 
-        val seekBarWithIconButtonsView: SeekBarWithIconButtonsView =
-            fontScalingDialog.findViewById(R.id.font_scaling_slider)!!
-        secureSettings.putInt(Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, OFF)
+        val iconStartFrame: ViewGroup = fontScalingDialog.findViewById(R.id.icon_start_frame)!!
+        secureSettings.putIntForUser(
+            Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED,
+            OFF,
+            userTracker.userId
+        )
 
-        // Default seekbar progress for font size is 1, set it to another progress 0
-        seekBarWithIconButtonsView.setProgress(0)
+        // Default seekbar progress for font size is 1, click start icon to decrease the progress
+        iconStartFrame.performClick()
+        backgroundDelayableExecutor.runAllReady()
+        backgroundDelayableExecutor.advanceClockToNext()
         backgroundDelayableExecutor.runAllReady()
 
         val currentSettings =
-            secureSettings.getInt(
+            secureSettings.getIntForUser(
                 Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED,
-                /* def = */ OFF
+                /* def = */ OFF,
+                userTracker.userId
             )
         assertThat(currentSettings).isEqualTo(ON)
 
@@ -184,7 +210,7 @@
             )
             .thenReturn(slider)
         fontScalingDialog.show()
-        verify(slider).setOnSeekBarChangeListener(capture(seekBarChangeCaptor))
+        verify(slider).setOnSeekBarWithIconButtonsChangeListener(capture(seekBarChangeCaptor))
         val seekBar: SeekBar = slider.findViewById(R.id.seekbar)!!
 
         // Default seekbar progress for font size is 1, simulate dragging to 0 without
@@ -199,7 +225,12 @@
         backgroundDelayableExecutor.runAllReady()
 
         // Verify that the scale of font size remains the default value 1.0f.
-        var systemScale = systemSettings.getFloat(Settings.System.FONT_SCALE, /* def= */ 1.0f)
+        var systemScale =
+            systemSettings.getFloatForUser(
+                Settings.System.FONT_SCALE,
+                /* def= */ 1.0f,
+                userTracker.userId
+            )
         assertThat(systemScale).isEqualTo(1.0f)
 
         // Simulate releasing the finger from the seekbar.
@@ -208,8 +239,22 @@
         backgroundDelayableExecutor.advanceClockToNext()
         backgroundDelayableExecutor.runAllReady()
 
+        // SeekBar interaction is finalized.
+        seekBarChangeCaptor.value.onUserInteractionFinalized(
+            seekBar,
+            OnSeekBarWithIconButtonsChangeListener.ControlUnitType.SLIDER
+        )
+        backgroundDelayableExecutor.runAllReady()
+        backgroundDelayableExecutor.advanceClockToNext()
+        backgroundDelayableExecutor.runAllReady()
+
         // Verify that the scale of font size has been updated.
-        systemScale = systemSettings.getFloat(Settings.System.FONT_SCALE, /* def= */ 1.0f)
+        systemScale =
+            systemSettings.getFloatForUser(
+                Settings.System.FONT_SCALE,
+                /* def= */ 1.0f,
+                userTracker.userId
+            )
         assertThat(systemScale).isEqualTo(fontSizeValueArray[0].toFloat())
 
         fontScalingDialog.dismiss()
@@ -224,7 +269,7 @@
             )
             .thenReturn(slider)
         fontScalingDialog.show()
-        verify(slider).setOnSeekBarChangeListener(capture(seekBarChangeCaptor))
+        verify(slider).setOnSeekBarWithIconButtonsChangeListener(capture(seekBarChangeCaptor))
         val seekBar: SeekBar = slider.findViewById(R.id.seekbar)!!
 
         // Default seekbar progress for font size is 1, simulate dragging to 0 without
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 0798d73..d1ac0e8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
@@ -21,6 +21,7 @@
 import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.view.LaunchableFrameLayout
 import org.junit.Assert.assertThrows
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index 1990c8f..ea3289c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.authentication.domain.interactor
 
+import android.app.admin.DevicePolicyManager
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.AuthenticationRepository
@@ -24,7 +25,6 @@
 import com.android.systemui.scene.SceneTestUtils
 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.Test
@@ -36,8 +36,8 @@
 @RunWith(JUnit4::class)
 class AuthenticationInteractorTest : SysuiTestCase() {
 
-    private val testScope = TestScope()
-    private val utils = SceneTestUtils(this, testScope)
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
     private val repository: AuthenticationRepository = utils.authenticationRepository()
     private val underTest =
         utils.authenticationInteractor(
@@ -45,47 +45,23 @@
         )
 
     @Test
-    fun authMethod() =
+    fun getAuthenticationMethod() =
         testScope.runTest {
-            val authMethod by collectLastValue(underTest.authenticationMethod)
-            assertThat(authMethod).isEqualTo(AuthenticationMethodModel.PIN(1234))
+            assertThat(underTest.getAuthenticationMethod())
+                .isEqualTo(AuthenticationMethodModel.Pin(1234))
 
-            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
-            assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Password("password"))
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Password("password")
+            )
+            assertThat(underTest.getAuthenticationMethod())
+                .isEqualTo(AuthenticationMethodModel.Password("password"))
         }
 
     @Test
     fun isUnlocked_whenAuthMethodIsNone_isTrue() =
         testScope.runTest {
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
             val isUnlocked by collectLastValue(underTest.isUnlocked)
-            assertThat(isUnlocked).isFalse()
-
-            underTest.setAuthenticationMethod(AuthenticationMethodModel.None)
-
-            assertThat(isUnlocked).isTrue()
-        }
-
-    @Test
-    fun unlockDevice() =
-        testScope.runTest {
-            val isUnlocked by collectLastValue(underTest.isUnlocked)
-            assertThat(isUnlocked).isFalse()
-
-            underTest.unlockDevice()
-            runCurrent()
-
-            assertThat(isUnlocked).isTrue()
-        }
-
-    @Test
-    fun biometricUnlock() =
-        testScope.runTest {
-            val isUnlocked by collectLastValue(underTest.isUnlocked)
-            assertThat(isUnlocked).isFalse()
-
-            underTest.biometricUnlock()
-            runCurrent()
-
             assertThat(isUnlocked).isTrue()
         }
 
@@ -105,9 +81,11 @@
     @Test
     fun isAuthenticationRequired_lockedAndSecured_true() =
         testScope.runTest {
-            underTest.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             runCurrent()
-            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Password("password")
+            )
 
             assertThat(underTest.isAuthenticationRequired()).isTrue()
         }
@@ -115,9 +93,9 @@
     @Test
     fun isAuthenticationRequired_lockedAndNotSecured_false() =
         testScope.runTest {
-            underTest.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             runCurrent()
-            underTest.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
 
             assertThat(underTest.isAuthenticationRequired()).isFalse()
         }
@@ -125,9 +103,11 @@
     @Test
     fun isAuthenticationRequired_unlockedAndSecured_false() =
         testScope.runTest {
-            underTest.unlockDevice()
+            utils.authenticationRepository.setUnlocked(true)
             runCurrent()
-            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Password("password")
+            )
 
             assertThat(underTest.isAuthenticationRequired()).isFalse()
         }
@@ -135,71 +115,108 @@
     @Test
     fun isAuthenticationRequired_unlockedAndNotSecured_false() =
         testScope.runTest {
-            underTest.unlockDevice()
+            utils.authenticationRepository.setUnlocked(true)
             runCurrent()
-            underTest.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
 
             assertThat(underTest.isAuthenticationRequired()).isFalse()
         }
 
     @Test
-    fun authenticate_withCorrectPin_returnsTrueAndUnlocksDevice() =
+    fun authenticate_withCorrectPin_returnsTrue() =
         testScope.runTest {
             val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
-            val isUnlocked by collectLastValue(underTest.isUnlocked)
-            underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            assertThat(isUnlocked).isFalse()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
 
             assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue()
-            assertThat(isUnlocked).isTrue()
             assertThat(failedAttemptCount).isEqualTo(0)
         }
 
     @Test
-    fun authenticate_withIncorrectPin_returnsFalseAndDoesNotUnlockDevice() =
+    fun authenticate_withIncorrectPin_returnsFalse() =
         testScope.runTest {
             val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
-            val isUnlocked by collectLastValue(underTest.isUnlocked)
-            underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            assertThat(isUnlocked).isFalse()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
 
             assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse()
-            assertThat(isUnlocked).isFalse()
             assertThat(failedAttemptCount).isEqualTo(1)
         }
 
     @Test
-    fun authenticate_withCorrectPassword_returnsTrueAndUnlocksDevice() =
+    fun authenticate_withEmptyPin_returnsFalse() =
         testScope.runTest {
             val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
-            val isUnlocked by collectLastValue(underTest.isUnlocked)
-            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
-            assertThat(isUnlocked).isFalse()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
 
-            assertThat(underTest.authenticate("password".toList())).isTrue()
-            assertThat(isUnlocked).isTrue()
+            assertThat(underTest.authenticate(listOf())).isFalse()
+            assertThat(failedAttemptCount).isEqualTo(1)
+        }
+
+    @Test
+    fun authenticate_withCorrectMaxLengthPin_returnsTrue() =
+        testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(9999999999999999)
+            )
+
+            assertThat(underTest.authenticate(List(16) { 9 })).isTrue()
             assertThat(failedAttemptCount).isEqualTo(0)
         }
 
     @Test
-    fun authenticate_withIncorrectPassword_returnsFalseAndDoesNotUnlockDevice() =
+    fun authenticate_withCorrectTooLongPin_returnsFalse() =
         testScope.runTest {
-            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
-            val isUnlocked by collectLastValue(underTest.isUnlocked)
-            underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
-            assertThat(isUnlocked).isFalse()
+            // Max pin length is 16 digits. To avoid issues with overflows, this test ensures
+            // that all pins > 16 decimal digits are rejected.
 
-            assertThat(underTest.authenticate("alohomora".toList())).isFalse()
-            assertThat(isUnlocked).isFalse()
+            // If the policy changes, there is work to do in SysUI.
+            assertThat(DevicePolicyManager.MAX_PASSWORD_LENGTH).isLessThan(17)
+
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(99999999999999999)
+            )
+
+            assertThat(underTest.authenticate(List(17) { 9 })).isFalse()
             assertThat(failedAttemptCount).isEqualTo(1)
         }
 
     @Test
-    fun authenticate_withCorrectPattern_returnsTrueAndUnlocksDevice() =
+    fun authenticate_withCorrectPassword_returnsTrue() =
         testScope.runTest {
             val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
-            val isUnlocked by collectLastValue(underTest.isUnlocked)
-            underTest.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Password("password")
+            )
+
+            assertThat(underTest.authenticate("password".toList())).isTrue()
+            assertThat(failedAttemptCount).isEqualTo(0)
+        }
+
+    @Test
+    fun authenticate_withIncorrectPassword_returnsFalse() =
+        testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Password("password")
+            )
+
+            assertThat(underTest.authenticate("alohomora".toList())).isFalse()
+            assertThat(failedAttemptCount).isEqualTo(1)
+        }
+
+    @Test
+    fun authenticate_withCorrectPattern_returnsTrue() =
+        testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pattern(
                     listOf(
                         AuthenticationMethodModel.Pattern.PatternCoordinate(
@@ -217,7 +234,6 @@
                     )
                 )
             )
-            assertThat(isUnlocked).isFalse()
 
             assertThat(
                     underTest.authenticate(
@@ -238,16 +254,14 @@
                     )
                 )
                 .isTrue()
-            assertThat(isUnlocked).isTrue()
             assertThat(failedAttemptCount).isEqualTo(0)
         }
 
     @Test
-    fun authenticate_withIncorrectPattern_returnsFalseAndDoesNotUnlockDevice() =
+    fun authenticate_withIncorrectPattern_returnsFalse() =
         testScope.runTest {
             val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
-            val isUnlocked by collectLastValue(underTest.isUnlocked)
-            underTest.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pattern(
                     listOf(
                         AuthenticationMethodModel.Pattern.PatternCoordinate(
@@ -265,7 +279,6 @@
                     )
                 )
             )
-            assertThat(isUnlocked).isFalse()
 
             assertThat(
                     underTest.authenticate(
@@ -286,18 +299,91 @@
                     )
                 )
                 .isFalse()
-            assertThat(isUnlocked).isFalse()
             assertThat(failedAttemptCount).isEqualTo(1)
         }
 
     @Test
-    fun unlocksDevice_whenAuthMethodBecomesNone() =
+    fun tryAutoConfirm_withAutoConfirmPinAndEmptyInput_returnsNull() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(underTest.isUnlocked)
-            assertThat(isUnlocked).isFalse()
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = true)
+            )
 
-            repository.setAuthenticationMethod(AuthenticationMethodModel.None)
+            assertThat(underTest.authenticate(listOf(), tryAutoConfirm = true)).isNull()
+            assertThat(failedAttemptCount).isEqualTo(0)
+        }
 
-            assertThat(isUnlocked).isTrue()
+    @Test
+    fun tryAutoConfirm_withAutoConfirmPinAndShorterPin_returnsNullAndHasNoEffect() =
+        testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = true)
+            )
+
+            assertThat(underTest.authenticate(listOf(1, 2, 3), tryAutoConfirm = true)).isNull()
+            assertThat(failedAttemptCount).isEqualTo(0)
+        }
+
+    @Test
+    fun tryAutoConfirm_withAutoConfirmWrongPinCorrectLength_returnsFalseAndDoesNotUnlockDevice() =
+        testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = true)
+            )
+
+            assertThat(underTest.authenticate(listOf(1, 2, 4, 4), tryAutoConfirm = true)).isFalse()
+            assertThat(failedAttemptCount).isEqualTo(1)
+        }
+
+    @Test
+    fun tryAutoConfirm_withAutoConfirmLongerPin_returnsFalseAndDoesNotUnlockDevice() =
+        testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = true)
+            )
+
+            assertThat(underTest.authenticate(listOf(1, 2, 3, 4, 5), tryAutoConfirm = true))
+                .isFalse()
+            assertThat(failedAttemptCount).isEqualTo(1)
+        }
+
+    @Test
+    fun tryAutoConfirm_withAutoConfirmCorrectPin_returnsTrueAndUnlocksDevice() =
+        testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = true)
+            )
+
+            assertThat(underTest.authenticate(listOf(1, 2, 4, 4), tryAutoConfirm = true)).isFalse()
+            assertThat(failedAttemptCount).isEqualTo(1)
+        }
+
+    @Test
+    fun tryAutoConfirm_withoutAutoConfirmButCorrectPin_returnsNullAndHasNoEffects() =
+        testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = false)
+            )
+
+            assertThat(underTest.authenticate(listOf(1, 2, 3, 4), tryAutoConfirm = true)).isNull()
+            assertThat(failedAttemptCount).isEqualTo(0)
+        }
+
+    @Test
+    fun tryAutoConfirm_withoutCorrectPassword_returnsNullAndHasNoEffects() =
+        testScope.runTest {
+            val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Password("password")
+            )
+
+            assertThat(underTest.authenticate("password".toList(), tryAutoConfirm = true)).isNull()
+            assertThat(failedAttemptCount).isEqualTo(0)
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt
new file mode 100644
index 0000000..88c710a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.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.back.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.QuickSettingsController
+import com.android.systemui.shade.ShadeController
+import com.android.systemui.shade.ShadeViewController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
+import com.android.systemui.util.mockito.whenever
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+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.atLeastOnce
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BackActionInteractorTest : SysuiTestCase() {
+    @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
+    @Mock private lateinit var shadeController: ShadeController
+    @Mock private lateinit var qsController: QuickSettingsController
+    @Mock private lateinit var shadeViewController: ShadeViewController
+
+    private lateinit var backActionInteractor: BackActionInteractor
+
+    @Before
+    fun setup() {
+        backActionInteractor =
+            BackActionInteractor(
+                statusBarStateController,
+                statusBarKeyguardViewManager,
+                shadeController,
+            )
+        backActionInteractor.setup(qsController, shadeViewController)
+    }
+
+    @Test
+    fun testOnBackRequested_keyguardCanHandleBackPressed() {
+        whenever(statusBarKeyguardViewManager.canHandleBackPressed()).thenReturn(true)
+
+        val result = backActionInteractor.onBackRequested()
+
+        assertTrue(result)
+        verify(statusBarKeyguardViewManager, atLeastOnce()).onBackPressed()
+    }
+
+    @Test
+    fun testOnBackRequested_quickSettingsIsCustomizing() {
+        whenever(qsController.isCustomizing).thenReturn(true)
+
+        val result = backActionInteractor.onBackRequested()
+
+        assertTrue(result)
+        verify(qsController, atLeastOnce()).closeQsCustomizer()
+        verify(statusBarKeyguardViewManager, never()).onBackPressed()
+    }
+
+    @Test
+    fun testOnBackRequested_quickSettingsExpanded() {
+        whenever(qsController.expanded).thenReturn(true)
+
+        val result = backActionInteractor.onBackRequested()
+
+        assertTrue(result)
+        verify(shadeViewController, atLeastOnce()).animateCollapseQs(anyBoolean())
+        verify(statusBarKeyguardViewManager, never()).onBackPressed()
+    }
+
+    @Test
+    fun testOnBackRequested_closeUserSwitcherIfOpen() {
+        whenever(shadeViewController.closeUserSwitcherIfOpen()).thenReturn(true)
+
+        val result = backActionInteractor.onBackRequested()
+
+        assertTrue(result)
+        verify(statusBarKeyguardViewManager, never()).onBackPressed()
+        verify(shadeViewController, never()).animateCollapseQs(anyBoolean())
+    }
+
+    @Test
+    fun testOnBackRequested_returnsFalse() {
+        // make shouldBackBeHandled return false
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+
+        val result = backActionInteractor.onBackRequested()
+
+        assertFalse(result)
+        verify(statusBarKeyguardViewManager, never()).onBackPressed()
+        verify(shadeViewController, never()).animateCollapseQs(anyBoolean())
+    }
+}
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 b9f92a0..5cae23c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -492,6 +492,50 @@
     }
 
     @Test
+    public void testOnAuthenticationFailedInvoked_whenFaceAuthRejected() throws RemoteException {
+        final int modality = BiometricAuthenticator.TYPE_FACE;
+        final int userId = 0;
+
+        enrollFingerprintAndFace(userId);
+
+        showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */);
+
+        mAuthController.onBiometricError(modality,
+                BiometricConstants.BIOMETRIC_PAUSED_REJECTED,
+                0 /* vendorCode */);
+
+        ArgumentCaptor<Integer> modalityCaptor = ArgumentCaptor.forClass(Integer.class);
+        ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
+        verify(mDialog1).onAuthenticationFailed(modalityCaptor.capture(), messageCaptor.capture());
+
+        assertEquals(modalityCaptor.getValue().intValue(), modality);
+        assertEquals(messageCaptor.getValue(),
+                mContext.getString(R.string.biometric_face_not_recognized));
+    }
+
+    @Test
+    public void testOnAuthenticationFailedInvoked_whenFingerprintAuthRejected() {
+        final int modality = BiometricAuthenticator.TYPE_FINGERPRINT;
+        final int userId = 0;
+
+        enrollFingerprintAndFace(userId);
+
+        showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */);
+
+        mAuthController.onBiometricError(modality,
+                BiometricConstants.BIOMETRIC_PAUSED_REJECTED,
+                0 /* vendorCode */);
+
+        ArgumentCaptor<Integer> modalityCaptor = ArgumentCaptor.forClass(Integer.class);
+        ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
+        verify(mDialog1).onAuthenticationFailed(modalityCaptor.capture(), messageCaptor.capture());
+
+        assertEquals(modalityCaptor.getValue().intValue(), modality);
+        assertEquals(messageCaptor.getValue(),
+                mContext.getString(R.string.fingerprint_error_not_match));
+    }
+
+    @Test
     public void testOnAuthenticationFailedInvoked_whenBiometricTimedOut() {
         showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */);
         final int modality = BiometricAuthenticator.TYPE_FACE;
@@ -954,6 +998,25 @@
                 eq(null) /* credentialAttestation */);
     }
 
+    @Test
+    public void testShowDialog_whenOwnerNotInForeground() {
+        PromptInfo promptInfo = createTestPromptInfo();
+        promptInfo.setAllowBackgroundAuthentication(false);
+        switchTask("other_package");
+        mAuthController.showAuthenticationDialog(promptInfo,
+                mReceiver /* receiver */,
+                new int[]{1} /* sensorIds */,
+                false /* credentialAllowed */,
+                true /* requireConfirmation */,
+                0 /* userId */,
+                0 /* operationId */,
+                "testPackage",
+                REQUEST_ID);
+
+        assertNull(mAuthController.mCurrentDialog);
+        verify(mDialog1, never()).show(any(), any());
+    }
+
     private void showDialog(int[] sensorIds, boolean credentialAllowed) {
         mAuthController.showAuthenticationDialog(createTestPromptInfo(),
                 mReceiver /* receiver */,
@@ -998,6 +1061,31 @@
         return HAT;
     }
 
+    private void enrollFingerprintAndFace(final int userId) {
+
+        // Enroll fingerprint
+        verify(mFingerprintManager).registerBiometricStateListener(
+                mBiometricStateCaptor.capture());
+        assertFalse(mAuthController.isFingerprintEnrolled(userId));
+
+        mBiometricStateCaptor.getValue().onEnrollmentsChanged(userId,
+                1 /* sensorId */, true /* hasEnrollments */);
+        waitForIdleSync();
+
+        assertTrue(mAuthController.isFingerprintEnrolled(userId));
+
+        // Enroll face
+        verify(mFaceManager).registerBiometricStateListener(
+                mBiometricStateCaptor.capture());
+        assertFalse(mAuthController.isFaceAuthEnrolled(userId));
+
+        mBiometricStateCaptor.getValue().onEnrollmentsChanged(userId,
+                2 /* sensorId */, true /* hasEnrollments */);
+        waitForIdleSync();
+
+        assertTrue(mAuthController.isFaceAuthEnrolled(userId));
+    }
+
     private final class TestableAuthController extends AuthController {
         private int mBuildCount = 0;
         private PromptInfo mLastBiometricPromptInfo;
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 2908e75..d022653 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -58,11 +58,11 @@
 import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.dump.DumpManager
 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.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.concurrency.FakeExecutor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt
new file mode 100644
index 0000000..7de78a6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
+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.phone.SystemUIDialogManager
+import org.junit.Assert.assertFalse
+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
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+class UdfpsBpViewControllerTest : SysuiTestCase() {
+
+    @JvmField @Rule var rule = MockitoJUnit.rule()
+
+    @Mock lateinit var udfpsBpView: UdfpsBpView
+    @Mock lateinit var statusBarStateController: StatusBarStateController
+    @Mock lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
+    @Mock lateinit var systemUIDialogManager: SystemUIDialogManager
+    @Mock lateinit var dumpManager: DumpManager
+
+    private lateinit var udfpsBpViewController: UdfpsBpViewController
+
+    @Before
+    fun setup() {
+        udfpsBpViewController =
+            UdfpsBpViewController(
+                udfpsBpView,
+                statusBarStateController,
+                shadeExpansionStateManager,
+                systemUIDialogManager,
+                dumpManager
+            )
+    }
+
+    @Test
+    fun testShouldNeverPauseAuth() {
+        assertFalse(udfpsBpViewController.shouldPauseAuth())
+    }
+}
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 9dcdc46..0e0d0e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -43,11 +43,12 @@
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 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.keyguard.ui.viewmodel.UdfpsKeyguardViewModels
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -58,6 +59,7 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.settings.SecureSettings
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -70,6 +72,7 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
+import javax.inject.Provider
 import org.mockito.Mockito.`when` as whenever
 
 private const val REQUEST_ID = 2L
@@ -80,6 +83,7 @@
 private const val SENSOR_WIDTH = 30
 private const val SENSOR_HEIGHT = 60
 
+@ExperimentalCoroutinesApi
 @SmallTest
 @RoboPilotTest
 @RunWith(AndroidJUnit4::class)
@@ -114,6 +118,9 @@
     @Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
     @Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
     @Mock private lateinit var udfpsUtils: UdfpsUtils
+    @Mock private lateinit var udfpsKeyguardAccessibilityDelegate:
+            UdfpsKeyguardAccessibilityDelegate
+    @Mock private lateinit var udfpsKeyguardViewModels: Provider<UdfpsKeyguardViewModels>
     @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
 
     private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true }
@@ -144,7 +151,9 @@
             configurationController, keyguardStateController, unlockedScreenOffAnimationController,
             udfpsDisplayMode, secureSettings, REQUEST_ID, reason,
             controllerCallback, onTouch, activityLaunchAnimator, featureFlags,
-            primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable, udfpsUtils
+            primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable, udfpsUtils,
+            udfpsKeyguardAccessibilityDelegate,
+            udfpsKeyguardViewModels,
         )
         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 0a556ab..58982d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -22,6 +22,7 @@
 import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.internal.util.FunctionalUtils.ThrowingConsumer;
+import static com.android.systemui.classifier.Classifier.UDFPS_AUTHENTICATION;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
@@ -59,6 +60,7 @@
 import android.os.RemoteException;
 import android.os.VibrationAttributes;
 import android.testing.TestableLooper.RunWithLooper;
+import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.Surface;
@@ -83,13 +85,14 @@
 import com.android.systemui.biometrics.udfps.NormalizedTouchData;
 import com.android.systemui.biometrics.udfps.SinglePointerTouchProcessor;
 import com.android.systemui.biometrics.udfps.TouchProcessorResult;
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.dump.DumpManager;
 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.KeyguardFaceAuthInteractor;
-import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
+import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -217,6 +220,10 @@
     private AlternateBouncerInteractor mAlternateBouncerInteractor;
     @Mock
     private SecureSettings mSecureSettings;
+    @Mock
+    private UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate;
+    @Mock
+    private Provider<UdfpsKeyguardViewModels> mUdfpsKeyguardViewModels;
 
     // Capture listeners so that they can be used to send events
     @Captor
@@ -315,7 +322,8 @@
                 mActivityLaunchAnimator, alternateTouchProvider, mBiometricExecutor,
                 mPrimaryBouncerInteractor, mSinglePointerTouchProcessor, mSessionTracker,
                 mAlternateBouncerInteractor, mSecureSettings, mInputManager, mUdfpsUtils,
-                mock(KeyguardFaceAuthInteractor.class));
+                mock(KeyguardFaceAuthInteractor.class),
+                mUdfpsKeyguardAccessibilityDelegate, mUdfpsKeyguardViewModels);
         verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
         mOverlayController = mOverlayCaptor.getValue();
         verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture());
@@ -1197,8 +1205,53 @@
     }
 
     @Test
+    public void fingerDown_falsingManagerInformed() throws RemoteException {
+        final Pair<TouchProcessorResult, TouchProcessorResult> touchProcessorResult =
+                givenAcceptFingerDownEvent();
+
+        // WHEN ACTION_DOWN is received
+        when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn(
+                touchProcessorResult.first);
+        MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0);
+        mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent);
+        mBiometricExecutor.runAllReady();
+        downEvent.recycle();
+
+        // THEN falsing manager is informed of the touch
+        verify(mFalsingManager).isFalseTouch(UDFPS_AUTHENTICATION);
+    }
+
+    @Test
     public void onTouch_withNewTouchDetection_shouldCallNewFingerprintManagerPath()
             throws RemoteException {
+        final Pair<TouchProcessorResult, TouchProcessorResult> processorResultDownAndUp =
+                givenAcceptFingerDownEvent();
+
+        // WHEN ACTION_DOWN is received
+        when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn(
+                processorResultDownAndUp.first);
+        MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0);
+        mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent);
+        mBiometricExecutor.runAllReady();
+        downEvent.recycle();
+
+        // AND ACTION_UP is received
+        when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn(
+                processorResultDownAndUp.second);
+        MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0);
+        mTouchListenerCaptor.getValue().onTouch(mUdfpsView, upEvent);
+        mBiometricExecutor.runAllReady();
+        upEvent.recycle();
+
+        // THEN the new FingerprintManager path is invoked.
+        verify(mFingerprintManager).onPointerDown(anyLong(), anyInt(), anyInt(), anyFloat(),
+                anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong(), anyLong(), anyBoolean());
+        verify(mFingerprintManager).onPointerUp(anyLong(), anyInt(), anyInt(), anyFloat(),
+                anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong(), anyLong(), anyBoolean());
+    }
+
+    private Pair<TouchProcessorResult, TouchProcessorResult> givenAcceptFingerDownEvent()
+            throws RemoteException {
         final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L,
                 0L);
         final TouchProcessorResult processorResultDown = new TouchProcessorResult.ProcessedTouch(
@@ -1224,27 +1277,7 @@
 
         verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
 
-        // WHEN ACTION_DOWN is received
-        when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn(
-                processorResultDown);
-        MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0);
-        mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent);
-        mBiometricExecutor.runAllReady();
-        downEvent.recycle();
-
-        // AND ACTION_UP is received
-        when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn(
-                processorResultUp);
-        MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0);
-        mTouchListenerCaptor.getValue().onTouch(mUdfpsView, upEvent);
-        mBiometricExecutor.runAllReady();
-        upEvent.recycle();
-
-        // THEN the new FingerprintManager path is invoked.
-        verify(mFingerprintManager).onPointerDown(anyLong(), anyInt(), anyInt(), anyFloat(),
-                anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong(), anyLong(), anyBoolean());
-        verify(mFingerprintManager).onPointerUp(anyLong(), anyInt(), anyInt(), anyFloat(),
-                anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong(), anyLong(), anyBoolean());
+        return new Pair<>(processorResultDown, processorResultUp);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegateTest.kt
new file mode 100644
index 0000000..921ff09
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegateTest.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.biometrics
+
+import android.testing.TestableLooper
+import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
+import com.android.systemui.util.mockito.argumentCaptor
+import org.junit.Assert.assertEquals
+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.mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@TestableLooper.RunWithLooper
+class UdfpsKeyguardAccessibilityDelegateTest : SysuiTestCase() {
+
+    @Mock private lateinit var keyguardViewManager: StatusBarKeyguardViewManager
+    @Mock private lateinit var hostView: View
+    private lateinit var underTest: UdfpsKeyguardAccessibilityDelegate
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest =
+            UdfpsKeyguardAccessibilityDelegate(
+                context.resources,
+                keyguardViewManager,
+            )
+    }
+
+    @Test
+    fun onInitializeAccessibilityNodeInfo_clickActionAdded() {
+        // WHEN node is initialized
+        val mockedNodeInfo = mock(AccessibilityNodeInfo::class.java)
+        underTest.onInitializeAccessibilityNodeInfo(hostView, mockedNodeInfo)
+
+        // THEN a11y action is added
+        val argumentCaptor = argumentCaptor<AccessibilityNodeInfo.AccessibilityAction>()
+        verify(mockedNodeInfo).addAction(argumentCaptor.capture())
+
+        // AND the a11y action is a click action
+        assertEquals(
+            AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
+            argumentCaptor.value.id
+        )
+    }
+
+    @Test
+    fun performAccessibilityAction_actionClick_showsPrimaryBouncer() {
+        // WHEN click action is performed
+        val mockedNodeInfo = mock(AccessibilityNodeInfo::class.java)
+        underTest.performAccessibilityAction(
+            hostView,
+            AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
+            null
+        )
+
+        // THEN primary bouncer shows
+        verify(keyguardViewManager).showPrimaryBouncer(anyBoolean())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
index 0d3b394..032753a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
@@ -31,8 +31,8 @@
 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.bouncer.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shade.ShadeExpansionListener;
@@ -73,6 +73,7 @@
     protected @Mock ActivityLaunchAnimator mActivityLaunchAnimator;
     protected @Mock PrimaryBouncerInteractor mPrimaryBouncerInteractor;
     protected @Mock AlternateBouncerInteractor mAlternateBouncerInteractor;
+    protected @Mock UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate;
 
     protected FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
 
@@ -167,7 +168,8 @@
                 mActivityLaunchAnimator,
                 mFeatureFlags,
                 mPrimaryBouncerInteractor,
-                mAlternateBouncerInteractor);
+                mAlternateBouncerInteractor,
+                mUdfpsKeyguardAccessibilityDelegate);
         return controller;
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
index ffad326..9df06dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
@@ -22,20 +22,20 @@
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.systemui.RoboPilotTest
+import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
+import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepositoryImpl
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
+import com.android.systemui.bouncer.ui.BouncerView
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 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.FakeTrustRepository
-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.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.StatusBarState
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
index 263ce1a..8f8004f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
@@ -7,7 +7,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -54,10 +54,11 @@
             LogContextInteractorImpl(
                 testScope.backgroundScope,
                 foldProvider,
-                KeyguardTransitionInteractor(
-                    keyguardTransitionRepository,
-                    testScope.backgroundScope
-                ),
+                KeyguardTransitionInteractorFactory.create(
+                        repository = keyguardTransitionRepository,
+                        scope = testScope.backgroundScope,
+                    )
+                    .keyguardTransitionInteractor,
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt
new file mode 100644
index 0000000..992ee1a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.data.factory
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
+import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Password
+import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Pattern
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.shared.model.BouncerMessageModel
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.StringSubject
+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.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BouncerMessageFactoryTest : SysuiTestCase() {
+    private lateinit var underTest: BouncerMessageFactory
+
+    @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
+
+    @Mock private lateinit var securityModel: KeyguardSecurityModel
+
+    private lateinit var testScope: TestScope
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        testScope = TestScope()
+        underTest = BouncerMessageFactory(updateMonitor, securityModel)
+    }
+
+    @Test
+    fun bouncerMessages_choosesTheRightMessage_basedOnSecurityModeAndFpAllowedInBouncer() =
+        testScope.runTest {
+            primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAllowedInBouncer = false)
+                .isEqualTo("Enter PIN")
+            primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAllowedInBouncer = true)
+                .isEqualTo("Unlock with PIN or fingerprint")
+
+            primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAllowedInBouncer = false)
+                .isEqualTo("Enter password")
+            primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAllowedInBouncer = true)
+                .isEqualTo("Unlock with password or fingerprint")
+
+            primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAllowedInBouncer = false)
+                .isEqualTo("Draw pattern")
+            primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAllowedInBouncer = true)
+                .isEqualTo("Unlock with pattern or fingerprint")
+        }
+
+    @Test
+    fun bouncerMessages_setsPrimaryAndSecondaryMessage_basedOnSecurityModeAndFpAllowedInBouncer() =
+        testScope.runTest {
+            primaryMessage(
+                    PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
+                    mode = PIN,
+                    fpAllowedInBouncer = true
+                )
+                .isEqualTo("Wrong PIN. Try again.")
+            secondaryMessage(
+                    PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
+                    mode = PIN,
+                    fpAllowedInBouncer = true
+                )
+                .isEqualTo("Or unlock with fingerprint")
+
+            primaryMessage(
+                    PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
+                    mode = Password,
+                    fpAllowedInBouncer = true
+                )
+                .isEqualTo("Wrong password. Try again.")
+            secondaryMessage(
+                    PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
+                    mode = Password,
+                    fpAllowedInBouncer = true
+                )
+                .isEqualTo("Or unlock with fingerprint")
+
+            primaryMessage(
+                    PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
+                    mode = Pattern,
+                    fpAllowedInBouncer = true
+                )
+                .isEqualTo("Wrong pattern. Try again.")
+            secondaryMessage(
+                    PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
+                    mode = Pattern,
+                    fpAllowedInBouncer = true
+                )
+                .isEqualTo("Or unlock with fingerprint")
+        }
+
+    private fun primaryMessage(
+        reason: Int,
+        mode: KeyguardSecurityModel.SecurityMode,
+        fpAllowedInBouncer: Boolean
+    ): StringSubject {
+        return assertThat(
+            context.resources.getString(
+                bouncerMessageModel(mode, fpAllowedInBouncer, reason)!!.message!!.messageResId!!
+            )
+        )!!
+    }
+
+    private fun secondaryMessage(
+        reason: Int,
+        mode: KeyguardSecurityModel.SecurityMode,
+        fpAllowedInBouncer: Boolean
+    ): StringSubject {
+        return assertThat(
+            context.resources.getString(
+                bouncerMessageModel(mode, fpAllowedInBouncer, reason)!!
+                    .secondaryMessage!!
+                    .messageResId!!
+            )
+        )!!
+    }
+
+    private fun bouncerMessageModel(
+        mode: KeyguardSecurityModel.SecurityMode,
+        fpAllowedInBouncer: Boolean,
+        reason: Int
+    ): BouncerMessageModel? {
+        whenever(securityModel.getSecurityMode(0)).thenReturn(mode)
+        whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(fpAllowedInBouncer)
+
+        return underTest.createFromPromptReason(reason, 0)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt
new file mode 100644
index 0000000..de712da
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.data.repo
+
+import android.content.pm.UserInfo
+import android.hardware.biometrics.BiometricSourceType
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST
+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.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.R
+import com.android.systemui.R.string.keyguard_enter_pin
+import com.android.systemui.R.string.kg_prompt_after_dpm_lock
+import com.android.systemui.R.string.kg_prompt_after_user_lockdown_pin
+import com.android.systemui.R.string.kg_prompt_auth_timeout
+import com.android.systemui.R.string.kg_prompt_pin_auth_timeout
+import com.android.systemui.R.string.kg_prompt_reason_restart_pin
+import com.android.systemui.R.string.kg_prompt_unattended_update
+import com.android.systemui.R.string.kg_trust_agent_disabled
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.factory.BouncerMessageFactory
+import com.android.systemui.bouncer.data.repository.BouncerMessageRepository
+import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl
+import com.android.systemui.bouncer.shared.model.BouncerMessageModel
+import com.android.systemui.bouncer.shared.model.Message
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.shared.model.AuthenticationFlags
+import com.android.systemui.user.data.repository.FakeUserRepository
+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.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.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidJUnit4::class)
+class BouncerMessageRepositoryTest : SysuiTestCase() {
+
+    @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var securityModel: KeyguardSecurityModel
+    @Captor
+    private lateinit var updateMonitorCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback>
+
+    private lateinit var underTest: BouncerMessageRepository
+    private lateinit var trustRepository: FakeTrustRepository
+    private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
+    private lateinit var userRepository: FakeUserRepository
+    private lateinit var fingerprintRepository: FakeDeviceEntryFingerprintAuthRepository
+    private lateinit var testScope: TestScope
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        trustRepository = FakeTrustRepository()
+        biometricSettingsRepository = FakeBiometricSettingsRepository()
+        userRepository = FakeUserRepository()
+        userRepository.setUserInfos(listOf(PRIMARY_USER))
+        fingerprintRepository = FakeDeviceEntryFingerprintAuthRepository()
+        testScope = TestScope()
+
+        whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(false)
+        whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
+        underTest =
+            BouncerMessageRepositoryImpl(
+                trustRepository = trustRepository,
+                biometricSettingsRepository = biometricSettingsRepository,
+                updateMonitor = updateMonitor,
+                bouncerMessageFactory = BouncerMessageFactory(updateMonitor, securityModel),
+                userRepository = userRepository,
+                fingerprintAuthRepository = fingerprintRepository
+            )
+    }
+
+    @Test
+    fun setCustomMessage_propagatesState() =
+        testScope.runTest {
+            underTest.setCustomMessage(message("not empty"))
+
+            val customMessage = collectLastValue(underTest.customMessage)
+
+            assertThat(customMessage()).isEqualTo(message("not empty"))
+        }
+
+    @Test
+    fun setFaceMessage_propagatesState() =
+        testScope.runTest {
+            underTest.setFaceAcquisitionMessage(message("not empty"))
+
+            val faceAcquisitionMessage = collectLastValue(underTest.faceAcquisitionMessage)
+
+            assertThat(faceAcquisitionMessage()).isEqualTo(message("not empty"))
+        }
+
+    @Test
+    fun setFpMessage_propagatesState() =
+        testScope.runTest {
+            underTest.setFingerprintAcquisitionMessage(message("not empty"))
+
+            val fpAcquisitionMsg = collectLastValue(underTest.fingerprintAcquisitionMessage)
+
+            assertThat(fpAcquisitionMsg()).isEqualTo(message("not empty"))
+        }
+
+    @Test
+    fun setPrimaryAuthMessage_propagatesState() =
+        testScope.runTest {
+            underTest.setPrimaryAuthMessage(message("not empty"))
+
+            val primaryAuthMessage = collectLastValue(underTest.primaryAuthMessage)
+
+            assertThat(primaryAuthMessage()).isEqualTo(message("not empty"))
+        }
+
+    @Test
+    fun biometricAuthMessage_propagatesBiometricAuthMessages() =
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            val biometricAuthMessage = collectLastValue(underTest.biometricAuthMessage)
+            runCurrent()
+
+            verify(updateMonitor).registerCallback(updateMonitorCallback.capture())
+
+            updateMonitorCallback.value.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT)
+
+            assertThat(biometricAuthMessage())
+                .isEqualTo(message(R.string.kg_fp_not_recognized, R.string.kg_bio_try_again_or_pin))
+
+            updateMonitorCallback.value.onBiometricAuthFailed(BiometricSourceType.FACE)
+
+            assertThat(biometricAuthMessage())
+                .isEqualTo(
+                    message(R.string.bouncer_face_not_recognized, R.string.kg_bio_try_again_or_pin)
+                )
+
+            updateMonitorCallback.value.onBiometricAcquired(BiometricSourceType.FACE, 0)
+
+            assertThat(biometricAuthMessage()).isNull()
+        }
+
+    @Test
+    fun onFaceLockout_propagatesState() =
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            val lockoutMessage = collectLastValue(underTest.biometricLockedOutMessage)
+            runCurrent()
+            verify(updateMonitor).registerCallback(updateMonitorCallback.capture())
+
+            whenever(updateMonitor.isFaceLockedOut).thenReturn(true)
+            updateMonitorCallback.value.onLockedOutStateChanged(BiometricSourceType.FACE)
+
+            assertThat(lockoutMessage())
+                .isEqualTo(message(keyguard_enter_pin, R.string.kg_face_locked_out))
+
+            whenever(updateMonitor.isFaceLockedOut).thenReturn(false)
+            updateMonitorCallback.value.onLockedOutStateChanged(BiometricSourceType.FACE)
+            assertThat(lockoutMessage()).isNull()
+        }
+
+    @Test
+    fun onFingerprintLockout_propagatesState() =
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            val lockedOutMessage = collectLastValue(underTest.biometricLockedOutMessage)
+            runCurrent()
+
+            fingerprintRepository.setLockedOut(true)
+
+            assertThat(lockedOutMessage())
+                .isEqualTo(message(keyguard_enter_pin, R.string.kg_fp_locked_out))
+
+            fingerprintRepository.setLockedOut(false)
+            assertThat(lockedOutMessage()).isNull()
+        }
+
+    @Test
+    fun onAuthFlagsChanged_withTrustNotManagedAndNoBiometrics_isANoop() =
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            trustRepository.setCurrentUserTrustManaged(false)
+            biometricSettingsRepository.setFaceEnrolled(false)
+            biometricSettingsRepository.setFingerprintEnrolled(false)
+
+            verifyMessagesForAuthFlag(
+                STRONG_AUTH_NOT_REQUIRED to null,
+                STRONG_AUTH_REQUIRED_AFTER_BOOT to null,
+                SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
+                STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
+                STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to null,
+                STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to null,
+                STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to null,
+                SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null,
+                STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to null,
+                STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+                    Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
+            )
+        }
+
+    @Test
+    fun authFlagsChanges_withTrustManaged_providesDifferentMessages() =
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            biometricSettingsRepository.setFaceEnrolled(false)
+            biometricSettingsRepository.setFingerprintEnrolled(false)
+
+            trustRepository.setCurrentUserTrustManaged(true)
+
+            verifyMessagesForAuthFlag(
+                STRONG_AUTH_NOT_REQUIRED to null,
+                STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
+                STRONG_AUTH_REQUIRED_AFTER_BOOT to
+                    Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin),
+                STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+                    Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout),
+                STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+                    Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
+                SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
+                    Pair(keyguard_enter_pin, kg_trust_agent_disabled),
+                SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
+                    Pair(keyguard_enter_pin, kg_trust_agent_disabled),
+                STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+                    Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin),
+                STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+                    Pair(keyguard_enter_pin, kg_prompt_unattended_update),
+                STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+                    Pair(keyguard_enter_pin, kg_prompt_auth_timeout),
+            )
+        }
+
+    @Test
+    fun authFlagsChanges_withFaceEnrolled_providesDifferentMessages() =
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            trustRepository.setCurrentUserTrustManaged(false)
+            biometricSettingsRepository.setFingerprintEnrolled(false)
+
+            biometricSettingsRepository.setIsFaceAuthEnabled(true)
+            biometricSettingsRepository.setFaceEnrolled(true)
+
+            verifyMessagesForAuthFlag(
+                STRONG_AUTH_NOT_REQUIRED to null,
+                STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
+                SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
+                SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null,
+                STRONG_AUTH_REQUIRED_AFTER_BOOT to
+                    Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin),
+                STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+                    Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout),
+                STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+                    Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
+                STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+                    Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin),
+                STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+                    Pair(keyguard_enter_pin, kg_prompt_unattended_update),
+                STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+                    Pair(keyguard_enter_pin, kg_prompt_auth_timeout),
+            )
+        }
+
+    @Test
+    fun authFlagsChanges_withFingerprintEnrolled_providesDifferentMessages() =
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            trustRepository.setCurrentUserTrustManaged(false)
+            biometricSettingsRepository.setIsFaceAuthEnabled(false)
+            biometricSettingsRepository.setFaceEnrolled(false)
+
+            biometricSettingsRepository.setFingerprintEnrolled(true)
+            biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(true)
+
+            verifyMessagesForAuthFlag(
+                STRONG_AUTH_NOT_REQUIRED to null,
+                STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
+                SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
+                SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null,
+                STRONG_AUTH_REQUIRED_AFTER_BOOT to
+                    Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin),
+                STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+                    Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout),
+                STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+                    Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
+                STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+                    Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin),
+                STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+                    Pair(keyguard_enter_pin, kg_prompt_unattended_update),
+                STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+                    Pair(keyguard_enter_pin, kg_prompt_auth_timeout),
+            )
+        }
+
+    private fun TestScope.verifyMessagesForAuthFlag(
+        vararg authFlagToExpectedMessages: Pair<Int, Pair<Int, Int>?>
+    ) {
+        val authFlagsMessage = collectLastValue(underTest.authFlagsMessage)
+
+        authFlagToExpectedMessages.forEach { (flag, messagePair) ->
+            biometricSettingsRepository.setAuthenticationFlags(
+                AuthenticationFlags(PRIMARY_USER_ID, flag)
+            )
+
+            assertThat(authFlagsMessage())
+                .isEqualTo(messagePair?.let { message(it.first, it.second) })
+        }
+    }
+
+    private fun message(primaryResId: Int, secondaryResId: Int): BouncerMessageModel {
+        return BouncerMessageModel(
+            message = Message(messageResId = primaryResId),
+            secondaryMessage = Message(messageResId = secondaryResId)
+        )
+    }
+    private fun message(value: String): BouncerMessageModel {
+        return BouncerMessageModel(message = Message(message = value))
+    }
+
+    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
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt
new file mode 100644
index 0000000..a049191
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt
@@ -0,0 +1,49 @@
+package com.android.systemui.bouncer.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.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.SystemClock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestCoroutineScope
+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
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@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
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        val testCoroutineScope = TestCoroutineScope()
+        underTest =
+            KeyguardBouncerRepositoryImpl(
+                systemClock,
+                testCoroutineScope,
+                bouncerLogger,
+            )
+    }
+
+    @Test
+    fun changingFlowValueTriggersLogging() = runBlocking {
+        underTest.setPrimaryShow(true)
+        Mockito.verify(bouncerLogger)
+            .logChange(eq(""), eq("PrimaryBouncerShow"), value = eq(false), any())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
new file mode 100644
index 0000000..37b9ca4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
+import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepositoryImpl
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+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.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RoboPilotTest
+@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 statusBarStateController: StatusBarStateController
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var systemClock: SystemClock
+    @Mock private lateinit var bouncerLogger: TableLogBuffer
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        bouncerRepository =
+            KeyguardBouncerRepositoryImpl(
+                FakeSystemClock(),
+                TestCoroutineScope(),
+                bouncerLogger,
+            )
+        biometricSettingsRepository = FakeBiometricSettingsRepository()
+        deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
+        underTest =
+            AlternateBouncerInteractor(
+                statusBarStateController,
+                keyguardStateController,
+                bouncerRepository,
+                biometricSettingsRepository,
+                deviceEntryFingerprintAuthRepository,
+                systemClock,
+            )
+    }
+
+    @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/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 374c28d6..d09353b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -26,7 +26,6 @@
 import com.android.systemui.scene.shared.model.SceneModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -40,8 +39,8 @@
 @RunWith(JUnit4::class)
 class BouncerInteractorTest : SysuiTestCase() {
 
-    private val testScope = TestScope()
-    private val utils = SceneTestUtils(this, testScope)
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
     private val authenticationInteractor =
         utils.authenticationInteractor(
             repository = utils.authenticationRepository(),
@@ -69,8 +68,10 @@
             val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
             val message by collectLastValue(underTest.message)
 
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(false)
             underTest.showOrUnlockDevice("container1")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
@@ -95,14 +96,69 @@
         }
 
     @Test
+    fun pinAuthMethod_tryAutoConfirm_withAutoConfirmPin() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
+            val message by collectLastValue(underTest.message)
+
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = true)
+            )
+            utils.authenticationRepository.setUnlocked(false)
+            underTest.showOrUnlockDevice("container1")
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
+            underTest.clearMessage()
+
+            // Incomplete input.
+            assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true)).isNull()
+            assertThat(message).isEmpty()
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+
+            // Wrong 4-digit pin
+            assertThat(underTest.authenticate(listOf(1, 2, 3, 5), tryAutoConfirm = true)).isFalse()
+            assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+
+            // Correct input.
+            assertThat(underTest.authenticate(listOf(1, 2, 3, 4), tryAutoConfirm = true)).isTrue()
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun pinAuthMethod_tryAutoConfirm_withoutAutoConfirmPin() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
+            val message by collectLastValue(underTest.message)
+
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = false)
+            )
+            utils.authenticationRepository.setUnlocked(false)
+            underTest.showOrUnlockDevice("container1")
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            underTest.clearMessage()
+
+            // Incomplete input.
+            assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true)).isNull()
+            assertThat(message).isEmpty()
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+
+            // Correct input.
+            assertThat(underTest.authenticate(listOf(1, 2, 3, 4), tryAutoConfirm = true)).isNull()
+            assertThat(message).isEmpty()
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+        }
+
+    @Test
     fun passwordAuthMethod() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
             val message by collectLastValue(underTest.message)
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Password("password")
             )
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             underTest.showOrUnlockDevice("container1")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
@@ -131,10 +187,10 @@
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
             val message by collectLastValue(underTest.message)
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pattern(emptyList())
             )
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             underTest.showOrUnlockDevice("container1")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
@@ -167,8 +223,10 @@
     fun showOrUnlockDevice_notLocked_switchesToGoneScene() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.unlockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(true)
             runCurrent()
 
             underTest.showOrUnlockDevice("container1")
@@ -180,8 +238,8 @@
     fun showOrUnlockDevice_authMethodNotSecure_switchesToGoneScene() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            utils.authenticationRepository.setUnlocked(false)
 
             underTest.showOrUnlockDevice("container1")
 
@@ -193,10 +251,10 @@
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
             val message by collectLastValue(underTest.message)
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Password("password")
             )
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
 
             val customMessage = "Hello there!"
             underTest.showOrUnlockDevice("container1", customMessage)
@@ -210,11 +268,17 @@
         testScope.runTest {
             val throttling by collectLastValue(underTest.throttling)
             val message by collectLastValue(underTest.message)
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            val currentScene by
+                collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            runCurrent()
+            underTest.showOrUnlockDevice(SceneTestUtils.CONTAINER_1)
+            runCurrent()
+            assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer)
             assertThat(throttling).isNull()
-            assertThat(message).isEqualTo("")
-            assertThat(isUnlocked).isFalse()
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
             repeat(BouncerInteractor.THROTTLE_EVERY) { times ->
                 // Wrong PIN.
                 assertThat(underTest.authenticate(listOf(6, 7, 8, 9))).isFalse()
@@ -225,9 +289,9 @@
             assertThat(throttling).isNotNull()
             assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC)
 
-            // Correct PIN, but throttled, so doesn't unlock:
+            // Correct PIN, but throttled, so doesn't change away from the bouncer scene:
             assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isFalse()
-            assertThat(isUnlocked).isFalse()
+            assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer)
             assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC)
 
             throttling?.totalDurationSec?.let { seconds ->
@@ -241,11 +305,28 @@
             }
             assertThat(message).isEqualTo("")
             assertThat(throttling).isNull()
-            assertThat(isUnlocked).isFalse()
+            assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer)
 
-            // Correct PIN and no longer throttled so unlocks:
+            // Correct PIN and no longer throttled so changes to the Gone scene:
             assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue()
-            assertThat(isUnlocked).isTrue()
+            assertThat(currentScene?.key).isEqualTo(SceneKey.Gone)
+        }
+
+    @Test
+    fun switchesToGone_whenUnlocked() =
+        testScope.runTest {
+            utils.authenticationRepository.setUnlocked(false)
+            sceneInteractor.setCurrentScene(
+                SceneTestUtils.CONTAINER_1,
+                SceneModel(SceneKey.Bouncer)
+            )
+            val currentScene by
+                collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+
+            utils.authenticationRepository.setUnlocked(true)
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
         }
 
     private fun assertTryAgainMessage(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
new file mode 100644
index 0000000..8e5256e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.domain.interactor
+
+import android.content.pm.UserInfo
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.R.string.keyguard_enter_pin
+import com.android.systemui.R.string.kg_too_many_failed_attempts_countdown
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.factory.BouncerMessageFactory
+import com.android.systemui.bouncer.data.repository.FakeBouncerMessageRepository
+import com.android.systemui.bouncer.shared.model.BouncerMessageModel
+import com.android.systemui.bouncer.shared.model.Message
+import com.android.systemui.coroutines.FlowValue
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.mockito.KotlinArgumentCaptor
+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.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidJUnit4::class)
+class BouncerMessageInteractorTest : SysuiTestCase() {
+
+    @Mock private lateinit var securityModel: KeyguardSecurityModel
+    @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var countDownTimerUtil: CountDownTimerUtil
+    private lateinit var countDownTimerCallback: KotlinArgumentCaptor<CountDownTimerCallback>
+    private lateinit var underTest: BouncerMessageInteractor
+    private lateinit var repository: FakeBouncerMessageRepository
+    private lateinit var userRepository: FakeUserRepository
+    private lateinit var testScope: TestScope
+    private lateinit var bouncerMessage: FlowValue<BouncerMessageModel?>
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        repository = FakeBouncerMessageRepository()
+        userRepository = FakeUserRepository()
+        userRepository.setUserInfos(listOf(PRIMARY_USER))
+        testScope = TestScope()
+        countDownTimerCallback = KotlinArgumentCaptor(CountDownTimerCallback::class.java)
+
+        allowTestableLooperAsMainThread()
+        whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
+        whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(false)
+    }
+
+    suspend fun TestScope.init() {
+        userRepository.setSelectedUserInfo(PRIMARY_USER)
+        val featureFlags = FakeFeatureFlags()
+        featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
+        underTest =
+            BouncerMessageInteractor(
+                repository = repository,
+                factory = BouncerMessageFactory(updateMonitor, securityModel),
+                userRepository = userRepository,
+                countDownTimerUtil = countDownTimerUtil,
+                featureFlags = featureFlags
+            )
+        bouncerMessage = collectLastValue(underTest.bouncerMessage)
+    }
+
+    @Test
+    fun onIncorrectSecurityInput_setsTheBouncerModelInTheRepository() =
+        testScope.runTest {
+            init()
+            underTest.onPrimaryAuthIncorrectAttempt()
+
+            assertThat(repository.primaryAuthMessage).isNotNull()
+            assertThat(
+                    context.resources.getString(
+                        repository.primaryAuthMessage.value!!.message!!.messageResId!!
+                    )
+                )
+                .isEqualTo("Wrong PIN. Try again.")
+        }
+
+    @Test
+    fun onUserStartsPrimaryAuthInput_clearsAllSetBouncerMessages() =
+        testScope.runTest {
+            init()
+            repository.setCustomMessage(message("not empty"))
+            repository.setFaceAcquisitionMessage(message("not empty"))
+            repository.setFingerprintAcquisitionMessage(message("not empty"))
+            repository.setPrimaryAuthMessage(message("not empty"))
+
+            underTest.onPrimaryBouncerUserInput()
+
+            assertThat(repository.customMessage.value).isNull()
+            assertThat(repository.faceAcquisitionMessage.value).isNull()
+            assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
+            assertThat(repository.primaryAuthMessage.value).isNull()
+        }
+
+    @Test
+    fun onBouncerBeingHidden_clearsAllSetBouncerMessages() =
+        testScope.runTest {
+            init()
+            repository.setCustomMessage(message("not empty"))
+            repository.setFaceAcquisitionMessage(message("not empty"))
+            repository.setFingerprintAcquisitionMessage(message("not empty"))
+            repository.setPrimaryAuthMessage(message("not empty"))
+
+            underTest.onBouncerBeingHidden()
+
+            assertThat(repository.customMessage.value).isNull()
+            assertThat(repository.faceAcquisitionMessage.value).isNull()
+            assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
+            assertThat(repository.primaryAuthMessage.value).isNull()
+        }
+
+    @Test
+    fun setCustomMessage_setsRepositoryValue() =
+        testScope.runTest {
+            init()
+
+            underTest.setCustomMessage("not empty")
+
+            assertThat(repository.customMessage.value)
+                .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
+
+            underTest.setCustomMessage(null)
+            assertThat(repository.customMessage.value).isNull()
+        }
+
+    @Test
+    fun setFaceMessage_setsRepositoryValue() =
+        testScope.runTest {
+            init()
+
+            underTest.setFaceAcquisitionMessage("not empty")
+
+            assertThat(repository.faceAcquisitionMessage.value)
+                .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
+
+            underTest.setFaceAcquisitionMessage(null)
+            assertThat(repository.faceAcquisitionMessage.value).isNull()
+        }
+
+    @Test
+    fun setFingerprintMessage_setsRepositoryValue() =
+        testScope.runTest {
+            init()
+
+            underTest.setFingerprintAcquisitionMessage("not empty")
+
+            assertThat(repository.fingerprintAcquisitionMessage.value)
+                .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
+
+            underTest.setFingerprintAcquisitionMessage(null)
+            assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
+        }
+
+    @Test
+    fun onPrimaryAuthLockout_startsTimerForSpecifiedNumberOfSeconds() =
+        testScope.runTest {
+            init()
+
+            underTest.onPrimaryAuthLockedOut(3)
+
+            verify(countDownTimerUtil)
+                .startNewTimer(eq(3000L), eq(1000L), countDownTimerCallback.capture())
+
+            countDownTimerCallback.value.onTick(2000L)
+
+            val primaryMessage = repository.primaryAuthMessage.value!!.message!!
+            assertThat(primaryMessage.messageResId!!)
+                .isEqualTo(kg_too_many_failed_attempts_countdown)
+            assertThat(primaryMessage.formatterArgs).isEqualTo(mapOf(Pair("count", 2)))
+        }
+
+    @Test
+    fun onPrimaryAuthLockout_timerComplete_resetsRepositoryMessages() =
+        testScope.runTest {
+            init()
+            repository.setCustomMessage(message("not empty"))
+            repository.setFaceAcquisitionMessage(message("not empty"))
+            repository.setFingerprintAcquisitionMessage(message("not empty"))
+            repository.setPrimaryAuthMessage(message("not empty"))
+
+            underTest.onPrimaryAuthLockedOut(3)
+
+            verify(countDownTimerUtil)
+                .startNewTimer(eq(3000L), eq(1000L), countDownTimerCallback.capture())
+
+            countDownTimerCallback.value.onFinish()
+
+            assertThat(repository.customMessage.value).isNull()
+            assertThat(repository.faceAcquisitionMessage.value).isNull()
+            assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
+            assertThat(repository.primaryAuthMessage.value).isNull()
+        }
+
+    @Test
+    fun bouncerMessage_hasPriorityOrderOfMessages() =
+        testScope.runTest {
+            init()
+            repository.setBiometricAuthMessage(message("biometric message"))
+            repository.setFaceAcquisitionMessage(message("face acquisition message"))
+            repository.setFingerprintAcquisitionMessage(message("fingerprint acquisition message"))
+            repository.setPrimaryAuthMessage(message("primary auth message"))
+            repository.setAuthFlagsMessage(message("auth flags message"))
+            repository.setBiometricLockedOutMessage(message("biometrics locked out"))
+            repository.setCustomMessage(message("custom message"))
+
+            assertThat(bouncerMessage()).isEqualTo(message("primary auth message"))
+
+            repository.setPrimaryAuthMessage(null)
+
+            assertThat(bouncerMessage()).isEqualTo(message("biometric message"))
+
+            repository.setBiometricAuthMessage(null)
+
+            assertThat(bouncerMessage()).isEqualTo(message("fingerprint acquisition message"))
+
+            repository.setFingerprintAcquisitionMessage(null)
+
+            assertThat(bouncerMessage()).isEqualTo(message("face acquisition message"))
+
+            repository.setFaceAcquisitionMessage(null)
+
+            assertThat(bouncerMessage()).isEqualTo(message("custom message"))
+
+            repository.setCustomMessage(null)
+
+            assertThat(bouncerMessage()).isEqualTo(message("auth flags message"))
+
+            repository.setAuthFlagsMessage(null)
+
+            assertThat(bouncerMessage()).isEqualTo(message("biometrics locked out"))
+
+            repository.setBiometricLockedOutMessage(null)
+
+            // sets the default message if everything else is null
+            assertThat(bouncerMessage()!!.message!!.messageResId).isEqualTo(keyguard_enter_pin)
+        }
+
+    private fun message(value: String): BouncerMessageModel {
+        return BouncerMessageModel(message = Message(message = value))
+    }
+
+    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
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
new file mode 100644
index 0000000..a81ca86
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.bouncer.domain.interactor
+
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
+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.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
+class PrimaryBouncerCallbackInteractorTest : SysuiTestCase() {
+    private val mPrimaryBouncerCallbackInteractor = PrimaryBouncerCallbackInteractor()
+    @Mock
+    private lateinit var mPrimaryBouncerExpansionCallback:
+        PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback
+    @Mock
+    private lateinit var keyguardResetCallback:
+        PrimaryBouncerCallbackInteractor.KeyguardResetCallback
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(
+            mPrimaryBouncerExpansionCallback
+        )
+        mPrimaryBouncerCallbackInteractor.addKeyguardResetCallback(keyguardResetCallback)
+    }
+
+    @Test
+    fun testOnFullyShown() {
+        mPrimaryBouncerCallbackInteractor.dispatchFullyShown()
+        verify(mPrimaryBouncerExpansionCallback).onFullyShown()
+    }
+
+    @Test
+    fun testOnFullyHidden() {
+        mPrimaryBouncerCallbackInteractor.dispatchFullyHidden()
+        verify(mPrimaryBouncerExpansionCallback).onFullyHidden()
+    }
+
+    @Test
+    fun testOnExpansionChanged() {
+        mPrimaryBouncerCallbackInteractor.dispatchExpansionChanged(5f)
+        verify(mPrimaryBouncerExpansionCallback).onExpansionChanged(5f)
+    }
+
+    @Test
+    fun testOnVisibilityChanged() {
+        mPrimaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.INVISIBLE)
+        verify(mPrimaryBouncerExpansionCallback).onVisibilityChanged(false)
+    }
+
+    @Test
+    fun testOnStartingToHide() {
+        mPrimaryBouncerCallbackInteractor.dispatchStartingToHide()
+        verify(mPrimaryBouncerExpansionCallback).onStartingToHide()
+    }
+
+    @Test
+    fun testOnStartingToShow() {
+        mPrimaryBouncerCallbackInteractor.dispatchStartingToShow()
+        verify(mPrimaryBouncerExpansionCallback).onStartingToShow()
+    }
+
+    @Test
+    fun testOnKeyguardReset() {
+        mPrimaryBouncerCallbackInteractor.dispatchReset()
+        verify(keyguardResetCallback).onKeyguardReset()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
new file mode 100644
index 0000000..f892453
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
@@ -0,0 +1,489 @@
+/*
+ * 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.bouncer.domain.interactor
+
+import android.hardware.biometrics.BiometricSourceType
+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.bouncer.data.repository.KeyguardBouncerRepository
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE
+import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
+import com.android.systemui.bouncer.ui.BouncerView
+import com.android.systemui.bouncer.ui.BouncerViewDelegate
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.utils.os.FakeHandler
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Answers
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class PrimaryBouncerInteractorTest : SysuiTestCase() {
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private lateinit var repository: KeyguardBouncerRepository
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var bouncerView: BouncerView
+    @Mock private lateinit var bouncerViewDelegate: BouncerViewDelegate
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
+    @Mock private lateinit var mPrimaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor
+    @Mock private lateinit var falsingCollector: FalsingCollector
+    @Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    private lateinit var mainHandler: FakeHandler
+    private lateinit var underTest: PrimaryBouncerInteractor
+    private lateinit var resources: TestableResources
+    private lateinit var trustRepository: FakeTrustRepository
+    private lateinit var featureFlags: FakeFeatureFlags
+    private lateinit var testScope: TestScope
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(keyguardSecurityModel.getSecurityMode(anyInt()))
+            .thenReturn(KeyguardSecurityModel.SecurityMode.PIN)
+
+        DejankUtils.setImmediate(true)
+        testScope = TestScope()
+        mainHandler = FakeHandler(android.os.Looper.getMainLooper())
+        trustRepository = FakeTrustRepository()
+        featureFlags = FakeFeatureFlags().apply { set(Flags.DELAY_BOUNCER, true) }
+        underTest =
+            PrimaryBouncerInteractor(
+                repository,
+                bouncerView,
+                mainHandler,
+                keyguardStateController,
+                keyguardSecurityModel,
+                mPrimaryBouncerCallbackInteractor,
+                falsingCollector,
+                dismissCallbackRegistry,
+                context,
+                keyguardUpdateMonitor,
+                trustRepository,
+                featureFlags,
+                testScope.backgroundScope,
+            )
+        whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
+        whenever(repository.primaryBouncerShow.value).thenReturn(false)
+        whenever(bouncerView.delegate).thenReturn(bouncerViewDelegate)
+        resources = context.orCreateTestableResources
+    }
+
+    @Test
+    fun testShow_isScrimmed() {
+        underTest.show(true)
+        verify(repository).setKeyguardAuthenticated(null)
+        verify(repository).setPrimaryStartingToHide(false)
+        verify(repository).setPrimaryScrimmed(true)
+        verify(repository).setPanelExpansion(EXPANSION_VISIBLE)
+        verify(repository).setPrimaryShowingSoon(true)
+        verify(keyguardStateController).notifyPrimaryBouncerShowing(true)
+        verify(mPrimaryBouncerCallbackInteractor).dispatchStartingToShow()
+        verify(repository).setPrimaryShow(true)
+        verify(repository).setPrimaryShowingSoon(false)
+        verify(mPrimaryBouncerCallbackInteractor).dispatchVisibilityChanged(View.VISIBLE)
+    }
+
+    @Test
+    fun testShow_isNotScrimmed() {
+        verify(repository, never()).setPanelExpansion(EXPANSION_VISIBLE)
+    }
+
+    @Test
+    fun testShow_keyguardIsDone() {
+        whenever(bouncerView.delegate?.showNextSecurityScreenOrFinish()).thenReturn(true)
+        verify(keyguardStateController, never()).notifyPrimaryBouncerShowing(true)
+        verify(mPrimaryBouncerCallbackInteractor, never()).dispatchStartingToShow()
+    }
+
+    @Test
+    fun testShow_isResumed() {
+        whenever(repository.primaryBouncerShow.value).thenReturn(true)
+        whenever(keyguardSecurityModel.getSecurityMode(anyInt()))
+            .thenReturn(KeyguardSecurityModel.SecurityMode.SimPuk)
+
+        underTest.show(true)
+        verify(repository).setPrimaryShow(false)
+        verify(repository).setPrimaryShow(true)
+    }
+
+    @Test
+    fun testHide() {
+        underTest.hide()
+        verify(falsingCollector).onBouncerHidden()
+        verify(keyguardStateController).notifyPrimaryBouncerShowing(false)
+        verify(repository).setPrimaryShowingSoon(false)
+        verify(repository).setPrimaryShow(false)
+        verify(mPrimaryBouncerCallbackInteractor).dispatchVisibilityChanged(View.INVISIBLE)
+        verify(repository).setPrimaryStartDisappearAnimation(null)
+        verify(repository).setPanelExpansion(EXPANSION_HIDDEN)
+    }
+
+    @Test
+    fun testExpansion() {
+        whenever(repository.panelExpansionAmount.value).thenReturn(0.5f)
+        underTest.setPanelExpansion(0.6f)
+        verify(repository).setPanelExpansion(0.6f)
+        verify(mPrimaryBouncerCallbackInteractor).dispatchExpansionChanged(0.6f)
+    }
+
+    @Test
+    fun testExpansion_fullyShown() {
+        whenever(repository.panelExpansionAmount.value).thenReturn(0.5f)
+        whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
+        underTest.setPanelExpansion(EXPANSION_VISIBLE)
+        verify(falsingCollector).onBouncerShown()
+        verify(mPrimaryBouncerCallbackInteractor).dispatchFullyShown()
+    }
+
+    @Test
+    fun testExpansion_fullyHidden() {
+        whenever(repository.panelExpansionAmount.value).thenReturn(0.5f)
+        whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
+        underTest.setPanelExpansion(EXPANSION_HIDDEN)
+        verify(repository).setPrimaryShow(false)
+        verify(falsingCollector).onBouncerHidden()
+        verify(mPrimaryBouncerCallbackInteractor).dispatchReset()
+        verify(mPrimaryBouncerCallbackInteractor).dispatchFullyHidden()
+    }
+
+    @Test
+    fun testExpansion_startingToHide() {
+        whenever(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
+        underTest.setPanelExpansion(0.1f)
+        verify(repository).setPrimaryStartingToHide(true)
+        verify(mPrimaryBouncerCallbackInteractor).dispatchStartingToHide()
+    }
+
+    @Test
+    fun testShowMessage() {
+        val argCaptor = ArgumentCaptor.forClass(BouncerShowMessageModel::class.java)
+        underTest.showMessage("abc", null)
+        verify(repository).setShowMessage(argCaptor.capture())
+        assertThat(argCaptor.value.message).isEqualTo("abc")
+    }
+
+    @Test
+    fun testDismissAction() {
+        val onDismissAction = mock(ActivityStarter.OnDismissAction::class.java)
+        val cancelAction = mock(Runnable::class.java)
+        underTest.setDismissAction(onDismissAction, cancelAction)
+        verify(bouncerViewDelegate).setDismissAction(onDismissAction, cancelAction)
+    }
+
+    @Test
+    fun testUpdateResources() {
+        underTest.updateResources()
+        verify(repository).setResourceUpdateRequests(true)
+    }
+
+    @Test
+    fun testNotifyKeyguardAuthenticated() {
+        underTest.notifyKeyguardAuthenticated(true)
+        verify(repository).setKeyguardAuthenticated(true)
+    }
+
+    @Test
+    fun testNotifyShowedMessage() {
+        underTest.onMessageShown()
+        verify(repository).setShowMessage(null)
+    }
+
+    @Test
+    fun testSetKeyguardPosition() {
+        underTest.setKeyguardPosition(0f)
+        verify(repository).setKeyguardPosition(0f)
+    }
+
+    @Test
+    fun testNotifyKeyguardAuthenticatedHandled() {
+        underTest.notifyKeyguardAuthenticatedHandled()
+        verify(repository).setKeyguardAuthenticated(null)
+    }
+
+    @Test
+    fun testNotifyUpdatedResources() {
+        underTest.notifyUpdatedResources()
+        verify(repository).setResourceUpdateRequests(false)
+    }
+
+    @Test
+    fun testSetBackButtonEnabled() {
+        underTest.setBackButtonEnabled(true)
+        verify(repository).setIsBackButtonEnabled(true)
+    }
+
+    @Test
+    fun testStartDisappearAnimation_willRunDismissFromKeyguard() {
+        whenever(bouncerViewDelegate.willRunDismissFromKeyguard()).thenReturn(true)
+
+        val runnable = mock(Runnable::class.java)
+        underTest.startDisappearAnimation(runnable)
+        // End runnable should run immediately
+        verify(runnable).run()
+        // ... while the disappear animation should never be run
+        verify(repository, never()).setPrimaryStartDisappearAnimation(any(Runnable::class.java))
+    }
+
+    @Test
+    fun testStartDisappearAnimation_willNotRunDismissFromKeyguard_() {
+        whenever(bouncerViewDelegate.willRunDismissFromKeyguard()).thenReturn(false)
+
+        val runnable = mock(Runnable::class.java)
+        underTest.startDisappearAnimation(runnable)
+        verify(repository).setPrimaryStartDisappearAnimation(any(Runnable::class.java))
+    }
+
+    @Test
+    fun testIsFullShowing() {
+        whenever(repository.primaryBouncerShow.value).thenReturn(true)
+        whenever(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
+        whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
+        assertThat(underTest.isFullyShowing()).isTrue()
+        whenever(repository.primaryBouncerShow.value).thenReturn(false)
+        assertThat(underTest.isFullyShowing()).isFalse()
+    }
+
+    @Test
+    fun testIsScrimmed() {
+        whenever(repository.primaryBouncerScrimmed.value).thenReturn(true)
+        assertThat(underTest.isScrimmed()).isTrue()
+        whenever(repository.primaryBouncerScrimmed.value).thenReturn(false)
+        assertThat(underTest.isScrimmed()).isFalse()
+    }
+
+    @Test
+    fun testIsInTransit() {
+        whenever(repository.primaryBouncerShowingSoon.value).thenReturn(true)
+        assertThat(underTest.isInTransit()).isTrue()
+        whenever(repository.primaryBouncerShowingSoon.value).thenReturn(false)
+        assertThat(underTest.isInTransit()).isFalse()
+        whenever(repository.panelExpansionAmount.value).thenReturn(0.5f)
+        assertThat(underTest.isInTransit()).isTrue()
+    }
+
+    @Test
+    fun testIsAnimatingAway() {
+        whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(Runnable {})
+        assertThat(underTest.isAnimatingAway()).isTrue()
+        whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
+        assertThat(underTest.isAnimatingAway()).isFalse()
+    }
+
+    @Test
+    fun testWillDismissWithAction() {
+        whenever(bouncerViewDelegate.willDismissWithActions()).thenReturn(true)
+        assertThat(underTest.willDismissWithAction()).isTrue()
+        whenever(bouncerViewDelegate.willDismissWithActions()).thenReturn(false)
+        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)
+    }
+
+    @Test
+    fun delayBouncerWhenFaceAuthPossible() {
+        mainHandler.setMode(FakeHandler.Mode.QUEUEING)
+
+        // GIVEN bouncer should be delayed due to face auth
+        whenever(keyguardStateController.isFaceAuthEnabled).thenReturn(true)
+        whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE))
+            .thenReturn(true)
+        whenever(keyguardUpdateMonitor.doesCurrentPostureAllowFaceAuth()).thenReturn(true)
+
+        // WHEN bouncer show is requested
+        underTest.show(true)
+
+        // THEN primary show & primary showing soon aren't updated immediately
+        verify(repository, never()).setPrimaryShow(true)
+        verify(repository, never()).setPrimaryShowingSoon(false)
+
+        // WHEN all queued messages are dispatched
+        mainHandler.dispatchQueuedMessages()
+
+        // THEN primary show & primary showing soon are updated
+        verify(repository).setPrimaryShow(true)
+        verify(repository).setPrimaryShowingSoon(false)
+    }
+
+    @Test
+    fun noDelayBouncer_biometricsAllowed_postureDoesNotAllowFaceAuth() {
+        mainHandler.setMode(FakeHandler.Mode.QUEUEING)
+
+        // GIVEN bouncer should not be delayed because device isn't in the right posture for
+        // face auth
+        whenever(keyguardStateController.isFaceAuthEnabled).thenReturn(true)
+        whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE))
+            .thenReturn(true)
+        whenever(keyguardUpdateMonitor.doesCurrentPostureAllowFaceAuth()).thenReturn(false)
+
+        // WHEN bouncer show is requested
+        underTest.show(true)
+
+        // THEN primary show & primary showing soon are updated immediately
+        verify(repository).setPrimaryShow(true)
+        verify(repository).setPrimaryShowingSoon(false)
+    }
+
+    @Test
+    fun delayBouncerWhenActiveUnlockPossible() {
+        testScope.run {
+            mainHandler.setMode(FakeHandler.Mode.QUEUEING)
+
+            // GIVEN bouncer should be delayed due to active unlock
+            trustRepository.setCurrentUserActiveUnlockAvailable(true)
+            whenever(keyguardUpdateMonitor.canTriggerActiveUnlockBasedOnDeviceState())
+                .thenReturn(true)
+            runCurrent()
+
+            // WHEN bouncer show is requested
+            underTest.show(true)
+
+            // THEN primary show & primary showing soon were scheduled to update
+            verify(repository, never()).setPrimaryShow(true)
+            verify(repository, never()).setPrimaryShowingSoon(false)
+
+            // WHEN all queued messages are dispatched
+            mainHandler.dispatchQueuedMessages()
+
+            // THEN primary show & primary showing soon are updated
+            verify(repository).setPrimaryShow(true)
+            verify(repository).setPrimaryShowingSoon(false)
+        }
+    }
+
+    private fun updateSideFpsVisibilityParameters(
+        isVisible: Boolean,
+        sfpsEnabled: Boolean,
+        fpsDetectionRunning: Boolean,
+        isUnlockingWithFpAllowed: Boolean,
+        isAnimatingAway: Boolean
+    ) {
+        whenever(repository.primaryBouncerShow.value).thenReturn(isVisible)
+        resources.addOverride(R.bool.config_show_sidefps_hint_on_bouncer, sfpsEnabled)
+        whenever(keyguardUpdateMonitor.isFingerprintDetectionRunning)
+            .thenReturn(fpsDetectionRunning)
+        whenever(keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
+            .thenReturn(isUnlockingWithFpAllowed)
+        whenever(repository.primaryBouncerStartingDisappearAnimation.value)
+            .thenReturn(if (isAnimatingAway) Runnable {} else null)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt
new file mode 100644
index 0000000..665456d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.bouncer.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.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.ui.BouncerView
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.repository.TrustRepository
+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.TestScope
+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
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RoboPilotTest
+@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,
+                Mockito.mock(TrustRepository::class.java),
+                FakeFeatureFlags().apply { set(Flags.DELAY_BOUNCER, true) },
+                TestScope().backgroundScope,
+            )
+    }
+
+    @Test
+    fun notInteractableWhenExpansionIsBelow90Percent() = runTest {
+        val isInteractable = collectLastValue(underTest.isInteractable)
+
+        repository.setPrimaryShow(true)
+        repository.setPanelExpansion(0.15f)
+
+        assertThat(isInteractable()).isFalse()
+    }
+
+    @Test
+    fun notInteractableWhenExpansionAbove90PercentButNotVisible() = runTest {
+        val isInteractable = collectLastValue(underTest.isInteractable)
+
+        repository.setPrimaryShow(false)
+        repository.setPanelExpansion(0.05f)
+
+        assertThat(isInteractable()).isFalse()
+    }
+
+    @Test
+    fun isInteractableWhenExpansionAbove90PercentAndVisible() = runTest {
+        var isInteractable = collectLastValue(underTest.isInteractable)
+
+        repository.setPrimaryShow(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/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index 1642410..f811ce0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -24,7 +24,6 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -35,14 +34,15 @@
 @RunWith(JUnit4::class)
 class AuthMethodBouncerViewModelTest : SysuiTestCase() {
 
-    private val testScope = TestScope()
-    private val utils = SceneTestUtils(this, testScope)
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
     private val authenticationInteractor =
         utils.authenticationInteractor(
             utils.authenticationRepository(),
         )
     private val underTest =
         PinBouncerViewModel(
+            applicationContext = context,
             applicationScope = testScope.backgroundScope,
             interactor =
                 utils.bouncerInteractor(
@@ -55,7 +55,9 @@
     @Test
     fun animateFailure() =
         testScope.runTest {
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
             val animateFailure by collectLastValue(underTest.animateFailure)
             assertThat(animateFailure).isFalse()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index e8c946c..5ffc471 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -23,11 +23,14 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.scene.SceneTestUtils
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -38,8 +41,8 @@
 @RunWith(JUnit4::class)
 class BouncerViewModelTest : SysuiTestCase() {
 
-    private val testScope = TestScope()
-    private val utils = SceneTestUtils(this, testScope)
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
     private val authenticationInteractor =
         utils.authenticationInteractor(
             repository = utils.authenticationRepository(),
@@ -54,16 +57,26 @@
     @Test
     fun authMethod_nonNullForSecureMethods_nullForNotSecureMethods() =
         testScope.runTest {
-            val authMethodViewModel: AuthMethodBouncerViewModel? by
-                collectLastValue(underTest.authMethod)
+            var authMethodViewModel: AuthMethodBouncerViewModel? = null
+
             authMethodsToTest().forEach { authMethod ->
-                authenticationInteractor.setAuthenticationMethod(authMethod)
+                utils.authenticationRepository.setAuthenticationMethod(authMethod)
+                val job = underTest.authMethod.onEach { authMethodViewModel = it }.launchIn(this)
+                runCurrent()
 
                 if (authMethod.isSecure) {
-                    assertThat(authMethodViewModel).isNotNull()
+                    assertWithMessage("View-model unexpectedly null for auth method $authMethod")
+                        .that(authMethodViewModel)
+                        .isNotNull()
                 } else {
-                    assertThat(authMethodViewModel).isNull()
+                    assertWithMessage(
+                            "View-model unexpectedly non-null for auth method $authMethod"
+                        )
+                        .that(authMethodViewModel)
+                        .isNull()
                 }
+
+                job.cancel()
             }
         }
 
@@ -75,13 +88,13 @@
                 collectLastValue(underTest.authMethod)
             // First pass, populate our "seen" map:
             authMethodsToTest().forEach { authMethod ->
-                authenticationInteractor.setAuthenticationMethod(authMethod)
+                utils.authenticationRepository.setAuthenticationMethod(authMethod)
                 authMethodViewModel?.let { seen[authMethod] = it }
             }
 
             // Second pass, assert same instances are reused:
             authMethodsToTest().forEach { authMethod ->
-                authenticationInteractor.setAuthenticationMethod(authMethod)
+                utils.authenticationRepository.setAuthenticationMethod(authMethod)
                 authMethodViewModel?.let { assertThat(it).isSameInstanceAs(seen[authMethod]) }
             }
         }
@@ -97,7 +110,9 @@
         testScope.runTest {
             val message by collectLastValue(underTest.message)
             val throttling by collectLastValue(bouncerInteractor.throttling)
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
             assertThat(message?.isUpdateAnimated).isTrue()
 
             repeat(BouncerInteractor.THROTTLE_EVERY) {
@@ -120,7 +135,9 @@
                     }
                 )
             val throttling by collectLastValue(bouncerInteractor.throttling)
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
             assertThat(isInputEnabled).isTrue()
 
             repeat(BouncerInteractor.THROTTLE_EVERY) {
@@ -137,7 +154,9 @@
     fun throttlingDialogMessage() =
         testScope.runTest {
             val throttlingDialogMessage by collectLastValue(underTest.throttlingDialogMessage)
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
 
             repeat(BouncerInteractor.THROTTLE_EVERY) {
                 // Wrong PIN.
@@ -154,7 +173,7 @@
         return listOf(
             AuthenticationMethodModel.None,
             AuthenticationMethodModel.Swipe,
-            AuthenticationMethodModel.PIN(1234),
+            AuthenticationMethodModel.Pin(1234),
             AuthenticationMethodModel.Password("password"),
             AuthenticationMethodModel.Pattern(
                 listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt
new file mode 100644
index 0000000..8236165
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.bouncer.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.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
+import com.android.systemui.bouncer.ui.BouncerView
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.repository.TrustRepository
+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.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+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.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class KeyguardBouncerViewModelTest : SysuiTestCase() {
+    lateinit var underTest: KeyguardBouncerViewModel
+    lateinit var bouncerInteractor: PrimaryBouncerInteractor
+    @Mock 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())
+    val repository = FakeKeyguardBouncerRepository()
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        bouncerInteractor =
+            PrimaryBouncerInteractor(
+                repository,
+                bouncerView,
+                mainHandler,
+                keyguardStateController,
+                keyguardSecurityModel,
+                primaryBouncerCallbackInteractor,
+                falsingCollector,
+                dismissCallbackRegistry,
+                context,
+                keyguardUpdateMonitor,
+                Mockito.mock(TrustRepository::class.java),
+                FakeFeatureFlags().apply { set(Flags.DELAY_BOUNCER, true) },
+                TestScope().backgroundScope,
+            )
+        underTest = KeyguardBouncerViewModel(bouncerView, bouncerInteractor)
+    }
+
+    @Test
+    fun setMessage() = runTest {
+        var message: BouncerShowMessageModel? = null
+        val job = underTest.bouncerShowMessage.onEach { message = it }.launchIn(this)
+
+        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()
+    }
+
+    @Test
+    fun shouldUpdateSideFps_show() = runTest {
+        var count = 0
+        val job = underTest.shouldUpdateSideFps.onEach { count++ }.launchIn(this)
+        repository.setPrimaryShow(true)
+        // Run the tasks that are pending at this point of virtual time.
+        runCurrent()
+        assertThat(count).isEqualTo(1)
+        job.cancel()
+    }
+
+    @Test
+    fun shouldUpdateSideFps_hide() = runTest {
+        repository.setPrimaryShow(true)
+        var count = 0
+        val job = underTest.shouldUpdateSideFps.onEach { count++ }.launchIn(this)
+        repository.setPrimaryShow(false)
+        // 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()
+    }
+
+    @Test
+    fun isShowing() = runTest {
+        var isShowing: Boolean? = null
+        val job = underTest.isShowing.onEach { isShowing = it }.launchIn(this)
+        repository.setPrimaryShow(true)
+        // Run the tasks that are pending at this point of virtual time.
+        runCurrent()
+        assertThat(isShowing).isEqualTo(true)
+        job.cancel()
+    }
+
+    @Test
+    fun isNotShowing() = runTest {
+        var isShowing: Boolean? = null
+        val job = underTest.isShowing.onEach { isShowing = it }.launchIn(this)
+        repository.setPrimaryShow(false)
+        // Run the tasks that are pending at this point of virtual time.
+        runCurrent()
+        assertThat(isShowing).isEqualTo(false)
+        job.cancel()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index f436aa3..699571b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -28,7 +28,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -40,8 +40,8 @@
 @RunWith(JUnit4::class)
 class PasswordBouncerViewModelTest : SysuiTestCase() {
 
-    private val testScope = TestScope()
-    private val utils = SceneTestUtils(this, testScope)
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
     private val authenticationInteractor =
         utils.authenticationInteractor(
             repository = utils.authenticationRepository(),
@@ -58,6 +58,7 @@
         )
     private val underTest =
         PasswordBouncerViewModel(
+            applicationScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
         )
@@ -71,84 +72,74 @@
     @Test
     fun onShown() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Password("password")
             )
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             underTest.onShown()
 
             assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
             assertThat(password).isEqualTo("")
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
     fun onPasswordInputChanged() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Password("password")
             )
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
+            runCurrent()
 
             underTest.onPasswordInputChanged("password")
 
             assertThat(message?.text).isEmpty()
             assertThat(password).isEqualTo("password")
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
     fun onAuthenticateKeyPressed_whenCorrect() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Password("password")
             )
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onPasswordInputChanged("password")
 
             underTest.onAuthenticateKeyPressed()
 
-            assertThat(isUnlocked).isTrue()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
         }
 
     @Test
     fun onAuthenticateKeyPressed_whenWrong() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Password("password")
             )
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onPasswordInputChanged("wrong")
@@ -157,30 +148,26 @@
 
             assertThat(password).isEqualTo("")
             assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
     fun onAuthenticateKeyPressed_correctAfterWrong() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Password("password")
             )
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onPasswordInputChanged("wrong")
             underTest.onAuthenticateKeyPressed()
             assertThat(password).isEqualTo("")
             assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             // Enter the correct password:
@@ -189,7 +176,6 @@
 
             underTest.onAuthenticateKeyPressed()
 
-            assertThat(isUnlocked).isTrue()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index d7d7154..9a1f584 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -29,7 +29,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -41,8 +41,8 @@
 @RunWith(JUnit4::class)
 class PatternBouncerViewModelTest : SysuiTestCase() {
 
-    private val testScope = TestScope()
-    private val utils = SceneTestUtils(this, testScope)
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
     private val authenticationInteractor =
         utils.authenticationInteractor(
             repository = utils.authenticationRepository(),
@@ -74,17 +74,15 @@
     @Test
     fun onShown() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pattern(CORRECT_PATTERN)
             )
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             underTest.onShown()
@@ -92,49 +90,44 @@
             assertThat(message?.text).isEqualTo(ENTER_YOUR_PATTERN)
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
     fun onDragStart() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pattern(CORRECT_PATTERN)
             )
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
+            runCurrent()
 
             underTest.onDragStart()
 
             assertThat(message?.text).isEmpty()
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
     fun onDragEnd_whenCorrect() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pattern(CORRECT_PATTERN)
             )
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onDragStart()
@@ -168,24 +161,21 @@
 
             underTest.onDragEnd()
 
-            assertThat(isUnlocked).isTrue()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
         }
 
     @Test
     fun onDragEnd_whenWrong() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pattern(CORRECT_PATTERN)
             )
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onDragStart()
@@ -203,24 +193,21 @@
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
             assertThat(message?.text).isEqualTo(WRONG_PATTERN)
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
     fun onDragEnd_correctAfterWrong() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pattern(CORRECT_PATTERN)
             )
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onDragStart()
@@ -236,7 +223,6 @@
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
             assertThat(message?.text).isEqualTo(WRONG_PATTERN)
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             // Enter the correct pattern:
@@ -251,7 +237,6 @@
 
             underTest.onDragEnd()
 
-            assertThat(isUnlocked).isTrue()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 3bdaf05..608a187 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -29,8 +29,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -42,8 +41,8 @@
 @RunWith(JUnit4::class)
 class PinBouncerViewModelTest : SysuiTestCase() {
 
-    private val testScope = TestScope()
-    private val utils = SceneTestUtils(this, testScope)
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
     private val sceneInteractor = utils.sceneInteractor()
     private val authenticationInteractor =
         utils.authenticationInteractor(
@@ -68,6 +67,7 @@
         )
     private val underTest =
         PinBouncerViewModel(
+            applicationContext = context,
             applicationScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
@@ -82,109 +82,128 @@
     @Test
     fun onShown() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val message by collectLastValue(bouncerViewModel.message)
-            val pinLengths by collectLastValue(underTest.pinLengths)
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.lockDevice()
+            val entries by collectLastValue(underTest.pinEntries)
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             underTest.onShown()
 
             assertThat(message?.text).isEqualTo(ENTER_YOUR_PIN)
-            assertThat(pinLengths).isEqualTo(0 to 0)
-            assertThat(isUnlocked).isFalse()
+            assertThat(entries).hasSize(0)
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
     fun onPinButtonClicked() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val message by collectLastValue(bouncerViewModel.message)
-            val pinLengths by collectLastValue(underTest.pinLengths)
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.lockDevice()
+            val entries by collectLastValue(underTest.pinEntries)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
+            runCurrent()
 
             underTest.onPinButtonClicked(1)
 
             assertThat(message?.text).isEmpty()
-            assertThat(pinLengths).isEqualTo(0 to 1)
-            assertThat(isUnlocked).isFalse()
+            assertThat(entries).hasSize(1)
+            assertThat(entries?.map { it.input }).containsExactly(1)
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
     fun onBackspaceButtonClicked() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val message by collectLastValue(bouncerViewModel.message)
-            val pinLengths by collectLastValue(underTest.pinLengths)
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.lockDevice()
+            val entries by collectLastValue(underTest.pinEntries)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
+            runCurrent()
             underTest.onPinButtonClicked(1)
-            assertThat(pinLengths).isEqualTo(0 to 1)
+            assertThat(entries).hasSize(1)
 
             underTest.onBackspaceButtonClicked()
 
             assertThat(message?.text).isEmpty()
-            assertThat(pinLengths).isEqualTo(1 to 0)
-            assertThat(isUnlocked).isFalse()
+            assertThat(entries).hasSize(0)
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
+    fun onPinEdit() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            val entries by collectLastValue(underTest.pinEntries)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(false)
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            underTest.onShown()
+
+            underTest.onPinButtonClicked(1)
+            underTest.onPinButtonClicked(2)
+            underTest.onPinButtonClicked(3)
+            underTest.onBackspaceButtonClicked()
+            underTest.onBackspaceButtonClicked()
+            underTest.onPinButtonClicked(4)
+            underTest.onPinButtonClicked(5)
+
+            assertThat(entries).hasSize(3)
+            assertThat(entries?.map { it.input }).containsExactly(1, 4, 5).inOrder()
+            assertThat(entries?.map { it.sequenceNumber }).isInStrictOrder()
+        }
+
+    @Test
     fun onBackspaceButtonLongPressed() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val message by collectLastValue(bouncerViewModel.message)
-            val pinLengths by collectLastValue(underTest.pinLengths)
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.lockDevice()
+            val entries by collectLastValue(underTest.pinEntries)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
+            runCurrent()
             underTest.onPinButtonClicked(1)
             underTest.onPinButtonClicked(2)
             underTest.onPinButtonClicked(3)
             underTest.onPinButtonClicked(4)
 
             underTest.onBackspaceButtonLongPressed()
-            repeat(4) { index ->
-                assertThat(pinLengths).isEqualTo(4 - index to 3 - index)
-                advanceTimeBy(PinBouncerViewModel.BACKSPACE_LONG_PRESS_DELAY_MS)
-            }
 
             assertThat(message?.text).isEmpty()
-            assertThat(pinLengths).isEqualTo(1 to 0)
-            assertThat(isUnlocked).isFalse()
+            assertThat(entries).hasSize(0)
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
     fun onAuthenticateButtonClicked_whenCorrect() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onPinButtonClicked(1)
@@ -194,21 +213,20 @@
 
             underTest.onAuthenticateButtonClicked()
 
-            assertThat(isUnlocked).isTrue()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
         }
 
     @Test
     fun onAuthenticateButtonClicked_whenWrong() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val message by collectLastValue(bouncerViewModel.message)
-            val pinLengths by collectLastValue(underTest.pinLengths)
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.lockDevice()
+            val entries by collectLastValue(underTest.pinEntries)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onPinButtonClicked(1)
@@ -219,23 +237,22 @@
 
             underTest.onAuthenticateButtonClicked()
 
-            assertThat(pinLengths).isEqualTo(0 to 0)
+            assertThat(entries).hasSize(0)
             assertThat(message?.text).isEqualTo(WRONG_PIN)
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
     fun onAuthenticateButtonClicked_correctAfterWrong() =
         testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
             val message by collectLastValue(bouncerViewModel.message)
-            val pinLengths by collectLastValue(underTest.pinLengths)
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.lockDevice()
+            val entries by collectLastValue(underTest.pinEntries)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(false)
             sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
-            assertThat(isUnlocked).isFalse()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             underTest.onShown()
             underTest.onPinButtonClicked(1)
@@ -245,8 +262,7 @@
             underTest.onPinButtonClicked(5) // PIN is now wrong!
             underTest.onAuthenticateButtonClicked()
             assertThat(message?.text).isEqualTo(WRONG_PIN)
-            assertThat(pinLengths).isEqualTo(0 to 0)
-            assertThat(isUnlocked).isFalse()
+            assertThat(entries).hasSize(0)
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             // Enter the correct PIN:
@@ -258,10 +274,154 @@
 
             underTest.onAuthenticateButtonClicked()
 
-            assertThat(isUnlocked).isTrue()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
         }
 
+    @Test
+    fun onAutoConfirm_whenCorrect() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = true)
+            )
+            utils.authenticationRepository.setUnlocked(false)
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            underTest.onShown()
+            underTest.onPinButtonClicked(1)
+            underTest.onPinButtonClicked(2)
+            underTest.onPinButtonClicked(3)
+            underTest.onPinButtonClicked(4)
+
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
+        }
+
+    @Test
+    fun onAutoConfirm_whenWrong() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
+            val message by collectLastValue(bouncerViewModel.message)
+            val entries by collectLastValue(underTest.pinEntries)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = true)
+            )
+            utils.authenticationRepository.setUnlocked(false)
+            sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            underTest.onShown()
+            underTest.onPinButtonClicked(1)
+            underTest.onPinButtonClicked(2)
+            underTest.onPinButtonClicked(3)
+            underTest.onPinButtonClicked(5) // PIN is now wrong!
+
+            assertThat(entries).hasSize(0)
+            assertThat(message?.text).isEqualTo(WRONG_PIN)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+        }
+
+    @Test
+    fun backspaceButtonAppearance_withoutAutoConfirm_alwaysShown() =
+        testScope.runTest {
+            val backspaceButtonAppearance by collectLastValue(underTest.backspaceButtonAppearance)
+
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = false)
+            )
+
+            assertThat(backspaceButtonAppearance).isEqualTo(ActionButtonAppearance.Shown)
+        }
+
+    @Test
+    fun backspaceButtonAppearance_withAutoConfirmButNoInput_isHidden() =
+        testScope.runTest {
+            val backspaceButtonAppearance by collectLastValue(underTest.backspaceButtonAppearance)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = true)
+            )
+
+            assertThat(backspaceButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden)
+        }
+
+    @Test
+    fun backspaceButtonAppearance_withAutoConfirmAndInput_isShownQuiet() =
+        testScope.runTest {
+            val backspaceButtonAppearance by collectLastValue(underTest.backspaceButtonAppearance)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = true)
+            )
+
+            underTest.onPinButtonClicked(1)
+
+            assertThat(backspaceButtonAppearance).isEqualTo(ActionButtonAppearance.Subtle)
+        }
+
+    @Test
+    fun confirmButtonAppearance_withoutAutoConfirm_alwaysShown() =
+        testScope.runTest {
+            val confirmButtonAppearance by collectLastValue(underTest.confirmButtonAppearance)
+
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = false)
+            )
+
+            assertThat(confirmButtonAppearance).isEqualTo(ActionButtonAppearance.Shown)
+        }
+
+    @Test
+    fun confirmButtonAppearance_withAutoConfirm_isHidden() =
+        testScope.runTest {
+            val confirmButtonAppearance by collectLastValue(underTest.confirmButtonAppearance)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = true)
+            )
+
+            assertThat(confirmButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden)
+        }
+
+    @Test
+    fun hintedPinLength_withoutAutoConfirm_isNull() =
+        testScope.runTest {
+            val hintedPinLength by collectLastValue(underTest.hintedPinLength)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234, autoConfirm = false)
+            )
+
+            assertThat(hintedPinLength).isNull()
+        }
+
+    @Test
+    fun hintedPinLength_withAutoConfirmPinLessThanSixDigits_isNull() =
+        testScope.runTest {
+            val hintedPinLength by collectLastValue(underTest.hintedPinLength)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(12345, autoConfirm = true)
+            )
+
+            assertThat(hintedPinLength).isNull()
+        }
+
+    @Test
+    fun hintedPinLength_withAutoConfirmPinExactlySixDigits_isSix() =
+        testScope.runTest {
+            val hintedPinLength by collectLastValue(underTest.hintedPinLength)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(123456, autoConfirm = true)
+            )
+
+            assertThat(hintedPinLength).isEqualTo(6)
+        }
+
+    @Test
+    fun hintedPinLength_withAutoConfirmPinMoreThanSixDigits_isNull() =
+        testScope.runTest {
+            val hintedPinLength by collectLastValue(underTest.hintedPinLength)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234567, autoConfirm = true)
+            )
+
+            assertThat(hintedPinLength).isNull()
+        }
+
     companion object {
         private const val CONTAINER_NAME = "container1"
         private const val ENTER_YOUR_PIN = "Enter your pin"
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 292fdff..f770a38 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java
@@ -187,4 +187,11 @@
         when(mFalsingDataProvider.isUnfolded()).thenReturn(true);
         assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse();
     }
+
+    @Test
+    public void testTrackpadGesture() {
+        assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue();
+        when(mFalsingDataProvider.isFromTrackpad()).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 9254617..4da151c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java
@@ -18,8 +18,11 @@
 
 import android.hardware.devicestate.DeviceStateManager.FoldStateListener;
 import android.util.DisplayMetrics;
+import android.view.InputDevice;
 import android.view.MotionEvent;
 
+import androidx.test.uiautomator.Configurator;
+
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dock.DockManagerFake;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -80,6 +83,10 @@
         mDataProvider.onSessionEnd();
     }
 
+    protected static int getPointerAction(int actionType, int index) {
+        return actionType + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
+    }
+
     protected MotionEvent appendDownEvent(float x, float y) {
         return appendMotionEvent(MotionEvent.ACTION_DOWN, x, y);
     }
@@ -124,4 +131,55 @@
 
         return motionEvent;
     }
+
+    protected MotionEvent appendTrackpadDownEvent(float x, float y) {
+        return appendTrackpadMotionEvent(MotionEvent.ACTION_DOWN, x, y, 1);
+    }
+
+    protected MotionEvent appendTrackpadMoveEvent(float x, float y, int pointerCount) {
+        return appendTrackpadMotionEvent(MotionEvent.ACTION_MOVE, x, y, pointerCount);
+    }
+
+    protected MotionEvent appendTrackpadPointerDownEvent(int actionType, float x, float y,
+            int pointerCount) {
+        return appendTrackpadMotionEvent(actionType, x, y, pointerCount);
+    }
+
+    private MotionEvent appendTrackpadMotionEvent(int actionType, float x, float y,
+            int pointerCount) {
+        long eventTime = mMotionEvents.isEmpty() ? 1 : mMotionEvents.get(
+                mMotionEvents.size() - 1).getEventTime() + 1;
+        return appendTrackpadMotionEvent(actionType, x, y, pointerCount, eventTime);
+    }
+
+    private MotionEvent appendTrackpadMotionEvent(int actionType, float x, float y,
+            int pointerCount, long eventTime) {
+        MotionEvent.PointerProperties[] pointerProperties =
+                new MotionEvent.PointerProperties[pointerCount];
+        MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
+        for (int i = 0; i < pointerCount; i++) {
+            pointerProperties[i] = getPointerProperties(i);
+            pointerCoords[i] = getPointerCoords(x, y);
+        }
+        return MotionEvent.obtain(1, eventTime, actionType, pointerCount, pointerProperties,
+                pointerCoords, 0, 0, 1.0f, 1.0f, 0, 0,
+                InputDevice.SOURCE_TOUCHPAD | InputDevice.SOURCE_MOUSE, 0, 0,
+                MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE);
+    }
+
+    private static MotionEvent.PointerProperties getPointerProperties(int pointerId) {
+        MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
+        properties.id = pointerId;
+        properties.toolType = Configurator.getInstance().getToolType();
+        return properties;
+    }
+
+    private static MotionEvent.PointerCoords getPointerCoords(float x, float y) {
+        MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+        coords.pressure = 1;
+        coords.size = 1;
+        coords.x = x;
+        coords.y = y;
+        return coords;
+    }
 }
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 7e06680..0353f87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java
@@ -282,6 +282,22 @@
     }
 
     @Test
+    public void test_IsFromTrackpad() {
+        MotionEvent motionEventOrigin = appendTrackpadDownEvent(0, 0);
+
+        mDataProvider.onMotionEvent(motionEventOrigin);
+        mDataProvider.onMotionEvent(
+                appendTrackpadPointerDownEvent(getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 1),
+                        0, 0, 2));
+        mDataProvider.onMotionEvent(
+                appendTrackpadPointerDownEvent(getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 2),
+                        0, 0, 3));
+        mDataProvider.onMotionEvent(appendTrackpadMoveEvent(1, -1, 3));
+        assertThat(mDataProvider.isFromTrackpad()).isTrue();
+        mDataProvider.onSessionEnd();
+    }
+
+    @Test
     public void test_isWirelessCharging() {
         assertThat(mDataProvider.isDocked()).isFalse();
 
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 39fb7b4..9671966 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -26,6 +26,7 @@
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_MINIMIZED;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
 import static com.android.systemui.flags.Flags.CLIPBOARD_IMAGE_TIMEOUT;
+import static com.android.systemui.flags.Flags.CLIPBOARD_SHARED_TRANSITIONS;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -93,12 +94,16 @@
     @Mock
     private ClipboardImageLoader mClipboardImageLoader;
     @Mock
+    private ClipboardTransitionExecutor mClipboardTransitionExecutor;
+    @Mock
     private UiEventLogger mUiEventLogger;
     private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
     private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
 
     @Mock
     private Animator mAnimator;
+    private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor =
+            ArgumentCaptor.forClass(Animator.AnimatorListener.class);
 
     private ClipData mSampleClipData;
 
@@ -117,6 +122,7 @@
 
         when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator);
         when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator);
+        when(mClipboardOverlayView.getFadeOutAnimation()).thenReturn(mAnimator);
         when(mClipboardOverlayWindow.getWindowInsets()).thenReturn(
                 getImeInsets(new Rect(0, 0, 0, 0)));
 
@@ -124,7 +130,16 @@
                 new ClipData.Item("Test Item"));
 
         mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, true); // turned off for legacy tests
+        mFeatureFlags.set(CLIPBOARD_SHARED_TRANSITIONS, true); // turned off for old tests
+    }
 
+    /**
+     * Needs to be done after setting flags for legacy tests, since the value of
+     * CLIPBOARD_SHARED_TRANSITIONS is checked during construction. This can be moved back into
+     * the setup method once CLIPBOARD_SHARED_TRANSITIONS is fully released and the tests where it
+     * is false are removed.[
+     */
+    private void initController() {
         mOverlayController = new ClipboardOverlayController(
                 mContext,
                 mClipboardOverlayView,
@@ -136,6 +151,7 @@
                 mClipboardUtils,
                 mExecutor,
                 mClipboardImageLoader,
+                mClipboardTransitionExecutor,
                 mUiEventLogger);
         verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture());
         mCallbacks = mOverlayCallbacksCaptor.getValue();
@@ -148,6 +164,8 @@
 
     @Test
     public void test_setClipData_invalidImageData_legacy() {
+        initController();
+
         ClipData clipData = new ClipData("", new String[]{"image/png"},
                 new ClipData.Item(Uri.parse("")));
         mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
@@ -161,6 +179,8 @@
 
     @Test
     public void test_setClipData_nonImageUri_legacy() {
+        initController();
+
         ClipData clipData = new ClipData("", new String[]{"resource/png"},
                 new ClipData.Item(Uri.parse("")));
         mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
@@ -175,6 +195,8 @@
     @Test
     public void test_setClipData_textData_legacy() {
         mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
+        initController();
+
         mOverlayController.setClipData(mSampleClipData, "abc");
 
         verify(mClipboardOverlayView, times(1)).showTextPreview("Test Item", false);
@@ -186,6 +208,8 @@
     @Test
     public void test_setClipData_sensitiveTextData_legacy() {
         mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
+        initController();
+
         ClipDescription description = mSampleClipData.getDescription();
         PersistableBundle b = new PersistableBundle();
         b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true);
@@ -202,6 +226,7 @@
     public void test_setClipData_repeatedCalls_legacy() {
         when(mAnimator.isRunning()).thenReturn(true);
         mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
+        initController();
 
         mOverlayController.setClipData(mSampleClipData, "");
         mOverlayController.setClipData(mSampleClipData, "");
@@ -211,6 +236,8 @@
 
     @Test
     public void test_setClipData_invalidImageData() {
+        initController();
+
         ClipData clipData = new ClipData("", new String[]{"image/png"},
                 new ClipData.Item(Uri.parse("")));
 
@@ -223,6 +250,7 @@
 
     @Test
     public void test_setClipData_nonImageUri() {
+        initController();
         ClipData clipData = new ClipData("", new String[]{"resource/png"},
                 new ClipData.Item(Uri.parse("")));
 
@@ -235,6 +263,7 @@
 
     @Test
     public void test_setClipData_textData() {
+        initController();
         mOverlayController.setClipData(mSampleClipData, "abc");
 
         verify(mClipboardOverlayView, times(1)).showTextPreview("Test Item", false);
@@ -245,6 +274,7 @@
 
     @Test
     public void test_setClipData_sensitiveTextData() {
+        initController();
         ClipDescription description = mSampleClipData.getDescription();
         PersistableBundle b = new PersistableBundle();
         b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true);
@@ -259,6 +289,7 @@
 
     @Test
     public void test_setClipData_repeatedCalls() {
+        initController();
         when(mAnimator.isRunning()).thenReturn(true);
 
         mOverlayController.setClipData(mSampleClipData, "");
@@ -268,7 +299,9 @@
     }
 
     @Test
-    public void test_viewCallbacks_onShareTapped() {
+    public void test_viewCallbacks_onShareTapped_sharedTransitionsOff() {
+        mFeatureFlags.set(CLIPBOARD_SHARED_TRANSITIONS, false);
+        initController();
         mOverlayController.setClipData(mSampleClipData, "");
 
         mCallbacks.onShareButtonTapped();
@@ -278,7 +311,22 @@
     }
 
     @Test
-    public void test_viewCallbacks_onDismissTapped() {
+    public void test_viewCallbacks_onShareTapped() {
+        initController();
+        mOverlayController.setClipData(mSampleClipData, "");
+
+        mCallbacks.onShareButtonTapped();
+        verify(mAnimator).addListener(mAnimatorListenerCaptor.capture());
+        mAnimatorListenerCaptor.getValue().onAnimationEnd(mAnimator);
+
+        verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "");
+        verify(mClipboardOverlayView, times(1)).getFadeOutAnimation();
+    }
+
+    @Test
+    public void test_viewCallbacks_onDismissTapped_sharedTransitionsOff() {
+        mFeatureFlags.set(CLIPBOARD_SHARED_TRANSITIONS, false);
+        initController();
         mOverlayController.setClipData(mSampleClipData, "");
 
         mCallbacks.onDismissButtonTapped();
@@ -288,7 +336,35 @@
     }
 
     @Test
+    public void test_viewCallbacks_onDismissTapped() {
+        initController();
+
+        mCallbacks.onDismissButtonTapped();
+        verify(mAnimator).addListener(mAnimatorListenerCaptor.capture());
+        mAnimatorListenerCaptor.getValue().onAnimationEnd(mAnimator);
+
+        // package name is null since we haven't actually set a source for this test
+        verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, null);
+        verify(mClipboardOverlayView, times(1)).getExitAnimation();
+    }
+
+    @Test
+    public void test_multipleDismissals_dismissesOnce_sharedTransitionsOff() {
+        mFeatureFlags.set(CLIPBOARD_SHARED_TRANSITIONS, false);
+        initController();
+        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_multipleDismissals_dismissesOnce() {
+        initController();
+
         mCallbacks.onSwipeDismissInitiated(mAnimator);
         mCallbacks.onDismissButtonTapped();
         mCallbacks.onSwipeDismissInitiated(mAnimator);
@@ -300,6 +376,7 @@
 
     @Test
     public void test_remoteCopy_withFlagOn() {
+        initController();
         when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true);
 
         mOverlayController.setClipData(mSampleClipData, "");
@@ -309,6 +386,7 @@
 
     @Test
     public void test_nonRemoteCopy() {
+        initController();
         when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(false);
 
         mOverlayController.setClipData(mSampleClipData, "");
@@ -318,13 +396,16 @@
 
     @Test
     public void test_logsUseLastClipSource() {
-        mOverlayController.setClipData(mSampleClipData, "first.package");
-        mCallbacks.onDismissButtonTapped();
-        mOverlayController.setClipData(mSampleClipData, "second.package");
-        mCallbacks.onDismissButtonTapped();
+        initController();
 
-        verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, "first.package");
-        verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, "second.package");
+        mOverlayController.setClipData(mSampleClipData, "first.package");
+        mCallbacks.onShareButtonTapped();
+
+        mOverlayController.setClipData(mSampleClipData, "second.package");
+        mCallbacks.onShareButtonTapped();
+
+        verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "first.package");
+        verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "second.package");
         verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "first.package");
         verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "second.package");
         verifyNoMoreInteractions(mUiEventLogger);
@@ -332,6 +413,7 @@
 
     @Test
     public void test_logOnClipboardActionsShown() {
+        initController();
         ClipData.Item item = mSampleClipData.getItemAt(0);
         item.setTextLinks(Mockito.mock(TextLinks.class));
         when(mClipboardUtils.isRemoteCopy(any(Context.class), any(ClipData.class), anyString()))
@@ -357,6 +439,7 @@
 
     @Test
     public void test_noInsets_showsExpanded() {
+        initController();
         mOverlayController.setClipData(mSampleClipData, "");
 
         verify(mClipboardOverlayView, never()).setMinimized(true);
@@ -366,6 +449,7 @@
 
     @Test
     public void test_insets_showsMinimized() {
+        initController();
         when(mClipboardOverlayWindow.getWindowInsets()).thenReturn(
                 getImeInsets(new Rect(0, 0, 0, 1)));
         mOverlayController.setClipData(mSampleClipData, "abc");
@@ -389,6 +473,7 @@
 
     @Test
     public void test_insetsChanged_minimizes() {
+        initController();
         mOverlayController.setClipData(mSampleClipData, "");
         verify(mClipboardOverlayView, never()).setMinimized(true);
 
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
index afd9be5..f0006e5 100644
--- 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
@@ -18,6 +18,13 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.ViewGroup;
@@ -28,10 +35,15 @@
 
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView.OnSeekBarWithIconButtonsChangeListener;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
 
 /**
  * Tests for {@link SeekBarWithIconButtonsView}
@@ -48,14 +60,22 @@
     private SeekBar mSeekbar;
     private SeekBarWithIconButtonsView mIconDiscreteSliderLinearLayout;
 
+    @Mock
+    private OnSeekBarWithIconButtonsChangeListener mOnSeekBarChangeListener;
+
     @Before
     public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
         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);
+
+        mIconDiscreteSliderLinearLayout.setOnSeekBarWithIconButtonsChangeListener(
+                mOnSeekBarChangeListener);
     }
 
     @Test
@@ -109,6 +129,49 @@
     }
 
     @Test
+    public void setProgress_onlyOnProgressChangedTriggeredWithFromUserFalse() {
+        reset(mOnSeekBarChangeListener);
+        mIconDiscreteSliderLinearLayout.setProgress(1);
+
+        verify(mOnSeekBarChangeListener).onProgressChanged(
+                eq(mSeekbar), /* progress= */ eq(1), /* fromUser= */ eq(false));
+        verify(mOnSeekBarChangeListener, never()).onStartTrackingTouch(/* seekBar= */ any());
+        verify(mOnSeekBarChangeListener, never()).onStopTrackingTouch(/* seekBar= */ any());
+        verify(mOnSeekBarChangeListener, never()).onUserInteractionFinalized(
+                /* seekBar= */any(), /* control= */ anyInt());
+    }
+
+    @Test
+    public void clickIconEnd_triggerCallbacksInSequence() {
+        final int magnitude = mIconDiscreteSliderLinearLayout.getChangeMagnitude();
+        mIconDiscreteSliderLinearLayout.setProgress(0);
+        reset(mOnSeekBarChangeListener);
+
+        mIconEndFrame.performClick();
+
+        InOrder inOrder = Mockito.inOrder(mOnSeekBarChangeListener);
+        inOrder.verify(mOnSeekBarChangeListener).onProgressChanged(
+                eq(mSeekbar), /* progress= */ eq(magnitude), /* fromUser= */ eq(true));
+        inOrder.verify(mOnSeekBarChangeListener).onUserInteractionFinalized(
+                eq(mSeekbar), eq(OnSeekBarWithIconButtonsChangeListener.ControlUnitType.BUTTON));
+    }
+
+    @Test
+    public void clickIconStart_triggerCallbacksInSequence() {
+        final int magnitude = mIconDiscreteSliderLinearLayout.getChangeMagnitude();
+        mIconDiscreteSliderLinearLayout.setProgress(magnitude);
+        reset(mOnSeekBarChangeListener);
+
+        mIconStartFrame.performClick();
+
+        InOrder inOrder = Mockito.inOrder(mOnSeekBarChangeListener);
+        inOrder.verify(mOnSeekBarChangeListener).onProgressChanged(
+                eq(mSeekbar), /* progress= */ eq(0), /* fromUser= */ eq(true));
+        inOrder.verify(mOnSeekBarChangeListener).onUserInteractionFinalized(
+                eq(mSeekbar), eq(OnSeekBarWithIconButtonsChangeListener.ControlUnitType.BUTTON));
+    }
+
+    @Test
     public void setProgressStateLabels_getExpectedStateDescriptionOnInitialization() {
         String[] stateLabels = new String[]{"1", "2", "3", "4", "5"};
         mIconDiscreteSliderLinearLayout.setMax(stateLabels.length);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java
index ca6282c..461ec65 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java
@@ -28,6 +28,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dreams.DreamLogger;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.Flags;
@@ -56,6 +57,8 @@
     private FakeFeatureFlags mFeatureFlags;
     @Mock
     private Observer mObserver;
+    @Mock
+    private DreamLogger mLogger;
 
     @Before
     public void setUp() {
@@ -66,7 +69,8 @@
         mStateController = new DreamOverlayStateController(
                 mExecutor,
                 /* overlayEnabled= */ true,
-                mFeatureFlags);
+                mFeatureFlags,
+                mLogger);
         mLiveData = new ComplicationCollectionLiveData(mStateController);
     }
 
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 26cbd77..724c9d1 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
@@ -116,11 +116,7 @@
 
     @Test
     fun testBindAndLoad_cancel() {
-        val callback = object : ControlsBindingController.LoadCallback {
-            override fun error(message: String) {}
-
-            override fun accept(t: List<Control>) {}
-        }
+        val callback = mock(ControlsBindingController.LoadCallback::class.java)
         val subscription = mock(IControlsSubscription::class.java)
 
         val canceller = controller.bindAndLoad(TEST_COMPONENT_NAME_1, callback)
@@ -130,6 +126,7 @@
 
         canceller.run()
         verify(providers[0]).cancelSubscription(subscription)
+        verify(callback).error(any())
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt
index 4210675..71d2ec1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt
@@ -11,9 +11,9 @@
 import android.window.OnBackInvokedDispatcher
 import androidx.test.filters.SmallTest
 import androidx.test.rule.ActivityTestRule
-import androidx.test.runner.intercepting.SingleActivityFactory
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.activity.SingleActivityFactory
 import com.android.systemui.controls.CustomIconCache
 import com.android.systemui.controls.controller.ControlsControllerImpl
 import com.android.systemui.flags.FakeFeatureFlags
@@ -63,24 +63,19 @@
     @JvmField
     var activityRule =
         ActivityTestRule(
-            object :
-                SingleActivityFactory<TestableControlsEditingActivity>(
-                    TestableControlsEditingActivity::class.java
-                ) {
-                override fun create(intent: Intent?): TestableControlsEditingActivity {
-                    return TestableControlsEditingActivity(
-                        featureFlags,
-                        uiExecutor,
-                        controller,
-                        userTracker,
-                        customIconCache,
-                        mockDispatcher,
-                        latch
-                    )
-                }
+            /* activityFactory= */ SingleActivityFactory {
+                TestableControlsEditingActivity(
+                    featureFlags,
+                    uiExecutor,
+                    controller,
+                    userTracker,
+                    customIconCache,
+                    mockDispatcher,
+                    latch
+                )
             },
-            false,
-            false
+            /* initialTouchMode= */ false,
+            /* launchActivity= */ false,
         )
 
     @Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt
index f4cc8bc..f11c296 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt
@@ -13,9 +13,9 @@
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.SmallTest
 import androidx.test.rule.ActivityTestRule
-import androidx.test.runner.intercepting.SingleActivityFactory
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.activity.SingleActivityFactory
 import com.android.systemui.controls.ControlStatus
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.controller.ControlsController
@@ -91,24 +91,19 @@
     @JvmField
     var activityRule =
         ActivityTestRule(
-            object :
-                SingleActivityFactory<TestableControlsFavoritingActivity>(
-                    TestableControlsFavoritingActivity::class.java
-                ) {
-                override fun create(intent: Intent?): TestableControlsFavoritingActivity {
-                    return TestableControlsFavoritingActivity(
-                        featureFlags,
-                        executor,
-                        controller,
-                        listingController,
-                        userTracker,
-                        mockDispatcher,
-                        latch
-                    )
-                }
+            /* activityFactory= */ SingleActivityFactory {
+                TestableControlsFavoritingActivity(
+                    featureFlags,
+                    executor,
+                    controller,
+                    listingController,
+                    userTracker,
+                    mockDispatcher,
+                    latch
+                )
             },
-            false,
-            false
+            /* initialTouchMode= */ false,
+            /* launchActivity= */ false,
         )
 
     @Before
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 4ba6718..d17495f 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
@@ -29,8 +29,8 @@
 import android.window.OnBackInvokedDispatcher
 import androidx.test.filters.SmallTest
 import androidx.test.rule.ActivityTestRule
-import androidx.test.runner.intercepting.SingleActivityFactory
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.activity.SingleActivityFactory
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.controller.ControlsController
 import com.android.systemui.controls.panels.AuthorizedPanelsRepository
@@ -91,26 +91,21 @@
     @JvmField
     var activityRule =
         ActivityTestRule(
-            object :
-                SingleActivityFactory<TestableControlsProviderSelectorActivity>(
-                    TestableControlsProviderSelectorActivity::class.java
-                ) {
-                override fun create(intent: Intent?): TestableControlsProviderSelectorActivity {
-                    return TestableControlsProviderSelectorActivity(
-                        executor,
-                        backExecutor,
-                        listingController,
-                        controlsController,
-                        userTracker,
-                        authorizedPanelsRepository,
-                        dialogFactory,
-                        mockDispatcher,
-                        latch
-                    )
-                }
+            /* activityFactory= */ SingleActivityFactory {
+                TestableControlsProviderSelectorActivity(
+                    executor,
+                    backExecutor,
+                    listingController,
+                    controlsController,
+                    userTracker,
+                    authorizedPanelsRepository,
+                    dialogFactory,
+                    mockDispatcher,
+                    latch
+                )
             },
-            false,
-            false
+            /* initialTouchMode= */ false,
+            /* launchActivity= */ false,
         )
 
     @Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestDialogTest.kt
index 314b176..ca970bb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestDialogTest.kt
@@ -30,8 +30,8 @@
 import androidx.lifecycle.Lifecycle
 import androidx.test.filters.MediumTest
 import androidx.test.rule.ActivityTestRule
-import androidx.test.runner.intercepting.SingleActivityFactory
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.activity.SingleActivityFactory
 import com.android.systemui.controls.controller.ControlInfo
 import com.android.systemui.controls.controller.ControlsController
 import com.android.systemui.settings.UserTracker
@@ -81,19 +81,18 @@
 
     @Rule
     @JvmField
-    var activityRule = ActivityTestRule<TestControlsRequestDialog>(
-            object : SingleActivityFactory<TestControlsRequestDialog>(
-                    TestControlsRequestDialog::class.java
-            ) {
-                    override fun create(intent: Intent?): TestControlsRequestDialog {
-                        return TestControlsRequestDialog(
-                                mainExecutor,
-                                controller,
-                                userTracker,
-                                listingController
-                        )
-                    }
-            }, false, false)
+    var activityRule = ActivityTestRule(
+        /* activityFactory= */ SingleActivityFactory {
+            TestControlsRequestDialog(
+                    mainExecutor,
+                    controller,
+                    userTracker,
+                    listingController
+            )
+        },
+        /* initialTouchMode= */ false,
+        /* launchActivity= */ false,
+    )
 
     private lateinit var control: Control
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsActivityTest.kt
index 2d3e10e..e279d28 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsActivityTest.kt
@@ -23,8 +23,8 @@
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import androidx.test.rule.ActivityTestRule
-import androidx.test.runner.intercepting.SingleActivityFactory
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.activity.SingleActivityFactory
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.controls.settings.ControlsSettingsDialogManager
 import com.android.systemui.flags.FeatureFlags
@@ -53,23 +53,18 @@
     @JvmField
     var activityRule =
         ActivityTestRule(
-            object :
-                SingleActivityFactory<TestableControlsActivity>(
-                    TestableControlsActivity::class.java
-                ) {
-                override fun create(intent: Intent?): TestableControlsActivity {
-                    return TestableControlsActivity(
-                        uiController,
-                        broadcastDispatcher,
-                        dreamManager,
-                        featureFlags,
-                        controlsSettingsDialogManager,
-                        keyguardStateController,
-                    )
-                }
+            /* activityFactory= */ SingleActivityFactory {
+                TestableControlsActivity(
+                    uiController,
+                    broadcastDispatcher,
+                    dreamManager,
+                    featureFlags,
+                    controlsSettingsDialogManager,
+                    keyguardStateController,
+                )
             },
-            false,
-            false
+            /* initialTouchMode= */ false,
+            /* launchActivity= */ false,
         )
 
     @Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerDecorProviderFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerDecorProviderFactoryTest.kt
index fcc3589..8f0b193 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerDecorProviderFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerDecorProviderFactoryTest.kt
@@ -39,7 +39,7 @@
 
     @Before
     fun setUp() {
-        roundedCornerResDelegate = spy(RoundedCornerResDelegate(mContext.resources, null))
+        roundedCornerResDelegate = spy(RoundedCornerResDelegateImpl(mContext.resources, null))
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt
index 93a1868..4feba7b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt
@@ -48,7 +48,7 @@
     @Test
     fun testTopAndBottomRoundedCornerExist() {
         setupResources(radius = 5)
-        roundedCornerResDelegate = RoundedCornerResDelegate(mContext.resources, null)
+        roundedCornerResDelegate = RoundedCornerResDelegateImpl(mContext.resources, null)
         assertEquals(true, roundedCornerResDelegate.hasTop)
         assertEquals(true, roundedCornerResDelegate.hasBottom)
     }
@@ -56,7 +56,7 @@
     @Test
     fun testTopRoundedCornerExist() {
         setupResources(radiusTop = 10)
-        roundedCornerResDelegate = RoundedCornerResDelegate(mContext.resources, null)
+        roundedCornerResDelegate = RoundedCornerResDelegateImpl(mContext.resources, null)
         assertEquals(true, roundedCornerResDelegate.hasTop)
         assertEquals(false, roundedCornerResDelegate.hasBottom)
     }
@@ -64,7 +64,7 @@
     @Test
     fun testBottomRoundedCornerExist() {
         setupResources(radiusBottom = 15)
-        roundedCornerResDelegate = RoundedCornerResDelegate(mContext.resources, null)
+        roundedCornerResDelegate = RoundedCornerResDelegateImpl(mContext.resources, null)
         assertEquals(false, roundedCornerResDelegate.hasTop)
         assertEquals(true, roundedCornerResDelegate.hasBottom)
     }
@@ -75,7 +75,7 @@
                 roundedTopDrawable = getTestsDrawable(R.drawable.rounded3px),
                 roundedBottomDrawable = getTestsDrawable(R.drawable.rounded4px))
 
-        roundedCornerResDelegate = RoundedCornerResDelegate(mContext.resources, null)
+        roundedCornerResDelegate = RoundedCornerResDelegateImpl(mContext.resources, null)
 
         assertEquals(Size(3, 3), roundedCornerResDelegate.topRoundedSize)
         assertEquals(Size(4, 4), roundedCornerResDelegate.bottomRoundedSize)
@@ -96,7 +96,7 @@
                 roundedTopDrawable = getTestsDrawable(R.drawable.rounded3px),
                 roundedBottomDrawable = getTestsDrawable(R.drawable.rounded4px))
 
-        roundedCornerResDelegate = RoundedCornerResDelegate(mContext.resources, null)
+        roundedCornerResDelegate = RoundedCornerResDelegateImpl(mContext.resources, null)
 
         assertEquals(Size(3, 3), roundedCornerResDelegate.topRoundedSize)
         assertEquals(Size(4, 4), roundedCornerResDelegate.bottomRoundedSize)
@@ -109,33 +109,12 @@
     }
 
     @Test
-    fun testUpdateTuningSizeFactor() {
-        setupResources(radius = 100,
-                roundedTopDrawable = getTestsDrawable(R.drawable.rounded3px),
-                roundedBottomDrawable = getTestsDrawable(R.drawable.rounded4px))
-
-        roundedCornerResDelegate = RoundedCornerResDelegate(mContext.resources, null)
-
-        val factor = 5
-        roundedCornerResDelegate.tuningSizeFactor = factor
-        val length = (factor * mContext.resources.displayMetrics.density).toInt()
-
-        assertEquals(Size(length, length), roundedCornerResDelegate.topRoundedSize)
-        assertEquals(Size(length, length), roundedCornerResDelegate.bottomRoundedSize)
-
-        roundedCornerResDelegate.tuningSizeFactor = null
-
-        assertEquals(Size(3, 3), roundedCornerResDelegate.topRoundedSize)
-        assertEquals(Size(4, 4), roundedCornerResDelegate.bottomRoundedSize)
-    }
-
-    @Test
     fun testPhysicalPixelDisplaySizeChanged() {
         setupResources(
                 roundedTopDrawable = getTestsDrawable(R.drawable.rounded4px),
                 roundedBottomDrawable = getTestsDrawable(R.drawable.rounded4px))
 
-        roundedCornerResDelegate = RoundedCornerResDelegate(mContext.resources, null)
+        roundedCornerResDelegate = RoundedCornerResDelegateImpl(mContext.resources, null)
         assertEquals(Size(4, 4), roundedCornerResDelegate.topRoundedSize)
         assertEquals(Size(4, 4), roundedCornerResDelegate.bottomRoundedSize)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt
index 75eec72..68ea7eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt
@@ -94,7 +94,7 @@
         intent.putExtra("command", command)
         args.forEach { arg -> intent.putExtra(arg.key, arg.value) }
 
-        fakeBroadcastDispatcher.registeredReceivers.forEach { it.onReceive(context, intent) }
+        fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayMetricsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayMetricsRepositoryTest.kt
new file mode 100644
index 0000000..dd741b4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayMetricsRepositoryTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.data.repository
+
+import android.content.Context
+import android.util.DisplayMetrics
+import android.view.Display
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.statusbar.policy.FakeConfigurationController
+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.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class DisplayMetricsRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: DisplayMetricsRepository
+
+    private val testScope = TestScope(StandardTestDispatcher())
+    private val configurationController = FakeConfigurationController()
+
+    private val displayMetrics =
+        DisplayMetrics().apply { this.heightPixels = INITIAL_HEIGHT_PIXELS }
+    private val mockContext: Context = mock()
+    private val mockDisplay: Display = mock()
+
+    @Before
+    fun setUp() {
+        underTest =
+            DisplayMetricsRepository(
+                testScope.backgroundScope,
+                configurationController,
+                displayMetrics,
+                mockContext,
+                LogBuffer("TestBuffer", maxSize = 10, logcatEchoTracker = mock())
+            )
+        whenever(mockContext.display).thenReturn(mockDisplay)
+    }
+
+    @Test
+    fun heightPixels_getsInitialValue() {
+        assertThat(underTest.heightPixels).isEqualTo(INITIAL_HEIGHT_PIXELS)
+    }
+
+    @Test
+    fun heightPixels_configChanged_heightUpdated() =
+        testScope.runTest {
+            runCurrent()
+
+            updateDisplayMetrics(456)
+            configurationController.notifyConfigurationChanged()
+            runCurrent()
+
+            assertThat(underTest.heightPixels).isEqualTo(456)
+
+            updateDisplayMetrics(23)
+            configurationController.notifyConfigurationChanged()
+            runCurrent()
+
+            assertThat(underTest.heightPixels).isEqualTo(23)
+        }
+
+    private fun updateDisplayMetrics(newHeight: Int) {
+        whenever(mockDisplay.getMetrics(displayMetrics)).thenAnswer {
+            it.getArgument<DisplayMetrics>(0).heightPixels = newHeight
+            Unit
+        }
+    }
+
+    private companion object {
+        const val INITIAL_HEIGHT_PIXELS = 345
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt
new file mode 100644
index 0000000..9be54fb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.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.display.data.repository
+
+import android.hardware.display.DisplayManager
+import android.os.Looper
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.Display
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.FlowValue
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.util.mockito.eq
+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.utils.os.FakeHandler
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class DisplayRepositoryTest : SysuiTestCase() {
+
+    private val displayManager = mock<DisplayManager>()
+    private val displayListener = kotlinArgumentCaptor<DisplayManager.DisplayListener>()
+
+    private val testHandler = FakeHandler(Looper.getMainLooper())
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+
+    private lateinit var displayRepository: DisplayRepositoryImpl
+
+    @Before
+    fun setup() {
+        setDisplays(emptyList())
+        displayRepository =
+            DisplayRepositoryImpl(
+                displayManager,
+                testHandler,
+                TestScope(UnconfinedTestDispatcher()),
+                UnconfinedTestDispatcher()
+            )
+        verify(displayManager, never()).registerDisplayListener(any(), any())
+    }
+
+    @Test
+    fun onFlowCollection_displayListenerRegistered() =
+        testScope.runTest {
+            val value by latestDisplayFlowValue()
+
+            assertThat(value).isEmpty()
+
+            verify(displayManager).registerDisplayListener(any(), eq(testHandler), anyLong())
+        }
+
+    @Test
+    fun afterFlowCollection_displayListenerUnregistered() {
+        testScope.runTest {
+            val value by latestDisplayFlowValue()
+
+            assertThat(value).isEmpty()
+
+            verify(displayManager).registerDisplayListener(any(), eq(testHandler), anyLong())
+        }
+        verify(displayManager).unregisterDisplayListener(any())
+    }
+
+    @Test
+    fun afterFlowCollection_multipleSusbcriptions_oneRemoved_displayListenerNotUnregistered() {
+        testScope.runTest {
+            val firstSubscriber by latestDisplayFlowValue()
+
+            assertThat(firstSubscriber).isEmpty()
+            verify(displayManager, times(1))
+                .registerDisplayListener(displayListener.capture(), eq(testHandler), anyLong())
+
+            val innerScope = TestScope()
+            innerScope.runTest {
+                val secondSubscriber by latestDisplayFlowValue()
+                assertThat(secondSubscriber).isEmpty()
+
+                // No new registration, just the precedent one.
+                verify(displayManager, times(1))
+                    .registerDisplayListener(any(), eq(testHandler), anyLong())
+            }
+
+            // Let's make sure it has *NOT* been unregistered, as there is still a subscriber.
+            setDisplays(1)
+            displayListener.value.onDisplayAdded(1)
+            assertThat(firstSubscriber?.ids()).containsExactly(1)
+        }
+
+        // All subscribers are done, unregister should have been called.
+        verify(displayManager).unregisterDisplayListener(any())
+    }
+    @Test
+    fun onDisplayAdded_propagated() =
+        testScope.runTest {
+            val value by latestDisplayFlowValue()
+
+            setDisplays(1)
+            displayListener.value.onDisplayAdded(1)
+
+            assertThat(value?.ids()).containsExactly(1)
+        }
+
+    @Test
+    fun onDisplayRemoved_propagated() =
+        testScope.runTest {
+            val value by latestDisplayFlowValue()
+
+            setDisplays(1, 2, 3, 4)
+            displayListener.value.onDisplayAdded(1)
+            displayListener.value.onDisplayAdded(2)
+            displayListener.value.onDisplayAdded(3)
+            displayListener.value.onDisplayAdded(4)
+
+            setDisplays(1, 2, 3)
+            displayListener.value.onDisplayRemoved(4)
+
+            assertThat(value?.ids()).containsExactly(1, 2, 3)
+        }
+
+    @Test
+    fun onDisplayChanged_propagated() =
+        testScope.runTest {
+            val value by latestDisplayFlowValue()
+
+            setDisplays(1, 2, 3, 4)
+            displayListener.value.onDisplayAdded(1)
+            displayListener.value.onDisplayAdded(2)
+            displayListener.value.onDisplayAdded(3)
+            displayListener.value.onDisplayAdded(4)
+
+            displayListener.value.onDisplayChanged(4)
+
+            assertThat(value?.ids()).containsExactly(1, 2, 3, 4)
+        }
+
+    private fun Iterable<Display>.ids(): List<Int> = map { it.displayId }
+
+    // Wrapper to capture the displayListener.
+    private fun TestScope.latestDisplayFlowValue(): FlowValue<Set<Display>?> {
+        val flowValue = collectLastValue(displayRepository.displays)
+        verify(displayManager)
+            .registerDisplayListener(displayListener.capture(), eq(testHandler), anyLong())
+        return flowValue
+    }
+
+    private fun setDisplays(displays: List<Display>) {
+        whenever(displayManager.displays).thenReturn(displays.toTypedArray())
+    }
+
+    private fun setDisplays(vararg ids: Int) {
+        setDisplays(ids.map { display(it) })
+    }
+
+    private fun display(id: Int): Display {
+        return mock<Display>().also { mockDisplay ->
+            whenever(mockDisplay.displayId).thenReturn(id)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt
new file mode 100644
index 0000000..1b597f4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.domain.interactor
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.Display
+import android.view.Display.TYPE_EXTERNAL
+import android.view.Display.TYPE_INTERNAL
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.FlowValue
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.display.data.repository.DisplayRepository
+import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State
+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.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class ConnectedDisplayInteractorTest : SysuiTestCase() {
+
+    private val fakeDisplayRepository = FakeDisplayRepository()
+    private val connectedDisplayStateProvider: ConnectedDisplayInteractor =
+        ConnectedDisplayInteractorImpl(fakeDisplayRepository)
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+
+    @Test
+    fun displayState_nullDisplays_disconnected() =
+        testScope.runTest {
+            val value by lastValue()
+
+            fakeDisplayRepository.emit(emptySet())
+
+            assertThat(value).isEqualTo(State.DISCONNECTED)
+        }
+
+    @Test
+    fun displayState_emptyDisplays_disconnected() =
+        testScope.runTest {
+            val value by lastValue()
+
+            fakeDisplayRepository.emit(emptySet())
+
+            assertThat(value).isEqualTo(State.DISCONNECTED)
+        }
+
+    @Test
+    fun displayState_internalDisplay_disconnected() =
+        testScope.runTest {
+            val value by lastValue()
+
+            fakeDisplayRepository.emit(setOf(display(type = TYPE_INTERNAL)))
+
+            assertThat(value).isEqualTo(State.DISCONNECTED)
+        }
+
+    @Test
+    fun displayState_externalDisplay_connected() =
+        testScope.runTest {
+            val value by lastValue()
+
+            fakeDisplayRepository.emit(setOf(display(type = TYPE_EXTERNAL)))
+
+            assertThat(value).isEqualTo(State.CONNECTED)
+        }
+
+    @Test
+    fun displayState_multipleExternalDisplays_connected() =
+        testScope.runTest {
+            val value by lastValue()
+
+            fakeDisplayRepository.emit(
+                setOf(display(type = TYPE_EXTERNAL), display(type = TYPE_EXTERNAL))
+            )
+
+            assertThat(value).isEqualTo(State.CONNECTED)
+        }
+
+    @Test
+    fun displayState_externalSecure_connectedSecure() =
+        testScope.runTest {
+            val value by lastValue()
+
+            fakeDisplayRepository.emit(
+                setOf(display(type = TYPE_EXTERNAL, flags = Display.FLAG_SECURE))
+            )
+
+            assertThat(value).isEqualTo(State.CONNECTED_SECURE)
+        }
+
+    @Test
+    fun displayState_multipleExternal_onlyOneSecure_connectedSecure() =
+        testScope.runTest {
+            val value by lastValue()
+
+            fakeDisplayRepository.emit(
+                setOf(
+                    display(type = TYPE_EXTERNAL, flags = Display.FLAG_SECURE),
+                    display(type = TYPE_EXTERNAL, flags = 0)
+                )
+            )
+
+            assertThat(value).isEqualTo(State.CONNECTED_SECURE)
+        }
+
+    private fun TestScope.lastValue(): FlowValue<State?> =
+        collectLastValue(connectedDisplayStateProvider.connectedDisplayState)
+
+    private fun display(type: Int, flags: Int = 0): Display {
+        return mock<Display>().also { mockDisplay ->
+            whenever(mockDisplay.type).thenReturn(type)
+            whenever(mockDisplay.flags).thenReturn(flags)
+        }
+    }
+
+    private class FakeDisplayRepository : DisplayRepository {
+        private val flow = MutableSharedFlow<Set<Display>>()
+        suspend fun emit(value: Set<Display>) = flow.emit(value)
+        override val displays: Flow<Set<Display>>
+            get() = flow
+    }
+}
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 47b7d49..8786520 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -41,8 +41,8 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.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.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
 import com.android.systemui.statusbar.BlurUtils;
 
 import org.junit.Before;
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 d0d348d..d99f0da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.dreams;
 
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -507,4 +509,23 @@
         mService.onWakeUp();
         verify(mDreamOverlayContainerViewController, never()).wakeUp();
     }
+
+    @Test
+    public void testSystemFlagShowForAllUsersSetOnWindow() throws RemoteException {
+        final IDreamOverlayClient client = getClient();
+
+        // Inform the overlay service of dream starting. Do not show dream complications.
+        client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+                false /*shouldShowComplication*/);
+        mMainExecutor.runAllReady();
+
+        final ArgumentCaptor<WindowManager.LayoutParams> paramsCaptor =
+                ArgumentCaptor.forClass(WindowManager.LayoutParams.class);
+
+        // Verify that a new window is added.
+        verify(mWindowManager).addView(any(), paramsCaptor.capture());
+
+        assertThat((paramsCaptor.getValue().privateFlags & SYSTEM_FLAG_SHOW_FOR_ALL_USERS)
+                == SYSTEM_FLAG_SHOW_FOR_ALL_USERS).isTrue();
+    }
 }
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 7b41605..2c1ebe4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -58,6 +58,9 @@
     @Mock
     private FeatureFlags mFeatureFlags;
 
+    @Mock
+    private DreamLogger mLogger;
+
     final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
 
     @Before
@@ -405,6 +408,6 @@
     }
 
     private DreamOverlayStateController getDreamOverlayStateController(boolean overlayEnabled) {
-        return new DreamOverlayStateController(mExecutor, overlayEnabled, mFeatureFlags);
+        return new DreamOverlayStateController(mExecutor, overlayEnabled, mFeatureFlags, mLogger);
     }
 }
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 d16b757..5dc0e55 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -113,6 +113,8 @@
     DreamOverlayStateController mDreamOverlayStateController;
     @Mock
     UserTracker mUserTracker;
+    @Mock
+    DreamLogger mLogger;
 
     @Captor
     private ArgumentCaptor<DreamOverlayStateController.Callback> mCallbackCaptor;
@@ -146,7 +148,8 @@
                 mStatusBarWindowStateController,
                 mDreamOverlayStatusBarItemsProvider,
                 mDreamOverlayStateController,
-                mUserTracker);
+                mUserTracker,
+                mLogger);
     }
 
     @Test
@@ -289,7 +292,8 @@
                 mStatusBarWindowStateController,
                 mDreamOverlayStatusBarItemsProvider,
                 mDreamOverlayStateController,
-                mUserTracker);
+                mUserTracker,
+                mLogger);
         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/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
index 1a89076..3f9b198 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
@@ -28,6 +28,7 @@
 
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
+import android.content.pm.UserInfo;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.testing.AndroidTestingRunner;
@@ -39,10 +40,12 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.UiEventLogger;
+import com.android.internal.widget.LockPatternUtils;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
 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.settings.FakeUserTracker;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shared.system.InputChannelCompat;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -57,6 +60,7 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Collections;
 import java.util.Optional;
 
 @SmallTest
@@ -100,21 +104,34 @@
     @Mock
     UiEventLogger mUiEventLogger;
 
+    @Mock
+    LockPatternUtils mLockPatternUtils;
+
+    FakeUserTracker mUserTracker;
+
     private static final float TOUCH_REGION = .3f;
     private static final int SCREEN_WIDTH_PX = 1024;
     private static final int SCREEN_HEIGHT_PX = 100;
 
     private static final Rect SCREEN_BOUNDS = new Rect(0, 0, 1024, 100);
+    private static final UserInfo CURRENT_USER_INFO = new UserInfo(
+            10,
+            /* name= */ "user10",
+            /* flags= */ 0
+    );
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        mUserTracker = new FakeUserTracker();
         mTouchHandler = new BouncerSwipeTouchHandler(
                 mScrimManager,
                 Optional.of(mCentralSurfaces),
                 mNotificationShadeWindowController,
                 mValueAnimatorCreator,
                 mVelocityTrackerFactory,
+                mLockPatternUtils,
+                mUserTracker,
                 mFlingAnimationUtils,
                 mFlingAnimationUtilsClosing,
                 TOUCH_REGION,
@@ -126,6 +143,9 @@
         when(mVelocityTrackerFactory.obtain()).thenReturn(mVelocityTracker);
         when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn(Float.MAX_VALUE);
         when(mTouchSession.getBounds()).thenReturn(SCREEN_BOUNDS);
+        when(mLockPatternUtils.isSecure(CURRENT_USER_INFO.id)).thenReturn(true);
+
+        mUserTracker.set(Collections.singletonList(CURRENT_USER_INFO), 0);
     }
 
     /**
@@ -265,6 +285,32 @@
         verifyScroll(.7f, Direction.DOWN, true, gestureListener);
     }
 
+    /**
+     * Makes sure the expansion amount is proportional to (1 - scroll).
+     */
+    @Test
+    public void testSwipeUp_keyguardNotSecure_doesNotExpand() {
+        when(mLockPatternUtils.isSecure(CURRENT_USER_INFO.id)).thenReturn(false);
+        mTouchHandler.onSessionStart(mTouchSession);
+        ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
+                ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
+        verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
+
+        final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
+
+        final float distanceY = SCREEN_HEIGHT_PX * 0.3f;
+        final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+                0, SCREEN_HEIGHT_PX, 0);
+        final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+                0, SCREEN_HEIGHT_PX - distanceY, 0);
+
+        reset(mScrimController);
+        assertThat(gestureListener.onScroll(event1, event2, 0, distanceY))
+                .isTrue();
+        // We should not expand since the keyguard is not secure
+        verify(mScrimController, never()).expand(any());
+    }
+
     private void verifyScroll(float percent, Direction direction,
             boolean isBouncerInitiallyShowing, GestureDetector.OnGestureListener gestureListener) {
         final float distanceY = SCREEN_HEIGHT_PX * percent;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
index e8cbdf3..2830476 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
@@ -22,7 +22,7 @@
 import com.android.systemui.ProtoDumpable
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
+import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.google.common.truth.Truth.assertThat
@@ -45,8 +45,6 @@
 
     @Mock
     private lateinit var logBufferEulogizer: LogBufferEulogizer
-    @Mock
-    private lateinit var exceptionHandlerManager: UncaughtExceptionPreHandlerManager
 
     @Mock
     private lateinit var pw: PrintWriter
@@ -70,6 +68,11 @@
     @Mock
     private lateinit var buffer2: LogBuffer
 
+    @Mock
+    private lateinit var table1: TableLogBuffer
+    @Mock
+    private lateinit var table2: TableLogBuffer
+
     private val dumpManager = DumpManager()
 
     @Before
@@ -83,21 +86,22 @@
             mutableMapOf(
                 EmptyCoreStartable::class.java to Provider { EmptyCoreStartable() }
             ),
-            exceptionHandlerManager
         )
     }
 
     @Test
     fun testDumpablesCanBeDumpedSelectively() {
         // GIVEN a variety of registered dumpables and buffers
-        dumpManager.registerDumpable("dumpable1", dumpable1)
-        dumpManager.registerDumpable("dumpable2", dumpable2)
-        dumpManager.registerDumpable("dumpable3", dumpable3)
+        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
+        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
+        dumpManager.registerCriticalDumpable("dumpable3", dumpable3)
         dumpManager.registerBuffer("buffer1", buffer1)
         dumpManager.registerBuffer("buffer2", buffer2)
+        dumpManager.registerTableLogBuffer("table1", table1)
+        dumpManager.registerTableLogBuffer("table2", table2)
 
         // WHEN some of them are dumped explicitly
-        val args = arrayOf("dumpable1", "dumpable3", "buffer2")
+        val args = arrayOf("dumpable1", "dumpable3", "buffer2", "table2")
         dumpHandler.dump(fd, pw, args)
 
         // THEN only the requested ones have their dump() method called
@@ -108,12 +112,14 @@
         verify(dumpable3).dump(pw, args)
         verify(buffer1, never()).dump(any(PrintWriter::class.java), anyInt())
         verify(buffer2).dump(pw, 0)
+        verify(table1, never()).dump(any(), any())
+        verify(table2).dump(pw, args)
     }
 
     @Test
     fun testDumpableMatchingIsBasedOnEndOfTag() {
         // GIVEN a dumpable registered to the manager
-        dumpManager.registerDumpable("com.android.foo.bar.dumpable1", dumpable1)
+        dumpManager.registerCriticalDumpable("com.android.foo.bar.dumpable1", dumpable1)
 
         // WHEN that module is dumped
         val args = arrayOf("dumpable1")
@@ -131,6 +137,8 @@
         dumpManager.registerNormalDumpable("dumpable3", dumpable3)
         dumpManager.registerBuffer("buffer1", buffer1)
         dumpManager.registerBuffer("buffer2", buffer2)
+        dumpManager.registerTableLogBuffer("table1", table1)
+        dumpManager.registerTableLogBuffer("table2", table2)
 
         // WHEN a critical dump is requested
         val args = arrayOf("--dump-priority", "CRITICAL")
@@ -144,6 +152,8 @@
             any(Array<String>::class.java))
         verify(buffer1, never()).dump(any(PrintWriter::class.java), anyInt())
         verify(buffer2, never()).dump(any(PrintWriter::class.java), anyInt())
+        verify(table1, never()).dump(any(), any())
+        verify(table2, never()).dump(any(), any())
     }
 
     @Test
@@ -154,6 +164,8 @@
         dumpManager.registerNormalDumpable("dumpable3", dumpable3)
         dumpManager.registerBuffer("buffer1", buffer1)
         dumpManager.registerBuffer("buffer2", buffer2)
+        dumpManager.registerTableLogBuffer("table1", table1)
+        dumpManager.registerTableLogBuffer("table2", table2)
 
         // WHEN a normal dump is requested
         val args = arrayOf("--dump-priority", "NORMAL")
@@ -169,6 +181,8 @@
         verify(dumpable3).dump(pw, args)
         verify(buffer1).dump(pw, 0)
         verify(buffer2).dump(pw, 0)
+        verify(table1).dump(pw, args)
+        verify(table2).dump(pw, args)
     }
 
     @Test
@@ -184,6 +198,81 @@
     }
 
     @Test
+    fun testDumpBuffers() {
+        // GIVEN a variety of registered dumpables and buffers and tables
+        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
+        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
+        dumpManager.registerNormalDumpable("dumpable3", dumpable3)
+        dumpManager.registerBuffer("buffer1", buffer1)
+        dumpManager.registerBuffer("buffer2", buffer2)
+        dumpManager.registerTableLogBuffer("table1", table1)
+        dumpManager.registerTableLogBuffer("table2", table2)
+
+        // WHEN a buffer dump is requested
+        val args = arrayOf("buffers", "--tail", "1")
+        dumpHandler.dump(fd, pw, args)
+
+        // THEN all buffers are dumped (and no dumpables or tables)
+        verify(dumpable1, never()).dump(any(), any())
+        verify(dumpable2, never()).dump(any(), any())
+        verify(dumpable3, never()).dump(any(), any())
+        verify(buffer1).dump(pw, tailLength = 1)
+        verify(buffer2).dump(pw, tailLength = 1)
+        verify(table1, never()).dump(any(), any())
+        verify(table2, never()).dump(any(), any())
+    }
+
+    @Test
+    fun testDumpDumpables() {
+        // GIVEN a variety of registered dumpables and buffers and tables
+        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
+        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
+        dumpManager.registerNormalDumpable("dumpable3", dumpable3)
+        dumpManager.registerBuffer("buffer1", buffer1)
+        dumpManager.registerBuffer("buffer2", buffer2)
+        dumpManager.registerTableLogBuffer("table1", table1)
+        dumpManager.registerTableLogBuffer("table2", table2)
+
+        // WHEN a dumpable dump is requested
+        val args = arrayOf("dumpables")
+        dumpHandler.dump(fd, pw, args)
+
+        // THEN all dumpables are dumped (both critical and normal) (and no dumpables)
+        verify(dumpable1).dump(pw, args)
+        verify(dumpable2).dump(pw, args)
+        verify(dumpable3).dump(pw, args)
+        verify(buffer1, never()).dump(any(), anyInt())
+        verify(buffer2, never()).dump(any(), anyInt())
+        verify(table1, never()).dump(any(), any())
+        verify(table2, never()).dump(any(), any())
+    }
+
+    @Test
+    fun testDumpTables() {
+        // GIVEN a variety of registered dumpables and buffers and tables
+        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
+        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
+        dumpManager.registerNormalDumpable("dumpable3", dumpable3)
+        dumpManager.registerBuffer("buffer1", buffer1)
+        dumpManager.registerBuffer("buffer2", buffer2)
+        dumpManager.registerTableLogBuffer("table1", table1)
+        dumpManager.registerTableLogBuffer("table2", table2)
+
+        // WHEN a dumpable dump is requested
+        val args = arrayOf("tables")
+        dumpHandler.dump(fd, pw, args)
+
+        // THEN all dumpables are dumped (both critical and normal) (and no dumpables)
+        verify(dumpable1, never()).dump(any(), any())
+        verify(dumpable2, never()).dump(any(), any())
+        verify(dumpable3, never()).dump(any(), any())
+        verify(buffer1, never()).dump(any(), anyInt())
+        verify(buffer2, never()).dump(any(), anyInt())
+        verify(table1).dump(pw, args)
+        verify(table2).dump(pw, args)
+    }
+
+    @Test
     fun testDumpAllProtoDumpables() {
         dumpManager.registerDumpable("protoDumpable1", protoDumpable1)
         dumpManager.registerDumpable("protoDumpable2", protoDumpable2)
@@ -207,6 +296,123 @@
         verify(protoDumpable2, never()).dumpProto(any(), any())
     }
 
+    @Test
+    fun testDumpTarget_selectsShortestNamedDumpable() {
+        // GIVEN a variety of registered dumpables and buffers
+        dumpManager.registerCriticalDumpable("first-dumpable", dumpable1)
+        dumpManager.registerCriticalDumpable("scnd-dumpable", dumpable2)
+        dumpManager.registerCriticalDumpable("third-dumpable", dumpable3)
+
+        // WHEN a dumpable is dumped by a suffix that matches multiple options
+        val args = arrayOf("dumpable")
+        dumpHandler.dump(fd, pw, args)
+
+        // THEN the matching dumpable with the shorter name is dumped
+        verify(dumpable1, never()).dump(any(), any())
+        verify(dumpable2).dump(pw, args)
+        verify(dumpable3, never()).dump(any(), any())
+    }
+
+    @Test
+    fun testDumpTarget_selectsShortestNamedBuffer() {
+        // GIVEN a variety of registered dumpables and buffers
+        dumpManager.registerBuffer("first-buffer", buffer1)
+        dumpManager.registerBuffer("scnd-buffer", buffer2)
+
+        // WHEN a dumpable is dumped by a suffix that matches multiple options
+        val args = arrayOf("buffer", "--tail", "14")
+        dumpHandler.dump(fd, pw, args)
+
+        // THEN the matching buffer with the shorter name is dumped
+        verify(buffer1, never()).dump(any(), anyInt())
+        verify(buffer2).dump(pw, tailLength = 14)
+    }
+
+    @Test
+    fun testDumpTarget_selectsShortestNamedMatch_dumpable() {
+        // GIVEN a variety of registered dumpables and buffers
+        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
+        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
+        dumpManager.registerCriticalDumpable("dumpable3", dumpable3)
+        dumpManager.registerBuffer("big-buffer1", buffer1)
+        dumpManager.registerBuffer("big-buffer2", buffer2)
+
+        // WHEN a dumpable is dumped by a suffix that matches multiple options
+        val args = arrayOf("2")
+        dumpHandler.dump(fd, pw, args)
+
+        // THEN the matching dumpable with the shorter name is dumped
+        verify(dumpable1, never()).dump(any(), any())
+        verify(dumpable2).dump(pw, args)
+        verify(dumpable3, never()).dump(any(), any())
+        verify(buffer1, never()).dump(any(), anyInt())
+        verify(buffer2, never()).dump(any(), anyInt())
+    }
+
+    @Test
+    fun testDumpTarget_selectsShortestNamedMatch_buffer() {
+        // GIVEN a variety of registered dumpables and buffers
+        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
+        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
+        dumpManager.registerCriticalDumpable("dumpable3", dumpable3)
+        dumpManager.registerBuffer("buffer1", buffer1)
+        dumpManager.registerBuffer("buffer2", buffer2)
+
+        // WHEN a dumpable is dumped by a suffix that matches multiple options
+        val args = arrayOf("2", "--tail", "14")
+        dumpHandler.dump(fd, pw, args)
+
+        // THEN the matching buffer with the shorter name is dumped
+        verify(dumpable1, never()).dump(any(), any())
+        verify(dumpable2, never()).dump(any(), any())
+        verify(dumpable3, never()).dump(any(), any())
+        verify(buffer1, never()).dump(any(), anyInt())
+        verify(buffer2).dump(pw, tailLength = 14)
+    }
+
+    @Test
+    fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_dumpable() {
+        // GIVEN a variety of registered dumpables and buffers
+        dumpManager.registerCriticalDumpable("d1x", dumpable1)
+        dumpManager.registerCriticalDumpable("d2x", dumpable2)
+        dumpManager.registerCriticalDumpable("a3x", dumpable3)
+        dumpManager.registerBuffer("ab1x", buffer1)
+        dumpManager.registerBuffer("b2x", buffer2)
+
+        // WHEN a dumpable is dumped by a suffix that matches multiple options
+        val args = arrayOf("x")
+        dumpHandler.dump(fd, pw, args)
+
+        // THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped
+        verify(dumpable1, never()).dump(any(), any())
+        verify(dumpable2, never()).dump(any(), any())
+        verify(dumpable3).dump(pw, args)
+        verify(buffer1, never()).dump(any(), anyInt())
+        verify(buffer2, never()).dump(any(), anyInt())
+    }
+
+    @Test
+    fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_buffer() {
+        // GIVEN a variety of registered dumpables and buffers
+        dumpManager.registerCriticalDumpable("d1x", dumpable1)
+        dumpManager.registerCriticalDumpable("d2x", dumpable2)
+        dumpManager.registerCriticalDumpable("az1x", dumpable3)
+        dumpManager.registerBuffer("b1x", buffer1)
+        dumpManager.registerBuffer("b2x", buffer2)
+
+        // WHEN a dumpable is dumped by a suffix that matches multiple options
+        val args = arrayOf("x", "--tail", "14")
+        dumpHandler.dump(fd, pw, args)
+
+        // THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped
+        verify(dumpable1, never()).dump(any(), any())
+        verify(dumpable2, never()).dump(any(), any())
+        verify(dumpable3, never()).dump(any(), any())
+        verify(buffer1).dump(pw, tailLength = 14)
+        verify(buffer2, never()).dump(any(), anyInt())
+    }
+
+
     private class EmptyCoreStartable : CoreStartable {
         override fun start() {}
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt
index 02555cf..6d5226f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt
@@ -20,21 +20,17 @@
 import com.android.systemui.Dumpable
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.util.mockito.any
-import java.io.PrintWriter
+import com.android.systemui.log.table.TableLogBuffer
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @SmallTest
 class DumpManagerTest : SysuiTestCase() {
 
-    @Mock private lateinit var pw: PrintWriter
-
     @Mock private lateinit var dumpable1: Dumpable
     @Mock private lateinit var dumpable2: Dumpable
     @Mock private lateinit var dumpable3: Dumpable
@@ -42,6 +38,9 @@
     @Mock private lateinit var buffer1: LogBuffer
     @Mock private lateinit var buffer2: LogBuffer
 
+    @Mock private lateinit var table1: TableLogBuffer
+    @Mock private lateinit var table2: TableLogBuffer
+
     private val dumpManager = DumpManager()
 
     @Before
@@ -50,276 +49,144 @@
     }
 
     @Test
-    fun testDumpTarget_dumpable() {
-        // GIVEN a variety of registered dumpables and buffers
-        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
-        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
-        dumpManager.registerCriticalDumpable("dumpable3", dumpable3)
-        dumpManager.registerBuffer("buffer1", buffer1)
-        dumpManager.registerBuffer("buffer2", buffer2)
-
-        // WHEN a dumpable is dumped explicitly
-        val args = arrayOf<String>()
-        dumpManager.dumpTarget("dumpable2", pw, args, tailLength = 0)
-
-        // THEN only the requested one has their dump() method called
-        verify(dumpable1, never()).dump(any(), any())
-        verify(dumpable2).dump(pw, args)
-        verify(dumpable3, never()).dump(any(), any())
-        verify(buffer1, never()).dump(any(), anyInt())
-        verify(buffer2, never()).dump(any(), anyInt())
-    }
-
-    @Test
-    fun testDumpTarget_buffer() {
-        // GIVEN a variety of registered dumpables and buffers
-        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
-        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
-        dumpManager.registerCriticalDumpable("dumpable3", dumpable3)
-        dumpManager.registerBuffer("buffer1", buffer1)
-        dumpManager.registerBuffer("buffer2", buffer2)
-
-        // WHEN a buffer is dumped explicitly
-        val args = arrayOf<String>()
-        dumpManager.dumpTarget("buffer1", pw, args, tailLength = 14)
-
-        // THEN only the requested one has their dump() method called
-        verify(dumpable1, never()).dump(any(), any())
-        verify(dumpable2, never()).dump(any(), any())
-        verify(dumpable3, never()).dump(any(), any())
-        verify(buffer1).dump(pw, tailLength = 14)
-        verify(buffer2, never()).dump(any(), anyInt())
-    }
-
-    @Test
-    fun testDumpableMatchingIsBasedOnEndOfTag() {
-        // GIVEN a dumpable registered to the manager
-        dumpManager.registerCriticalDumpable("com.android.foo.bar.dumpable1", dumpable1)
-
-        // WHEN that module is dumped
-        val args = arrayOf<String>()
-        dumpManager.dumpTarget("dumpable1", pw, arrayOf(), tailLength = 14)
-
-        // THEN its dump() method is called
-        verify(dumpable1).dump(pw, args)
-    }
-
-    @Test
-    fun testDumpTarget_selectsShortestNamedDumpable() {
-        // GIVEN a variety of registered dumpables and buffers
-        dumpManager.registerCriticalDumpable("first-dumpable", dumpable1)
-        dumpManager.registerCriticalDumpable("scnd-dumpable", dumpable2)
-        dumpManager.registerCriticalDumpable("third-dumpable", dumpable3)
-
-        // WHEN a dumpable is dumped by a suffix that matches multiple options
-        val args = arrayOf<String>()
-        dumpManager.dumpTarget("dumpable", pw, args, tailLength = 0)
-
-        // THEN the matching dumpable with the shorter name is dumped
-        verify(dumpable1, never()).dump(any(), any())
-        verify(dumpable2).dump(pw, args)
-        verify(dumpable3, never()).dump(any(), any())
-    }
-
-    @Test
-    fun testDumpTarget_selectsShortestNamedBuffer() {
-        // GIVEN a variety of registered dumpables and buffers
-        dumpManager.registerBuffer("first-buffer", buffer1)
-        dumpManager.registerBuffer("scnd-buffer", buffer2)
-
-        // WHEN a dumpable is dumped by a suffix that matches multiple options
-        val args = arrayOf<String>()
-        dumpManager.dumpTarget("buffer", pw, args, tailLength = 14)
-
-        // THEN the matching buffer with the shorter name is dumped
-        verify(buffer1, never()).dump(any(), anyInt())
-        verify(buffer2).dump(pw, tailLength = 14)
-    }
-
-    @Test
-    fun testDumpTarget_selectsShortestNamedMatch_dumpable() {
-        // GIVEN a variety of registered dumpables and buffers
-        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
-        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
-        dumpManager.registerCriticalDumpable("dumpable3", dumpable3)
-        dumpManager.registerBuffer("big-buffer1", buffer1)
-        dumpManager.registerBuffer("big-buffer2", buffer2)
-
-        // WHEN a dumpable is dumped by a suffix that matches multiple options
-        val args = arrayOf<String>()
-        dumpManager.dumpTarget("2", pw, args, tailLength = 14)
-
-        // THEN the matching dumpable with the shorter name is dumped
-        verify(dumpable1, never()).dump(any(), any())
-        verify(dumpable2).dump(pw, args)
-        verify(dumpable3, never()).dump(any(), any())
-        verify(buffer1, never()).dump(any(), anyInt())
-        verify(buffer2, never()).dump(any(), anyInt())
-    }
-
-    @Test
-    fun testDumpTarget_selectsShortestNamedMatch_buffer() {
-        // GIVEN a variety of registered dumpables and buffers
-        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
-        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
-        dumpManager.registerCriticalDumpable("dumpable3", dumpable3)
-        dumpManager.registerBuffer("buffer1", buffer1)
-        dumpManager.registerBuffer("buffer2", buffer2)
-
-        // WHEN a dumpable is dumped by a suffix that matches multiple options
-        val args = arrayOf<String>()
-        dumpManager.dumpTarget("2", pw, args, tailLength = 14)
-
-        // THEN the matching buffer with the shorter name is dumped
-        verify(dumpable1, never()).dump(any(), any())
-        verify(dumpable2, never()).dump(any(), any())
-        verify(dumpable3, never()).dump(any(), any())
-        verify(buffer1, never()).dump(any(), anyInt())
-        verify(buffer2).dump(pw, tailLength = 14)
-    }
-
-    @Test
-    fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_dumpable() {
-        // GIVEN a variety of registered dumpables and buffers
-        dumpManager.registerCriticalDumpable("d1x", dumpable1)
-        dumpManager.registerCriticalDumpable("d2x", dumpable2)
-        dumpManager.registerCriticalDumpable("a3x", dumpable3)
-        dumpManager.registerBuffer("ab1x", buffer1)
-        dumpManager.registerBuffer("b2x", buffer2)
-
-        // WHEN a dumpable is dumped by a suffix that matches multiple options
-        val args = arrayOf<String>()
-        dumpManager.dumpTarget("x", pw, args, tailLength = 14)
-
-        // THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped
-        verify(dumpable1, never()).dump(any(), any())
-        verify(dumpable2, never()).dump(any(), any())
-        verify(dumpable3).dump(pw, args)
-        verify(buffer1, never()).dump(any(), anyInt())
-        verify(buffer2, never()).dump(any(), anyInt())
-    }
-
-    @Test
-    fun testDumpTarget_selectsTheAlphabeticallyFirstShortestMatch_buffer() {
-        // GIVEN a variety of registered dumpables and buffers
-        dumpManager.registerCriticalDumpable("d1x", dumpable1)
-        dumpManager.registerCriticalDumpable("d2x", dumpable2)
-        dumpManager.registerCriticalDumpable("az1x", dumpable3)
-        dumpManager.registerBuffer("b1x", buffer1)
-        dumpManager.registerBuffer("b2x", buffer2)
-
-        // WHEN a dumpable is dumped by a suffix that matches multiple options
-        val args = arrayOf<String>()
-        dumpManager.dumpTarget("x", pw, args, tailLength = 14)
-
-        // THEN the alphabetically first dumpable/buffer (of the 3 letter names) is dumped
-        verify(dumpable1, never()).dump(any(), any())
-        verify(dumpable2, never()).dump(any(), any())
-        verify(dumpable3, never()).dump(any(), any())
-        verify(buffer1).dump(pw, tailLength = 14)
-        verify(buffer2, never()).dump(any(), anyInt())
-    }
-
-    @Test
-    fun testDumpDumpables() {
-        // GIVEN a variety of registered dumpables and buffers
-        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
-        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
-        dumpManager.registerNormalDumpable("dumpable3", dumpable3)
-        dumpManager.registerBuffer("buffer1", buffer1)
-        dumpManager.registerBuffer("buffer2", buffer2)
-
-        // WHEN a dumpable dump is requested
-        val args = arrayOf<String>()
-        dumpManager.dumpDumpables(pw, args)
-
-        // THEN all dumpables are dumped (both critical and normal) (and no dumpables)
-        verify(dumpable1).dump(pw, args)
-        verify(dumpable2).dump(pw, args)
-        verify(dumpable3).dump(pw, args)
-        verify(buffer1, never()).dump(any(), anyInt())
-        verify(buffer2, never()).dump(any(), anyInt())
-    }
-
-    @Test
-    fun testDumpBuffers() {
-        // GIVEN a variety of registered dumpables and buffers
-        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
-        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
-        dumpManager.registerNormalDumpable("dumpable3", dumpable3)
-        dumpManager.registerBuffer("buffer1", buffer1)
-        dumpManager.registerBuffer("buffer2", buffer2)
-
-        // WHEN a buffer dump is requested
-        dumpManager.dumpBuffers(pw, tailLength = 1)
-
-        // THEN all buffers are dumped (and no dumpables)
-        verify(dumpable1, never()).dump(any(), any())
-        verify(dumpable2, never()).dump(any(), any())
-        verify(dumpable3, never()).dump(any(), any())
-        verify(buffer1).dump(pw, tailLength = 1)
-        verify(buffer2).dump(pw, tailLength = 1)
-    }
-
-    @Test
-    fun testCriticalDump() {
-        // GIVEN a variety of registered dumpables and buffers
-        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
-        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
-        dumpManager.registerNormalDumpable("dumpable3", dumpable3)
-        dumpManager.registerBuffer("buffer1", buffer1)
-        dumpManager.registerBuffer("buffer2", buffer2)
-
-        // WHEN a critical dump is requested
-        val args = arrayOf<String>()
-        dumpManager.dumpCritical(pw, args)
-
-        // THEN only critical modules are dumped (and no buffers)
-        verify(dumpable1).dump(pw, args)
-        verify(dumpable2).dump(pw, args)
-        verify(dumpable3, never()).dump(any(), any())
-        verify(buffer1, never()).dump(any(), anyInt())
-        verify(buffer2, never()).dump(any(), anyInt())
-    }
-
-    @Test
-    fun testNormalDump() {
-        // GIVEN a variety of registered dumpables and buffers
-        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
-        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
-        dumpManager.registerNormalDumpable("dumpable3", dumpable3)
-        dumpManager.registerBuffer("buffer1", buffer1)
-        dumpManager.registerBuffer("buffer2", buffer2)
-
-        // WHEN a normal dump is requested
-        val args = arrayOf<String>()
-        dumpManager.dumpNormal(pw, args, tailLength = 2)
-
-        // THEN the normal module and all buffers are dumped
-        verify(dumpable1, never()).dump(any(), any())
-        verify(dumpable2, never()).dump(any(), any())
-        verify(dumpable3).dump(pw, args)
-        verify(buffer1).dump(pw, tailLength = 2)
-        verify(buffer2).dump(pw, tailLength = 2)
-    }
-
-    @Test
-    fun testUnregister() {
-        // GIVEN a variety of registered dumpables and buffers
+    fun testRegisterUnregister_dumpables() {
+        // GIVEN a variety of registered dumpables
         dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
         dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
         dumpManager.registerNormalDumpable("dumpable3", dumpable3)
 
+        // WHEN the collection is requested
+        var dumpables = dumpManager.getDumpables().map { it.dumpable }
+
+        // THEN it contains the registered entries
+        assertThat(dumpables).containsExactly(dumpable1, dumpable2, dumpable3)
+
+        // WHEN the dumpables are unregistered
         dumpManager.unregisterDumpable("dumpable2")
         dumpManager.unregisterDumpable("dumpable3")
 
-        // WHEN a dumpables dump is requested
-        val args = arrayOf<String>()
-        dumpManager.dumpDumpables(pw, args)
+        // WHEN the dumpable collection is requests
+        dumpables = dumpManager.getDumpables().map { it.dumpable }
 
-        // THEN the unregistered dumpables (both normal and critical) are not dumped
-        verify(dumpable1).dump(pw, args)
-        verify(dumpable2, never()).dump(any(), any())
-        verify(dumpable3, never()).dump(any(), any())
+        // THEN it contains only the currently-registered entry
+        assertThat(dumpables).containsExactly(dumpable1)
+    }
+
+    @Test
+    fun testRegister_buffers() {
+        // GIVEN a set of registered buffers
+        dumpManager.registerBuffer("buffer1", buffer1)
+        dumpManager.registerBuffer("buffer2", buffer2)
+
+        // WHEN the collection is requested
+        val dumpables = dumpManager.getLogBuffers().map { it.buffer }
+
+        // THEN it contains the registered entries
+        assertThat(dumpables).containsExactly(buffer1, buffer2)
+    }
+
+    @Test
+    fun testRegister_tableLogBuffers() {
+        // GIVEN a set of registered buffers
+        dumpManager.registerTableLogBuffer("table1", table1)
+        dumpManager.registerTableLogBuffer("table2", table2)
+
+        // WHEN the collection is requested
+        val tables = dumpManager.getTableLogBuffers().map { it.table }
+
+        // THEN it contains the registered entries
+        assertThat(tables).containsExactly(table1, table2)
+    }
+
+    @Test
+    fun registerDumpable_throwsWhenNameCannotBeAssigned() {
+        // GIVEN dumpable1 and buffer1 and table1 are registered
+        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
+        dumpManager.registerBuffer("buffer1", buffer1)
+        dumpManager.registerTableLogBuffer("table1", table1)
+
+        // THEN an exception is thrown when trying to re-register a new dumpable under the same key
+        assertThrows(IllegalArgumentException::class.java) {
+            dumpManager.registerCriticalDumpable("dumpable1", dumpable2)
+        }
+        assertThrows(IllegalArgumentException::class.java) {
+            dumpManager.registerBuffer("buffer1", buffer2)
+        }
+        assertThrows(IllegalArgumentException::class.java) {
+            dumpManager.registerTableLogBuffer("table1", table2)
+        }
+    }
+
+    @Test
+    fun registerDumpable_doesNotThrowWhenReRegistering() {
+        // GIVEN dumpable1 and buffer1 are registered
+        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
+        dumpManager.registerBuffer("buffer1", buffer1)
+
+        // THEN no exception is thrown when trying to re-register a new dumpable under the same key
+        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
+        dumpManager.registerBuffer("buffer1", buffer1)
+
+        // No exception thrown
+    }
+
+    @Test
+    fun getDumpables_returnsSafeCollection() {
+        // GIVEN a variety of registered dumpables
+        dumpManager.registerCriticalDumpable("dumpable1", dumpable1)
+        dumpManager.registerCriticalDumpable("dumpable2", dumpable2)
+        dumpManager.registerNormalDumpable("dumpable3", dumpable3)
+
+        // WHEN the collection is retrieved
+        val dumpables = dumpManager.getDumpables()
+
+        // WHEN the collection changes from underneath
+        dumpManager.unregisterDumpable("dumpable1")
+        dumpManager.unregisterDumpable("dumpable2")
+        dumpManager.unregisterDumpable("dumpable3")
+
+        // THEN new collections are empty
+        assertThat(dumpManager.getDumpables()).isEmpty()
+
+        // AND the collection is still safe to use
+        assertThat(dumpables).hasSize(3)
+    }
+
+    @Test
+    fun getBuffers_returnsSafeCollection() {
+        // GIVEN a set of registered buffers
+        dumpManager.registerBuffer("buffer1", buffer1)
+        dumpManager.registerBuffer("buffer2", buffer2)
+
+        // WHEN the collection is requested
+        val buffers = dumpManager.getLogBuffers()
+
+        // WHEN the collection changes
+        dumpManager.registerBuffer("buffer3", buffer1)
+
+        // THEN the new entry is represented
+        assertThat(dumpManager.getLogBuffers()).hasSize(3)
+
+        // AND the previous collection is unchanged
+        assertThat(buffers).hasSize(2)
+    }
+
+    @Test
+    fun getTableBuffers_returnsSafeCollection() {
+        // GIVEN a set of registered buffers
+        dumpManager.registerTableLogBuffer("table1", table1)
+        dumpManager.registerTableLogBuffer("table2", table2)
+
+        // WHEN the collection is requested
+        val tables = dumpManager.getTableLogBuffers()
+
+        // WHEN the collection changes
+        dumpManager.registerTableLogBuffer("table3", table1)
+
+        // THEN the new entry is represented
+        assertThat(dumpManager.getTableLogBuffers()).hasSize(3)
+
+        // AND the previous collection is unchanged
+        assertThat(tables).hasSize(2)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
index bd029a7..a341ca3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.dump
 
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.LogcatEchoTracker
 
 /**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt
index cb38846..3ff7202 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt
@@ -18,20 +18,14 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpHandler.Companion.dump
+import com.android.systemui.log.LogBuffer
 import com.android.systemui.util.io.FakeBasicFileAttributes
 import com.android.systemui.util.io.Files
 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 org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
 import java.io.BufferedWriter
 import java.io.ByteArrayOutputStream
 import java.io.IOException
@@ -42,17 +36,29 @@
 import java.nio.file.Paths
 import java.nio.file.attribute.BasicFileAttributes
 import java.util.Arrays
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
 
 @SmallTest
 class LogEulogizerTest : SysuiTestCase() {
 
     lateinit var eulogizer: LogBufferEulogizer
 
-    @Mock
-    lateinit var dumpManager: DumpManager
+    @Mock lateinit var dumpManager: DumpManager
+    @Mock lateinit var logBuffer1: LogBuffer
+    lateinit var logBufferEntry1: DumpsysEntry.LogBufferEntry
+    @Mock lateinit var logBuffer2: LogBuffer
+    lateinit var logBufferEntry2: DumpsysEntry.LogBufferEntry
 
-    @Mock
-    lateinit var files: Files
+    @Mock lateinit var files: Files
 
     private val clock = FakeSystemClock()
 
@@ -67,37 +73,47 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        logBufferEntry1 = DumpsysEntry.LogBufferEntry(logBuffer1, "logbuffer1")
+        logBufferEntry2 = DumpsysEntry.LogBufferEntry(logBuffer2, "logbuffer2")
 
-        eulogizer =
-                LogBufferEulogizer(dumpManager, clock, files, path, MIN_WRITE_GAP, MAX_READ_AGE)
+        eulogizer = LogBufferEulogizer(dumpManager, clock, files, path, MIN_WRITE_GAP, MAX_READ_AGE)
 
         Mockito.`when`(files.newBufferedWriter(eq(path), any(OpenOption::class.java)))
-                .thenReturn(fileWriter)
+            .thenReturn(fileWriter)
 
         Mockito.`when`(
-                files.readAttributes(eq(path),
-                eq(BasicFileAttributes::class.java),
-                any(LinkOption::class.java))
-        ).thenReturn(fileAttrs)
+                files.readAttributes(
+                    eq(path),
+                    eq(BasicFileAttributes::class.java),
+                    any(LinkOption::class.java)
+                )
+            )
+            .thenReturn(fileAttrs)
 
         Mockito.`when`(files.lines(eq(path))).thenReturn(Arrays.stream(FAKE_LINES))
+
+        whenever(dumpManager.getLogBuffers()).thenReturn(listOf(logBufferEntry1, logBufferEntry2))
     }
 
     @Test
     fun testFileIsCreated() {
         // GIVEN that the log file doesn't already exist
         Mockito.`when`(
-                files.readAttributes(eq(path),
-                        eq(BasicFileAttributes::class.java),
-                        any(LinkOption::class.java))
-        ).thenThrow(IOException("File not found"))
+                files.readAttributes(
+                    eq(path),
+                    eq(BasicFileAttributes::class.java),
+                    any(LinkOption::class.java)
+                )
+            )
+            .thenThrow(IOException("File not found"))
 
         // WHEN .record() is called
         val exception = RuntimeException("Something bad happened")
         assertEquals(exception, eulogizer.record(exception))
 
         // THEN the buffers are dumped to the file
-        verify(dumpManager).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt())
+        verify(logBuffer1).dump(any(PrintWriter::class.java), anyInt())
+        verify(logBuffer2).dump(any(PrintWriter::class.java), anyInt())
         assertTrue(fileStream.toString().isNotEmpty())
     }
 
@@ -111,7 +127,8 @@
         assertEquals(exception, eulogizer.record(exception))
 
         // THEN the buffers are dumped to the file
-        verify(dumpManager).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt())
+        verify(logBuffer1).dump(any(PrintWriter::class.java), anyInt())
+        verify(logBuffer2).dump(any(PrintWriter::class.java), anyInt())
         assertTrue(fileStream.toString().isNotEmpty())
     }
 
@@ -125,7 +142,8 @@
         assertEquals(exception, eulogizer.record(exception))
 
         // THEN the file isn't written to
-        verify(dumpManager, never()).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt())
+        verify(logBuffer1, never()).dump(any(PrintWriter::class.java), anyInt())
+        verify(logBuffer2, never()).dump(any(PrintWriter::class.java), anyInt())
         assertTrue(fileStream.toString().isEmpty())
     }
 
@@ -161,9 +179,4 @@
 private const val MIN_WRITE_GAP = 10L
 private const val MAX_READ_AGE = 100L
 
-private val FAKE_LINES =
-        arrayOf(
-                "First line",
-                "Second line",
-                "Third line"
-        )
+private val FAKE_LINES = arrayOf("First line", "Second line", "Third line")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
index c9ee1e8..6aa5a00 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
@@ -67,6 +67,7 @@
 import com.android.systemui.plugins.GlobalActions;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -128,6 +129,7 @@
     @Mock private UserContextProvider mUserContextProvider;
     @Mock private VibratorHelper mVibratorHelper;
     @Mock private CentralSurfaces mCentralSurfaces;
+    @Mock private ShadeController mShadeController;
     @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @Mock private DialogLaunchAnimator mDialogLaunchAnimator;
     @Mock private OnBackInvokedDispatcher mOnBackInvokedDispatcher;
@@ -177,6 +179,7 @@
                 mHandler,
                 mPackageManager,
                 Optional.of(mCentralSurfaces),
+                mShadeController,
                 mKeyguardUpdateMonitor,
                 mDialogLaunchAnimator);
         mGlobalActionsDialogLite.setZeroDialogPressDelayForTesting();
@@ -317,7 +320,7 @@
         MotionEvent end = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 500, 0);
         gestureListener.onFling(start, end, 0, 1000);
         verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_CLOSE_TAP_OUTSIDE);
-        verify(mCentralSurfaces).animateExpandSettingsPanel(null);
+        verify(mShadeController).animateExpandQs();
     }
 
     @Test
@@ -341,7 +344,7 @@
         MotionEvent end = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 500, 0);
         gestureListener.onFling(start, end, 0, 1000);
         verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_CLOSE_TAP_OUTSIDE);
-        verify(mCentralSurfaces).animateExpandNotificationsPanel();
+        verify(mShadeController).animateExpandShade();
     }
 
     @Test
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 cdc99af3..3383516 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
@@ -34,6 +34,8 @@
 import com.android.systemui.SystemUIAppComponentFactoryBase
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.dock.DockManagerFake
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
@@ -43,7 +45,6 @@
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
 import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-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
@@ -188,6 +189,7 @@
                         commandQueue = commandQueue,
                         featureFlags = featureFlags,
                         bouncerRepository = FakeKeyguardBouncerRepository(),
+                        configurationRepository = FakeConfigurationRepository(),
                     ),
                 registry = mock(),
                 lockPatternUtils = lockPatternUtils,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
index 729a1cc..ce8028c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
@@ -19,6 +19,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 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;
@@ -31,6 +32,7 @@
 import android.media.MediaMetadata;
 import android.media.session.PlaybackState;
 import android.net.Uri;
+import android.os.Handler;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -166,6 +168,7 @@
 
     @Test
     public void updatesClock() {
+        clearInvocations(mContentResolver);
         mProvider.mKeyguardUpdateMonitorCallback.onTimeChanged();
         TestableLooper.get(this).processAllMessages();
         verify(mContentResolver).notifyChange(eq(mProvider.getUri()), eq(null));
@@ -217,11 +220,13 @@
         reset(mContentResolver);
         mProvider.onPrimaryMetadataOrStateChanged(mock(MediaMetadata.class),
                 PlaybackState.STATE_PLAYING);
+        TestableLooper.get(this).processAllMessages();
         verify(mContentResolver).notifyChange(eq(mProvider.getUri()), eq(null));
 
         // Hides after waking up
         reset(mContentResolver);
         mProvider.onDozingChanged(false);
+        TestableLooper.get(this).processAllMessages();
         verify(mContentResolver).notifyChange(eq(mProvider.getUri()), eq(null));
     }
 
@@ -231,6 +236,7 @@
         mProvider.onPrimaryMetadataOrStateChanged(mock(MediaMetadata.class),
                 PlaybackState.STATE_PLAYING);
         reset(mContentResolver);
+        TestableLooper.get(this).processAllMessages();
         // Show media when dozing
         mProvider.onDozingChanged(true);
         verify(mContentResolver).notifyChange(eq(mProvider.getUri()), eq(null));
@@ -272,6 +278,8 @@
             mMediaManager = KeyguardSliceProviderTest.this.mNotificationMediaManager;
             mKeyguardUpdateMonitor = KeyguardSliceProviderTest.this.mKeyguardUpdateMonitor;
             mUserTracker = KeyguardSliceProviderTest.this.mUserTracker;
+            mBgHandler =
+                    new Handler(TestableLooper.get(KeyguardSliceProviderTest.this).getLooper());
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
index 2230841..9a2936e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
@@ -385,8 +385,6 @@
         // We expect that we've set the surface behind to alpha = 0f since we're not interactive.
         assertEquals(0f, params.alpha)
         assertTrue(params.matrix.isIdentity)
-        assertEquals("Wallpaper surface was expected to have opacity 0",
-                0f, captorWp.getLastValue().alpha)
 
         verifyNoMoreInteractions(surfaceTransactionApplier)
     }
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 7a501a8..666978e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -16,12 +16,17 @@
 
 package com.android.systemui.keyguard;
 
+import static android.os.PowerManager.WAKE_REASON_WAKE_MOTION;
+import static android.provider.Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT;
 import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManagerPolicyConstants.OFF_BECAUSE_OF_TIMEOUT;
 import static android.view.WindowManagerPolicyConstants.OFF_BECAUSE_OF_USER;
 
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
+import static com.android.systemui.keyguard.KeyguardViewMediator.DELAYED_KEYGUARD_ACTION;
+import static com.android.systemui.keyguard.KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT;
 import static com.android.systemui.keyguard.KeyguardViewMediator.REBOOT_MAINLINE_UPDATE;
 import static com.android.systemui.keyguard.KeyguardViewMediator.SYS_BOOT_REASON_PROP;
 
@@ -31,6 +36,7 @@
 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.Mockito.atLeast;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
@@ -38,9 +44,12 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.AlarmManager;
 import android.app.IActivityManager;
+import android.app.PendingIntent;
 import android.app.admin.DevicePolicyManager;
 import android.app.trust.TrustManager;
+import android.content.Context;
 import android.os.PowerManager;
 import android.os.PowerManager.WakeLock;
 import android.telephony.TelephonyManager;
@@ -99,9 +108,12 @@
 import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.util.DeviceConfigProxyFake;
 import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.settings.SystemSettings;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.wm.shell.keyguard.KeyguardTransitions;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -166,21 +178,28 @@
     private @Mock CentralSurfaces mCentralSurfaces;
     private @Mock UiEventLogger mUiEventLogger;
     private @Mock SessionTracker mSessionTracker;
-    private @Mock SystemPropertiesHelper mSystemPropertiesHelper;
+    private @Mock SystemSettings mSystemSettings;
+    private @Mock SecureSettings mSecureSettings;
+    private @Mock AlarmManager mAlarmManager;
+    private FakeSystemClock mSystemClock;
+
     private @Mock CoroutineDispatcher mDispatcher;
     private @Mock DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
+    private @Mock SystemPropertiesHelper mSystemPropertiesHelper;
 
     private FakeFeatureFlags mFeatureFlags;
+    private int mInitialUserId;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         mFalsingCollector = new FalsingCollectorFake();
-
+        mSystemClock = new FakeSystemClock();
         when(mLockPatternUtils.getDevicePolicyManager()).thenReturn(mDevicePolicyManager);
         when(mPowerManager.newWakeLock(anyInt(), any())).thenReturn(mock(WakeLock.class));
         when(mInteractionJankMonitor.begin(any(), anyInt())).thenReturn(true);
         when(mInteractionJankMonitor.end(anyInt())).thenReturn(true);
+        mContext.addMockSystemService(Context.ALARM_SERVICE, mAlarmManager);
         final ViewRootImpl testViewRoot = mock(ViewRootImpl.class);
         when(testViewRoot.getView()).thenReturn(mock(View.class));
         when(mStatusBarKeyguardViewManager.getViewRootImpl()).thenReturn(testViewRoot);
@@ -200,6 +219,12 @@
         DejankUtils.setImmediate(true);
 
         createAndStartViewMediator();
+        mInitialUserId = KeyguardUpdateMonitor.getCurrentUser();
+    }
+
+    @After
+    public void teardown() {
+        KeyguardUpdateMonitor.setCurrentUser(mInitialUserId);
     }
 
     @Test
@@ -298,6 +323,27 @@
 
     @Test
     @TestableLooper.RunWithLooper(setAsMainLooper = true)
+    public void wakeupFromDreamingWhenKeyguardHides() {
+        mViewMediator.onSystemReady();
+        TestableLooper.get(this).processAllMessages();
+
+        // Given device is dreaming
+        when(mUpdateMonitor.isDreaming()).thenReturn(true);
+
+        // When keyguard is going away
+        mKeyguardStateController.notifyKeyguardGoingAway(true);
+
+        // And keyguard is disabled which will call #handleHide
+        mViewMediator.setKeyguardEnabled(false);
+        TestableLooper.get(this).processAllMessages();
+
+        // Then dream should wake up
+        verify(mPowerManager).wakeUp(anyLong(), anyInt(),
+                eq("com.android.systemui:UNLOCK_DREAMING"));
+    }
+
+    @Test
+    @TestableLooper.RunWithLooper(setAsMainLooper = true)
     public void restoreBouncerWhenSimLockedAndKeyguardIsGoingAway() {
         // When showing and provisioned
         mViewMediator.onSystemReady();
@@ -440,6 +486,49 @@
     }
 
     @Test
+    public void lockAfterScreenTimeoutUsesValueFromSettings() {
+        int currentUserId = 99;
+        int userSpecificTimeout = 5999;
+        KeyguardUpdateMonitor.setCurrentUser(currentUserId);
+
+        when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
+        when(mDevicePolicyManager.getMaximumTimeToLock(null, currentUserId)).thenReturn(0L);
+        when(mSecureSettings.getIntForUser(LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+                KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, currentUserId)).thenReturn(userSpecificTimeout);
+        mSystemClock.setElapsedRealtime(0L);
+        ArgumentCaptor<PendingIntent> pendingIntent = ArgumentCaptor.forClass(PendingIntent.class);
+
+        mViewMediator.onStartedGoingToSleep(OFF_BECAUSE_OF_TIMEOUT);
+
+        verify(mAlarmManager).setExactAndAllowWhileIdle(eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
+                eq(Long.valueOf(userSpecificTimeout)), pendingIntent.capture());
+        assertEquals(DELAYED_KEYGUARD_ACTION, pendingIntent.getValue().getIntent().getAction());
+    }
+
+    @Test
+    public void lockAfterSpecifiedAfterDreamStarted() {
+        int currentUserId = 99;
+        int userSpecificTimeout = 5999;
+        KeyguardUpdateMonitor.setCurrentUser(currentUserId);
+
+        // set mDeviceInteractive to true
+        mViewMediator.onStartedWakingUp(WAKE_REASON_WAKE_MOTION, false);
+        mFeatureFlags.set(Flags.LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING, false);
+        when(mLockPatternUtils.isSecure(currentUserId)).thenReturn(true);
+        when(mDevicePolicyManager.getMaximumTimeToLock(null, currentUserId)).thenReturn(0L);
+        when(mSecureSettings.getIntForUser(LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+                KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, currentUserId)).thenReturn(userSpecificTimeout);
+        mSystemClock.setElapsedRealtime(0L);
+        ArgumentCaptor<PendingIntent> pendingIntent = ArgumentCaptor.forClass(PendingIntent.class);
+
+        mViewMediator.onDreamingStarted();
+
+        verify(mAlarmManager).setExactAndAllowWhileIdle(eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
+                eq(Long.valueOf(userSpecificTimeout)), pendingIntent.capture());
+        assertEquals(DELAYED_KEYGUARD_ACTION, pendingIntent.getValue().getIntent().getAction());
+    }
+
+    @Test
     public void testHideSurfaceBehindKeyguardMarksKeyguardNotGoingAway() {
         mViewMediator.hideSurfaceBehindKeyguard();
 
@@ -693,6 +782,11 @@
         assertTrue(mViewMediator.isShowingAndNotOccluded());
     }
 
+    @Test
+    public void testBouncerSwipeDown() {
+        mViewMediator.getViewMediatorCallback().onBouncerSwipeDown();
+        verify(mStatusBarKeyguardViewManager).reset(true);
+    }
     private void createAndStartViewMediator() {
         mViewMediator = new KeyguardViewMediator(
                 mContext,
@@ -728,6 +822,9 @@
                 () -> mActivityLaunchAnimator,
                 () -> mScrimController,
                 mFeatureFlags,
+                mSecureSettings,
+                mSystemSettings,
+                mSystemClock,
                 mDispatcher,
                 () -> mDreamingToLockscreenTransitionViewModel,
                 mSystemPropertiesHelper);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
index 6447368..925ac30 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
@@ -7,12 +7,10 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.FakeCommandQueue
-import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.shared.model.WakeSleepReason
@@ -60,20 +58,20 @@
         keyguardRepository.setDozeAmount(0f)
         keyguardRepository.setKeyguardGoingAway(false)
 
-        val keyguardInteractor =
-            KeyguardInteractor(
-                keyguardRepository,
-                FakeCommandQueue(),
-                featureFlags,
-                FakeKeyguardBouncerRepository()
+        val withDeps =
+            KeyguardInteractorFactory.create(
+                repository = keyguardRepository,
+                featureFlags = featureFlags,
             )
+        val keyguardInteractor = withDeps.keyguardInteractor
         resourceTrimmer =
             ResourceTrimmer(
                 keyguardInteractor,
-                KeyguardTransitionInteractor(
-                    keyguardTransitionRepository,
-                    testScope.backgroundScope
-                ),
+                KeyguardTransitionInteractorFactory.create(
+                        scope = TestScope().backgroundScope,
+                        repository = keyguardTransitionRepository,
+                    )
+                    .keyguardTransitionInteractor,
                 globalWindowManager,
                 testScope.backgroundScope,
                 testDispatcher,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/factory/BouncerMessageFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/factory/BouncerMessageFactoryTest.kt
deleted file mode 100644
index 0ef77e8..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/factory/BouncerMessageFactoryTest.kt
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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.bouncer.data.factory
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.keyguard.KeyguardSecurityModel
-import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
-import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Password
-import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Pattern
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT
-import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.StringSubject
-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.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@Ignore("b/236891644")
-class BouncerMessageFactoryTest : SysuiTestCase() {
-    private lateinit var underTest: BouncerMessageFactory
-
-    @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
-
-    @Mock private lateinit var securityModel: KeyguardSecurityModel
-
-    private lateinit var testScope: TestScope
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        testScope = TestScope()
-        underTest = BouncerMessageFactory(updateMonitor, securityModel)
-    }
-
-    @Test
-    fun bouncerMessages_choosesTheRightMessage_basedOnSecurityModeAndFpAllowedInBouncer() =
-        testScope.runTest {
-            primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAllowedInBouncer = false)
-                .isEqualTo("Enter PIN")
-            primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAllowedInBouncer = true)
-                .isEqualTo("Unlock with PIN or fingerprint")
-
-            primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAllowedInBouncer = false)
-                .isEqualTo("Enter password")
-            primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAllowedInBouncer = true)
-                .isEqualTo("Unlock with password or fingerprint")
-
-            primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAllowedInBouncer = false)
-                .isEqualTo("Draw pattern")
-            primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAllowedInBouncer = true)
-                .isEqualTo("Unlock with pattern or fingerprint")
-        }
-
-    @Test
-    fun bouncerMessages_setsPrimaryAndSecondaryMessage_basedOnSecurityModeAndFpAllowedInBouncer() =
-        testScope.runTest {
-            primaryMessage(
-                    PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
-                    mode = PIN,
-                    fpAllowedInBouncer = true
-                )
-                .isEqualTo("Wrong PIN. Try again.")
-            secondaryMessage(
-                    PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
-                    mode = PIN,
-                    fpAllowedInBouncer = true
-                )
-                .isEqualTo("Or unlock with fingerprint")
-
-            primaryMessage(
-                    PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
-                    mode = Password,
-                    fpAllowedInBouncer = true
-                )
-                .isEqualTo("Wrong password. Try again.")
-            secondaryMessage(
-                    PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
-                    mode = Password,
-                    fpAllowedInBouncer = true
-                )
-                .isEqualTo("Or unlock with fingerprint")
-
-            primaryMessage(
-                    PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
-                    mode = Pattern,
-                    fpAllowedInBouncer = true
-                )
-                .isEqualTo("Wrong pattern. Try again.")
-            secondaryMessage(
-                    PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
-                    mode = Pattern,
-                    fpAllowedInBouncer = true
-                )
-                .isEqualTo("Or unlock with fingerprint")
-        }
-
-    private fun primaryMessage(
-        reason: Int,
-        mode: KeyguardSecurityModel.SecurityMode,
-        fpAllowedInBouncer: Boolean
-    ): StringSubject {
-        return assertThat(
-            context.resources.getString(
-                bouncerMessageModel(mode, fpAllowedInBouncer, reason)!!.message!!.messageResId!!
-            )
-        )!!
-    }
-
-    private fun secondaryMessage(
-        reason: Int,
-        mode: KeyguardSecurityModel.SecurityMode,
-        fpAllowedInBouncer: Boolean
-    ): StringSubject {
-        return assertThat(
-            context.resources.getString(
-                bouncerMessageModel(mode, fpAllowedInBouncer, reason)!!
-                    .secondaryMessage!!
-                    .messageResId!!
-            )
-        )!!
-    }
-
-    private fun bouncerMessageModel(
-        mode: KeyguardSecurityModel.SecurityMode,
-        fpAllowedInBouncer: Boolean,
-        reason: Int
-    ): BouncerMessageModel? {
-        whenever(securityModel.getSecurityMode(0)).thenReturn(mode)
-        whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(fpAllowedInBouncer)
-
-        return underTest.createFromPromptReason(reason, 0)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepositoryTest.kt
deleted file mode 100644
index a729de5..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepositoryTest.kt
+++ /dev/null
@@ -1,365 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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.bouncer.data.repository
-
-import android.content.pm.UserInfo
-import android.hardware.biometrics.BiometricSourceType
-import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED
-import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST
-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.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW
-import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT
-import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT
-import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT
-import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN
-import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE
-import com.android.keyguard.KeyguardSecurityModel
-import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.keyguard.KeyguardUpdateMonitorCallback
-import com.android.systemui.R
-import com.android.systemui.R.string.keyguard_enter_pin
-import com.android.systemui.R.string.kg_prompt_after_dpm_lock
-import com.android.systemui.R.string.kg_prompt_after_user_lockdown_pin
-import com.android.systemui.R.string.kg_prompt_auth_timeout
-import com.android.systemui.R.string.kg_prompt_pin_auth_timeout
-import com.android.systemui.R.string.kg_prompt_reason_restart_pin
-import com.android.systemui.R.string.kg_prompt_unattended_update
-import com.android.systemui.R.string.kg_trust_agent_disabled
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory
-import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
-import com.android.systemui.keyguard.bouncer.shared.model.Message
-import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
-import com.android.systemui.keyguard.shared.model.AuthenticationFlags
-import com.android.systemui.user.data.repository.FakeUserRepository
-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.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.verify
-import org.mockito.MockitoAnnotations
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidJUnit4::class)
-@Ignore("b/236891644")
-class BouncerMessageRepositoryTest : SysuiTestCase() {
-
-    @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
-    @Mock private lateinit var securityModel: KeyguardSecurityModel
-    @Captor
-    private lateinit var updateMonitorCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback>
-
-    private lateinit var underTest: BouncerMessageRepository
-    private lateinit var trustRepository: FakeTrustRepository
-    private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
-    private lateinit var userRepository: FakeUserRepository
-    private lateinit var fingerprintRepository: FakeDeviceEntryFingerprintAuthRepository
-    private lateinit var testScope: TestScope
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        trustRepository = FakeTrustRepository()
-        biometricSettingsRepository = FakeBiometricSettingsRepository()
-        userRepository = FakeUserRepository()
-        userRepository.setUserInfos(listOf(PRIMARY_USER))
-        fingerprintRepository = FakeDeviceEntryFingerprintAuthRepository()
-        testScope = TestScope()
-
-        whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(false)
-        whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
-        underTest =
-            BouncerMessageRepositoryImpl(
-                trustRepository = trustRepository,
-                biometricSettingsRepository = biometricSettingsRepository,
-                updateMonitor = updateMonitor,
-                bouncerMessageFactory = BouncerMessageFactory(updateMonitor, securityModel),
-                userRepository = userRepository,
-                fingerprintAuthRepository = fingerprintRepository
-            )
-    }
-
-    @Test
-    fun setCustomMessage_propagatesState() =
-        testScope.runTest {
-            underTest.setCustomMessage(message("not empty"))
-
-            val customMessage = collectLastValue(underTest.customMessage)
-
-            assertThat(customMessage()).isEqualTo(message("not empty"))
-        }
-
-    @Test
-    fun setFaceMessage_propagatesState() =
-        testScope.runTest {
-            underTest.setFaceAcquisitionMessage(message("not empty"))
-
-            val faceAcquisitionMessage = collectLastValue(underTest.faceAcquisitionMessage)
-
-            assertThat(faceAcquisitionMessage()).isEqualTo(message("not empty"))
-        }
-
-    @Test
-    fun setFpMessage_propagatesState() =
-        testScope.runTest {
-            underTest.setFingerprintAcquisitionMessage(message("not empty"))
-
-            val fpAcquisitionMsg = collectLastValue(underTest.fingerprintAcquisitionMessage)
-
-            assertThat(fpAcquisitionMsg()).isEqualTo(message("not empty"))
-        }
-
-    @Test
-    fun setPrimaryAuthMessage_propagatesState() =
-        testScope.runTest {
-            underTest.setPrimaryAuthMessage(message("not empty"))
-
-            val primaryAuthMessage = collectLastValue(underTest.primaryAuthMessage)
-
-            assertThat(primaryAuthMessage()).isEqualTo(message("not empty"))
-        }
-
-    @Test
-    fun biometricAuthMessage_propagatesBiometricAuthMessages() =
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(PRIMARY_USER)
-            val biometricAuthMessage = collectLastValue(underTest.biometricAuthMessage)
-            runCurrent()
-
-            verify(updateMonitor).registerCallback(updateMonitorCallback.capture())
-
-            updateMonitorCallback.value.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT)
-
-            assertThat(biometricAuthMessage())
-                .isEqualTo(message(R.string.kg_fp_not_recognized, R.string.kg_bio_try_again_or_pin))
-
-            updateMonitorCallback.value.onBiometricAuthFailed(BiometricSourceType.FACE)
-
-            assertThat(biometricAuthMessage())
-                .isEqualTo(
-                    message(R.string.bouncer_face_not_recognized, R.string.kg_bio_try_again_or_pin)
-                )
-
-            updateMonitorCallback.value.onBiometricAcquired(BiometricSourceType.FACE, 0)
-
-            assertThat(biometricAuthMessage()).isNull()
-        }
-
-    @Test
-    fun onFaceLockout_propagatesState() =
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(PRIMARY_USER)
-            val lockoutMessage = collectLastValue(underTest.biometricLockedOutMessage)
-            runCurrent()
-            verify(updateMonitor).registerCallback(updateMonitorCallback.capture())
-
-            whenever(updateMonitor.isFaceLockedOut).thenReturn(true)
-            updateMonitorCallback.value.onLockedOutStateChanged(BiometricSourceType.FACE)
-
-            assertThat(lockoutMessage())
-                .isEqualTo(message(keyguard_enter_pin, R.string.kg_face_locked_out))
-
-            whenever(updateMonitor.isFaceLockedOut).thenReturn(false)
-            updateMonitorCallback.value.onLockedOutStateChanged(BiometricSourceType.FACE)
-            assertThat(lockoutMessage()).isNull()
-        }
-
-    @Test
-    fun onFingerprintLockout_propagatesState() =
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(PRIMARY_USER)
-            val lockedOutMessage = collectLastValue(underTest.biometricLockedOutMessage)
-            runCurrent()
-
-            fingerprintRepository.setLockedOut(true)
-
-            assertThat(lockedOutMessage())
-                .isEqualTo(message(keyguard_enter_pin, R.string.kg_fp_locked_out))
-
-            fingerprintRepository.setLockedOut(false)
-            assertThat(lockedOutMessage()).isNull()
-        }
-
-    @Test
-    fun onAuthFlagsChanged_withTrustNotManagedAndNoBiometrics_isANoop() =
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(PRIMARY_USER)
-            trustRepository.setCurrentUserTrustManaged(false)
-            biometricSettingsRepository.setFaceEnrolled(false)
-            biometricSettingsRepository.setFingerprintEnrolled(false)
-
-            verifyMessagesForAuthFlag(
-                STRONG_AUTH_NOT_REQUIRED to null,
-                STRONG_AUTH_REQUIRED_AFTER_BOOT to null,
-                SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
-                STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
-                STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to null,
-                STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to null,
-                STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to null,
-                SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null,
-                STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to null,
-                STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
-                    Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
-            )
-        }
-
-    @Test
-    fun authFlagsChanges_withTrustManaged_providesDifferentMessages() =
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(PRIMARY_USER)
-            biometricSettingsRepository.setFaceEnrolled(false)
-            biometricSettingsRepository.setFingerprintEnrolled(false)
-
-            trustRepository.setCurrentUserTrustManaged(true)
-
-            verifyMessagesForAuthFlag(
-                STRONG_AUTH_NOT_REQUIRED to null,
-                STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
-                STRONG_AUTH_REQUIRED_AFTER_BOOT to
-                    Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin),
-                STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
-                    Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout),
-                STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
-                    Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
-                SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
-                    Pair(keyguard_enter_pin, kg_trust_agent_disabled),
-                SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
-                    Pair(keyguard_enter_pin, kg_trust_agent_disabled),
-                STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
-                    Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin),
-                STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
-                    Pair(keyguard_enter_pin, kg_prompt_unattended_update),
-                STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
-                    Pair(keyguard_enter_pin, kg_prompt_auth_timeout),
-            )
-        }
-
-    @Test
-    fun authFlagsChanges_withFaceEnrolled_providesDifferentMessages() =
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(PRIMARY_USER)
-            trustRepository.setCurrentUserTrustManaged(false)
-            biometricSettingsRepository.setFingerprintEnrolled(false)
-
-            biometricSettingsRepository.setIsFaceAuthEnabled(true)
-            biometricSettingsRepository.setFaceEnrolled(true)
-
-            verifyMessagesForAuthFlag(
-                STRONG_AUTH_NOT_REQUIRED to null,
-                STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
-                SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
-                SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null,
-                STRONG_AUTH_REQUIRED_AFTER_BOOT to
-                    Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin),
-                STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
-                    Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout),
-                STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
-                    Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
-                STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
-                    Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin),
-                STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
-                    Pair(keyguard_enter_pin, kg_prompt_unattended_update),
-                STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
-                    Pair(keyguard_enter_pin, kg_prompt_auth_timeout),
-            )
-        }
-
-    @Test
-    fun authFlagsChanges_withFingerprintEnrolled_providesDifferentMessages() =
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(PRIMARY_USER)
-            trustRepository.setCurrentUserTrustManaged(false)
-            biometricSettingsRepository.setIsFaceAuthEnabled(false)
-            biometricSettingsRepository.setFaceEnrolled(false)
-
-            biometricSettingsRepository.setFingerprintEnrolled(true)
-            biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(true)
-
-            verifyMessagesForAuthFlag(
-                STRONG_AUTH_NOT_REQUIRED to null,
-                STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
-                SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
-                SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null,
-                STRONG_AUTH_REQUIRED_AFTER_BOOT to
-                    Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin),
-                STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
-                    Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout),
-                STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
-                    Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
-                STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
-                    Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin),
-                STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
-                    Pair(keyguard_enter_pin, kg_prompt_unattended_update),
-                STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
-                    Pair(keyguard_enter_pin, kg_prompt_auth_timeout),
-            )
-        }
-
-    private fun TestScope.verifyMessagesForAuthFlag(
-        vararg authFlagToExpectedMessages: Pair<Int, Pair<Int, Int>?>
-    ) {
-        val authFlagsMessage = collectLastValue(underTest.authFlagsMessage)
-
-        authFlagToExpectedMessages.forEach { (flag, messagePair) ->
-            biometricSettingsRepository.setAuthenticationFlags(
-                AuthenticationFlags(PRIMARY_USER_ID, flag)
-            )
-
-            assertThat(authFlagsMessage())
-                .isEqualTo(messagePair?.let { message(it.first, it.second) })
-        }
-    }
-
-    private fun message(primaryResId: Int, secondaryResId: Int): BouncerMessageModel {
-        return BouncerMessageModel(
-            message = Message(messageResId = primaryResId),
-            secondaryMessage = Message(messageResId = secondaryResId)
-        )
-    }
-    private fun message(value: String): BouncerMessageModel {
-        return BouncerMessageModel(message = Message(message = value))
-    }
-
-    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
-            )
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
deleted file mode 100644
index 3a87680..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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.bouncer.domain.interactor
-
-import android.content.pm.UserInfo
-import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.keyguard.KeyguardSecurityModel
-import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.R.string.keyguard_enter_pin
-import com.android.systemui.R.string.kg_too_many_failed_attempts_countdown
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.FlowValue
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory
-import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
-import com.android.systemui.keyguard.bouncer.shared.model.Message
-import com.android.systemui.keyguard.data.repository.FakeBouncerMessageRepository
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.util.mockito.KotlinArgumentCaptor
-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.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidJUnit4::class)
-@Ignore("b/236891644")
-class BouncerMessageInteractorTest : SysuiTestCase() {
-
-    @Mock private lateinit var securityModel: KeyguardSecurityModel
-    @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
-    @Mock private lateinit var countDownTimerUtil: CountDownTimerUtil
-    private lateinit var countDownTimerCallback: KotlinArgumentCaptor<CountDownTimerCallback>
-    private lateinit var underTest: BouncerMessageInteractor
-    private lateinit var repository: FakeBouncerMessageRepository
-    private lateinit var userRepository: FakeUserRepository
-    private lateinit var testScope: TestScope
-    private lateinit var bouncerMessage: FlowValue<BouncerMessageModel?>
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        repository = FakeBouncerMessageRepository()
-        userRepository = FakeUserRepository()
-        userRepository.setUserInfos(listOf(PRIMARY_USER))
-        testScope = TestScope()
-        countDownTimerCallback = KotlinArgumentCaptor(CountDownTimerCallback::class.java)
-
-        allowTestableLooperAsMainThread()
-        whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
-        whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(false)
-    }
-
-    suspend fun TestScope.init() {
-        userRepository.setSelectedUserInfo(PRIMARY_USER)
-        val featureFlags = FakeFeatureFlags()
-        featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
-        underTest =
-            BouncerMessageInteractor(
-                repository = repository,
-                factory = BouncerMessageFactory(updateMonitor, securityModel),
-                userRepository = userRepository,
-                countDownTimerUtil = countDownTimerUtil,
-                featureFlags = featureFlags
-            )
-        bouncerMessage = collectLastValue(underTest.bouncerMessage)
-    }
-
-    @Test
-    fun onIncorrectSecurityInput_setsTheBouncerModelInTheRepository() =
-        testScope.runTest {
-            init()
-            underTest.onPrimaryAuthIncorrectAttempt()
-
-            assertThat(repository.primaryAuthMessage).isNotNull()
-            assertThat(
-                    context.resources.getString(
-                        repository.primaryAuthMessage.value!!.message!!.messageResId!!
-                    )
-                )
-                .isEqualTo("Wrong PIN. Try again.")
-        }
-
-    @Test
-    fun onUserStartsPrimaryAuthInput_clearsAllSetBouncerMessages() =
-        testScope.runTest {
-            init()
-            repository.setCustomMessage(message("not empty"))
-            repository.setFaceAcquisitionMessage(message("not empty"))
-            repository.setFingerprintAcquisitionMessage(message("not empty"))
-            repository.setPrimaryAuthMessage(message("not empty"))
-
-            underTest.onPrimaryBouncerUserInput()
-
-            assertThat(repository.customMessage.value).isNull()
-            assertThat(repository.faceAcquisitionMessage.value).isNull()
-            assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
-            assertThat(repository.primaryAuthMessage.value).isNull()
-        }
-
-    @Test
-    fun onBouncerBeingHidden_clearsAllSetBouncerMessages() =
-        testScope.runTest {
-            init()
-            repository.setCustomMessage(message("not empty"))
-            repository.setFaceAcquisitionMessage(message("not empty"))
-            repository.setFingerprintAcquisitionMessage(message("not empty"))
-            repository.setPrimaryAuthMessage(message("not empty"))
-
-            underTest.onBouncerBeingHidden()
-
-            assertThat(repository.customMessage.value).isNull()
-            assertThat(repository.faceAcquisitionMessage.value).isNull()
-            assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
-            assertThat(repository.primaryAuthMessage.value).isNull()
-        }
-
-    @Test
-    fun setCustomMessage_setsRepositoryValue() =
-        testScope.runTest {
-            init()
-
-            underTest.setCustomMessage("not empty")
-
-            assertThat(repository.customMessage.value)
-                .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
-
-            underTest.setCustomMessage(null)
-            assertThat(repository.customMessage.value).isNull()
-        }
-
-    @Test
-    fun setFaceMessage_setsRepositoryValue() =
-        testScope.runTest {
-            init()
-
-            underTest.setFaceAcquisitionMessage("not empty")
-
-            assertThat(repository.faceAcquisitionMessage.value)
-                .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
-
-            underTest.setFaceAcquisitionMessage(null)
-            assertThat(repository.faceAcquisitionMessage.value).isNull()
-        }
-
-    @Test
-    fun setFingerprintMessage_setsRepositoryValue() =
-        testScope.runTest {
-            init()
-
-            underTest.setFingerprintAcquisitionMessage("not empty")
-
-            assertThat(repository.fingerprintAcquisitionMessage.value)
-                .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
-
-            underTest.setFingerprintAcquisitionMessage(null)
-            assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
-        }
-
-    @Test
-    fun onPrimaryAuthLockout_startsTimerForSpecifiedNumberOfSeconds() =
-        testScope.runTest {
-            init()
-
-            underTest.onPrimaryAuthLockedOut(3)
-
-            verify(countDownTimerUtil)
-                .startNewTimer(eq(3000L), eq(1000L), countDownTimerCallback.capture())
-
-            countDownTimerCallback.value.onTick(2000L)
-
-            val primaryMessage = repository.primaryAuthMessage.value!!.message!!
-            assertThat(primaryMessage.messageResId!!)
-                .isEqualTo(kg_too_many_failed_attempts_countdown)
-            assertThat(primaryMessage.formatterArgs).isEqualTo(mapOf(Pair("count", 2)))
-        }
-
-    @Test
-    fun onPrimaryAuthLockout_timerComplete_resetsRepositoryMessages() =
-        testScope.runTest {
-            init()
-            repository.setCustomMessage(message("not empty"))
-            repository.setFaceAcquisitionMessage(message("not empty"))
-            repository.setFingerprintAcquisitionMessage(message("not empty"))
-            repository.setPrimaryAuthMessage(message("not empty"))
-
-            underTest.onPrimaryAuthLockedOut(3)
-
-            verify(countDownTimerUtil)
-                .startNewTimer(eq(3000L), eq(1000L), countDownTimerCallback.capture())
-
-            countDownTimerCallback.value.onFinish()
-
-            assertThat(repository.customMessage.value).isNull()
-            assertThat(repository.faceAcquisitionMessage.value).isNull()
-            assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
-            assertThat(repository.primaryAuthMessage.value).isNull()
-        }
-
-    @Test
-    fun bouncerMessage_hasPriorityOrderOfMessages() =
-        testScope.runTest {
-            init()
-            repository.setBiometricAuthMessage(message("biometric message"))
-            repository.setFaceAcquisitionMessage(message("face acquisition message"))
-            repository.setFingerprintAcquisitionMessage(message("fingerprint acquisition message"))
-            repository.setPrimaryAuthMessage(message("primary auth message"))
-            repository.setAuthFlagsMessage(message("auth flags message"))
-            repository.setBiometricLockedOutMessage(message("biometrics locked out"))
-            repository.setCustomMessage(message("custom message"))
-
-            assertThat(bouncerMessage()).isEqualTo(message("primary auth message"))
-
-            repository.setPrimaryAuthMessage(null)
-
-            assertThat(bouncerMessage()).isEqualTo(message("biometric message"))
-
-            repository.setBiometricAuthMessage(null)
-
-            assertThat(bouncerMessage()).isEqualTo(message("fingerprint acquisition message"))
-
-            repository.setFingerprintAcquisitionMessage(null)
-
-            assertThat(bouncerMessage()).isEqualTo(message("face acquisition message"))
-
-            repository.setFaceAcquisitionMessage(null)
-
-            assertThat(bouncerMessage()).isEqualTo(message("custom message"))
-
-            repository.setCustomMessage(null)
-
-            assertThat(bouncerMessage()).isEqualTo(message("auth flags message"))
-
-            repository.setAuthFlagsMessage(null)
-
-            assertThat(bouncerMessage()).isEqualTo(message("biometrics locked out"))
-
-            repository.setBiometricLockedOutMessage(null)
-
-            // sets the default message if everything else is null
-            assertThat(bouncerMessage()!!.message!!.messageResId).isEqualTo(keyguard_enter_pin)
-        }
-
-    private fun message(value: String): BouncerMessageModel {
-        return BouncerMessageModel(message = Message(message = value))
-    }
-
-    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
-            )
-    }
-}
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 b2528c5..edaff44 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
@@ -25,6 +25,7 @@
 import com.android.systemui.R
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.backup.BackupHelper
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.util.FakeSharedPreferences
@@ -360,7 +361,10 @@
             }
         clearInvocations(userFileManager)
 
-        fakeBroadcastDispatcher.registeredReceivers.firstOrNull()?.onReceive(context, Intent())
+        fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+            context,
+            Intent(BackupHelper.ACTION_RESTORE_FINISHED),
+        )
 
         verify(userFileManager, atLeastOnce()).getSharedPreferences(anyString(), anyInt(), anyInt())
         job.cancel()
@@ -374,12 +378,13 @@
             arrayOf("leftTest:testShortcut1", "rightTest:testShortcut2")
         )
 
-        assertThat(underTest.getSelections()).isEqualTo(
-            mapOf(
-                "leftTest" to listOf("testShortcut1"),
-                "rightTest" to listOf("testShortcut2"),
+        assertThat(underTest.getSelections())
+            .isEqualTo(
+                mapOf(
+                    "leftTest" to listOf("testShortcut1"),
+                    "rightTest" to listOf("testShortcut2"),
+                )
             )
-        )
     }
 
     private fun assertSelections(
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
index b925aeb..f9070b3 100644
--- 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
@@ -461,12 +461,10 @@
     }
 
     private fun broadcastDPMStateChange() {
-        fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
-            receiver.onReceive(
-                context,
-                Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED)
-            )
-        }
+        fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+            context,
+            Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED),
+        )
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 1090c15..d62db5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -39,15 +39,17 @@
 import com.android.systemui.R
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 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.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR
-import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.AuthenticationStatus
 import com.android.systemui.keyguard.shared.model.DetectionStatus
 import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
@@ -159,17 +161,16 @@
         biometricSettingsRepository = FakeBiometricSettingsRepository()
         deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
         trustRepository = FakeTrustRepository()
-        keyguardRepository = FakeKeyguardRepository()
-        bouncerRepository = FakeKeyguardBouncerRepository()
         featureFlags = FakeFeatureFlags().apply { set(FACE_AUTH_REFACTOR, true) }
-        fakeCommandQueue = FakeCommandQueue()
-        keyguardInteractor =
-            KeyguardInteractor(
-                keyguardRepository,
-                fakeCommandQueue,
-                featureFlags,
-                bouncerRepository
+        val withDeps =
+            KeyguardInteractorFactory.create(
+                featureFlags = featureFlags,
             )
+        keyguardInteractor = withDeps.keyguardInteractor
+        keyguardRepository = withDeps.repository
+        bouncerRepository = withDeps.bouncerRepository
+        fakeCommandQueue = withDeps.commandQueue
+
         alternateBouncerInteractor =
             AlternateBouncerInteractor(
                 bouncerRepository = bouncerRepository,
@@ -215,7 +216,11 @@
             )
         keyguardTransitionRepository = FakeKeyguardTransitionRepository()
         val keyguardTransitionInteractor =
-            KeyguardTransitionInteractor(keyguardTransitionRepository, testScope.backgroundScope)
+            KeyguardTransitionInteractorFactory.create(
+                    scope = TestScope().backgroundScope,
+                    repository = keyguardTransitionRepository,
+                )
+                .keyguardTransitionInteractor
         return DeviceEntryFaceAuthRepositoryImpl(
             mContext,
             fmOverride,
@@ -259,6 +264,7 @@
             assertThat(authStatus()).isEqualTo(SuccessAuthenticationStatus(successResult))
             assertThat(authenticated()).isTrue()
             assertThat(authRunning()).isFalse()
+            assertThat(canFaceAuthRun()).isFalse()
         }
 
     private fun uiEventIsLogged(faceAuthUiEvent: FaceAuthUiEvent) {
@@ -416,13 +422,9 @@
                 FACE_ERROR_CANCELED,
                 "First auth attempt cancellation completed"
             )
-            assertThat(authStatus())
-                .isEqualTo(
-                    ErrorAuthenticationStatus(
-                        FACE_ERROR_CANCELED,
-                        "First auth attempt cancellation completed"
-                    )
-                )
+            val value = authStatus() as ErrorAuthenticationStatus
+            assertThat(value.msgId).isEqualTo(FACE_ERROR_CANCELED)
+            assertThat(value.msg).isEqualTo("First auth attempt cancellation completed")
 
             faceAuthenticateIsCalled()
             uiEventIsLogged(FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN)
@@ -506,6 +508,18 @@
         }
 
     @Test
+    fun authenticateDoesNotRunIfKeyguardIsNotShowing() =
+        testScope.runTest {
+            testGatingCheckForFaceAuth { keyguardRepository.setKeyguardShowing(false) }
+        }
+
+    @Test
+    fun detectDoesNotRunIfKeyguardIsNotShowing() =
+        testScope.runTest {
+            testGatingCheckForDetect { keyguardRepository.setKeyguardShowing(false) }
+        }
+
+    @Test
     fun authenticateDoesNotRunWhenFpIsLockedOut() =
         testScope.runTest {
             testGatingCheckForFaceAuth { deviceEntryFingerprintAuthRepository.setLockedOut(true) }
@@ -538,20 +552,6 @@
         }
 
     @Test
-    fun authenticateDoesNotRunWhenDeviceIsSleeping() =
-        testScope.runTest {
-            testGatingCheckForFaceAuth {
-                keyguardRepository.setWakefulnessModel(
-                    WakefulnessModel(
-                        state = WakefulnessState.ASLEEP,
-                        lastWakeReason = WakeSleepReason.OTHER,
-                        lastSleepReason = WakeSleepReason.OTHER,
-                    )
-                )
-            }
-        }
-
-    @Test
     fun authenticateDoesNotRunWhenNonStrongBiometricIsNotAllowed() =
         testScope.runTest {
             testGatingCheckForFaceAuth {
@@ -564,6 +564,8 @@
         testScope.runTest {
             testGatingCheckForFaceAuth {
                 bouncerRepository.setAlternateVisible(false)
+                // Keyguard is occluded when secure camera is active.
+                keyguardRepository.setKeyguardOccluded(true)
                 fakeCommandQueue.doForEachCallback {
                     it.onCameraLaunchGestureDetected(CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP)
                 }
@@ -571,6 +573,29 @@
         }
 
     @Test
+    fun authenticateRunsWhenSecureCameraIsActiveIfBouncerIsShowing() =
+        testScope.runTest {
+            initCollectors()
+            allPreconditionsToRunFaceAuthAreTrue()
+            bouncerRepository.setAlternateVisible(false)
+            bouncerRepository.setPrimaryShow(false)
+
+            assertThat(canFaceAuthRun()).isTrue()
+
+            // launch secure camera
+            fakeCommandQueue.doForEachCallback {
+                it.onCameraLaunchGestureDetected(CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP)
+            }
+            keyguardRepository.setKeyguardOccluded(true)
+            runCurrent()
+            assertThat(canFaceAuthRun()).isFalse()
+
+            // but bouncer is shown after that.
+            bouncerRepository.setPrimaryShow(true)
+            assertThat(canFaceAuthRun()).isTrue()
+        }
+
+    @Test
     fun authenticateDoesNotRunOnUnsupportedPosture() =
         testScope.runTest {
             testGatingCheckForFaceAuth {
@@ -601,6 +626,27 @@
         }
 
     @Test
+    fun authenticateFallbacksToDetectionWhenUserIsAlreadyTrustedByTrustManager() =
+        testScope.runTest {
+            whenever(faceManager.sensorPropertiesInternal)
+                .thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = true)))
+            whenever(bypassController.bypassEnabled).thenReturn(true)
+            underTest = createDeviceEntryFaceAuthRepositoryImpl()
+            initCollectors()
+            allPreconditionsToRunFaceAuthAreTrue()
+
+            trustRepository.setCurrentUserTrusted(true)
+            assertThat(canFaceAuthRun()).isFalse()
+            underTest.authenticate(
+                FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER,
+                fallbackToDetection = true
+            )
+            faceAuthenticateIsNotCalled()
+
+            faceDetectIsCalled()
+        }
+
+    @Test
     fun everythingWorksWithFaceAuthRefactorFlagDisabled() =
         testScope.runTest {
             featureFlags.set(FACE_AUTH_REFACTOR, false)
@@ -773,6 +819,8 @@
         testScope.runTest {
             testGatingCheckForDetect {
                 bouncerRepository.setAlternateVisible(false)
+                // Keyguard is occluded when secure camera is active.
+                keyguardRepository.setKeyguardOccluded(true)
                 fakeCommandQueue.doForEachCallback {
                     it.onCameraLaunchGestureDetected(CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP)
                 }
@@ -1010,6 +1058,7 @@
         fakeUserRepository.setSelectedUserInfo(primaryUser)
         biometricSettingsRepository.setIsFaceAuthSupportedInCurrentPosture(true)
         bouncerRepository.setAlternateVisible(true)
+        keyguardRepository.setKeyguardShowing(true)
         runCurrent()
     }
 
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
deleted file mode 100644
index b3104b7..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt
+++ /dev/null
@@ -1,64 +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.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.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.time.SystemClock
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineScope
-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
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@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
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        val testCoroutineScope = TestCoroutineScope()
-        underTest =
-            KeyguardBouncerRepositoryImpl(
-                systemClock,
-                testCoroutineScope,
-                bouncerLogger,
-            )
-    }
-
-    @Test
-    fun changingFlowValueTriggersLogging() = runBlocking {
-        underTest.setPrimaryShow(true)
-        verify(bouncerLogger).logChange(eq(""), eq("PrimaryBouncerShow"), value = eq(false), any())
-    }
-}
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 4b797cb..25573de 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
@@ -97,7 +97,8 @@
                 dozeParameters,
                 authController,
                 dreamOverlayCallbackController,
-                mainDispatcher
+                mainDispatcher,
+                testScope.backgroundScope,
             )
     }
 
@@ -234,11 +235,10 @@
     fun isKeyguardUnlocked() =
         testScope.runTest {
             whenever(keyguardStateController.isUnlocked).thenReturn(false)
-            var latest: Boolean? = null
-            val job = underTest.isKeyguardUnlocked.onEach { latest = it }.launchIn(this)
+            val isKeyguardUnlocked by collectLastValue(underTest.isKeyguardUnlocked)
 
             runCurrent()
-            assertThat(latest).isFalse()
+            assertThat(isKeyguardUnlocked).isFalse()
 
             val captor = argumentCaptor<KeyguardStateController.Callback>()
             verify(keyguardStateController).addCallback(captor.capture())
@@ -246,14 +246,12 @@
             whenever(keyguardStateController.isUnlocked).thenReturn(true)
             captor.value.onUnlockedChanged()
             runCurrent()
-            assertThat(latest).isTrue()
+            assertThat(isKeyguardUnlocked).isTrue()
 
             whenever(keyguardStateController.isUnlocked).thenReturn(false)
-            captor.value.onUnlockedChanged()
+            captor.value.onKeyguardShowingChanged()
             runCurrent()
-            assertThat(latest).isFalse()
-
-            job.cancel()
+            assertThat(isKeyguardUnlocked).isFalse()
         }
 
     @Test
@@ -343,8 +341,6 @@
                 )
 
             job.cancel()
-            runCurrent()
-            verify(wakefulnessLifecycle).removeObserver(captor.value)
         }
 
     @Test
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
deleted file mode 100644
index ca6b8d5..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt
+++ /dev/null
@@ -1,178 +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 androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.RoboPilotTest
-import com.android.systemui.SysuiTestCase
-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.plugins.statusbar.StatusBarStateController
-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.MockitoAnnotations
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RoboPilotTest
-@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 statusBarStateController: StatusBarStateController
-    @Mock private lateinit var keyguardStateController: KeyguardStateController
-    @Mock private lateinit var systemClock: SystemClock
-    @Mock private lateinit var bouncerLogger: TableLogBuffer
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        bouncerRepository =
-            KeyguardBouncerRepositoryImpl(
-                FakeSystemClock(),
-                TestCoroutineScope(),
-                bouncerLogger,
-            )
-        biometricSettingsRepository = FakeBiometricSettingsRepository()
-        deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
-        underTest =
-            AlternateBouncerInteractor(
-                statusBarStateController,
-                keyguardStateController,
-                bouncerRepository,
-                biometricSettingsRepository,
-                deviceEntryFingerprintAuthRepository,
-                systemClock,
-            )
-    }
-
-    @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/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
index 71e73cbd..3e81cd3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.hardware.biometrics.BiometricFaceConstants
 import android.os.Handler
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -24,18 +25,23 @@
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.bouncer.ui.BouncerView
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.dump.logcatLogBuffer
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 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.FakeDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -83,10 +89,15 @@
         faceAuthRepository = FakeDeviceEntryFaceAuthRepository()
         keyguardTransitionRepository = FakeKeyguardTransitionRepository()
         keyguardTransitionInteractor =
-            KeyguardTransitionInteractor(keyguardTransitionRepository, testScope.backgroundScope)
+            KeyguardTransitionInteractorFactory.create(
+                    scope = TestScope().backgroundScope,
+                    repository = keyguardTransitionRepository,
+                )
+                .keyguardTransitionInteractor
 
         underTest =
             SystemUIKeyguardFaceAuthInteractor(
+                mContext,
                 testScope.backgroundScope,
                 dispatcher,
                 faceAuthRepository,
@@ -141,6 +152,22 @@
         }
 
     @Test
+    fun whenFaceIsLockedOutAnyAttemptsToTriggerFaceAuthMustProvideLockoutError() =
+        testScope.runTest {
+            underTest.start()
+            val authenticationStatus = collectLastValue(underTest.authenticationStatus)
+            faceAuthRepository.setLockedOut(true)
+
+            underTest.onDeviceLifted()
+
+            val outputValue = authenticationStatus()!! as ErrorAuthenticationStatus
+            assertThat(outputValue.msgId)
+                .isEqualTo(BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT)
+            assertThat(outputValue.msg).isEqualTo("Face Unlock unavailable")
+            assertThat(faceAuthRepository.runningAuthRequest.value).isNull()
+        }
+
+    @Test
     fun faceAuthIsRequestedWhenLockscreenBecomesVisibleFromAodState() =
         testScope.runTest {
             underTest.start()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
index 0d695aa..4b09468 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -22,11 +22,12 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 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.FakeCommandQueue
-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.google.common.truth.Truth.assertThat
@@ -50,6 +51,7 @@
     private lateinit var underTest: KeyguardInteractor
     private lateinit var repository: FakeKeyguardRepository
     private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
+    private lateinit var configurationRepository: FakeConfigurationRepository
 
     @Before
     fun setUp() {
@@ -59,12 +61,14 @@
         testScope = TestScope()
         repository = FakeKeyguardRepository()
         bouncerRepository = FakeKeyguardBouncerRepository()
+        configurationRepository = FakeConfigurationRepository()
         underTest =
             KeyguardInteractor(
                 repository,
                 commandQueue,
                 featureFlags,
                 bouncerRepository,
+                configurationRepository,
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
index f63be61..0050d64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
@@ -194,9 +194,10 @@
             underTest.onLongPress()
             assertThat(isMenuVisible).isTrue()
 
-            fakeBroadcastDispatcher.registeredReceivers.forEach { broadcastReceiver ->
-                broadcastReceiver.onReceive(context, Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
-            }
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
+            )
 
             assertThat(isMenuVisible).isFalse()
         }
@@ -291,10 +292,11 @@
                 appContext = mContext,
                 scope = testScope.backgroundScope,
                 transitionInteractor =
-                    KeyguardTransitionInteractor(
-                        keyguardTransitionRepository,
-                        testScope.backgroundScope
-                    ),
+                    KeyguardTransitionInteractorFactory.create(
+                            scope = testScope.backgroundScope,
+                            repository = keyguardTransitionRepository,
+                        )
+                        .keyguardTransitionInteractor,
                 repository = keyguardRepository,
                 logger = logger,
                 featureFlags =
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 2c90d53..8540bf7 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
@@ -39,8 +39,6 @@
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
 import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-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
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
@@ -49,7 +47,6 @@
 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
@@ -226,7 +223,6 @@
     @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
     @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger
 
@@ -312,12 +308,10 @@
         underTest =
             KeyguardQuickAffordanceInteractor(
                 keyguardInteractor =
-                    KeyguardInteractor(
-                        repository = FakeKeyguardRepository(),
-                        commandQueue = commandQueue,
-                        featureFlags = featureFlags,
-                        bouncerRepository = FakeKeyguardBouncerRepository(),
-                    ),
+                    KeyguardInteractorFactory.create(
+                            featureFlags = featureFlags,
+                        )
+                        .keyguardInteractor,
                 registry =
                     FakeKeyguardQuickAffordanceRegistry(
                         mapOf(
@@ -372,11 +366,11 @@
                     KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
                 }
 
-        underTest.onQuickAffordanceTriggered(
-            configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS,
-            expandable = expandable,
-            slotId = "",
-        )
+            underTest.onQuickAffordanceTriggered(
+                configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS,
+                expandable = expandable,
+                slotId = "",
+            )
 
             if (startActivity) {
                 if (needsToUnlockFirst) {
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 7cab680..a0c5a75 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
@@ -41,7 +41,6 @@
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
 import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-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
@@ -54,7 +53,6 @@
 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
@@ -85,7 +83,6 @@
     @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
     @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger
 
@@ -171,15 +168,14 @@
                 set(Flags.FACE_AUTH_REFACTOR, true)
             }
 
+        val withDeps =
+            KeyguardInteractorFactory.create(
+                featureFlags = featureFlags,
+                repository = repository,
+            )
         underTest =
             KeyguardQuickAffordanceInteractor(
-                keyguardInteractor =
-                    KeyguardInteractor(
-                        repository = repository,
-                        commandQueue = commandQueue,
-                        featureFlags = featureFlags,
-                        bouncerRepository = FakeKeyguardBouncerRepository(),
-                    ),
+                keyguardInteractor = withDeps.keyguardInteractor,
                 registry =
                     FakeKeyguardQuickAffordanceRegistry(
                         mapOf(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index fa4941c..9e9c25e 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,9 +17,9 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
-import com.android.systemui.RoboPilotTest
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -54,7 +54,10 @@
     @Before
     fun setUp() {
         repository = FakeKeyguardTransitionRepository()
-        underTest = KeyguardTransitionInteractor(repository, testScope.backgroundScope)
+        underTest = KeyguardTransitionInteractorFactory.create(
+                scope = testScope.backgroundScope,
+                repository = repository,
+        ).keyguardTransitionInteractor
     }
 
     @Test
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 30dba23..d01a46e 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
@@ -19,14 +19,13 @@
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
+import com.android.keyguard.TestScopeProvider
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
 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.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.DozeTransitionModel
@@ -39,7 +38,6 @@
 import com.android.systemui.keyguard.shared.model.WakefulnessState
 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.any
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.mockito.withArgCaptor
@@ -58,6 +56,7 @@
 import org.mockito.Mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -74,10 +73,10 @@
     private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
     private lateinit var shadeRepository: ShadeRepository
     private lateinit var transitionRepository: FakeKeyguardTransitionRepository
+    private lateinit var transitionInteractor: KeyguardTransitionInteractor
+    private lateinit var featureFlags: FakeFeatureFlags
 
     // 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
@@ -94,97 +93,97 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        testScope = TestScope()
+        testScope = TestScopeProvider.getTestScope()
 
         keyguardRepository = FakeKeyguardRepository()
         bouncerRepository = FakeKeyguardBouncerRepository()
         shadeRepository = FakeShadeRepository()
-        transitionRepository = FakeKeyguardTransitionRepository()
+        transitionRepository = spy(FakeKeyguardTransitionRepository())
 
         whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN)
 
-        val featureFlags = FakeFeatureFlags().apply { set(Flags.FACE_AUTH_REFACTOR, true) }
+        featureFlags = FakeFeatureFlags().apply { set(Flags.FACE_AUTH_REFACTOR, true) }
+
+        transitionInteractor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = testScope,
+                    repository = transitionRepository,
+                )
+                .keyguardTransitionInteractor
+
         fromLockscreenTransitionInteractor =
             FromLockscreenTransitionInteractor(
-                scope = testScope,
-                keyguardInteractor = createKeyguardInteractor(featureFlags),
-                shadeRepository = shadeRepository,
-                keyguardTransitionRepository = mockTransitionRepository,
-                keyguardTransitionInteractor =
-                    KeyguardTransitionInteractor(transitionRepository, testScope.backgroundScope),
-            )
-        fromLockscreenTransitionInteractor.start()
-
-        fromDreamingTransitionInteractor =
-            FromDreamingTransitionInteractor(
-                scope = testScope,
-                keyguardInteractor = createKeyguardInteractor(featureFlags),
-                keyguardTransitionRepository = mockTransitionRepository,
-                keyguardTransitionInteractor =
-                    KeyguardTransitionInteractor(transitionRepository, testScope.backgroundScope),
-            )
-        fromDreamingTransitionInteractor.start()
-
-        fromAodTransitionInteractor =
-            FromAodTransitionInteractor(
-                scope = testScope,
-                keyguardInteractor = createKeyguardInteractor(featureFlags),
-                keyguardTransitionRepository = mockTransitionRepository,
-                keyguardTransitionInteractor =
-                    KeyguardTransitionInteractor(transitionRepository, testScope.backgroundScope),
-            )
-        fromAodTransitionInteractor.start()
-
-        fromGoneTransitionInteractor =
-            FromGoneTransitionInteractor(
-                scope = testScope,
-                keyguardInteractor = createKeyguardInteractor(featureFlags),
-                keyguardTransitionRepository = mockTransitionRepository,
-                keyguardTransitionInteractor =
-                    KeyguardTransitionInteractor(transitionRepository, testScope.backgroundScope),
-            )
-        fromGoneTransitionInteractor.start()
-
-        fromDozingTransitionInteractor =
-            FromDozingTransitionInteractor(
-                scope = testScope,
-                keyguardInteractor = createKeyguardInteractor(featureFlags),
-                keyguardTransitionRepository = mockTransitionRepository,
-                keyguardTransitionInteractor =
-                    KeyguardTransitionInteractor(transitionRepository, testScope.backgroundScope),
-            )
-        fromDozingTransitionInteractor.start()
-
-        fromOccludedTransitionInteractor =
-            FromOccludedTransitionInteractor(
-                scope = testScope,
-                keyguardInteractor = createKeyguardInteractor(featureFlags),
-                keyguardTransitionRepository = mockTransitionRepository,
-                keyguardTransitionInteractor =
-                    KeyguardTransitionInteractor(transitionRepository, testScope.backgroundScope),
-            )
-        fromOccludedTransitionInteractor.start()
-
-        fromAlternateBouncerTransitionInteractor =
-            FromAlternateBouncerTransitionInteractor(
-                scope = testScope,
-                keyguardInteractor = createKeyguardInteractor(featureFlags),
-                keyguardTransitionRepository = mockTransitionRepository,
-                keyguardTransitionInteractor =
-                    KeyguardTransitionInteractor(transitionRepository, testScope.backgroundScope),
-            )
-        fromAlternateBouncerTransitionInteractor.start()
+                    scope = testScope,
+                    keyguardInteractor = createKeyguardInteractor(),
+                    transitionRepository = transitionRepository,
+                    transitionInteractor = transitionInteractor,
+                    shadeRepository = shadeRepository,
+                )
+                .apply { start() }
 
         fromPrimaryBouncerTransitionInteractor =
             FromPrimaryBouncerTransitionInteractor(
-                scope = testScope,
-                keyguardInteractor = createKeyguardInteractor(featureFlags),
-                keyguardTransitionRepository = mockTransitionRepository,
-                keyguardTransitionInteractor =
-                    KeyguardTransitionInteractor(transitionRepository, testScope.backgroundScope),
-                keyguardSecurityModel = keyguardSecurityModel,
-            )
-        fromPrimaryBouncerTransitionInteractor.start()
+                    scope = testScope,
+                    keyguardInteractor = createKeyguardInteractor(),
+                    transitionRepository = transitionRepository,
+                    transitionInteractor = transitionInteractor,
+                    keyguardSecurityModel = keyguardSecurityModel,
+                )
+                .apply { start() }
+
+        fromDreamingTransitionInteractor =
+            FromDreamingTransitionInteractor(
+                    scope = testScope,
+                    keyguardInteractor = createKeyguardInteractor(),
+                    transitionRepository = transitionRepository,
+                    transitionInteractor = transitionInteractor,
+                )
+                .apply { start() }
+
+        fromAodTransitionInteractor =
+            FromAodTransitionInteractor(
+                    scope = testScope,
+                    keyguardInteractor = createKeyguardInteractor(),
+                    transitionRepository = transitionRepository,
+                    transitionInteractor = transitionInteractor,
+                )
+                .apply { start() }
+
+        fromGoneTransitionInteractor =
+            FromGoneTransitionInteractor(
+                    scope = testScope,
+                    keyguardInteractor = createKeyguardInteractor(),
+                    transitionRepository = transitionRepository,
+                    transitionInteractor = transitionInteractor,
+                )
+                .apply { start() }
+
+        fromDozingTransitionInteractor =
+            FromDozingTransitionInteractor(
+                    scope = testScope,
+                    keyguardInteractor = createKeyguardInteractor(),
+                    transitionRepository = transitionRepository,
+                    transitionInteractor = transitionInteractor,
+                )
+                .apply { start() }
+
+        fromOccludedTransitionInteractor =
+            FromOccludedTransitionInteractor(
+                    scope = testScope,
+                    keyguardInteractor = createKeyguardInteractor(),
+                    transitionRepository = transitionRepository,
+                    transitionInteractor = transitionInteractor,
+                )
+                .apply { start() }
+
+        fromAlternateBouncerTransitionInteractor =
+            FromAlternateBouncerTransitionInteractor(
+                    scope = testScope,
+                    keyguardInteractor = createKeyguardInteractor(),
+                    transitionRepository = transitionRepository,
+                    transitionInteractor = transitionInteractor,
+                )
+                .apply { start() }
     }
 
     @Test
@@ -203,7 +202,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to PRIMARY_BOUNCER should occur
             assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
@@ -230,7 +229,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to DOZING should occur
             assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
@@ -257,7 +256,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to DOZING should occur
             assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
@@ -288,7 +287,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to DREAMING should occur
             assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
@@ -315,7 +314,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to DOZING should occur
             assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
@@ -342,7 +341,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to DOZING should occur
             assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
@@ -365,7 +364,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to DOZING should occur
             assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor")
@@ -390,14 +389,14 @@
                 )
             )
             runCurrent()
-            reset(mockTransitionRepository)
+            reset(transitionRepository)
 
             // WHEN a signal comes that dreaming is enabled
             keyguardRepository.setDreamingWithOverlay(true)
             advanceUntilIdle()
 
             // THEN the transition is ignored
-            verify(mockTransitionRepository, never()).startTransition(any(), anyBoolean())
+            verify(transitionRepository, never()).startTransition(any(), anyBoolean())
 
             coroutineContext.cancelChildren()
         }
@@ -414,7 +413,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to DOZING should occur
             assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor")
@@ -441,7 +440,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to DOZING should occur
             assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
@@ -468,7 +467,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to AOD should occur
             assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
@@ -491,7 +490,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to AOD should occur
             assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
@@ -522,7 +521,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to DREAMING should occur
             assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
@@ -545,7 +544,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to PRIMARY_BOUNCER should occur
             assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor")
@@ -574,7 +573,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to AOD should occur
             assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor")
@@ -604,7 +603,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to DOZING should occur
             assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor")
@@ -632,7 +631,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to LOCKSCREEN should occur
             assertThat(info.ownerName).isEqualTo("FromAlternateBouncerTransitionInteractor")
@@ -660,7 +659,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to AOD should occur
             assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor")
@@ -688,7 +687,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to DOZING should occur
             assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor")
@@ -715,7 +714,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to LOCKSCREEN should occur
             assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor")
@@ -746,7 +745,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to GONE should occur
             assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
@@ -775,7 +774,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to LOCKSCREEN should occur
             assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
@@ -801,7 +800,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to AlternateBouncer should occur
             assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
@@ -830,7 +829,7 @@
 
             val info =
                 withArgCaptor<TransitionInfo> {
-                    verify(mockTransitionRepository).startTransition(capture(), anyBoolean())
+                    verify(transitionRepository).startTransition(capture(), anyBoolean())
                 }
             // THEN a transition to AlternateBouncer should occur
             assertThat(info.ownerName).isEqualTo("FromPrimaryBouncerTransitionInteractor")
@@ -855,13 +854,13 @@
             WakeSleepReason.OTHER
         )
 
-    private fun createKeyguardInteractor(featureFlags: FeatureFlags): KeyguardInteractor {
-        return KeyguardInteractor(
-            keyguardRepository,
-            commandQueue,
-            featureFlags,
-            bouncerRepository,
-        )
+    private fun createKeyguardInteractor(): KeyguardInteractor {
+        return KeyguardInteractorFactory.create(
+                featureFlags = featureFlags,
+                repository = keyguardRepository,
+                bouncerRepository = bouncerRepository,
+            )
+            .keyguardInteractor
     }
 
     private suspend fun TestScope.runTransition(from: KeyguardState, to: KeyguardState) {
@@ -892,6 +891,6 @@
             )
         )
         runCurrent()
-        reset(mockTransitionRepository)
+        reset(transitionRepository)
     }
 }
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 08e99dc..6e7ba6d 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
@@ -46,7 +46,11 @@
     private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository()
 
     private val keyguardTransitionInteractor =
-        KeyguardTransitionInteractor(fakeKeyguardTransitionRepository, TestScope().backgroundScope)
+        KeyguardTransitionInteractorFactory.create(
+                scope = TestScope().backgroundScope,
+                repository = fakeKeyguardTransitionRepository,
+            )
+            .keyguardTransitionInteractor
 
     private lateinit var underTest: LightRevealScrimInteractor
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt
index d622f1c..abbdc3d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt
@@ -26,7 +26,6 @@
 import com.android.systemui.scene.shared.model.SceneModel
 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.Test
@@ -38,8 +37,8 @@
 @RunWith(JUnit4::class)
 class LockscreenSceneInteractorTest : SysuiTestCase() {
 
-    private val testScope = TestScope()
-    private val utils = SceneTestUtils(this, testScope)
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
     private val sceneInteractor = utils.sceneInteractor()
     private val authenticationInteractor =
         utils.authenticationInteractor(
@@ -61,10 +60,10 @@
         testScope.runTest {
             val isDeviceLocked by collectLastValue(underTest.isDeviceLocked)
 
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
             assertThat(isDeviceLocked).isTrue()
 
-            authenticationInteractor.unlockDevice()
+            utils.authenticationRepository.setUnlocked(true)
             assertThat(isDeviceLocked).isFalse()
         }
 
@@ -73,8 +72,8 @@
         testScope.runTest {
             val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled)
 
-            authenticationInteractor.lockDevice()
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            utils.authenticationRepository.setUnlocked(false)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
 
             assertThat(isSwipeToDismissEnabled).isTrue()
         }
@@ -84,8 +83,8 @@
         testScope.runTest {
             val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled)
 
-            authenticationInteractor.unlockDevice()
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            utils.authenticationRepository.setUnlocked(true)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
 
             assertThat(isSwipeToDismissEnabled).isFalse()
         }
@@ -94,8 +93,10 @@
     fun dismissLockScreen_deviceLockedWithSecureAuthMethod_switchesToBouncer() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            authenticationInteractor.lockDevice()
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            utils.authenticationRepository.setUnlocked(false)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
 
             underTest.dismissLockscreen()
@@ -107,8 +108,10 @@
     fun dismissLockScreen_deviceUnlocked_switchesToGone() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            authenticationInteractor.unlockDevice()
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            utils.authenticationRepository.setUnlocked(true)
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
 
             underTest.dismissLockscreen()
@@ -120,8 +123,8 @@
     fun dismissLockScreen_deviceLockedWithInsecureAuthMethod_switchesToGone() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            authenticationInteractor.lockDevice()
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            utils.authenticationRepository.setUnlocked(false)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
 
             underTest.dismissLockscreen()
@@ -136,66 +139,23 @@
             runCurrent()
             sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone))
             runCurrent()
-            authenticationInteractor.unlockDevice()
+            utils.authenticationRepository.setUnlocked(true)
             runCurrent()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
 
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
 
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
         }
 
     @Test
-    fun deviceBiometricUnlockedInLockScreen_bypassEnabled_switchesToGone() =
-        testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            authenticationInteractor.lockDevice()
-            sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Lockscreen))
-            if (!authenticationInteractor.isBypassEnabled.value) {
-                authenticationInteractor.toggleBypassEnabled()
-            }
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
-
-            authenticationInteractor.biometricUnlock()
-
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
-        }
-
-    @Test
-    fun deviceBiometricUnlockedInLockScreen_bypassNotEnabled_doesNotSwitch() =
-        testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            authenticationInteractor.lockDevice()
-            sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Lockscreen))
-            if (authenticationInteractor.isBypassEnabled.value) {
-                authenticationInteractor.toggleBypassEnabled()
-            }
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
-
-            authenticationInteractor.biometricUnlock()
-
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
-        }
-
-    @Test
-    fun switchFromLockScreenToGone_authMethodSwipe_unlocksDevice() =
-        testScope.runTest {
-            val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
-            sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Lockscreen))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
-            assertThat(isUnlocked).isFalse()
-
-            sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone))
-
-            assertThat(isUnlocked).isTrue()
-        }
-
-    @Test
     fun switchFromLockScreenToGone_authMethodNotSwipe_doesNotUnlockDevice() =
         testScope.runTest {
             val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
             sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Lockscreen))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
             assertThat(isUnlocked).isFalse()
 
             sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone))
@@ -210,7 +170,7 @@
             runCurrent()
             sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Shade))
             runCurrent()
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
             runCurrent()
             assertThat(isUnlocked).isFalse()
 
@@ -220,29 +180,16 @@
         }
 
     @Test
-    fun authMethodChangedToNone_onLockScreenScene_dismissesLockScreen() =
-        testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Lockscreen))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
-
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None)
-
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
-        }
-
-    @Test
     fun authMethodChangedToNone_notOnLockScreenScene_doesNotDismissLockScreen() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
             runCurrent()
             sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.QuickSettings))
             runCurrent()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings))
 
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None)
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
 
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings))
         }
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
deleted file mode 100644
index 2b135cc..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
+++ /dev/null
@@ -1,93 +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.view.View
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.RoboPilotTest
-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.verify
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RoboPilotTest
-@RunWith(AndroidJUnit4::class)
-class PrimaryBouncerCallbackInteractorTest : SysuiTestCase() {
-    private val mPrimaryBouncerCallbackInteractor = PrimaryBouncerCallbackInteractor()
-    @Mock
-    private lateinit var mPrimaryBouncerExpansionCallback:
-        PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback
-    @Mock
-    private lateinit var keyguardResetCallback:
-        PrimaryBouncerCallbackInteractor.KeyguardResetCallback
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(
-            mPrimaryBouncerExpansionCallback
-        )
-        mPrimaryBouncerCallbackInteractor.addKeyguardResetCallback(keyguardResetCallback)
-    }
-
-    @Test
-    fun testOnFullyShown() {
-        mPrimaryBouncerCallbackInteractor.dispatchFullyShown()
-        verify(mPrimaryBouncerExpansionCallback).onFullyShown()
-    }
-
-    @Test
-    fun testOnFullyHidden() {
-        mPrimaryBouncerCallbackInteractor.dispatchFullyHidden()
-        verify(mPrimaryBouncerExpansionCallback).onFullyHidden()
-    }
-
-    @Test
-    fun testOnExpansionChanged() {
-        mPrimaryBouncerCallbackInteractor.dispatchExpansionChanged(5f)
-        verify(mPrimaryBouncerExpansionCallback).onExpansionChanged(5f)
-    }
-
-    @Test
-    fun testOnVisibilityChanged() {
-        mPrimaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.INVISIBLE)
-        verify(mPrimaryBouncerExpansionCallback).onVisibilityChanged(false)
-    }
-
-    @Test
-    fun testOnStartingToHide() {
-        mPrimaryBouncerCallbackInteractor.dispatchStartingToHide()
-        verify(mPrimaryBouncerExpansionCallback).onStartingToHide()
-    }
-
-    @Test
-    fun testOnStartingToShow() {
-        mPrimaryBouncerCallbackInteractor.dispatchStartingToShow()
-        verify(mPrimaryBouncerExpansionCallback).onStartingToShow()
-    }
-
-    @Test
-    fun testOnKeyguardReset() {
-        mPrimaryBouncerCallbackInteractor.dispatchReset()
-        verify(keyguardResetCallback).onKeyguardReset()
-    }
-}
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
deleted file mode 100644
index b5cb44a..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
+++ /dev/null
@@ -1,469 +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.hardware.biometrics.BiometricSourceType
-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.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
-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.FakeTrustRepository
-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.plugins.ActivityStarter
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.utils.os.FakeHandler
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Answers
-import org.mockito.ArgumentCaptor
-import org.mockito.Mock
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidTestingRunner::class)
-class PrimaryBouncerInteractorTest : SysuiTestCase() {
-    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
-    private lateinit var repository: KeyguardBouncerRepository
-    @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var bouncerView: BouncerView
-    @Mock private lateinit var bouncerViewDelegate: BouncerViewDelegate
-    @Mock private lateinit var keyguardStateController: KeyguardStateController
-    @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
-    @Mock private lateinit var mPrimaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor
-    @Mock private lateinit var falsingCollector: FalsingCollector
-    @Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry
-    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
-    private lateinit var mainHandler: FakeHandler
-    private lateinit var underTest: PrimaryBouncerInteractor
-    private lateinit var resources: TestableResources
-    private lateinit var trustRepository: FakeTrustRepository
-    private lateinit var featureFlags: FakeFeatureFlags
-    private lateinit var testScope: TestScope
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        whenever(keyguardSecurityModel.getSecurityMode(anyInt()))
-            .thenReturn(KeyguardSecurityModel.SecurityMode.PIN)
-
-        DejankUtils.setImmediate(true)
-        testScope = TestScope()
-        mainHandler = FakeHandler(android.os.Looper.getMainLooper())
-        trustRepository = FakeTrustRepository()
-        featureFlags = FakeFeatureFlags().apply { set(Flags.DELAY_BOUNCER, true) }
-        underTest =
-            PrimaryBouncerInteractor(
-                repository,
-                bouncerView,
-                mainHandler,
-                keyguardStateController,
-                keyguardSecurityModel,
-                mPrimaryBouncerCallbackInteractor,
-                falsingCollector,
-                dismissCallbackRegistry,
-                context,
-                keyguardUpdateMonitor,
-                trustRepository,
-                featureFlags,
-                testScope.backgroundScope,
-            )
-        whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
-        whenever(repository.primaryBouncerShow.value).thenReturn(false)
-        whenever(bouncerView.delegate).thenReturn(bouncerViewDelegate)
-        resources = context.orCreateTestableResources
-    }
-
-    @Test
-    fun testShow_isScrimmed() {
-        underTest.show(true)
-        verify(repository).setKeyguardAuthenticated(null)
-        verify(repository).setPrimaryStartingToHide(false)
-        verify(repository).setPrimaryScrimmed(true)
-        verify(repository).setPanelExpansion(EXPANSION_VISIBLE)
-        verify(repository).setPrimaryShowingSoon(true)
-        verify(keyguardStateController).notifyPrimaryBouncerShowing(true)
-        verify(mPrimaryBouncerCallbackInteractor).dispatchStartingToShow()
-        verify(repository).setPrimaryShow(true)
-        verify(repository).setPrimaryShowingSoon(false)
-        verify(mPrimaryBouncerCallbackInteractor).dispatchVisibilityChanged(View.VISIBLE)
-    }
-
-    @Test
-    fun testShow_isNotScrimmed() {
-        verify(repository, never()).setPanelExpansion(EXPANSION_VISIBLE)
-    }
-
-    @Test
-    fun testShow_keyguardIsDone() {
-        whenever(bouncerView.delegate?.showNextSecurityScreenOrFinish()).thenReturn(true)
-        verify(keyguardStateController, never()).notifyPrimaryBouncerShowing(true)
-        verify(mPrimaryBouncerCallbackInteractor, never()).dispatchStartingToShow()
-    }
-
-    @Test
-    fun testShow_isResumed() {
-        whenever(repository.primaryBouncerShow.value).thenReturn(true)
-        whenever(keyguardSecurityModel.getSecurityMode(anyInt()))
-            .thenReturn(KeyguardSecurityModel.SecurityMode.SimPuk)
-
-        underTest.show(true)
-        verify(repository).setPrimaryShow(false)
-        verify(repository).setPrimaryShow(true)
-    }
-
-    @Test
-    fun testHide() {
-        underTest.hide()
-        verify(falsingCollector).onBouncerHidden()
-        verify(keyguardStateController).notifyPrimaryBouncerShowing(false)
-        verify(repository).setPrimaryShowingSoon(false)
-        verify(repository).setPrimaryShow(false)
-        verify(mPrimaryBouncerCallbackInteractor).dispatchVisibilityChanged(View.INVISIBLE)
-        verify(repository).setPrimaryStartDisappearAnimation(null)
-        verify(repository).setPanelExpansion(EXPANSION_HIDDEN)
-    }
-
-    @Test
-    fun testExpansion() {
-        whenever(repository.panelExpansionAmount.value).thenReturn(0.5f)
-        underTest.setPanelExpansion(0.6f)
-        verify(repository).setPanelExpansion(0.6f)
-        verify(mPrimaryBouncerCallbackInteractor).dispatchExpansionChanged(0.6f)
-    }
-
-    @Test
-    fun testExpansion_fullyShown() {
-        whenever(repository.panelExpansionAmount.value).thenReturn(0.5f)
-        whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
-        underTest.setPanelExpansion(EXPANSION_VISIBLE)
-        verify(falsingCollector).onBouncerShown()
-        verify(mPrimaryBouncerCallbackInteractor).dispatchFullyShown()
-    }
-
-    @Test
-    fun testExpansion_fullyHidden() {
-        whenever(repository.panelExpansionAmount.value).thenReturn(0.5f)
-        whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
-        underTest.setPanelExpansion(EXPANSION_HIDDEN)
-        verify(repository).setPrimaryShow(false)
-        verify(falsingCollector).onBouncerHidden()
-        verify(mPrimaryBouncerCallbackInteractor).dispatchReset()
-        verify(mPrimaryBouncerCallbackInteractor).dispatchFullyHidden()
-    }
-
-    @Test
-    fun testExpansion_startingToHide() {
-        whenever(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
-        underTest.setPanelExpansion(0.1f)
-        verify(repository).setPrimaryStartingToHide(true)
-        verify(mPrimaryBouncerCallbackInteractor).dispatchStartingToHide()
-    }
-
-    @Test
-    fun testShowMessage() {
-        val argCaptor = ArgumentCaptor.forClass(BouncerShowMessageModel::class.java)
-        underTest.showMessage("abc", null)
-        verify(repository).setShowMessage(argCaptor.capture())
-        assertThat(argCaptor.value.message).isEqualTo("abc")
-    }
-
-    @Test
-    fun testDismissAction() {
-        val onDismissAction = mock(ActivityStarter.OnDismissAction::class.java)
-        val cancelAction = mock(Runnable::class.java)
-        underTest.setDismissAction(onDismissAction, cancelAction)
-        verify(bouncerViewDelegate).setDismissAction(onDismissAction, cancelAction)
-    }
-
-    @Test
-    fun testUpdateResources() {
-        underTest.updateResources()
-        verify(repository).setResourceUpdateRequests(true)
-    }
-
-    @Test
-    fun testNotifyKeyguardAuthenticated() {
-        underTest.notifyKeyguardAuthenticated(true)
-        verify(repository).setKeyguardAuthenticated(true)
-    }
-
-    @Test
-    fun testNotifyShowedMessage() {
-        underTest.onMessageShown()
-        verify(repository).setShowMessage(null)
-    }
-
-    @Test
-    fun testSetKeyguardPosition() {
-        underTest.setKeyguardPosition(0f)
-        verify(repository).setKeyguardPosition(0f)
-    }
-
-    @Test
-    fun testNotifyKeyguardAuthenticatedHandled() {
-        underTest.notifyKeyguardAuthenticatedHandled()
-        verify(repository).setKeyguardAuthenticated(null)
-    }
-
-    @Test
-    fun testNotifyUpdatedResources() {
-        underTest.notifyUpdatedResources()
-        verify(repository).setResourceUpdateRequests(false)
-    }
-
-    @Test
-    fun testSetBackButtonEnabled() {
-        underTest.setBackButtonEnabled(true)
-        verify(repository).setIsBackButtonEnabled(true)
-    }
-
-    @Test
-    fun testStartDisappearAnimation_willRunDismissFromKeyguard() {
-        whenever(bouncerViewDelegate.willRunDismissFromKeyguard()).thenReturn(true)
-
-        val runnable = mock(Runnable::class.java)
-        underTest.startDisappearAnimation(runnable)
-        // End runnable should run immediately
-        verify(runnable).run()
-        // ... while the disappear animation should never be run
-        verify(repository, never()).setPrimaryStartDisappearAnimation(any(Runnable::class.java))
-    }
-
-    @Test
-    fun testStartDisappearAnimation_willNotRunDismissFromKeyguard_() {
-        whenever(bouncerViewDelegate.willRunDismissFromKeyguard()).thenReturn(false)
-
-        val runnable = mock(Runnable::class.java)
-        underTest.startDisappearAnimation(runnable)
-        verify(repository).setPrimaryStartDisappearAnimation(any(Runnable::class.java))
-    }
-
-    @Test
-    fun testIsFullShowing() {
-        whenever(repository.primaryBouncerShow.value).thenReturn(true)
-        whenever(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
-        whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
-        assertThat(underTest.isFullyShowing()).isTrue()
-        whenever(repository.primaryBouncerShow.value).thenReturn(false)
-        assertThat(underTest.isFullyShowing()).isFalse()
-    }
-
-    @Test
-    fun testIsScrimmed() {
-        whenever(repository.primaryBouncerScrimmed.value).thenReturn(true)
-        assertThat(underTest.isScrimmed()).isTrue()
-        whenever(repository.primaryBouncerScrimmed.value).thenReturn(false)
-        assertThat(underTest.isScrimmed()).isFalse()
-    }
-
-    @Test
-    fun testIsInTransit() {
-        whenever(repository.primaryBouncerShowingSoon.value).thenReturn(true)
-        assertThat(underTest.isInTransit()).isTrue()
-        whenever(repository.primaryBouncerShowingSoon.value).thenReturn(false)
-        assertThat(underTest.isInTransit()).isFalse()
-        whenever(repository.panelExpansionAmount.value).thenReturn(0.5f)
-        assertThat(underTest.isInTransit()).isTrue()
-    }
-
-    @Test
-    fun testIsAnimatingAway() {
-        whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(Runnable {})
-        assertThat(underTest.isAnimatingAway()).isTrue()
-        whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
-        assertThat(underTest.isAnimatingAway()).isFalse()
-    }
-
-    @Test
-    fun testWillDismissWithAction() {
-        whenever(bouncerViewDelegate.willDismissWithActions()).thenReturn(true)
-        assertThat(underTest.willDismissWithAction()).isTrue()
-        whenever(bouncerViewDelegate.willDismissWithActions()).thenReturn(false)
-        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)
-    }
-
-    @Test
-    fun delayBouncerWhenFaceAuthPossible() {
-        mainHandler.setMode(FakeHandler.Mode.QUEUEING)
-
-        // GIVEN bouncer should be delayed due to face auth
-        whenever(keyguardStateController.isFaceAuthEnabled).thenReturn(true)
-        whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE))
-            .thenReturn(true)
-
-        // WHEN bouncer show is requested
-        underTest.show(true)
-
-        // THEN primary show & primary showing soon aren't updated immediately
-        verify(repository, never()).setPrimaryShow(true)
-        verify(repository, never()).setPrimaryShowingSoon(false)
-
-        // WHEN all queued messages are dispatched
-        mainHandler.dispatchQueuedMessages()
-
-        // THEN primary show & primary showing soon are updated
-        verify(repository).setPrimaryShow(true)
-        verify(repository).setPrimaryShowingSoon(false)
-    }
-
-    @Test
-    fun delayBouncerWhenActiveUnlockPossible() {
-        testScope.run {
-            mainHandler.setMode(FakeHandler.Mode.QUEUEING)
-
-            // GIVEN bouncer should be delayed due to active unlock
-            trustRepository.setCurrentUserActiveUnlockAvailable(true)
-            whenever(keyguardUpdateMonitor.canTriggerActiveUnlockBasedOnDeviceState())
-                .thenReturn(true)
-            runCurrent()
-
-            // WHEN bouncer show is requested
-            underTest.show(true)
-
-            // THEN primary show & primary showing soon were scheduled to update
-            verify(repository, never()).setPrimaryShow(true)
-            verify(repository, never()).setPrimaryShowingSoon(false)
-
-            // WHEN all queued messages are dispatched
-            mainHandler.dispatchQueuedMessages()
-
-            // THEN primary show & primary showing soon are updated
-            verify(repository).setPrimaryShow(true)
-            verify(repository).setPrimaryShowingSoon(false)
-        }
-    }
-
-    private fun updateSideFpsVisibilityParameters(
-        isVisible: Boolean,
-        sfpsEnabled: Boolean,
-        fpsDetectionRunning: Boolean,
-        isUnlockingWithFpAllowed: Boolean,
-        isAnimatingAway: Boolean
-    ) {
-        whenever(repository.primaryBouncerShow.value).thenReturn(isVisible)
-        resources.addOverride(R.bool.config_show_sidefps_hint_on_bouncer, sfpsEnabled)
-        whenever(keyguardUpdateMonitor.isFingerprintDetectionRunning)
-            .thenReturn(fpsDetectionRunning)
-        whenever(keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
-            .thenReturn(isUnlockingWithFpAllowed)
-        whenever(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
deleted file mode 100644
index b288fbc..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt
+++ /dev/null
@@ -1,120 +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.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.RoboPilotTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
-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.data.repository.TrustRepository
-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.TestScope
-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
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RoboPilotTest
-@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,
-                Mockito.mock(TrustRepository::class.java),
-                FakeFeatureFlags().apply { set(Flags.DELAY_BOUNCER, true) },
-                TestScope().backgroundScope,
-            )
-    }
-
-    @Test
-    fun notInteractableWhenExpansionIsBelow90Percent() = runTest {
-        val isInteractable = collectLastValue(underTest.isInteractable)
-
-        repository.setPrimaryShow(true)
-        repository.setPanelExpansion(0.15f)
-
-        assertThat(isInteractable()).isFalse()
-    }
-
-    @Test
-    fun notInteractableWhenExpansionAbove90PercentButNotVisible() = runTest {
-        val isInteractable = collectLastValue(underTest.isInteractable)
-
-        repository.setPrimaryShow(false)
-        repository.setPanelExpansion(0.05f)
-
-        assertThat(isInteractable()).isFalse()
-    }
-
-    @Test
-    fun isInteractableWhenExpansionAbove90PercentAndVisible() = runTest {
-        var isInteractable = collectLastValue(underTest.isInteractable)
-
-        repository.setPrimaryShow(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/domain/interactor/UdfpsKeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
new file mode 100644
index 0000000..1baca21
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.doze.util.BurnInHelperWrapper
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.keyguard.shared.model.WakeSleepReason
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.keyguard.shared.model.WakefulnessState
+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 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.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UdfpsKeyguardInteractorTest : SysuiTestCase() {
+    private val burnInProgress = 1f
+    private val burnInYOffset = 20
+    private val burnInXOffset = 10
+
+    private lateinit var testScope: TestScope
+    private lateinit var configRepository: FakeConfigurationRepository
+    private lateinit var bouncerRepository: KeyguardBouncerRepository
+    private lateinit var keyguardRepository: FakeKeyguardRepository
+    private lateinit var fakeCommandQueue: FakeCommandQueue
+    private lateinit var featureFlags: FakeFeatureFlags
+    private lateinit var burnInInteractor: BurnInInteractor
+
+    @Mock private lateinit var burnInHelper: BurnInHelperWrapper
+
+    private lateinit var underTest: UdfpsKeyguardInteractor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        testScope = TestScope()
+        configRepository = FakeConfigurationRepository()
+        keyguardRepository = FakeKeyguardRepository()
+        bouncerRepository = FakeKeyguardBouncerRepository()
+        fakeCommandQueue = FakeCommandQueue()
+        featureFlags =
+            FakeFeatureFlags().apply {
+                set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true)
+                set(Flags.FACE_AUTH_REFACTOR, false)
+            }
+        burnInInteractor =
+            BurnInInteractor(
+                context,
+                burnInHelper,
+                testScope.backgroundScope,
+                configRepository,
+                FakeSystemClock(),
+            )
+
+        underTest =
+            UdfpsKeyguardInteractor(
+                configRepository,
+                burnInInteractor,
+                KeyguardInteractor(
+                    keyguardRepository,
+                    fakeCommandQueue,
+                    featureFlags,
+                    bouncerRepository,
+                    configRepository,
+                ),
+            )
+    }
+
+    @Test
+    fun dozeChanges_updatesUdfpsAodModel() =
+        testScope.runTest {
+            val burnInOffsets by collectLastValue(underTest.burnInOffsets)
+            initializeBurnInOffsets()
+
+            // WHEN we're not dozing
+            setAwake()
+            runCurrent()
+
+            // THEN burn in offsets are 0
+            assertThat(burnInOffsets?.burnInProgress).isEqualTo(0f)
+            assertThat(burnInOffsets?.burnInYOffset).isEqualTo(0)
+            assertThat(burnInOffsets?.burnInXOffset).isEqualTo(0)
+
+            // WHEN we're in the middle of the doze amount change
+            keyguardRepository.setDozeAmount(.50f)
+            runCurrent()
+
+            // THEN burn in is updated (between 0 and the full offset)
+            assertThat(burnInOffsets?.burnInProgress).isGreaterThan(0f)
+            assertThat(burnInOffsets?.burnInYOffset).isGreaterThan(0)
+            assertThat(burnInOffsets?.burnInXOffset).isGreaterThan(0)
+            assertThat(burnInOffsets?.burnInProgress).isLessThan(burnInProgress)
+            assertThat(burnInOffsets?.burnInYOffset).isLessThan(burnInYOffset)
+            assertThat(burnInOffsets?.burnInXOffset).isLessThan(burnInXOffset)
+
+            // WHEN we're fully dozing
+            keyguardRepository.setDozeAmount(1f)
+            runCurrent()
+
+            // THEN burn in offsets are updated to final current values (for the given time)
+            assertThat(burnInOffsets?.burnInProgress).isEqualTo(burnInProgress)
+            assertThat(burnInOffsets?.burnInYOffset).isEqualTo(burnInYOffset)
+            assertThat(burnInOffsets?.burnInXOffset).isEqualTo(burnInXOffset)
+        }
+
+    private fun initializeBurnInOffsets() {
+        whenever(burnInHelper.burnInProgressOffset()).thenReturn(burnInProgress)
+        whenever(burnInHelper.burnInOffset(anyInt(), /* xAxis */ eq(true)))
+            .thenReturn(burnInXOffset)
+        whenever(burnInHelper.burnInOffset(anyInt(), /* xAxis */ eq(false)))
+            .thenReturn(burnInYOffset)
+    }
+
+    private fun setAwake() {
+        keyguardRepository.setDozeAmount(0f)
+        burnInInteractor.dozeTimeTick()
+
+        bouncerRepository.setAlternateVisible(false)
+        keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+        bouncerRepository.setPrimaryShow(false)
+        keyguardRepository.setWakefulnessModel(
+            WakefulnessModel(
+                WakefulnessState.AWAKE,
+                WakeSleepReason.POWER_BUTTON,
+                WakeSleepReason.POWER_BUTTON,
+            )
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/DefaultLockscreenLayoutTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/DefaultLockscreenLayoutTest.kt
new file mode 100644
index 0000000..2e97208
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/DefaultLockscreenLayoutTest.kt
@@ -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.keyguard.ui.view.layout
+
+import android.graphics.Point
+import android.view.ViewGroup
+import android.view.WindowManager
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.AuthController
+import com.android.systemui.keyguard.ui.view.KeyguardRootView
+import com.android.systemui.monet.utils.ArgbSubject.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Answers
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@RunWith(JUnit4::class)
+@SmallTest
+class DefaultLockscreenLayoutTest : SysuiTestCase() {
+    private lateinit var defaultLockscreenLayout: DefaultLockscreenLayout
+    private lateinit var rootView: KeyguardRootView
+    @Mock private lateinit var authController: AuthController
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var windowManager: WindowManager
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        rootView = KeyguardRootView(context, null)
+        defaultLockscreenLayout =
+            DefaultLockscreenLayout(authController, keyguardUpdateMonitor, windowManager, context)
+    }
+
+    @Test
+    fun testLayoutViews_KeyguardIndicationArea() {
+        defaultLockscreenLayout.layoutViews(rootView)
+        val constraint = getViewConstraint(R.id.keyguard_indication_area)
+        assertThat(constraint.layout.bottomToBottom).isEqualTo(ConstraintSet.PARENT_ID)
+        assertThat(constraint.layout.startToStart).isEqualTo(ConstraintSet.PARENT_ID)
+        assertThat(constraint.layout.endToEnd).isEqualTo(ConstraintSet.PARENT_ID)
+        assertThat(constraint.layout.mWidth).isEqualTo(ViewGroup.LayoutParams.MATCH_PARENT)
+        assertThat(constraint.layout.mHeight).isEqualTo(ViewGroup.LayoutParams.WRAP_CONTENT)
+    }
+
+    @Test
+    fun testLayoutViews_lockIconView() {
+        defaultLockscreenLayout.layoutViews(rootView)
+        val constraint = getViewConstraint(R.id.lock_icon_view)
+        assertThat(constraint.layout.topToTop).isEqualTo(ConstraintSet.PARENT_ID)
+        assertThat(constraint.layout.startToStart).isEqualTo(ConstraintSet.PARENT_ID)
+    }
+
+    @Test
+    fun testCenterLockIcon() {
+        defaultLockscreenLayout.centerLockIcon(Point(5, 6), 1F, 5, rootView)
+        val constraint = getViewConstraint(R.id.lock_icon_view)
+
+        assertThat(constraint.layout.mWidth).isEqualTo(2)
+        assertThat(constraint.layout.mHeight).isEqualTo(2)
+        assertThat(constraint.layout.topToTop).isEqualTo(ConstraintSet.PARENT_ID)
+        assertThat(constraint.layout.startToStart).isEqualTo(ConstraintSet.PARENT_ID)
+        assertThat(constraint.layout.topMargin).isEqualTo(5)
+        assertThat(constraint.layout.startMargin).isEqualTo(4)
+    }
+
+    /** Get the ConstraintLayout constraint of the view. */
+    private fun getViewConstraint(viewId: Int): ConstraintSet.Constraint {
+        val constraintSet = ConstraintSet()
+        constraintSet.clone(rootView)
+        return constraintSet.getConstraint(viewId)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManagerCommandListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManagerCommandListenerTest.kt
new file mode 100644
index 0000000..145b2fd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManagerCommandListenerTest.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.view.layout
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.commandline.Command
+import com.android.systemui.statusbar.commandline.CommandRegistry
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import java.io.PrintWriter
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mock
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(JUnit4::class)
+@SmallTest
+class KeyguardLayoutManagerCommandListenerTest : SysuiTestCase() {
+    private lateinit var keyguardLayoutManagerCommandListener: KeyguardLayoutManagerCommandListener
+    @Mock private lateinit var commandRegistry: CommandRegistry
+    @Mock private lateinit var keyguardLayoutManager: KeyguardLayoutManager
+    @Mock private lateinit var pw: PrintWriter
+    private lateinit var command: () -> Command
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        keyguardLayoutManagerCommandListener =
+            KeyguardLayoutManagerCommandListener(
+                commandRegistry,
+                keyguardLayoutManager,
+            )
+        keyguardLayoutManagerCommandListener.start()
+        command =
+            withArgCaptor<() -> Command> {
+                verify(commandRegistry).registerCommand(eq("layout"), capture())
+            }
+    }
+
+    @Test
+    fun testHelp() {
+        command().execute(pw, listOf("help"))
+        verify(pw, atLeastOnce()).println(anyString())
+        verify(keyguardLayoutManager, never()).transitionToLayout(anyString())
+    }
+
+    @Test
+    fun testBlank() {
+        command().execute(pw, listOf())
+        verify(pw, atLeastOnce()).println(anyString())
+        verify(keyguardLayoutManager, never()).transitionToLayout(anyString())
+    }
+
+    @Test
+    fun testValidArg() {
+        bindFakeIdMapToLayoutManager()
+        command().execute(pw, listOf("fake"))
+        verify(keyguardLayoutManager).transitionToLayout("fake")
+    }
+
+    private fun bindFakeIdMapToLayoutManager() {
+        val map = mapOf("fake" to mock(LockscreenLayout::class.java))
+        whenever(keyguardLayoutManager.layoutIdMap).thenReturn(map)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManagerTest.kt
new file mode 100644
index 0000000..95b2030
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardLayoutManagerTest.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.view.layout
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.ui.view.KeyguardRootView
+import com.android.systemui.keyguard.ui.view.layout.DefaultLockscreenLayout.Companion.DEFAULT
+import com.android.systemui.statusbar.policy.ConfigurationController
+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.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(JUnit4::class)
+@SmallTest
+class KeyguardLayoutManagerTest : SysuiTestCase() {
+    private lateinit var keyguardLayoutManager: KeyguardLayoutManager
+    @Mock lateinit var configurationController: ConfigurationController
+    @Mock lateinit var defaultLockscreenLayout: DefaultLockscreenLayout
+    @Mock lateinit var keyguardRootView: KeyguardRootView
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        whenever(defaultLockscreenLayout.id).thenReturn(DEFAULT)
+        keyguardLayoutManager =
+            KeyguardLayoutManager(
+                configurationController,
+                setOf(defaultLockscreenLayout),
+                keyguardRootView
+            )
+    }
+
+    @Test
+    fun testDefaultLayout() {
+        keyguardLayoutManager.transitionToLayout(DEFAULT)
+        verify(defaultLockscreenLayout).layoutViews(keyguardRootView)
+    }
+
+    @Test
+    fun testTransitionToLayout_validId() {
+        assertThat(keyguardLayoutManager.transitionToLayout(DEFAULT)).isTrue()
+    }
+    @Test
+    fun testTransitionToLayout_invalidId() {
+        assertThat(keyguardLayoutManager.transitionToLayout("abc")).isFalse()
+    }
+}
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 ab994b7..c67f535 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
@@ -22,6 +22,8 @@
 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.domain.interactor.KeyguardTransitionInteractorFactory
+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.DOZING
 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
@@ -55,7 +57,12 @@
     @Before
     fun setUp() {
         repository = FakeKeyguardTransitionRepository()
-        val interactor = KeyguardTransitionInteractor(repository, TestScope().backgroundScope)
+        val interactor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = TestScope().backgroundScope,
+                    repository = repository,
+                )
+                .keyguardTransitionInteractor
         underTest = DreamingToLockscreenTransitionViewModel(interactor, mock())
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
index 694539b..75c8bff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.RoboPilotTest
 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.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -46,7 +46,12 @@
     @Before
     fun setUp() {
         repository = FakeKeyguardTransitionRepository()
-        val interactor = KeyguardTransitionInteractor(repository, TestScope().backgroundScope)
+        val interactor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = TestScope().backgroundScope,
+                    repository = repository,
+                )
+                .keyguardTransitionInteractor
         underTest = GoneToDreamingTransitionViewModel(interactor)
     }
 
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 b6368cf..d02b3fc 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
@@ -40,15 +40,13 @@
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
 import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
 import com.android.systemui.keyguard.domain.interactor.KeyguardLongPressInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
 import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
@@ -57,7 +55,6 @@
 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.AccessibilityManagerWrapper
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.FakeSharedPreferences
@@ -96,7 +93,6 @@
     @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
     @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger
     @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
@@ -143,7 +139,6 @@
                         ),
                 ),
             )
-        repository = FakeKeyguardRepository()
         val featureFlags =
             FakeFeatureFlags().apply {
                 set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
@@ -152,13 +147,10 @@
                 set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, false)
             }
 
-        val keyguardInteractor =
-            KeyguardInteractor(
-                repository = repository,
-                commandQueue = commandQueue,
-                featureFlags = featureFlags,
-                bouncerRepository = FakeKeyguardBouncerRepository(),
-            )
+        val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags)
+        val keyguardInteractor = withDeps.keyguardInteractor
+        repository = withDeps.repository
+
         whenever(userTracker.userHandle).thenReturn(mock())
         whenever(userTracker.userId).thenReturn(10)
         whenever(lockPatternUtils.getStrongAuthForUser(anyInt()))
@@ -217,10 +209,10 @@
                 appContext = mContext,
                 scope = testScope.backgroundScope,
                 transitionInteractor =
-                    KeyguardTransitionInteractor(
-                        repository = FakeKeyguardTransitionRepository(),
-                        scope = testScope.backgroundScope
-                    ),
+                    KeyguardTransitionInteractorFactory.create(
+                            scope = TestScope().backgroundScope,
+                        )
+                        .keyguardTransitionInteractor,
                 repository = repository,
                 logger = UiEventLoggerFake(),
                 featureFlags = featureFlags,
@@ -555,91 +547,6 @@
         }
 
     @Test
-    fun isIndicationAreaPadded() =
-        testScope.runTest {
-            repository.setKeyguardShowing(true)
-            val value = collectLastValue(underTest.isIndicationAreaPadded)
-
-            assertThat(value()).isFalse()
-            setUpQuickAffordanceModel(
-                position = KeyguardQuickAffordancePosition.BOTTOM_START,
-                testConfig =
-                    TestConfig(
-                        isVisible = true,
-                        isClickable = true,
-                        icon = mock(),
-                        canShowWhileLocked = true,
-                        slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
-                    )
-            )
-            assertThat(value()).isTrue()
-            setUpQuickAffordanceModel(
-                position = KeyguardQuickAffordancePosition.BOTTOM_END,
-                testConfig =
-                    TestConfig(
-                        isVisible = true,
-                        isClickable = true,
-                        icon = mock(),
-                        canShowWhileLocked = false,
-                        slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(),
-                    )
-            )
-            assertThat(value()).isTrue()
-            setUpQuickAffordanceModel(
-                position = KeyguardQuickAffordancePosition.BOTTOM_START,
-                testConfig =
-                    TestConfig(
-                        isVisible = false,
-                        slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
-                    )
-            )
-            assertThat(value()).isTrue()
-            setUpQuickAffordanceModel(
-                position = KeyguardQuickAffordancePosition.BOTTOM_END,
-                testConfig =
-                    TestConfig(
-                        isVisible = false,
-                        slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(),
-                    )
-            )
-            assertThat(value()).isFalse()
-        }
-
-    @Test
-    fun indicationAreaTranslationX() =
-        testScope.runTest {
-            val value = collectLastValue(underTest.indicationAreaTranslationX)
-
-            assertThat(value()).isEqualTo(0f)
-            repository.setClockPosition(100, 100)
-            assertThat(value()).isEqualTo(100f)
-            repository.setClockPosition(200, 100)
-            assertThat(value()).isEqualTo(200f)
-            repository.setClockPosition(200, 200)
-            assertThat(value()).isEqualTo(200f)
-            repository.setClockPosition(300, 100)
-            assertThat(value()).isEqualTo(300f)
-        }
-
-    @Test
-    fun indicationAreaTranslationY() =
-        testScope.runTest {
-            val value =
-                collectLastValue(underTest.indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET))
-
-            // Negative 0 - apparently there's a difference in floating point arithmetic - FML
-            assertThat(value()).isEqualTo(-0f)
-            val expected1 = setDozeAmountAndCalculateExpectedTranslationY(0.1f)
-            assertThat(value()).isEqualTo(expected1)
-            val expected2 = setDozeAmountAndCalculateExpectedTranslationY(0.2f)
-            assertThat(value()).isEqualTo(expected2)
-            val expected3 = setDozeAmountAndCalculateExpectedTranslationY(0.5f)
-            assertThat(value()).isEqualTo(expected3)
-            val expected4 = setDozeAmountAndCalculateExpectedTranslationY(1f)
-            assertThat(value()).isEqualTo(expected4)
-        }
-
-    @Test
     fun isClickable_trueWhenAlphaAtThreshold() =
         testScope.runTest {
             repository.setKeyguardShowing(true)
@@ -762,11 +669,6 @@
             )
         }
 
-    private fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
-        repository.setDozeAmount(dozeAmount)
-        return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
-    }
-
     private suspend fun setUpQuickAffordanceModel(
         position: KeyguardQuickAffordancePosition,
         testConfig: TestConfig,
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
deleted file mode 100644
index 15707c9..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt
+++ /dev/null
@@ -1,159 +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.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.RoboPilotTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
-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.data.repository.TrustRepository
-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.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-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.Mock
-import org.mockito.Mockito
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RoboPilotTest
-@RunWith(AndroidJUnit4::class)
-@kotlinx.coroutines.ExperimentalCoroutinesApi
-class KeyguardBouncerViewModelTest : SysuiTestCase() {
-    lateinit var underTest: KeyguardBouncerViewModel
-    lateinit var bouncerInteractor: PrimaryBouncerInteractor
-    @Mock 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())
-    val repository = FakeKeyguardBouncerRepository()
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        bouncerInteractor =
-            PrimaryBouncerInteractor(
-                repository,
-                bouncerView,
-                mainHandler,
-                keyguardStateController,
-                keyguardSecurityModel,
-                primaryBouncerCallbackInteractor,
-                falsingCollector,
-                dismissCallbackRegistry,
-                context,
-                keyguardUpdateMonitor,
-                Mockito.mock(TrustRepository::class.java),
-                FakeFeatureFlags().apply { set(Flags.DELAY_BOUNCER, true) },
-                TestScope().backgroundScope,
-            )
-        underTest = KeyguardBouncerViewModel(bouncerView, bouncerInteractor)
-    }
-
-    @Test
-    fun setMessage() = runTest {
-        var message: BouncerShowMessageModel? = null
-        val job = underTest.bouncerShowMessage.onEach { message = it }.launchIn(this)
-
-        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()
-    }
-
-    @Test
-    fun shouldUpdateSideFps_show() = runTest {
-        var count = 0
-        val job = underTest.shouldUpdateSideFps.onEach { count++ }.launchIn(this)
-        repository.setPrimaryShow(true)
-        // Run the tasks that are pending at this point of virtual time.
-        runCurrent()
-        assertThat(count).isEqualTo(1)
-        job.cancel()
-    }
-
-    @Test
-    fun shouldUpdateSideFps_hide() = runTest {
-        repository.setPrimaryShow(true)
-        var count = 0
-        val job = underTest.shouldUpdateSideFps.onEach { count++ }.launchIn(this)
-        repository.setPrimaryShow(false)
-        // 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()
-    }
-
-    @Test
-    fun isShowing() = runTest {
-        var isShowing: Boolean? = null
-        val job = underTest.isShowing.onEach { isShowing = it }.launchIn(this)
-        repository.setPrimaryShow(true)
-        // Run the tasks that are pending at this point of virtual time.
-        runCurrent()
-        assertThat(isShowing).isEqualTo(true)
-        job.cancel()
-    }
-
-    @Test
-    fun isNotShowing() = runTest {
-        var isShowing: Boolean? = null
-        val job = underTest.isShowing.onEach { isShowing = it }.launchIn(this)
-        repository.setPrimaryShow(false)
-        // Run the tasks that are pending at this point of virtual time.
-        runCurrent()
-        assertThat(isShowing).isEqualTo(false)
-        job.cancel()
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
new file mode 100644
index 0000000..dff0f29
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.doze.util.BurnInHelperWrapper
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
+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.flow.MutableStateFlow
+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.Mock
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardIndicationAreaViewModelTest : SysuiTestCase() {
+
+    @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
+
+    private lateinit var underTest: KeyguardIndicationAreaViewModel
+    private lateinit var repository: FakeKeyguardRepository
+
+    private val startButtonFlow =
+        MutableStateFlow<KeyguardQuickAffordanceViewModel>(
+            KeyguardQuickAffordanceViewModel(
+                slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId()
+            )
+        )
+    private val endButtonFlow =
+        MutableStateFlow<KeyguardQuickAffordanceViewModel>(
+            KeyguardQuickAffordanceViewModel(
+                slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId()
+            )
+        )
+    private val alphaFlow = MutableStateFlow<Float>(1f)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(burnInHelperWrapper.burnInOffset(anyInt(), any()))
+            .thenReturn(RETURNED_BURN_IN_OFFSET)
+
+        val withDeps = KeyguardInteractorFactory.create()
+        val keyguardInteractor = withDeps.keyguardInteractor
+        repository = withDeps.repository
+
+        val bottomAreaViewModel: KeyguardBottomAreaViewModel = mock()
+        whenever(bottomAreaViewModel.startButton).thenReturn(startButtonFlow)
+        whenever(bottomAreaViewModel.endButton).thenReturn(endButtonFlow)
+        whenever(bottomAreaViewModel.alpha).thenReturn(alphaFlow)
+        underTest =
+            KeyguardIndicationAreaViewModel(
+                keyguardInteractor = keyguardInteractor,
+                bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository),
+                keyguardBottomAreaViewModel = bottomAreaViewModel,
+                burnInHelperWrapper = burnInHelperWrapper,
+            )
+    }
+
+    @Test
+    fun alpha() = runTest {
+        val value = collectLastValue(underTest.alpha)
+
+        assertThat(value()).isEqualTo(1f)
+        alphaFlow.value = 0.1f
+        assertThat(value()).isEqualTo(0.1f)
+        alphaFlow.value = 0.5f
+        assertThat(value()).isEqualTo(0.5f)
+        alphaFlow.value = 0.2f
+        assertThat(value()).isEqualTo(0.2f)
+        alphaFlow.value = 0f
+        assertThat(value()).isEqualTo(0f)
+    }
+
+    @Test
+    fun isIndicationAreaPadded() = runTest {
+        repository.setKeyguardShowing(true)
+        val value = collectLastValue(underTest.isIndicationAreaPadded)
+
+        assertThat(value()).isFalse()
+        startButtonFlow.value = startButtonFlow.value.copy(isVisible = true)
+        assertThat(value()).isTrue()
+        endButtonFlow.value = endButtonFlow.value.copy(isVisible = true)
+        assertThat(value()).isTrue()
+        startButtonFlow.value = startButtonFlow.value.copy(isVisible = false)
+        assertThat(value()).isTrue()
+        endButtonFlow.value = endButtonFlow.value.copy(isVisible = false)
+        assertThat(value()).isFalse()
+    }
+
+    @Test
+    fun indicationAreaTranslationX() = runTest {
+        val value = collectLastValue(underTest.indicationAreaTranslationX)
+
+        assertThat(value()).isEqualTo(0f)
+        repository.setClockPosition(100, 100)
+        assertThat(value()).isEqualTo(100f)
+        repository.setClockPosition(200, 100)
+        assertThat(value()).isEqualTo(200f)
+        repository.setClockPosition(200, 200)
+        assertThat(value()).isEqualTo(200f)
+        repository.setClockPosition(300, 100)
+        assertThat(value()).isEqualTo(300f)
+    }
+
+    @Test
+    fun indicationAreaTranslationY() = runTest {
+        val value = collectLastValue(underTest.indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET))
+
+        // Negative 0 - apparently there's a difference in floating point arithmetic - FML
+        assertThat(value()).isEqualTo(-0f)
+        val expected1 = setDozeAmountAndCalculateExpectedTranslationY(0.1f)
+        assertThat(value()).isEqualTo(expected1)
+        val expected2 = setDozeAmountAndCalculateExpectedTranslationY(0.2f)
+        assertThat(value()).isEqualTo(expected2)
+        val expected3 = setDozeAmountAndCalculateExpectedTranslationY(0.5f)
+        assertThat(value()).isEqualTo(expected3)
+        val expected4 = setDozeAmountAndCalculateExpectedTranslationY(1f)
+        assertThat(value()).isEqualTo(expected4)
+    }
+
+    private fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
+        repository.setDozeAmount(dozeAmount)
+        return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
+    }
+
+    companion object {
+        private const val DEFAULT_BURN_IN_OFFSET = 5
+        private const val RETURNED_BURN_IN_OFFSET = 3
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
index 8ba3f0f..ff4ec4b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
@@ -29,7 +29,6 @@
 import com.android.systemui.scene.shared.model.SceneModel
 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.Test
@@ -41,8 +40,8 @@
 @RunWith(JUnit4::class)
 class LockscreenSceneViewModelTest : SysuiTestCase() {
 
-    private val testScope = TestScope()
-    private val utils = SceneTestUtils(this, testScope)
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
     private val sceneInteractor = utils.sceneInteractor()
     private val authenticationInteractor =
         utils.authenticationInteractor(
@@ -73,10 +72,10 @@
     fun lockButtonIcon_whenLocked() =
         testScope.runTest {
             val lockButtonIcon by collectLastValue(underTest.lockButtonIcon)
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Password("password")
             )
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setUnlocked(false)
 
             assertThat((lockButtonIcon as? Icon.Resource)?.res)
                 .isEqualTo(R.drawable.ic_device_lock_on)
@@ -86,10 +85,10 @@
     fun lockButtonIcon_whenUnlocked() =
         testScope.runTest {
             val lockButtonIcon by collectLastValue(underTest.lockButtonIcon)
-            authenticationInteractor.setAuthenticationMethod(
+            utils.authenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Password("password")
             )
-            authenticationInteractor.unlockDevice()
+            utils.authenticationRepository.setUnlocked(true)
 
             assertThat((lockButtonIcon as? Icon.Resource)?.res)
                 .isEqualTo(R.drawable.ic_device_lock_off)
@@ -99,8 +98,8 @@
     fun upTransitionSceneKey_swipeToUnlockedEnabled_gone() =
         testScope.runTest {
             val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+            utils.authenticationRepository.setUnlocked(false)
 
             assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone)
         }
@@ -109,8 +108,10 @@
     fun upTransitionSceneKey_swipeToUnlockedNotEnabled_bouncer() =
         testScope.runTest {
             val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(false)
 
             assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer)
         }
@@ -119,8 +120,10 @@
     fun onLockButtonClicked_deviceLockedSecurely_switchesToBouncer() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(false)
             runCurrent()
 
             underTest.onLockButtonClicked()
@@ -132,8 +135,10 @@
     fun onContentClicked_deviceUnlocked_switchesToGone() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.unlockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(true)
             runCurrent()
 
             underTest.onContentClicked()
@@ -145,8 +150,10 @@
     fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(false)
             runCurrent()
 
             underTest.onContentClicked()
@@ -158,8 +165,10 @@
     fun onLockButtonClicked_deviceUnlocked_switchesToGone() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.unlockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(true)
             runCurrent()
 
             underTest.onLockButtonClicked()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index ea17751..12fe07f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.RoboPilotTest
 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.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -46,7 +46,12 @@
     @Before
     fun setUp() {
         repository = FakeKeyguardTransitionRepository()
-        val interactor = KeyguardTransitionInteractor(repository, TestScope().backgroundScope)
+        val interactor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = TestScope().backgroundScope,
+                    repository = repository,
+                )
+                .keyguardTransitionInteractor
         underTest = LockscreenToDreamingTransitionViewModel(interactor)
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index bf56a98..83ae631 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.RoboPilotTest
 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.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -46,7 +46,12 @@
     @Before
     fun setUp() {
         repository = FakeKeyguardTransitionRepository()
-        val interactor = KeyguardTransitionInteractor(repository, TestScope().backgroundScope)
+        val interactor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = TestScope().backgroundScope,
+                    repository = repository,
+                )
+                .keyguardTransitionInteractor
         underTest = LockscreenToOccludedTransitionViewModel(interactor)
     }
 
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 34da26e..8860399 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
@@ -21,7 +21,7 @@
 import com.android.systemui.RoboPilotTest
 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.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -46,7 +46,12 @@
     @Before
     fun setUp() {
         repository = FakeKeyguardTransitionRepository()
-        val interactor = KeyguardTransitionInteractor(repository, TestScope().backgroundScope)
+        val interactor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = TestScope().backgroundScope,
+                    repository = repository,
+                )
+                .keyguardTransitionInteractor
         underTest = OccludedToLockscreenTransitionViewModel(interactor)
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
index 65b2ef7..d8c78eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
@@ -20,9 +20,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.ScrimAlpha
 import com.android.systemui.keyguard.shared.model.TransitionState
@@ -55,7 +55,12 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         repository = FakeKeyguardTransitionRepository()
-        val interactor = KeyguardTransitionInteractor(repository, TestScope().backgroundScope)
+        val interactor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = TestScope().backgroundScope,
+                    repository = repository,
+                )
+                .keyguardTransitionInteractor
         underTest =
             PrimaryBouncerToGoneTransitionViewModel(
                 interactor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt
new file mode 100644
index 0000000..436c09c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.doze.util.BurnInHelperWrapper
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
+import com.android.systemui.util.time.FakeSystemClock
+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.Mock
+import org.mockito.MockitoAnnotations
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
+class UdfpsAodViewModelTest : SysuiTestCase() {
+    private val defaultPadding = 12
+    private lateinit var underTest: UdfpsAodViewModel
+
+    private lateinit var testScope: TestScope
+    private lateinit var configRepository: FakeConfigurationRepository
+    private lateinit var bouncerRepository: KeyguardBouncerRepository
+    private lateinit var keyguardRepository: FakeKeyguardRepository
+    private lateinit var fakeCommandQueue: FakeCommandQueue
+    private lateinit var featureFlags: FakeFeatureFlags
+
+    @Mock private lateinit var burnInHelper: BurnInHelperWrapper
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        overrideResource(com.android.systemui.R.dimen.lock_icon_padding, defaultPadding)
+        testScope = TestScope()
+        configRepository = FakeConfigurationRepository()
+        keyguardRepository = FakeKeyguardRepository()
+        bouncerRepository = FakeKeyguardBouncerRepository()
+        fakeCommandQueue = FakeCommandQueue()
+        featureFlags =
+            FakeFeatureFlags().apply {
+                set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true)
+                set(Flags.FACE_AUTH_REFACTOR, false)
+            }
+
+        val udfpsKeyguardInteractor =
+            UdfpsKeyguardInteractor(
+                configRepository,
+                BurnInInteractor(
+                    context,
+                    burnInHelper,
+                    testScope.backgroundScope,
+                    configRepository,
+                    FakeSystemClock(),
+                ),
+                KeyguardInteractor(
+                    keyguardRepository,
+                    fakeCommandQueue,
+                    featureFlags,
+                    bouncerRepository,
+                    configRepository,
+                ),
+            )
+
+        underTest =
+            UdfpsAodViewModel(
+                udfpsKeyguardInteractor,
+                context,
+            )
+    }
+
+    @Test
+    fun alphaAndVisibleUpdates_onDozeAmountChanges() =
+        testScope.runTest {
+            val alpha by collectLastValue(underTest.alpha)
+            val visible by collectLastValue(underTest.isVisible)
+
+            keyguardRepository.setDozeAmount(0f)
+            runCurrent()
+            assertThat(alpha).isEqualTo(0f)
+            assertThat(visible).isFalse()
+
+            keyguardRepository.setDozeAmount(.65f)
+            runCurrent()
+            assertThat(alpha).isEqualTo(.65f)
+            assertThat(visible).isTrue()
+
+            keyguardRepository.setDozeAmount(.23f)
+            runCurrent()
+            assertThat(alpha).isEqualTo(.23f)
+            assertThat(visible).isTrue()
+
+            keyguardRepository.setDozeAmount(1f)
+            runCurrent()
+            assertThat(alpha).isEqualTo(1f)
+            assertThat(visible).isTrue()
+        }
+
+    @Test
+    fun paddingUpdates_onScaleForResolutionChanges() =
+        testScope.runTest {
+            val padding by collectLastValue(underTest.padding)
+
+            configRepository.setScaleForResolution(1f)
+            runCurrent()
+            assertThat(padding).isEqualTo(defaultPadding)
+
+            configRepository.setScaleForResolution(2f)
+            runCurrent()
+            assertThat(padding).isEqualTo(defaultPadding * 2)
+
+            configRepository.setScaleForResolution(.5f)
+            runCurrent()
+            assertThat(padding).isEqualTo((defaultPadding * .5f).toInt())
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt
new file mode 100644
index 0000000..a30e2a6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.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.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.doze.util.BurnInHelperWrapper
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
+import com.android.systemui.util.time.FakeSystemClock
+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.Mock
+import org.mockito.MockitoAnnotations
+
+/** Tests UdfpsFingerprintViewModel specific flows. */
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UdfpsFingerprintViewModelTest : SysuiTestCase() {
+    private val defaultPadding = 12
+    private lateinit var underTest: FingerprintViewModel
+
+    private lateinit var testScope: TestScope
+    private lateinit var configRepository: FakeConfigurationRepository
+    private lateinit var bouncerRepository: KeyguardBouncerRepository
+    private lateinit var keyguardRepository: FakeKeyguardRepository
+    private lateinit var fakeCommandQueue: FakeCommandQueue
+    private lateinit var featureFlags: FakeFeatureFlags
+    private lateinit var transitionRepository: FakeKeyguardTransitionRepository
+
+    @Mock private lateinit var burnInHelper: BurnInHelperWrapper
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        overrideResource(com.android.systemui.R.dimen.lock_icon_padding, defaultPadding)
+        testScope = TestScope()
+        configRepository = FakeConfigurationRepository()
+        keyguardRepository = FakeKeyguardRepository()
+        bouncerRepository = FakeKeyguardBouncerRepository()
+        fakeCommandQueue = FakeCommandQueue()
+        featureFlags =
+            FakeFeatureFlags().apply {
+                set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true)
+                set(Flags.FACE_AUTH_REFACTOR, false)
+            }
+        bouncerRepository = FakeKeyguardBouncerRepository()
+        transitionRepository = FakeKeyguardTransitionRepository()
+        val transitionInteractor =
+            KeyguardTransitionInteractor(
+                transitionRepository,
+                testScope.backgroundScope,
+            )
+        val udfpsKeyguardInteractor =
+            UdfpsKeyguardInteractor(
+                configRepository,
+                BurnInInteractor(
+                    context,
+                    burnInHelper,
+                    testScope.backgroundScope,
+                    configRepository,
+                    FakeSystemClock(),
+                ),
+                KeyguardInteractor(
+                    keyguardRepository,
+                    fakeCommandQueue,
+                    featureFlags,
+                    bouncerRepository,
+                    configRepository,
+                ),
+            )
+
+        underTest =
+            FingerprintViewModel(
+                context,
+                transitionInteractor,
+                udfpsKeyguardInteractor,
+            )
+    }
+
+    @Test
+    fun paddingUpdates_onScaleForResolutionChanges() =
+        testScope.runTest {
+            val padding by collectLastValue(underTest.padding)
+
+            configRepository.setScaleForResolution(1f)
+            runCurrent()
+            assertThat(padding).isEqualTo(defaultPadding)
+
+            configRepository.setScaleForResolution(2f)
+            runCurrent()
+            assertThat(padding).isEqualTo(defaultPadding * 2)
+
+            configRepository.setScaleForResolution(.5f)
+            runCurrent()
+            assertThat(padding).isEqualTo((defaultPadding * .5).toInt())
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
new file mode 100644
index 0000000..d58ceee
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
@@ -0,0 +1,505 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.settingslib.Utils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+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.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.MockitoAnnotations
+
+/** Tests UDFPS lockscreen view model transitions. */
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UdfpsLockscreenViewModelTest : SysuiTestCase() {
+    private val lockscreenColorResId = android.R.attr.textColorPrimary
+    private val alternateBouncerResId = com.android.internal.R.attr.materialColorOnPrimaryFixed
+    private val lockscreenColor = Utils.getColorAttrDefaultColor(context, lockscreenColorResId)
+    private val alternateBouncerColor =
+        Utils.getColorAttrDefaultColor(context, alternateBouncerResId)
+
+    private lateinit var underTest: UdfpsLockscreenViewModel
+    private lateinit var testScope: TestScope
+    private lateinit var transitionRepository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        testScope = TestScope()
+        transitionRepository = FakeKeyguardTransitionRepository()
+        val transitionInteractor =
+            KeyguardTransitionInteractor(
+                transitionRepository,
+                testScope.backgroundScope,
+            )
+        underTest =
+            UdfpsLockscreenViewModel(
+                context,
+                lockscreenColorResId,
+                alternateBouncerResId,
+                transitionInteractor,
+            )
+    }
+
+    @Test
+    fun goneToAodTransition() =
+        testScope.runTest {
+            val transition by collectLastValue(underTest.transition)
+            val visible by collectLastValue(underTest.visible)
+
+            // TransitionState.STARTED: gone -> AOD
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.AOD,
+                    value = 0f,
+                    transitionState = TransitionState.STARTED,
+                    ownerName = "goneToAodTransition",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(0f)
+            assertThat(visible).isFalse()
+
+            // TransitionState.RUNNING: gone -> AOD
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.AOD,
+                    value = .6f,
+                    transitionState = TransitionState.RUNNING,
+                    ownerName = "goneToAodTransition",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(0f)
+            assertThat(visible).isFalse()
+
+            // TransitionState.FINISHED: gone -> AOD
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.AOD,
+                    value = 1f,
+                    transitionState = TransitionState.FINISHED,
+                    ownerName = "goneToAodTransition",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(0f)
+            assertThat(visible).isFalse()
+        }
+
+    @Test
+    fun lockscreenToAod() =
+        testScope.runTest {
+            val transition by collectLastValue(underTest.transition)
+            val visible by collectLastValue(underTest.visible)
+
+            // TransitionState.STARTED: lockscreen -> AOD
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    value = 0f,
+                    transitionState = TransitionState.STARTED,
+                    ownerName = "lockscreenToAod",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(1f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(lockscreenColor)
+            assertThat(visible).isTrue()
+
+            // TransitionState.RUNNING: lockscreen -> AOD
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    value = .6f,
+                    transitionState = TransitionState.RUNNING,
+                    ownerName = "lockscreenToAod",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f))
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(lockscreenColor)
+            assertThat(visible).isTrue()
+
+            // TransitionState.FINISHED: lockscreen -> AOD
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    value = 1f,
+                    transitionState = TransitionState.FINISHED,
+                    ownerName = "lockscreenToAod",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(0f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(lockscreenColor)
+            assertThat(visible).isFalse()
+        }
+
+    @Test
+    fun aodToLockscreen() =
+        testScope.runTest {
+            val transition by collectLastValue(underTest.transition)
+            val visible by collectLastValue(underTest.visible)
+
+            // TransitionState.STARTED: AOD -> lockscreen
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                    value = 0f,
+                    transitionState = TransitionState.STARTED,
+                    ownerName = "aodToLockscreen",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(0f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(lockscreenColor)
+            assertThat(visible).isFalse()
+
+            // TransitionState.RUNNING: AOD -> lockscreen
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                    value = .6f,
+                    transitionState = TransitionState.RUNNING,
+                    ownerName = "aodToLockscreen",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isIn(Range.closed(.59f, .61f))
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(lockscreenColor)
+            assertThat(visible).isTrue()
+
+            // TransitionState.FINISHED: AOD -> lockscreen
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                    value = 1f,
+                    transitionState = TransitionState.FINISHED,
+                    ownerName = "aodToLockscreen",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(1f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(lockscreenColor)
+            assertThat(visible).isTrue()
+        }
+
+    @Test
+    fun lockscreenToAlternateBouncer() =
+        testScope.runTest {
+            val transition by collectLastValue(underTest.transition)
+            val visible by collectLastValue(underTest.visible)
+
+            // TransitionState.STARTED: lockscreen -> alternate bouncer
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.ALTERNATE_BOUNCER,
+                    value = 0f,
+                    transitionState = TransitionState.STARTED,
+                    ownerName = "lockscreenToAlternateBouncer",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(1f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
+            assertThat(visible).isTrue()
+
+            // TransitionState.RUNNING: lockscreen -> alternate bouncer
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.ALTERNATE_BOUNCER,
+                    value = .6f,
+                    transitionState = TransitionState.RUNNING,
+                    ownerName = "lockscreenToAlternateBouncer",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(1f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
+            assertThat(visible).isTrue()
+
+            // TransitionState.FINISHED: lockscreen -> alternate bouncer
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.ALTERNATE_BOUNCER,
+                    value = 1f,
+                    transitionState = TransitionState.FINISHED,
+                    ownerName = "lockscreenToAlternateBouncer",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(1f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
+            assertThat(visible).isTrue()
+        }
+
+    fun alternateBouncerToPrimaryBouncer() =
+        testScope.runTest {
+            val transition by collectLastValue(underTest.transition)
+            val visible by collectLastValue(underTest.visible)
+
+            // TransitionState.STARTED: alternate bouncer -> primary bouncer
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.ALTERNATE_BOUNCER,
+                    to = KeyguardState.PRIMARY_BOUNCER,
+                    value = 0f,
+                    transitionState = TransitionState.STARTED,
+                    ownerName = "alternateBouncerToPrimaryBouncer",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(1f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
+            assertThat(visible).isTrue()
+
+            // TransitionState.RUNNING: alternate bouncer -> primary bouncer
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.ALTERNATE_BOUNCER,
+                    to = KeyguardState.PRIMARY_BOUNCER,
+                    value = .6f,
+                    transitionState = TransitionState.RUNNING,
+                    ownerName = "alternateBouncerToPrimaryBouncer",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isIn(Range.closed(.59f, .61f))
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
+            assertThat(visible).isTrue()
+
+            // TransitionState.FINISHED: alternate bouncer -> primary bouncer
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.ALTERNATE_BOUNCER,
+                    to = KeyguardState.PRIMARY_BOUNCER,
+                    value = 1f,
+                    transitionState = TransitionState.FINISHED,
+                    ownerName = "alternateBouncerToPrimaryBouncer",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(0f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
+            assertThat(visible).isFalse()
+        }
+
+    fun alternateBouncerToAod() =
+        testScope.runTest {
+            val transition by collectLastValue(underTest.transition)
+            val visible by collectLastValue(underTest.visible)
+
+            // TransitionState.STARTED: alternate bouncer -> aod
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.ALTERNATE_BOUNCER,
+                    to = KeyguardState.AOD,
+                    value = 0f,
+                    transitionState = TransitionState.STARTED,
+                    ownerName = "alternateBouncerToAod",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(1f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
+            assertThat(visible).isTrue()
+
+            // TransitionState.RUNNING: alternate bouncer -> aod
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.ALTERNATE_BOUNCER,
+                    to = KeyguardState.AOD,
+                    value = .6f,
+                    transitionState = TransitionState.RUNNING,
+                    ownerName = "alternateBouncerToAod",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f))
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
+            assertThat(visible).isTrue()
+
+            // TransitionState.FINISHED: alternate bouncer -> aod
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.ALTERNATE_BOUNCER,
+                    to = KeyguardState.AOD,
+                    value = 1f,
+                    transitionState = TransitionState.FINISHED,
+                    ownerName = "alternateBouncerToAod",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(0f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(alternateBouncerColor)
+            assertThat(visible).isFalse()
+        }
+
+    @Test
+    fun lockscreenToOccluded() =
+        testScope.runTest {
+            val transition by collectLastValue(underTest.transition)
+            val visible by collectLastValue(underTest.visible)
+
+            // TransitionState.STARTED: lockscreen -> occluded
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.OCCLUDED,
+                    value = 0f,
+                    transitionState = TransitionState.STARTED,
+                    ownerName = "lockscreenToOccluded",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(1f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(lockscreenColor)
+            assertThat(visible).isTrue()
+
+            // TransitionState.RUNNING: lockscreen -> occluded
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.OCCLUDED,
+                    value = .6f,
+                    transitionState = TransitionState.RUNNING,
+                    ownerName = "lockscreenToOccluded",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isIn(Range.closed(.39f, .41f))
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(lockscreenColor)
+            assertThat(visible).isTrue()
+
+            // TransitionState.FINISHED: lockscreen -> occluded
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.OCCLUDED,
+                    value = 1f,
+                    transitionState = TransitionState.FINISHED,
+                    ownerName = "lockscreenToOccluded",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(0f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(lockscreenColor)
+            assertThat(visible).isFalse()
+        }
+
+    @Test
+    fun occludedToLockscreen() =
+        testScope.runTest {
+            val transition by collectLastValue(underTest.transition)
+            val visible by collectLastValue(underTest.visible)
+
+            // TransitionState.STARTED: occluded -> lockscreen
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.OCCLUDED,
+                    to = KeyguardState.LOCKSCREEN,
+                    value = 0f,
+                    transitionState = TransitionState.STARTED,
+                    ownerName = "occludedToLockscreen",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(1f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(lockscreenColor)
+            assertThat(visible).isTrue()
+
+            // TransitionState.RUNNING: occluded -> lockscreen
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.OCCLUDED,
+                    to = KeyguardState.LOCKSCREEN,
+                    value = .6f,
+                    transitionState = TransitionState.RUNNING,
+                    ownerName = "occludedToLockscreen",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(1f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(lockscreenColor)
+            assertThat(visible).isTrue()
+
+            // TransitionState.FINISHED: occluded -> lockscreen
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.OCCLUDED,
+                    to = KeyguardState.LOCKSCREEN,
+                    value = 1f,
+                    transitionState = TransitionState.FINISHED,
+                    ownerName = "occludedToLockscreen",
+                )
+            )
+            runCurrent()
+            assertThat(transition?.alpha).isEqualTo(1f)
+            assertThat(transition?.scale).isEqualTo(1f)
+            assertThat(transition?.color).isEqualTo(lockscreenColor)
+            assertThat(visible).isTrue()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/IndicationHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/IndicationHelperTest.kt
new file mode 100644
index 0000000..fd0ff9b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/IndicationHelperTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.util
+
+import android.hardware.biometrics.BiometricFaceConstants.BIOMETRIC_ERROR_POWER_PRESSED
+import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_CANCELED
+import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT
+import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT
+import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_TIMEOUT
+import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_UNABLE_TO_PROCESS
+import android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_VENDOR
+import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_CANCELED
+import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT
+import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT
+import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_TIMEOUT
+import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_USER_CANCELED
+import android.hardware.biometrics.BiometricSourceType
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.whenever
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+class IndicationHelperTest : SysuiTestCase() {
+
+    @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+    @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    private lateinit var underTest: IndicationHelper
+
+    @Before
+    fun setup() {
+        underTest =
+            IndicationHelper(
+                keyguardUpdateMonitor,
+            )
+    }
+
+    @Test
+    fun suppressErrorMsg_faceErrorCancelled() {
+        givenPrimaryAuthNotRequired()
+        assertTrue(underTest.shouldSuppressErrorMsg(BiometricSourceType.FACE, FACE_ERROR_CANCELED))
+    }
+
+    @Test
+    fun suppressErrorMsg_faceErrorUnableToProcess() {
+        givenPrimaryAuthNotRequired()
+        assertTrue(
+            underTest.shouldSuppressErrorMsg(BiometricSourceType.FACE, FACE_ERROR_UNABLE_TO_PROCESS)
+        )
+    }
+
+    @Test
+    fun suppressErrorMsg_facePrimaryAuthRequired() {
+        givenPrimaryAuthRequired()
+        assertTrue(underTest.shouldSuppressErrorMsg(BiometricSourceType.FACE, FACE_ERROR_TIMEOUT))
+    }
+
+    @Test
+    fun doNotSuppressErrorMsg_facePrimaryAuthRequired_faceLockout() {
+        givenPrimaryAuthRequired()
+        assertFalse(underTest.shouldSuppressErrorMsg(BiometricSourceType.FACE, FACE_ERROR_LOCKOUT))
+        assertFalse(
+            underTest.shouldSuppressErrorMsg(BiometricSourceType.FACE, FACE_ERROR_LOCKOUT_PERMANENT)
+        )
+    }
+
+    @Test
+    fun suppressErrorMsg_fingerprintErrorCancelled() {
+        givenPrimaryAuthNotRequired()
+        assertTrue(
+            underTest.shouldSuppressErrorMsg(
+                BiometricSourceType.FINGERPRINT,
+                FINGERPRINT_ERROR_CANCELED
+            )
+        )
+    }
+
+    @Test
+    fun suppressErrorMsg_fingerprintErrorUserCancelled() {
+        givenPrimaryAuthNotRequired()
+        assertTrue(
+            underTest.shouldSuppressErrorMsg(
+                BiometricSourceType.FINGERPRINT,
+                FINGERPRINT_ERROR_USER_CANCELED
+            )
+        )
+    }
+
+    @Test
+    fun suppressErrorMsg_fingerprintErrorPowerPressed() {
+        givenPrimaryAuthNotRequired()
+        assertTrue(
+            underTest.shouldSuppressErrorMsg(
+                BiometricSourceType.FINGERPRINT,
+                BIOMETRIC_ERROR_POWER_PRESSED
+            )
+        )
+    }
+
+    @Test
+    fun suppressErrorMsg_fingerprintPrimaryAuthRequired() {
+        givenPrimaryAuthRequired()
+        assertTrue(
+            underTest.shouldSuppressErrorMsg(BiometricSourceType.FACE, FINGERPRINT_ERROR_TIMEOUT)
+        )
+    }
+
+    @Test
+    fun doNotSuppressErrorMsg_fingerprintPrimaryAuthRequired_fingerprintLockout() {
+        givenPrimaryAuthRequired()
+        assertFalse(
+            underTest.shouldSuppressErrorMsg(
+                BiometricSourceType.FINGERPRINT,
+                FINGERPRINT_ERROR_LOCKOUT
+            )
+        )
+        assertFalse(
+            underTest.shouldSuppressErrorMsg(
+                BiometricSourceType.FACE,
+                FINGERPRINT_ERROR_LOCKOUT_PERMANENT
+            )
+        )
+    }
+
+    @Test
+    fun isFaceLockoutErrorMsgId() {
+        givenPrimaryAuthRequired()
+        assertTrue(underTest.isFaceLockoutErrorMsg(FACE_ERROR_LOCKOUT))
+        assertTrue(underTest.isFaceLockoutErrorMsg(FACE_ERROR_LOCKOUT_PERMANENT))
+        assertFalse(underTest.isFaceLockoutErrorMsg(FACE_ERROR_TIMEOUT))
+        assertFalse(underTest.isFaceLockoutErrorMsg(FACE_ERROR_CANCELED))
+        assertFalse(underTest.isFaceLockoutErrorMsg(FACE_ERROR_UNABLE_TO_PROCESS))
+        assertFalse(underTest.isFaceLockoutErrorMsg(FACE_ERROR_VENDOR))
+    }
+
+    private fun givenPrimaryAuthNotRequired() {
+        whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+            .thenReturn(true)
+    }
+
+    private fun givenPrimaryAuthRequired() {
+        whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+            .thenReturn(false)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
index 0cf6d3d..b5eae5b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
@@ -2,6 +2,7 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.core.Logger
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
 import java.io.StringWriter
@@ -32,7 +33,8 @@
 
     @Test
     fun log_shouldSaveLogToBuffer() {
-        buffer.log("Test", LogLevel.INFO, "Some test message")
+        val logger = Logger(buffer, "Test")
+        logger.i("Some test message")
 
         val dumpedString = dumpBuffer()
 
@@ -41,8 +43,9 @@
 
     @Test
     fun log_shouldRotateIfLogBufferIsFull() {
-        buffer.log("Test", LogLevel.INFO, "This should be rotated")
-        buffer.log("Test", LogLevel.INFO, "New test message")
+        val logger = Logger(buffer, "Test")
+        logger.i("This should be rotated")
+        logger.i("New test message")
 
         val dumpedString = dumpBuffer()
 
@@ -53,7 +56,8 @@
     fun dump_writesExceptionAndStacktrace() {
         buffer = createBuffer()
         val exception = createTestException("Exception message", "TestClass")
-        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
+        val logger = Logger(buffer, "Test")
+        logger.e("Extra message", exception)
 
         val dumpedString = dumpBuffer()
 
@@ -72,7 +76,8 @@
                 "TestClass",
                 cause = createTestException("The real cause!", "TestClass")
             )
-        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
+        val logger = Logger(buffer, "Test")
+        logger.e("Extra message", exception)
 
         val dumpedString = dumpBuffer()
 
@@ -93,7 +98,8 @@
             )
         )
         exception.addSuppressed(createTestException("Second suppressed exception", "SecondClass"))
-        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
+        val logger = Logger(buffer, "Test")
+        logger.e("Extra message", exception)
 
         val dumpedStr = dumpBuffer()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/core/LoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/core/LoggerTest.kt
new file mode 100644
index 0000000..ab19b3a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/core/LoggerTest.kt
@@ -0,0 +1,140 @@
+package com.android.systemui.log.core
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.LogMessageImpl
+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.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.anyString
+import org.mockito.Mockito.isNull
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.junit.MockitoJUnitRunner
+
+@SmallTest
+@RunWith(MockitoJUnitRunner::class)
+class LoggerTest : SysuiTestCase() {
+    @Mock private lateinit var buffer: MessageBuffer
+    private lateinit var message: LogMessage
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        whenever(buffer.obtain(any(), any(), any(), isNull())).thenAnswer {
+            message = LogMessageImpl.Factory.create()
+            return@thenAnswer message
+        }
+    }
+
+    @Test
+    fun log_shouldCommitLogMessage() {
+        val logger = Logger(buffer, "LoggerTest")
+        logger.log(LogLevel.DEBUG, { "count=$int1" }) {
+            int1 = 1
+            str1 = "test"
+            bool1 = true
+        }
+
+        assertThat(message.int1).isEqualTo(1)
+        assertThat(message.str1).isEqualTo("test")
+        assertThat(message.bool1).isEqualTo(true)
+    }
+
+    @Test
+    fun log_shouldUseCorrectLoggerTag() {
+        val logger = Logger(buffer, "LoggerTest")
+        logger.log(LogLevel.DEBUG, { "count=$int1" }) { int1 = 1 }
+        verify(buffer).obtain(eq("LoggerTest"), any(), any(), nullable())
+    }
+
+    @Test
+    fun v_withMessageInitializer_shouldLogAtCorrectLevel() {
+        val logger = Logger(buffer, "LoggerTest")
+        logger.v({ "count=$int1" }) { int1 = 1 }
+        verify(buffer).obtain(anyString(), eq(LogLevel.VERBOSE), any(), nullable())
+    }
+
+    @Test
+    fun v_withCompileTimeMessage_shouldLogAtCorrectLevel() {
+        val logger = Logger(buffer, "LoggerTest")
+        logger.v("Message")
+        verify(buffer).obtain(anyString(), eq(LogLevel.VERBOSE), any(), nullable())
+    }
+
+    @Test
+    fun d_withMessageInitializer_shouldLogAtCorrectLevel() {
+        val logger = Logger(buffer, "LoggerTest")
+        logger.d({ "count=$int1" }) { int1 = 1 }
+        verify(buffer).obtain(anyString(), eq(LogLevel.DEBUG), any(), nullable())
+    }
+
+    @Test
+    fun d_withCompileTimeMessage_shouldLogAtCorrectLevel() {
+        val logger = Logger(buffer, "LoggerTest")
+        logger.d("Message")
+        verify(buffer).obtain(anyString(), eq(LogLevel.DEBUG), any(), nullable())
+    }
+
+    @Test
+    fun i_withMessageInitializer_shouldLogAtCorrectLevel() {
+        val logger = Logger(buffer, "LoggerTest")
+        logger.i({ "count=$int1" }) { int1 = 1 }
+        verify(buffer).obtain(anyString(), eq(LogLevel.INFO), any(), nullable())
+    }
+
+    @Test
+    fun i_withCompileTimeMessage_shouldLogAtCorrectLevel() {
+        val logger = Logger(buffer, "LoggerTest")
+        logger.i("Message")
+        verify(buffer).obtain(anyString(), eq(LogLevel.INFO), any(), nullable())
+    }
+
+    @Test
+    fun w_withMessageInitializer_shouldLogAtCorrectLevel() {
+        val logger = Logger(buffer, "LoggerTest")
+        logger.w({ "count=$int1" }) { int1 = 1 }
+        verify(buffer).obtain(anyString(), eq(LogLevel.WARNING), any(), nullable())
+    }
+
+    @Test
+    fun w_withCompileTimeMessage_shouldLogAtCorrectLevel() {
+        val logger = Logger(buffer, "LoggerTest")
+        logger.w("Message")
+        verify(buffer).obtain(anyString(), eq(LogLevel.WARNING), any(), nullable())
+    }
+
+    @Test
+    fun e_withMessageInitializer_shouldLogAtCorrectLevel() {
+        val logger = Logger(buffer, "LoggerTest")
+        logger.e({ "count=$int1" }) { int1 = 1 }
+        verify(buffer).obtain(anyString(), eq(LogLevel.ERROR), any(), nullable())
+    }
+
+    @Test
+    fun e_withCompileTimeMessage_shouldLogAtCorrectLevel() {
+        val logger = Logger(buffer, "LoggerTest")
+        logger.e("Message")
+        verify(buffer).obtain(anyString(), eq(LogLevel.ERROR), any(), nullable())
+    }
+
+    @Test
+    fun wtf_withMessageInitializer_shouldLogAtCorrectLevel() {
+        val logger = Logger(buffer, "LoggerTest")
+        logger.wtf({ "count=$int1" }) { int1 = 1 }
+        verify(buffer).obtain(anyString(), eq(LogLevel.WTF), any(), nullable())
+    }
+
+    @Test
+    fun wtf_withCompileTimeMessage_shouldLogAtCorrectLevel() {
+        val logger = Logger(buffer, "LoggerTest")
+        logger.wtf("Message")
+        verify(buffer).obtain(anyString(), eq(LogLevel.WTF), any(), nullable())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt
index 12f4689..83182c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt
@@ -18,8 +18,8 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.table.TableChange.Companion.IS_INITIAL_PREFIX
 import com.android.systemui.log.table.TableChange.Companion.MAX_STRING_LENGTH
 import com.android.systemui.util.mockito.any
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 9ab7289..530b86e 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
@@ -98,6 +98,8 @@
     @Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback>
     @Captor lateinit var actionCaptor: ArgumentCaptor<Runnable>
     @Captor lateinit var componentCaptor: ArgumentCaptor<String>
+    @Captor lateinit var userIdCaptor: ArgumentCaptor<Int>
+    @Captor lateinit var userCallbackCaptor: ArgumentCaptor<UserTracker.Callback>
 
     private lateinit var executor: FakeExecutor
     private lateinit var data: MediaData
@@ -124,7 +126,7 @@
         )
         Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
 
-        whenever(resumeBrowserFactory.create(capture(callbackCaptor), any()))
+        whenever(resumeBrowserFactory.create(capture(callbackCaptor), any(), capture(userIdCaptor)))
             .thenReturn(resumeBrowser)
 
         // resume components are stored in sharedpreferences
@@ -334,6 +336,7 @@
     @Test
     fun testOnUserUnlock_loadsTracks() {
         // Set up mock service to successfully find valid media
+        setUpMbsWithValidResolveInfo()
         val description = MediaDescription.Builder().setTitle(TITLE).build()
         val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
         whenever(resumeBrowser.token).thenReturn(token)
@@ -417,6 +420,7 @@
     @Test
     fun testLoadComponents_recentlyPlayed_adds() {
         // Set up browser to return successfully
+        setUpMbsWithValidResolveInfo()
         val description = MediaDescription.Builder().setTitle(TITLE).build()
         val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
         whenever(resumeBrowser.token).thenReturn(token)
@@ -600,7 +604,7 @@
 
         // Set up our factory to return a new browser so we can verify we disconnected the old one
         val newResumeBrowser = mock(ResumeMediaBrowser::class.java)
-        whenever(resumeBrowserFactory.create(capture(callbackCaptor), any()))
+        whenever(resumeBrowserFactory.create(capture(callbackCaptor), any(), anyInt()))
             .thenReturn(newResumeBrowser)
 
         // When the resume action is run
@@ -610,6 +614,66 @@
         verify(resumeBrowser).disconnect()
     }
 
+    @Test
+    fun testUserUnlocked_userChangeWhileQuerying() {
+        val firstUserId = 1
+        val secondUserId = 2
+        val description = MediaDescription.Builder().setTitle(TITLE).build()
+        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+
+        setUpMbsWithValidResolveInfo()
+        whenever(resumeBrowser.token).thenReturn(token)
+        whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
+
+        val unlockIntent =
+            Intent(Intent.ACTION_USER_UNLOCKED).apply {
+                putExtra(Intent.EXTRA_USER_HANDLE, firstUserId)
+            }
+        verify(userTracker).addCallback(capture(userCallbackCaptor), any())
+
+        // When the first user unlocks and we query their recent media
+        userCallbackCaptor.value.onUserChanged(firstUserId, context)
+        resumeListener.userUnlockReceiver.onReceive(context, unlockIntent)
+        whenever(resumeBrowser.userId).thenReturn(userIdCaptor.value)
+        verify(resumeBrowser, times(3)).findRecentMedia()
+
+        // And the user changes before the MBS response is received
+        userCallbackCaptor.value.onUserChanged(secondUserId, context)
+        callbackCaptor.value.addTrack(description, component, resumeBrowser)
+
+        // Then the loaded media is correctly associated with the first user
+        verify(mediaDataManager)
+            .addResumptionControls(
+                eq(firstUserId),
+                eq(description),
+                any(),
+                eq(token),
+                eq(PACKAGE_NAME),
+                eq(pendingIntent),
+                eq(PACKAGE_NAME)
+            )
+    }
+
+    @Test
+    fun testUserUnlocked_noComponent_doesNotQuery() {
+        // Set up a valid MBS, but user does not have the service available
+        setUpMbsWithValidResolveInfo()
+        val pm = mock(PackageManager::class.java)
+        whenever(mockContext.packageManager).thenReturn(pm)
+        whenever(pm.resolveServiceAsUser(any(), anyInt(), anyInt())).thenReturn(null)
+
+        val unlockIntent =
+            Intent(Intent.ACTION_USER_UNLOCKED).apply {
+                putExtra(Intent.EXTRA_USER_HANDLE, context.userId)
+            }
+
+        // When the user is unlocked, but does not have the component installed
+        resumeListener.userUnlockReceiver.onReceive(context, unlockIntent)
+
+        // Then we never attempt to connect to it
+        verify(resumeBrowser, never()).findRecentMedia()
+    }
+
     /** Sets up mocks to successfully find a MBS that returns valid media. */
     private fun setUpMbsWithValidResolveInfo() {
         val pm = mock(PackageManager::class.java)
@@ -620,6 +684,8 @@
         resolveInfo.serviceInfo = serviceInfo
         resolveInfo.serviceInfo.name = CLASS_NAME
         val resumeInfo = listOf(resolveInfo)
-        whenever(pm.queryIntentServices(any(), anyInt())).thenReturn(resumeInfo)
+        whenever(pm.queryIntentServicesAsUser(any(), anyInt(), anyInt())).thenReturn(resumeInfo)
+        whenever(pm.resolveServiceAsUser(any(), anyInt(), anyInt())).thenReturn(resolveInfo)
+        whenever(pm.getApplicationLabel(any())).thenReturn(PACKAGE_NAME)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt
index a04cfd4..b45e66b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt
@@ -93,7 +93,8 @@
                 component,
                 browserFactory,
                 logger,
-                mediaController
+                mediaController,
+                context.userId,
             )
     }
 
@@ -381,8 +382,9 @@
         componentName: ComponentName,
         browserFactory: MediaBrowserFactory,
         logger: ResumeMediaBrowserLogger,
-        private val fakeController: MediaController
-    ) : ResumeMediaBrowser(context, callback, componentName, browserFactory, logger) {
+        private val fakeController: MediaController,
+        userId: Int,
+    ) : ResumeMediaBrowser(context, callback, componentName, browserFactory, logger, userId) {
 
         override fun createMediaController(token: MediaSession.Token): MediaController {
             return fakeController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartableTest.kt
new file mode 100644
index 0000000..bcbf666
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartableTest.kt
@@ -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.systemui.mediaprojection.taskswitcher
+
+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.taskswitcher.ui.TaskSwitcherNotificationCoordinator
+import com.android.systemui.util.mockito.whenever
+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
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class MediaProjectionTaskSwitcherCoreStartableTest : SysuiTestCase() {
+
+    @Mock private lateinit var flags: FeatureFlags
+    @Mock private lateinit var coordinator: TaskSwitcherNotificationCoordinator
+
+    private lateinit var coreStartable: MediaProjectionTaskSwitcherCoreStartable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        coreStartable = MediaProjectionTaskSwitcherCoreStartable(coordinator, flags)
+    }
+
+    @Test
+    fun start_flagEnabled_startsCoordinator() {
+        whenever(flags.isEnabled(Flags.PARTIAL_SCREEN_SHARING_TASK_SWITCHER)).thenReturn(true)
+
+        coreStartable.start()
+
+        verify(coordinator).start()
+    }
+
+    @Test
+    fun start_flagDisabled_doesNotStartCoordinator() {
+        whenever(flags.isEnabled(Flags.PARTIAL_SCREEN_SHARING_TASK_SWITCHER)).thenReturn(false)
+
+        coreStartable.start()
+
+        verifyZeroInteractions(coordinator)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt
new file mode 100644
index 0000000..83932b0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.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.mediaprojection.taskswitcher.data.repository
+
+import android.os.Binder
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createToken
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class ActivityTaskManagerTasksRepositoryTest : SysuiTestCase() {
+
+    private val fakeActivityTaskManager = FakeActivityTaskManager()
+
+    private val dispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+
+    private val repo =
+        ActivityTaskManagerTasksRepository(
+            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
+            applicationScope = testScope.backgroundScope,
+            backgroundDispatcher = dispatcher
+        )
+
+    @Test
+    fun findRunningTaskFromWindowContainerToken_noMatch_returnsNull() {
+        fakeActivityTaskManager.addRunningTasks(createTask(taskId = 1), createTask(taskId = 2))
+
+        testScope.runTest {
+            val matchingTask =
+                repo.findRunningTaskFromWindowContainerToken(windowContainerToken = Binder())
+
+            assertThat(matchingTask).isNull()
+        }
+    }
+
+    @Test
+    fun findRunningTaskFromWindowContainerToken_matchingToken_returnsTaskInfo() {
+        val expectedToken = createToken()
+        val expectedTask = createTask(taskId = 1, token = expectedToken)
+
+        fakeActivityTaskManager.addRunningTasks(
+            createTask(taskId = 2),
+            expectedTask,
+        )
+
+        testScope.runTest {
+            val actualTask =
+                repo.findRunningTaskFromWindowContainerToken(
+                    windowContainerToken = expectedToken.asBinder()
+                )
+
+            assertThat(actualTask).isEqualTo(expectedTask)
+        }
+    }
+
+    @Test
+    fun foregroundTask_returnsStreamOfTasksMovedToFront() =
+        testScope.runTest {
+            val foregroundTask by collectLastValue(repo.foregroundTask)
+
+            fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 1))
+            assertThat(foregroundTask?.taskId).isEqualTo(1)
+
+            fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 2))
+            assertThat(foregroundTask?.taskId).isEqualTo(2)
+
+            fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 3))
+            assertThat(foregroundTask?.taskId).isEqualTo(3)
+        }
+
+    @Test
+    fun foregroundTask_lastValueIsCached() =
+        testScope.runTest {
+            val foregroundTaskA by collectLastValue(repo.foregroundTask)
+            fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 1))
+            assertThat(foregroundTaskA?.taskId).isEqualTo(1)
+
+            val foregroundTaskB by collectLastValue(repo.foregroundTask)
+            assertThat(foregroundTaskB?.taskId).isEqualTo(1)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt
new file mode 100644
index 0000000..1c4870b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.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 this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.taskswitcher.data.repository
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.ActivityTaskManager
+import android.app.TaskStackListener
+import android.content.Intent
+import android.window.IWindowContainerToken
+import android.window.WindowContainerToken
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+
+class FakeActivityTaskManager {
+
+    private val runningTasks = mutableListOf<RunningTaskInfo>()
+    private val taskTaskListeners = mutableListOf<TaskStackListener>()
+
+    val activityTaskManager = mock<ActivityTaskManager>()
+
+    init {
+        whenever(activityTaskManager.registerTaskStackListener(any())).thenAnswer {
+            taskTaskListeners += it.arguments[0] as TaskStackListener
+            return@thenAnswer Unit
+        }
+        whenever(activityTaskManager.unregisterTaskStackListener(any())).thenAnswer {
+            taskTaskListeners -= it.arguments[0] as TaskStackListener
+            return@thenAnswer Unit
+        }
+        whenever(activityTaskManager.getTasks(any())).thenAnswer {
+            val maxNumTasks = it.arguments[0] as Int
+            return@thenAnswer runningTasks.take(maxNumTasks)
+        }
+    }
+
+    fun moveTaskToForeground(task: RunningTaskInfo) {
+        taskTaskListeners.forEach { it.onTaskMovedToFront(task) }
+    }
+
+    fun addRunningTasks(vararg tasks: RunningTaskInfo) {
+        runningTasks += tasks
+    }
+
+    companion object {
+
+        fun createTask(
+            taskId: Int,
+            token: WindowContainerToken = createToken(),
+            baseIntent: Intent = Intent()
+        ) =
+            RunningTaskInfo().apply {
+                this.taskId = taskId
+                this.token = token
+                this.baseIntent = baseIntent
+            }
+
+        fun createToken(): WindowContainerToken {
+            val realToken = object : IWindowContainerToken.Stub() {}
+            return WindowContainerToken(realToken)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionRepository.kt
new file mode 100644
index 0000000..c59fd60
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionRepository.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.mediaprojection.taskswitcher.data.repository
+
+import android.app.TaskInfo
+import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeMediaProjectionRepository : MediaProjectionRepository {
+
+    private val state = MutableStateFlow<MediaProjectionState>(MediaProjectionState.NotProjecting)
+
+    fun switchProjectedTask(newTask: TaskInfo) {
+        state.value = MediaProjectionState.SingleTask(newTask)
+    }
+
+    override val mediaProjectionState: Flow<MediaProjectionState> = state.asStateFlow()
+
+    fun projectEntireScreen() {
+        state.value = MediaProjectionState.EntireScreen
+    }
+
+    fun stopProjecting() {
+        state.value = MediaProjectionState.NotProjecting
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt
new file mode 100644
index 0000000..593e389
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.taskswitcher.data.repository
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Intent
+import android.os.IBinder
+import android.window.IWindowContainerToken
+import android.window.WindowContainerToken
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeTasksRepository : TasksRepository {
+
+    private val _foregroundTask = MutableStateFlow(DEFAULT_TASK)
+
+    override val foregroundTask: Flow<RunningTaskInfo> = _foregroundTask.asStateFlow()
+
+    private val runningTasks = mutableListOf(DEFAULT_TASK)
+
+    override suspend fun findRunningTaskFromWindowContainerToken(
+        windowContainerToken: IBinder
+    ): RunningTaskInfo? = runningTasks.firstOrNull { it.token.asBinder() == windowContainerToken }
+
+    fun addRunningTask(task: RunningTaskInfo) {
+        runningTasks.add(task)
+    }
+
+    fun moveTaskToForeground(task: RunningTaskInfo) {
+        _foregroundTask.value = task
+    }
+
+    companion object {
+        val DEFAULT_TASK = createTask(taskId = -1)
+        val LAUNCHER_INTENT: Intent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
+
+        fun createTask(
+            taskId: Int,
+            token: WindowContainerToken = createToken(),
+            baseIntent: Intent = Intent()
+        ) =
+            RunningTaskInfo().apply {
+                this.taskId = taskId
+                this.token = token
+                this.baseIntent = baseIntent
+            }
+
+        fun createToken(): WindowContainerToken {
+            val realToken = object : IWindowContainerToken.Stub() {}
+            return WindowContainerToken(realToken)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt
new file mode 100644
index 0000000..2b07465
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.taskswitcher.data.repository
+
+import android.media.projection.MediaProjectionInfo
+import android.media.projection.MediaProjectionManager
+import android.os.Binder
+import android.os.Handler
+import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import android.view.ContentRecordingSession
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState
+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.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class MediaProjectionManagerRepositoryTest : SysuiTestCase() {
+
+    private val mediaProjectionManager = mock<MediaProjectionManager>()
+
+    private val dispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+    private val tasksRepo = FakeTasksRepository()
+
+    private lateinit var callback: MediaProjectionManager.Callback
+    private lateinit var repo: MediaProjectionManagerRepository
+
+    @Before
+    fun setUp() {
+        whenever(mediaProjectionManager.addCallback(any(), any())).thenAnswer {
+            callback = it.arguments[0] as MediaProjectionManager.Callback
+            return@thenAnswer Unit
+        }
+        repo =
+            MediaProjectionManagerRepository(
+                mediaProjectionManager = mediaProjectionManager,
+                handler = Handler.getMain(),
+                applicationScope = testScope.backgroundScope,
+                tasksRepository = tasksRepo
+            )
+    }
+
+    @Test
+    fun mediaProjectionState_onStart_emitsNotProjecting() =
+        testScope.runTest {
+            val state by collectLastValue(repo.mediaProjectionState)
+            runCurrent()
+
+            callback.onStart(TEST_MEDIA_INFO)
+
+            assertThat(state).isEqualTo(MediaProjectionState.NotProjecting)
+        }
+
+    @Test
+    fun mediaProjectionState_onStop_emitsNotProjecting() =
+        testScope.runTest {
+            val state by collectLastValue(repo.mediaProjectionState)
+            runCurrent()
+
+            callback.onStop(TEST_MEDIA_INFO)
+
+            assertThat(state).isEqualTo(MediaProjectionState.NotProjecting)
+        }
+
+    @Test
+    fun mediaProjectionState_onSessionSet_sessionNull_emitsNotProjecting() =
+        testScope.runTest {
+            val state by collectLastValue(repo.mediaProjectionState)
+            runCurrent()
+
+            callback.onRecordingSessionSet(TEST_MEDIA_INFO, /* session= */ null)
+
+            assertThat(state).isEqualTo(MediaProjectionState.NotProjecting)
+        }
+
+    @Test
+    fun mediaProjectionState_onSessionSet_contentToRecordDisplay_emitsEntireScreen() =
+        testScope.runTest {
+            val state by collectLastValue(repo.mediaProjectionState)
+            runCurrent()
+
+            val session = ContentRecordingSession.createDisplaySession(/* displayToMirror= */ 123)
+            callback.onRecordingSessionSet(TEST_MEDIA_INFO, session)
+
+            assertThat(state).isEqualTo(MediaProjectionState.EntireScreen)
+        }
+
+    @Test
+    fun mediaProjectionState_onSessionSet_tokenNull_emitsEntireScreen() =
+        testScope.runTest {
+            val state by collectLastValue(repo.mediaProjectionState)
+            runCurrent()
+
+            val session =
+                ContentRecordingSession.createTaskSession(/* taskWindowContainerToken= */ null)
+            callback.onRecordingSessionSet(TEST_MEDIA_INFO, session)
+
+            assertThat(state).isEqualTo(MediaProjectionState.EntireScreen)
+        }
+
+    @Test
+    fun mediaProjectionState_sessionSet_taskWithToken_noMatchingRunningTask_emitsEntireScreen() =
+        testScope.runTest {
+            val state by collectLastValue(repo.mediaProjectionState)
+            runCurrent()
+
+            val taskWindowContainerToken = Binder()
+            val session = ContentRecordingSession.createTaskSession(taskWindowContainerToken)
+            callback.onRecordingSessionSet(TEST_MEDIA_INFO, session)
+
+            assertThat(state).isEqualTo(MediaProjectionState.EntireScreen)
+        }
+
+    @Test
+    fun mediaProjectionState_sessionSet_taskWithToken_matchingRunningTask_emitsSingleTask() =
+        testScope.runTest {
+            val token = FakeTasksRepository.createToken()
+            val task = FakeTasksRepository.createTask(taskId = 1, token = token)
+            tasksRepo.addRunningTask(task)
+            val state by collectLastValue(repo.mediaProjectionState)
+            runCurrent()
+
+            val session = ContentRecordingSession.createTaskSession(token.asBinder())
+            callback.onRecordingSessionSet(TEST_MEDIA_INFO, session)
+
+            assertThat(state).isEqualTo(MediaProjectionState.SingleTask(task))
+        }
+
+    companion object {
+        val TEST_MEDIA_INFO =
+            MediaProjectionInfo(/* packageName= */ "com.test.package", UserHandle.CURRENT)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt
new file mode 100644
index 0000000..112950b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.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.systemui.mediaprojection.taskswitcher.domain.interactor
+
+import android.content.Intent
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionRepository
+import com.android.systemui.mediaprojection.taskswitcher.domain.model.TaskSwitchState
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class TaskSwitchInteractorTest : SysuiTestCase() {
+
+    private val dispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+
+    private val fakeActivityTaskManager = FakeActivityTaskManager()
+    private val mediaRepo = FakeMediaProjectionRepository()
+    private val tasksRepo =
+        ActivityTaskManagerTasksRepository(
+            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
+            applicationScope = testScope.backgroundScope,
+            backgroundDispatcher = dispatcher
+        )
+
+    private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo)
+
+    @Test
+    fun taskSwitchChanges_notProjecting_foregroundTaskChange_emitsNotProjectingTask() =
+        testScope.runTest {
+            mediaRepo.stopProjecting()
+            val taskSwitchState by collectLastValue(interactor.taskSwitchChanges)
+
+            fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 1))
+
+            assertThat(taskSwitchState).isEqualTo(TaskSwitchState.NotProjectingTask)
+        }
+
+    @Test
+    fun taskSwitchChanges_projectingScreen_foregroundTaskChange_emitsNotProjectingTask() =
+        testScope.runTest {
+            mediaRepo.projectEntireScreen()
+            val taskSwitchState by collectLastValue(interactor.taskSwitchChanges)
+
+            fakeActivityTaskManager.moveTaskToForeground(createTask(taskId = 1))
+
+            assertThat(taskSwitchState).isEqualTo(TaskSwitchState.NotProjectingTask)
+        }
+
+    @Test
+    fun taskSwitchChanges_projectingTask_foregroundTaskDifferent_emitsTaskChanged() =
+        testScope.runTest {
+            val projectedTask = createTask(taskId = 0)
+            val foregroundTask = createTask(taskId = 1)
+            mediaRepo.switchProjectedTask(projectedTask)
+            val taskSwitchState by collectLastValue(interactor.taskSwitchChanges)
+
+            fakeActivityTaskManager.moveTaskToForeground(foregroundTask)
+
+            assertThat(taskSwitchState)
+                .isEqualTo(
+                    TaskSwitchState.TaskSwitched(
+                        projectedTask = projectedTask,
+                        foregroundTask = foregroundTask
+                    )
+                )
+        }
+
+    @Test
+    fun taskSwitchChanges_projectingTask_foregroundTaskLauncher_emitsTaskUnchanged() =
+        testScope.runTest {
+            val projectedTask = createTask(taskId = 0)
+            val foregroundTask = createTask(taskId = 1, baseIntent = LAUNCHER_INTENT)
+            mediaRepo.switchProjectedTask(projectedTask)
+            val taskSwitchState by collectLastValue(interactor.taskSwitchChanges)
+
+            fakeActivityTaskManager.moveTaskToForeground(foregroundTask)
+
+            assertThat(taskSwitchState).isEqualTo(TaskSwitchState.TaskUnchanged)
+        }
+
+    @Test
+    fun taskSwitchChanges_projectingTask_foregroundTaskSame_emitsTaskUnchanged() =
+        testScope.runTest {
+            val projectedTask = createTask(taskId = 0)
+            val foregroundTask = createTask(taskId = 0)
+            mediaRepo.switchProjectedTask(projectedTask)
+            val taskSwitchState by collectLastValue(interactor.taskSwitchChanges)
+
+            fakeActivityTaskManager.moveTaskToForeground(foregroundTask)
+
+            assertThat(taskSwitchState).isEqualTo(TaskSwitchState.TaskUnchanged)
+        }
+
+    companion object {
+        private val LAUNCHER_INTENT: Intent =
+            Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt
new file mode 100644
index 0000000..ea44fb3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.taskswitcher.ui.viewmodel
+
+import android.content.Intent
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionRepository
+import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor
+import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class TaskSwitcherNotificationViewModelTest : SysuiTestCase() {
+
+    private val dispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+
+    private val fakeActivityTaskManager = FakeActivityTaskManager()
+    private val mediaRepo = FakeMediaProjectionRepository()
+    private val tasksRepo =
+        ActivityTaskManagerTasksRepository(
+            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
+            applicationScope = testScope.backgroundScope,
+            backgroundDispatcher = dispatcher
+        )
+    private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo)
+
+    private val viewModel = TaskSwitcherNotificationViewModel(interactor)
+
+    @Test
+    fun uiState_notProjecting_emitsNotShowing() =
+        testScope.runTest {
+            mediaRepo.stopProjecting()
+            val uiState by collectLastValue(viewModel.uiState)
+
+            assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing)
+        }
+
+    @Test
+    fun uiState_notProjecting_foregroundTaskChanged_emitsNotShowing() =
+        testScope.runTest {
+            mediaRepo.stopProjecting()
+            val uiState by collectLastValue(viewModel.uiState)
+
+            mediaRepo.switchProjectedTask(createTask(taskId = 1))
+
+            assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing)
+        }
+
+    @Test
+    fun uiState_projectingEntireScreen_emitsNotShowing() =
+        testScope.runTest {
+            mediaRepo.projectEntireScreen()
+            val uiState by collectLastValue(viewModel.uiState)
+
+            assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing)
+        }
+
+    @Test
+    fun uiState_projectingEntireScreen_foregroundTaskChanged_emitsNotShowing() =
+        testScope.runTest {
+            mediaRepo.projectEntireScreen()
+            val uiState by collectLastValue(viewModel.uiState)
+
+            mediaRepo.switchProjectedTask(createTask(taskId = 1))
+
+            assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing)
+        }
+
+    @Test
+    fun uiState_projectingTask_foregroundTaskChanged_different_emitsShowing() =
+        testScope.runTest {
+            val projectedTask = createTask(taskId = 1)
+            val foregroundTask = createTask(taskId = 2)
+            mediaRepo.switchProjectedTask(projectedTask)
+            val uiState by collectLastValue(viewModel.uiState)
+
+            fakeActivityTaskManager.moveTaskToForeground(foregroundTask)
+
+            assertThat(uiState)
+                .isEqualTo(TaskSwitcherNotificationUiState.Showing(projectedTask, foregroundTask))
+        }
+
+    @Test
+    fun uiState_projectingTask_foregroundTaskChanged_same_emitsNotShowing() =
+        testScope.runTest {
+            val projectedTask = createTask(taskId = 1)
+            mediaRepo.switchProjectedTask(projectedTask)
+            val uiState by collectLastValue(viewModel.uiState)
+
+            fakeActivityTaskManager.moveTaskToForeground(projectedTask)
+
+            assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing)
+        }
+
+    @Test
+    fun uiState_projectingTask_foregroundTaskChanged_different_taskIsLauncher_emitsNotShowing() =
+        testScope.runTest {
+            val projectedTask = createTask(taskId = 1)
+            val foregroundTask = createTask(taskId = 2, baseIntent = LAUNCHER_INTENT)
+            mediaRepo.switchProjectedTask(projectedTask)
+            val uiState by collectLastValue(viewModel.uiState)
+
+            fakeActivityTaskManager.moveTaskToForeground(foregroundTask)
+
+            assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing)
+        }
+
+    companion object {
+        private val LAUNCHER_INTENT: Intent =
+            Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt
index 9e54224..5890cbd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt
@@ -25,7 +25,7 @@
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -97,10 +97,11 @@
                 multiShadeInteractor = interactor,
                 featureFlags = featureFlags,
                 keyguardTransitionInteractor =
-                    KeyguardTransitionInteractor(
-                        repository = keyguardTransitionRepository,
-                        scope = testScope.backgroundScope
-                    ),
+                    KeyguardTransitionInteractorFactory.create(
+                            scope = TestScope().backgroundScope,
+                            repository = keyguardTransitionRepository,
+                        )
+                        .keyguardTransitionInteractor,
                 falsingManager = falsingManager,
                 shadeController = shadeController,
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
index 9b8605d..8d306cce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
@@ -52,6 +52,7 @@
 import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
@@ -112,6 +113,9 @@
     EdgeBackGestureHandler mEdgeBackGestureHandler;
     @Mock
     EdgeBackGestureHandler.Factory mEdgeBackGestureHandlerFactory;
+    @Mock
+    NotificationShadeWindowController mNotificationShadeWindowController;
+
     private AccessibilityManager.AccessibilityServicesStateChangeListener
             mAccessibilityServicesStateChangeListener;
 
@@ -136,7 +140,7 @@
                 mSystemActions, mOverviewProxyService, mAssistManagerLazy,
                 () -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardStateController.class),
                 mNavigationModeController, mEdgeBackGestureHandlerFactory, mWm, mUserTracker,
-                mDisplayTracker, mDumpManager, mCommandQueue);
+                mDisplayTracker, mNotificationShadeWindowController, mDumpManager, mCommandQueue);
 
     }
 
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 f062ec7..25d494c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -99,6 +99,7 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.LightBarController;
@@ -209,6 +210,8 @@
     private EdgeBackGestureHandler.Factory mEdgeBackGestureHandlerFactory;
     @Mock
     private EdgeBackGestureHandler mEdgeBackGestureHandler;
+    @Mock
+    private NotificationShadeWindowController mNotificationShadeWindowController;
     private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
     private DeviceConfigProxyFake mDeviceConfigProxyFake = new DeviceConfigProxyFake();
     private TaskStackChangeListeners mTaskStackChangeListeners =
@@ -256,7 +259,8 @@
                     () -> mock(AssistManager.class), () -> Optional.of(mCentralSurfaces),
                     mKeyguardStateController, mock(NavigationModeController.class),
                     mEdgeBackGestureHandlerFactory, mock(IWindowManager.class),
-                    mock(UserTracker.class), mock(DisplayTracker.class), mock(DumpManager.class),
+                    mock(UserTracker.class), mock(DisplayTracker.class),
+                    mNotificationShadeWindowController, mock(DumpManager.class),
                     mock(CommandQueue.class)));
             mNavigationBar = createNavBar(mContext);
             mExternalDisplayNavigationBar = createNavBar(mSysuiTestableContextExternal);
@@ -339,7 +343,6 @@
         NotificationShadeWindowView mockShadeWindowView = mock(NotificationShadeWindowView.class);
         WindowInsets windowInsets = new WindowInsets.Builder().setVisible(ime(), false).build();
         doReturn(windowInsets).when(mockShadeWindowView).getRootWindowInsets();
-        doReturn(mockShadeWindowView).when(mCentralSurfaces).getNotificationShadeWindowView();
         doReturn(true).when(mockShadeWindowView).isAttachedToWindow();
         doNothing().when(defaultNavBar).checkNavBarModes();
         doNothing().when(externalNavBar).checkNavBarModes();
@@ -375,7 +378,7 @@
     @Test
     public void testSetImeWindowStatusWhenKeyguardLockingAndImeInsetsChange() {
         NotificationShadeWindowView mockShadeWindowView = mock(NotificationShadeWindowView.class);
-        doReturn(mockShadeWindowView).when(mCentralSurfaces).getNotificationShadeWindowView();
+        doReturn(mockShadeWindowView).when(mNotificationShadeWindowController).getWindowRootView();
         doReturn(true).when(mockShadeWindowView).isAttachedToWindow();
         doNothing().when(mNavigationBar).checkNavBarModes();
         mNavigationBar.init();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
new file mode 100644
index 0000000..450aadd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
@@ -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.notetask
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.os.UserHandle
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
+import kotlinx.coroutines.CoroutineDispatcher
+
+/**
+ * Fake for [NoteTaskBubblesController] as mocking suspending functions is not supported in the
+ * Android tree's version of mockito. Ideally the [NoteTaskBubblesController] should be implemented
+ * using an interface for effectively providing multiple implementations but as this fake primarily
+ * for dealing with old version of mockito there isn't any benefit in adding complexity.
+ */
+class FakeNoteTaskBubbleController(
+    unUsed1: Context,
+    unsUsed2: CoroutineDispatcher,
+    private val optionalBubbles: Optional<Bubbles>
+) : NoteTaskBubblesController(unUsed1, unsUsed2) {
+    override suspend fun areBubblesAvailable() = optionalBubbles.isPresent
+
+    override suspend fun showOrHideAppBubble(intent: Intent, userHandle: UserHandle, icon: Icon) {
+        optionalBubbles.ifPresentOrElse(
+            { bubbles -> bubbles.showOrHideAppBubble(intent, userHandle, icon) },
+            { throw IllegalAccessException() }
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivityTest.kt
index 36b913f..bdb095a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivityTest.kt
@@ -22,9 +22,9 @@
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import androidx.test.rule.ActivityTestRule
-import androidx.test.runner.intercepting.SingleActivityFactory
 import com.android.dx.mockito.inline.extended.ExtendedMockito.verify
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.activity.SingleActivityFactory
 import com.android.systemui.notetask.LaunchNotesRoleSettingsTrampolineActivity.Companion.ACTION_MANAGE_NOTES_ROLE_FROM_QUICK_AFFORDANCE
 import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE
 import com.android.systemui.util.mockito.any
@@ -47,13 +47,9 @@
     @Rule
     @JvmField
     val activityRule =
-        ActivityTestRule<LaunchNotesRoleSettingsTrampolineActivity>(
-            /* activityFactory= */ object :
-                SingleActivityFactory<LaunchNotesRoleSettingsTrampolineActivity>(
-                    LaunchNotesRoleSettingsTrampolineActivity::class.java
-                ) {
-                override fun create(intent: Intent?) =
-                    LaunchNotesRoleSettingsTrampolineActivity(noteTaskController)
+        ActivityTestRule(
+            /* activityFactory= */ SingleActivityFactory {
+                LaunchNotesRoleSettingsTrampolineActivity(noteTaskController)
             },
             /* initialTouchMode= */ false,
             /* launchActivity= */ false,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
new file mode 100644
index 0000000..baac9e0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.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
+
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.os.UserHandle
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.notetask.NoteTaskBubblesController.NoteTaskBubblesService
+import com.android.wm.shell.bubbles.Bubbles
+import com.google.common.truth.Truth.assertThat
+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.verify
+import org.mockito.MockitoAnnotations
+
+/** atest SystemUITests:NoteTaskBubblesServiceTest */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskBubblesServiceTest : SysuiTestCase() {
+
+    @Mock private lateinit var bubbles: Bubbles
+
+    private fun createServiceBinder(bubbles: Bubbles? = this.bubbles) =
+        NoteTaskBubblesService(Optional.ofNullable(bubbles)).onBind(Intent())
+            as INoteTaskBubblesService
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun areBubblesAvailable_bubblesNotNull_shouldReturnTrue() {
+        assertThat(createServiceBinder().areBubblesAvailable()).isTrue()
+    }
+
+    @Test
+    fun areBubblesAvailable_bubblesNull_shouldReturnFalse() {
+        assertThat(createServiceBinder(bubbles = null).areBubblesAvailable()).isFalse()
+    }
+
+    @Test
+    fun showOrHideAppBubble() {
+        val intent = Intent()
+        val user = UserHandle.SYSTEM
+        val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
+
+        createServiceBinder().showOrHideAppBubble(intent, user, icon)
+
+        verify(bubbles).showOrHideAppBubble(intent, user, icon)
+    }
+}
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 0954f6f..a76af8e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -36,12 +36,12 @@
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
 import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
-import android.content.pm.ShortcutInfo
 import android.content.pm.ShortcutManager
 import android.content.pm.UserInfo
 import android.graphics.drawable.Icon
 import android.os.UserHandle
 import android.os.UserManager
+import android.provider.Settings
 import androidx.test.ext.truth.content.IntentSubject.assertThat
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
@@ -56,17 +56,22 @@
 import com.android.systemui.notetask.NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT
 import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
 import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity
-import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity
 import com.android.systemui.settings.FakeUserTracker
 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.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.settings.SecureSettings
 import com.android.wm.shell.bubbles.Bubble
 import com.android.wm.shell.bubbles.Bubbles
 import com.google.common.truth.Truth.assertThat
 import java.util.Optional
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -81,6 +86,7 @@
 import org.mockito.MockitoAnnotations
 
 /** atest SystemUITests:NoteTaskControllerTest */
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 internal class NoteTaskControllerTest : SysuiTestCase() {
@@ -98,7 +104,10 @@
     @Mock private lateinit var shortcutManager: ShortcutManager
     @Mock private lateinit var activityManager: ActivityManager
     @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+    @Mock private lateinit var secureSettings: SecureSettings
     private val userTracker = FakeUserTracker()
+    private val testDispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
 
     @Before
     fun setUp() {
@@ -106,7 +115,11 @@
 
         whenever(context.getString(R.string.note_task_button_label))
             .thenReturn(NOTE_TASK_SHORT_LABEL)
+        whenever(context.getString(eq(R.string.note_task_shortcut_long_label), any()))
+            .thenReturn(NOTE_TASK_LONG_LABEL)
         whenever(context.packageManager).thenReturn(packageManager)
+        whenever(packageManager.getApplicationInfo(any(), any<Int>())).thenReturn(mock())
+        whenever(packageManager.getApplicationLabel(any())).thenReturn(NOTE_TASK_LONG_LABEL)
         whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(NOTE_TASK_INFO)
         whenever(userManager.isUserUnlocked).thenReturn(true)
         whenever(userManager.isUserUnlocked(any<Int>())).thenReturn(true)
@@ -123,6 +136,7 @@
         whenever(activityManager.getRunningTasks(anyInt())).thenReturn(emptyList())
         whenever(userManager.isManagedProfile(workUserInfo.id)).thenReturn(true)
         whenever(context.resources).thenReturn(getContext().resources)
+        whenever(secureSettings.userTracker).thenReturn(userTracker)
     }
 
     private fun createNoteTaskController(
@@ -133,7 +147,6 @@
             context = context,
             resolver = resolver,
             eventLogger = eventLogger,
-            optionalBubbles = Optional.ofNullable(bubbles),
             userManager = userManager,
             keyguardManager = keyguardManager,
             isEnabled = isEnabled,
@@ -142,6 +155,10 @@
             roleManager = roleManager,
             shortcutManager = shortcutManager,
             activityManager = activityManager,
+            secureSettings = secureSettings,
+            noteTaskBubblesController =
+                FakeNoteTaskBubbleController(context, testDispatcher, Optional.ofNullable(bubbles)),
+            applicationScope = testScope,
         )
 
     // region onBubbleExpandChanged
@@ -157,7 +174,7 @@
             )
 
         verify(eventLogger).logNoteTaskOpened(expectedInfo)
-        verifyZeroInteractions(context, bubbles, keyguardManager, userManager)
+        verifyZeroInteractions(bubbles, keyguardManager, userManager)
     }
 
     @Test
@@ -172,7 +189,7 @@
             )
 
         verify(eventLogger).logNoteTaskClosed(expectedInfo)
-        verifyZeroInteractions(context, bubbles, keyguardManager, userManager)
+        verifyZeroInteractions(bubbles, keyguardManager, userManager)
     }
 
     @Test
@@ -186,7 +203,7 @@
                 key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user),
             )
 
-        verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger)
+        verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger)
     }
 
     @Test
@@ -200,7 +217,7 @@
                 key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user),
             )
 
-        verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger)
+        verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger)
     }
 
     @Test
@@ -211,7 +228,7 @@
                 key = "any other key",
             )
 
-        verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger)
+        verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger)
     }
 
     @Test
@@ -222,16 +239,19 @@
                 key = Bubble.getAppBubbleKeyForApp(NOTE_TASK_INFO.packageName, NOTE_TASK_INFO.user),
             )
 
-        verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger)
+        verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger)
     }
     // endregion
 
     // region showNoteTask
-    @Test
     fun showNoteTaskAsUser_keyguardIsLocked_shouldStartActivityWithExpectedUserAndLogUiEvent() {
         val user10 = UserHandle.of(/* userId= */ 10)
         val expectedInfo =
-            NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = true, user = user10)
+            NOTE_TASK_INFO.copy(
+                entryPoint = TAIL_BUTTON,
+                isKeyguardLocked = true,
+                user = user10,
+            )
         whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked)
         whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo)
 
@@ -332,10 +352,48 @@
     }
 
     @Test
+    fun showNoteTask_defaultUserSet_shouldStartActivityWithExpectedUserAndLogUiEvent() {
+        whenever(secureSettings.getInt(eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), any()))
+            .thenReturn(10)
+        val user10 = UserHandle.of(/* userId= */ 10)
+
+        val expectedInfo =
+            NOTE_TASK_INFO.copy(
+                entryPoint = NoteTaskEntryPoint.TAIL_BUTTON,
+                isKeyguardLocked = true,
+                user = user10,
+            )
+        whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked)
+        whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo)
+
+        createNoteTaskController()
+            .showNoteTask(
+                entryPoint = expectedInfo.entryPoint!!,
+            )
+
+        val intentCaptor = argumentCaptor<Intent>()
+        val userCaptor = argumentCaptor<UserHandle>()
+        verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor))
+        intentCaptor.value.let { intent ->
+            assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE)
+            assertThat(intent.`package`).isEqualTo(NOTE_TASK_PACKAGE_NAME)
+            assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK)
+            assertThat(intent.flags and FLAG_ACTIVITY_MULTIPLE_TASK)
+                .isEqualTo(FLAG_ACTIVITY_MULTIPLE_TASK)
+            assertThat(intent.flags and FLAG_ACTIVITY_NEW_DOCUMENT)
+                .isEqualTo(FLAG_ACTIVITY_NEW_DOCUMENT)
+            assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue()
+        }
+        assertThat(userCaptor.value).isEqualTo(user10)
+        verify(eventLogger).logNoteTaskOpened(expectedInfo)
+        verifyZeroInteractions(bubbles)
+    }
+
+    @Test
     fun showNoteTask_bubblesIsNull_shouldDoNothing() {
         createNoteTaskController(bubbles = null).showNoteTask(entryPoint = TAIL_BUTTON)
 
-        verifyZeroInteractions(context, bubbles, eventLogger)
+        verifyZeroInteractions(bubbles, eventLogger)
     }
 
     @Test
@@ -347,14 +405,14 @@
         noteTaskController.showNoteTask(entryPoint = TAIL_BUTTON)
 
         verify(noteTaskController).showNoDefaultNotesAppToast()
-        verifyZeroInteractions(context, bubbles, eventLogger)
+        verifyZeroInteractions(bubbles, eventLogger)
     }
 
     @Test
     fun showNoteTask_flagDisabled_shouldDoNothing() {
         createNoteTaskController(isEnabled = false).showNoteTask(entryPoint = TAIL_BUTTON)
 
-        verifyZeroInteractions(context, bubbles, eventLogger)
+        verifyZeroInteractions(bubbles, eventLogger)
     }
 
     @Test
@@ -363,7 +421,7 @@
 
         createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON)
 
-        verifyZeroInteractions(context, bubbles, eventLogger)
+        verifyZeroInteractions(bubbles, eventLogger)
     }
 
     @Test
@@ -483,7 +541,7 @@
 
         createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE)
 
-        verifyZeroInteractions(context, bubbles, eventLogger)
+        verifyZeroInteractions(bubbles, eventLogger)
     }
 
     @Test
@@ -499,7 +557,7 @@
 
         createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE)
 
-        verifyZeroInteractions(context, bubbles, eventLogger)
+        verifyZeroInteractions(bubbles, eventLogger)
     }
 
     @Test
@@ -611,11 +669,11 @@
 
         createNoteTaskController(isEnabled = true).onRoleHoldersChanged("NOT_NOTES", user)
 
-        verifyZeroInteractions(context)
+        verify(context, never()).startActivityAsUser(any(), any())
     }
 
     @Test
-    fun onRoleHoldersChanged_notesRole_sameUser_shouldUpdateShortcuts() {
+    fun onRoleHoldersChanged_notesRole_shouldUpdateShortcuts() {
         val user = userTracker.userHandle
         val controller = spy(createNoteTaskController())
         doNothing().whenever(controller).updateNoteTaskAsUser(any())
@@ -624,22 +682,41 @@
 
         verify(controller).updateNoteTaskAsUser(user)
     }
-
-    @Test
-    fun onRoleHoldersChanged_notesRole_differentUser_shouldUpdateShortcutsInUserProcess() {
-        // FakeUserTracker will default to UserHandle.SYSTEM.
-        val user = UserHandle.CURRENT
-
-        createNoteTaskController(isEnabled = true).onRoleHoldersChanged(ROLE_NOTES, user)
-
-        verify(context).startServiceAsUser(any(), eq(user))
-    }
     // endregion
 
     // region updateNoteTaskAsUser
     @Test
-    fun updateNoteTaskAsUser_withNotesRole_withShortcuts_shouldUpdateShortcuts() {
-        createNoteTaskController(isEnabled = true).updateNoteTaskAsUser(userTracker.userHandle)
+    fun updateNoteTaskAsUser_sameUser_shouldUpdateShortcuts() {
+        val user = userTracker.userHandle
+        val controller = spy(createNoteTaskController())
+        doNothing().whenever(controller).updateNoteTaskAsUserInternal(any())
+
+        controller.updateNoteTaskAsUser(user)
+
+        verify(controller).updateNoteTaskAsUserInternal(user)
+        verify(context, never()).startServiceAsUser(any(), any())
+    }
+
+    @Test
+    fun updateNoteTaskAsUser_differentUser_shouldUpdateShortcutsInUserProcess() {
+        // FakeUserTracker will default to UserHandle.SYSTEM.
+        val user = UserHandle.CURRENT
+        val controller = spy(createNoteTaskController(isEnabled = true))
+        doNothing().whenever(controller).updateNoteTaskAsUserInternal(any())
+
+        controller.updateNoteTaskAsUser(user)
+
+        verify(controller, never()).updateNoteTaskAsUserInternal(any())
+        val intent = withArgCaptor { verify(context).startServiceAsUser(capture(), eq(user)) }
+        assertThat(intent).hasComponentClass(NoteTaskControllerUpdateService::class.java)
+    }
+    // endregion
+
+    // region internalUpdateNoteTaskAsUser
+    @Test
+    fun updateNoteTaskAsUserInternal_withNotesRole_withShortcuts_shouldUpdateShortcuts() {
+        createNoteTaskController(isEnabled = true)
+            .updateNoteTaskAsUserInternal(userTracker.userHandle)
 
         val actualComponent = argumentCaptor<ComponentName>()
         verify(context.packageManager)
@@ -652,27 +729,29 @@
             .isEqualTo(CreateNoteTaskShortcutActivity::class.java.name)
         verify(shortcutManager, never()).disableShortcuts(any())
         verify(shortcutManager).enableShortcuts(listOf(SHORTCUT_ID))
-        val actualShortcuts = argumentCaptor<List<ShortcutInfo>>()
-        verify(shortcutManager).updateShortcuts(actualShortcuts.capture())
-        val actualShortcut = actualShortcuts.value.first()
-        assertThat(actualShortcut.id).isEqualTo(SHORTCUT_ID)
-        assertThat(actualShortcut.intent).run {
-            hasComponentClass(LaunchNoteTaskActivity::class.java)
-            hasAction(ACTION_CREATE_NOTE)
+        val shortcutInfo = withArgCaptor { verify(shortcutManager).updateShortcuts(capture()) }
+        with(shortcutInfo.first()) {
+            assertThat(id).isEqualTo(SHORTCUT_ID)
+            assertThat(intent).run {
+                hasComponentClass(LaunchNoteTaskActivity::class.java)
+                hasAction(ACTION_CREATE_NOTE)
+            }
+            assertThat(shortLabel).isEqualTo(NOTE_TASK_SHORT_LABEL)
+            assertThat(longLabel).isEqualTo(NOTE_TASK_LONG_LABEL)
+            assertThat(isLongLived).isEqualTo(true)
+            assertThat(icon.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget)
+            assertThat(extras?.getString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE))
+                .isEqualTo(NOTE_TASK_PACKAGE_NAME)
         }
-        assertThat(actualShortcut.shortLabel).isEqualTo(NOTE_TASK_SHORT_LABEL)
-        assertThat(actualShortcut.isLongLived).isEqualTo(true)
-        assertThat(actualShortcut.icon.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget)
-        assertThat(actualShortcut.extras?.getString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE))
-            .isEqualTo(NOTE_TASK_PACKAGE_NAME)
     }
 
     @Test
-    fun updateNoteTaskAsUser_noNotesRole_shouldDisableShortcuts() {
+    fun updateNoteTaskAsUserInternal_noNotesRole_shouldDisableShortcuts() {
         whenever(roleManager.getRoleHoldersAsUser(ROLE_NOTES, userTracker.userHandle))
             .thenReturn(emptyList())
 
-        createNoteTaskController(isEnabled = true).updateNoteTaskAsUser(userTracker.userHandle)
+        createNoteTaskController(isEnabled = true)
+            .updateNoteTaskAsUserInternal(userTracker.userHandle)
 
         val argument = argumentCaptor<ComponentName>()
         verify(context.packageManager)
@@ -689,8 +768,9 @@
     }
 
     @Test
-    fun updateNoteTaskAsUser_flagDisabled_shouldDisableShortcuts() {
-        createNoteTaskController(isEnabled = false).updateNoteTaskAsUser(userTracker.userHandle)
+    fun updateNoteTaskAsUserInternal_flagDisabled_shouldDisableShortcuts() {
+        createNoteTaskController(isEnabled = false)
+            .updateNoteTaskAsUserInternal(userTracker.userHandle)
 
         val argument = argumentCaptor<ComponentName>()
         verify(context.packageManager)
@@ -707,18 +787,17 @@
     }
     // endregion
 
-    // startregion startNoteTaskProxyActivityForUser
+    // startregion updateNoteTaskForAllUsers
     @Test
-    fun startNoteTaskProxyActivityForUser_shouldStartLaunchNoteTaskProxyActivityWithExpectedUser() {
-        val user0 = UserHandle.of(0)
-        createNoteTaskController().startNoteTaskProxyActivityForUser(user0)
+    fun updateNoteTaskForAllUsers_shouldRunUpdateForCurrentUserAndProfiles() {
+        userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
+        val controller = spy(createNoteTaskController())
+        doNothing().whenever(controller).updateNoteTaskAsUser(any())
 
-        val intentCaptor = argumentCaptor<Intent>()
-        verify(context).startActivityAsUser(intentCaptor.capture(), eq(user0))
-        assertThat(intentCaptor.value).run {
-            hasComponentClass(LaunchNoteTaskManagedProfileProxyActivity::class.java)
-            hasFlags(FLAG_ACTIVITY_NEW_TASK)
-        }
+        controller.updateNoteTaskForCurrentUserAndManagedProfiles()
+
+        verify(controller).updateNoteTaskAsUser(mainUserInfo.userHandle)
+        verify(controller).updateNoteTaskAsUser(workUserInfo.userHandle)
     }
     // endregion
 
@@ -838,7 +917,8 @@
     // endregion
 
     private companion object {
-        const val NOTE_TASK_SHORT_LABEL = "Notetaking"
+        const val NOTE_TASK_SHORT_LABEL = "Note-taking"
+        const val NOTE_TASK_LONG_LABEL = "Note-taking, App"
         const val NOTE_TASK_ACTIVITY_NAME = "NoteTaskActivity"
         const val NOTE_TASK_PACKAGE_NAME = "com.android.note.app"
         const val NOTE_TASK_UID = 123456
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 4e85b6c..95bb3e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -32,9 +32,12 @@
 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.mockito.withArgCaptor
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.wm.shell.bubbles.Bubbles
+import com.google.common.truth.Truth.assertThat
 import java.util.Optional
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.junit.Before
@@ -71,19 +74,19 @@
     }
 
     private fun createUnderTest(
-        isEnabled: Boolean,
-        bubbles: Bubbles?,
+            isEnabled: Boolean,
+            bubbles: Bubbles?,
     ): NoteTaskInitializer =
-        NoteTaskInitializer(
-            controller = controller,
-            commandQueue = commandQueue,
-            optionalBubbles = Optional.ofNullable(bubbles),
-            isEnabled = isEnabled,
-            roleManager = roleManager,
-            userTracker = userTracker,
-            keyguardUpdateMonitor = keyguardMonitor,
-            backgroundExecutor = executor,
-        )
+            NoteTaskInitializer(
+                    controller = controller,
+                    commandQueue = commandQueue,
+                    optionalBubbles = Optional.ofNullable(bubbles),
+                    isEnabled = isEnabled,
+                    roleManager = roleManager,
+                    userTracker = userTracker,
+                    keyguardUpdateMonitor = keyguardMonitor,
+                    backgroundExecutor = executor,
+            )
 
     @Test
     fun initialize_withUserUnlocked() {
@@ -93,8 +96,8 @@
 
         verify(commandQueue).addCallback(any())
         verify(roleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any())
-        verify(controller).setNoteTaskShortcutEnabled(any(), any())
-        verify(keyguardMonitor, never()).registerCallback(any())
+        verify(controller).updateNoteTaskForCurrentUserAndManagedProfiles()
+        verify(keyguardMonitor).registerCallback(any())
     }
 
     @Test
@@ -107,6 +110,7 @@
         verify(roleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any())
         verify(controller, never()).setNoteTaskShortcutEnabled(any(), any())
         verify(keyguardMonitor).registerCallback(any())
+        assertThat(userTracker.callbacks).isNotEmpty()
     }
 
     @Test
@@ -116,12 +120,12 @@
         underTest.initialize()
 
         verifyZeroInteractions(
-            commandQueue,
-            bubbles,
-            controller,
-            roleManager,
-            userManager,
-            keyguardMonitor,
+                commandQueue,
+                bubbles,
+                controller,
+                roleManager,
+                userManager,
+                keyguardMonitor,
         )
     }
 
@@ -132,12 +136,12 @@
         underTest.initialize()
 
         verifyZeroInteractions(
-            commandQueue,
-            bubbles,
-            controller,
-            roleManager,
-            userManager,
-            keyguardMonitor,
+                commandQueue,
+                bubbles,
+                controller,
+                roleManager,
+                userManager,
+                keyguardMonitor,
         )
     }
 
@@ -146,7 +150,7 @@
         val expectedKeyEvent = KeyEvent(ACTION_DOWN, KEYCODE_STYLUS_BUTTON_TAIL)
         val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
         underTest.initialize()
-        val callback = captureArgument { verify(commandQueue).addCallback(capture()) }
+        val callback = withArgCaptor { verify(commandQueue).addCallback(capture()) }
 
         callback.handleSystemKey(expectedKeyEvent)
 
@@ -154,31 +158,49 @@
     }
 
     @Test
-    fun initialize_userUnlocked() {
+    fun initialize_userUnlocked_shouldUpdateNoteTask() {
         whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(false)
         val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
         underTest.initialize()
-        val callback = captureArgument { verify(keyguardMonitor).registerCallback(capture()) }
+        val callback = withArgCaptor { verify(keyguardMonitor).registerCallback(capture()) }
         whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(true)
 
         callback.onUserUnlocked()
-        verify(controller).setNoteTaskShortcutEnabled(any(), any())
+
+        verify(controller).updateNoteTaskForCurrentUserAndManagedProfiles()
     }
 
     @Test
-    fun initialize_onRoleHoldersChanged() {
+    fun initialize_onRoleHoldersChanged_shouldRunOnRoleHoldersChanged() {
         val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
         underTest.initialize()
-        val callback = captureArgument {
+        val callback = withArgCaptor {
             verify(roleManager)
-                .addOnRoleHoldersChangedListenerAsUser(any(), capture(), eq(UserHandle.ALL))
+                    .addOnRoleHoldersChangedListenerAsUser(any(), capture(), eq(UserHandle.ALL))
         }
 
         callback.onRoleHoldersChanged(ROLE_NOTES, userTracker.userHandle)
 
         verify(controller).onRoleHoldersChanged(ROLE_NOTES, userTracker.userHandle)
     }
-}
 
-private inline fun <reified T : Any> captureArgument(block: ArgumentCaptor<T>.() -> Unit) =
-    argumentCaptor<T>().apply(block).value
+    @Test
+    fun initialize_onProfilesChanged_shouldUpdateNoteTask() {
+        val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
+        underTest.initialize()
+
+        userTracker.callbacks.first().onProfilesChanged(emptyList())
+
+        verify(controller, times(2)).updateNoteTaskForCurrentUserAndManagedProfiles()
+    }
+
+    @Test
+    fun initialize_onUserChanged_shouldUpdateNoteTask() {
+        val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
+        underTest.initialize()
+
+        userTracker.callbacks.first().onUserChanged(0, mock())
+
+        verify(controller, times(2)).updateNoteTaskForCurrentUserAndManagedProfiles()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt
index a0c376f..1f0f0d7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt
@@ -16,30 +16,23 @@
 
 package com.android.systemui.notetask.shortcut
 
-import android.content.Intent
-import android.content.pm.UserInfo
-import android.os.UserHandle
-import android.os.UserManager
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import androidx.test.rule.ActivityTestRule
-import androidx.test.runner.intercepting.SingleActivityFactory
 import com.android.dx.mockito.inline.extended.ExtendedMockito.verify
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.activity.SingleActivityFactory
 import com.android.systemui.notetask.NoteTaskController
 import com.android.systemui.notetask.NoteTaskEntryPoint
-import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.whenever
 import org.junit.After
 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.never
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
@@ -48,21 +41,13 @@
 class LaunchNoteTaskActivityTest : SysuiTestCase() {
 
     @Mock lateinit var noteTaskController: NoteTaskController
-    @Mock lateinit var userManager: UserManager
-    private val userTracker: FakeUserTracker = FakeUserTracker()
 
     @Rule
     @JvmField
     val activityRule =
         ActivityTestRule<LaunchNoteTaskActivity>(
-            /* activityFactory= */ object :
-                SingleActivityFactory<LaunchNoteTaskActivity>(LaunchNoteTaskActivity::class.java) {
-                override fun create(intent: Intent?) =
-                    LaunchNoteTaskActivity(
-                        controller = noteTaskController,
-                        userManager = userManager,
-                        userTracker = userTracker
-                    )
+            /* activityFactory= */ SingleActivityFactory {
+                LaunchNoteTaskActivity(controller = noteTaskController)
             },
             /* initialTouchMode= */ false,
             /* launchActivity= */ false,
@@ -71,7 +56,6 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        whenever(userManager.isManagedProfile(eq(workProfileUser.id))).thenReturn(true)
     }
 
     @After
@@ -83,36 +67,7 @@
     fun startActivityOnNonWorkProfileUser_shouldLaunchNoteTask() {
         activityRule.launchActivity(/* startIntent= */ null)
 
-        verify(noteTaskController).showNoteTask(eq(NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT))
-    }
-
-    @Test
-    fun startActivityOnWorkProfileUser_shouldLaunchProxyActivity() {
-        val mainUserHandle: UserHandle = mainUser.userHandle
-        userTracker.set(listOf(mainUser, workProfileUser), selectedUserIndex = 1)
-        whenever(userManager.isManagedProfile).thenReturn(true)
-        whenever(userManager.mainUser).thenReturn(mainUserHandle)
-
-        activityRule.launchActivity(/* startIntent= */ null)
-
-        verify(noteTaskController).startNoteTaskProxyActivityForUser(eq(mainUserHandle))
-    }
-
-    @Test
-    fun startActivityOnWorkProfileUser_noMainUser_shouldNotLaunch() {
-        userTracker.set(listOf(mainUser, workProfileUser), selectedUserIndex = 1)
-        whenever(userManager.isManagedProfile).thenReturn(true)
-        whenever(userManager.mainUser).thenReturn(null)
-
-        activityRule.launchActivity(/* startIntent= */ null)
-
-        verify(noteTaskController, never()).showNoteTask(any())
-        verify(noteTaskController, never()).startNoteTaskProxyActivityForUser(any())
-    }
-
-    private companion object {
-        val mainUser = UserInfo(/* id= */ 0, /* name= */ "primary", /* flags= */ UserInfo.FLAG_MAIN)
-        val workProfileUser =
-            UserInfo(/* id= */ 10, /* name= */ "work", /* flags= */ UserInfo.FLAG_PROFILE)
+        verify(noteTaskController)
+            .showNoteTaskAsUser(eq(NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT), any())
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt
deleted file mode 100644
index 6347c34..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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.shortcut
-
-import android.content.Intent
-import android.content.pm.UserInfo
-import android.os.UserHandle
-import android.os.UserManager
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import androidx.test.filters.SmallTest
-import androidx.test.rule.ActivityTestRule
-import androidx.test.runner.intercepting.SingleActivityFactory
-import com.android.dx.mockito.inline.extended.ExtendedMockito.never
-import com.android.dx.mockito.inline.extended.ExtendedMockito.verify
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskController
-import com.android.systemui.notetask.NoteTaskEntryPoint
-import com.android.systemui.settings.FakeUserTracker
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.whenever
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-
-@RunWith(AndroidTestingRunner::class)
-@SmallTest
-@TestableLooper.RunWithLooper
-class LaunchNoteTaskManagedProfileProxyActivityTest : SysuiTestCase() {
-
-    @Mock lateinit var noteTaskController: NoteTaskController
-    @Mock lateinit var userManager: UserManager
-    private val userTracker = FakeUserTracker()
-
-    @Rule
-    @JvmField
-    val activityRule =
-        ActivityTestRule<LaunchNoteTaskManagedProfileProxyActivity>(
-            /* activityFactory= */ object :
-                SingleActivityFactory<LaunchNoteTaskManagedProfileProxyActivity>(
-                    LaunchNoteTaskManagedProfileProxyActivity::class.java
-                ) {
-                override fun create(intent: Intent?) =
-                    LaunchNoteTaskManagedProfileProxyActivity(
-                        controller = noteTaskController,
-                        userManager = userManager,
-                        userTracker = userTracker
-                    )
-            },
-            /* initialTouchMode= */ false,
-            /* launchActivity= */ false,
-        )
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        whenever(userManager.isManagedProfile(eq(workProfileUser.id))).thenReturn(true)
-    }
-
-    @After
-    fun tearDown() {
-        activityRule.finishActivity()
-    }
-
-    @Test
-    fun startActivity_noWorkProfileUser_shouldNotLaunchNoteTask() {
-        userTracker.set(listOf(mainUser), selectedUserIndex = 0)
-        activityRule.launchActivity(/* startIntent= */ null)
-
-        verify(noteTaskController, never()).showNoteTaskAsUser(any(), any())
-    }
-
-    @Test
-    fun startActivity_hasWorkProfileUser_shouldLaunchNoteTaskOnTheWorkProfileUser() {
-        userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUser))
-        activityRule.launchActivity(/* startIntent= */ null)
-
-        val workProfileUserHandle: UserHandle = workProfileUser.userHandle
-        verify(noteTaskController)
-            .showNoteTaskAsUser(
-                eq(NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT),
-                eq(workProfileUserHandle)
-            )
-    }
-
-    private companion object {
-        val mainUser = UserInfo(/* id= */ 0, /* name= */ "primary", /* flags= */ UserInfo.FLAG_MAIN)
-        val workProfileUser =
-            UserInfo(/* id= */ 10, /* name= */ "work", /* flags= */ UserInfo.FLAG_PROFILE)
-        val mainAndWorkProfileUsers = listOf(mainUser, workProfileUser)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt
index bb3b3f7..a01394f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt
@@ -24,7 +24,11 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
+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.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.launchIn
@@ -47,6 +51,8 @@
 @RunWith(JUnit4::class)
 class PowerRepositoryImplTest : SysuiTestCase() {
 
+    private val systemClock = FakeSystemClock()
+
     @Mock private lateinit var manager: PowerManager
     @Mock private lateinit var dispatcher: BroadcastDispatcher
     @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver>
@@ -62,7 +68,13 @@
         isInteractive = true
         whenever(manager.isInteractive).then { isInteractive }
 
-        underTest = PowerRepositoryImpl(manager = manager, dispatcher = dispatcher)
+        underTest =
+            PowerRepositoryImpl(
+                manager,
+                context.applicationContext,
+                systemClock,
+                dispatcher,
+            )
     }
 
     @Test
@@ -160,6 +172,27 @@
             job.cancel()
         }
 
+    @Test
+    fun wakeUp_notifiesPowerManager() {
+        systemClock.setUptimeMillis(345000)
+
+        underTest.wakeUp("fakeWhy", PowerManager.WAKE_REASON_GESTURE)
+
+        val reasonCaptor = argumentCaptor<String>()
+        verify(manager)
+            .wakeUp(eq(345000L), eq(PowerManager.WAKE_REASON_GESTURE), capture(reasonCaptor))
+        assertThat(reasonCaptor.value).contains("fakeWhy")
+    }
+
+    @Test
+    fun wakeUp_usesApplicationPackageName() {
+        underTest.wakeUp("fakeWhy", PowerManager.WAKE_REASON_GESTURE)
+
+        val reasonCaptor = argumentCaptor<String>()
+        verify(manager).wakeUp(any(), any(), capture(reasonCaptor))
+        assertThat(reasonCaptor.value).contains(context.applicationContext.packageName)
+    }
+
     private fun verifyRegistered() {
         // We must verify with all arguments, even those that are optional because they have default
         // values because Mockito is forcing us to. Once we can use mockito-kotlin, we should be
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
index 31d4512..023ed06 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
@@ -17,9 +17,14 @@
 
 package com.android.systemui.power.domain.interactor
 
+import android.os.PowerManager
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.launchIn
@@ -29,6 +34,9 @@
 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)
@@ -36,14 +44,25 @@
 
     private lateinit var underTest: PowerInteractor
     private lateinit var repository: FakePowerRepository
+    @Mock private lateinit var falsingCollector: FalsingCollector
+    @Mock private lateinit var screenOffAnimationController: ScreenOffAnimationController
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
 
     @Before
     fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
         repository =
             FakePowerRepository(
                 initialInteractive = true,
             )
-        underTest = PowerInteractor(repository = repository)
+        underTest =
+            PowerInteractor(
+                repository,
+                falsingCollector,
+                screenOffAnimationController,
+                statusBarStateController,
+            )
     }
 
     @Test
@@ -72,6 +91,40 @@
             job.cancel()
         }
 
+    @Test
+    fun wakeUpIfDozing_notDozing_notWoken() {
+        whenever(statusBarStateController.isDozing).thenReturn(false)
+        whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true)
+
+        underTest.wakeUpIfDozing("why", PowerManager.WAKE_REASON_TAP)
+
+        assertThat(repository.lastWakeWhy).isNull()
+        assertThat(repository.lastWakeReason).isNull()
+    }
+
+    @Test
+    fun wakeUpIfDozing_notAllowed_notWoken() {
+        whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(false)
+        whenever(statusBarStateController.isDozing).thenReturn(true)
+
+        underTest.wakeUpIfDozing("why", PowerManager.WAKE_REASON_TAP)
+
+        assertThat(repository.lastWakeWhy).isNull()
+        assertThat(repository.lastWakeReason).isNull()
+    }
+
+    @Test
+    fun wakeUpIfDozing_dozingAndAllowed_wokenAndFalsingNotified() {
+        whenever(statusBarStateController.isDozing).thenReturn(true)
+        whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true)
+
+        underTest.wakeUpIfDozing("testReason", PowerManager.WAKE_REASON_GESTURE)
+
+        assertThat(repository.lastWakeWhy).isEqualTo("testReason")
+        assertThat(repository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE)
+        verify(falsingCollector).onScreenOnFromTouch()
+    }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
index aacc695..93f316e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
@@ -49,7 +49,7 @@
         buffer.dump(PrintWriter(stringWriter), tailLength = 0)
         val actualString = stringWriter.toString()
         val expectedLogString = disableFlagsLogger.getDisableFlagsString(
-            old = null, new = state, newAfterLocalModification = state
+            new = state, newAfterLocalModification = state
         )
 
         assertThat(actualString).contains(expectedLogString)
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 87ca9df..fcda5f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -50,10 +50,8 @@
 import com.android.keyguard.BouncerPanelExpansionCalculator;
 import com.android.systemui.R;
 import com.android.systemui.SysuiBaseFragmentTest;
-import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSFragmentComponent;
@@ -169,9 +167,7 @@
     }
 
     @Test
-    public void transitionToFullShade_largeScreen_flagEnabled_alphaLargeScreenShadeInterpolator() {
-        when(mFeatureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION))
-                .thenReturn(true);
+    public void transitionToFullShade_largeScreen_alphaLargeScreenShadeInterpolator() {
         QSFragment fragment = resumeAndGetFragment();
         setIsLargeScreen();
         setStatusBarCurrentAndUpcomingState(StatusBarState.SHADE);
@@ -188,25 +184,6 @@
     }
 
     @Test
-    public void transitionToFullShade_largeScreen_flagDisabled_alphaStandardInterpolator() {
-        when(mFeatureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION))
-                .thenReturn(false);
-        QSFragment fragment = resumeAndGetFragment();
-        setIsLargeScreen();
-        setStatusBarCurrentAndUpcomingState(StatusBarState.SHADE);
-        boolean isTransitioningToFullShade = true;
-        float transitionProgress = 0.5f;
-        float squishinessFraction = 0.5f;
-        when(mLargeScreenShadeInterpolator.getQsAlpha(transitionProgress)).thenReturn(123f);
-
-        fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
-                squishinessFraction);
-
-        assertThat(mQsFragmentView.getAlpha())
-                .isEqualTo(ShadeInterpolation.getContentAlpha(transitionProgress));
-    }
-
-    @Test
     public void
             transitionToFullShade_onKeyguard_noBouncer_setsAlphaUsingLinearInterpolator() {
         QSFragment fragment = resumeAndGetFragment();
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 a60dad4..fe6c9b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
@@ -15,9 +15,11 @@
 
 import android.graphics.Rect
 import android.testing.AndroidTestingRunner
+import android.testing.TestableContext
 import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
 import android.testing.ViewUtils
+import android.view.ContextThemeWrapper
 import android.view.View
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.view.accessibility.AccessibilityNodeInfo
@@ -55,19 +57,24 @@
 
     private lateinit var footer: View
 
+    private val themedContext = TestableContext(
+            ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
+    )
+
     @Before
     @Throws(Exception::class)
     fun setup() {
         MockitoAnnotations.initMocks(this)
         testableLooper = TestableLooper.get(this)
+        // Apply only the values of the theme that are not defined
 
         testableLooper.runWithLooper {
-            qsPanel = QSPanel(context, null)
+            qsPanel = QSPanel(themedContext, null)
             qsPanel.mUsingMediaPlayer = true
 
             qsPanel.initialize(qsLogger)
             // QSPanel inflates a footer inside of it, mocking it here
-            footer = LinearLayout(context).apply { id = R.id.qs_footer }
+            footer = LinearLayout(themedContext).apply { id = R.id.qs_footer }
             qsPanel.addView(footer, MATCH_PARENT, 100)
             qsPanel.onFinishInflate()
             // Provides a parent with non-zero size for QSPanel
@@ -105,12 +112,12 @@
         qsPanel.tileLayout?.addTile(
                 QSPanelControllerBase.TileRecord(
                     mock(QSTile::class.java),
-                    QSTileViewImpl(context, QSIconViewImpl(context))
+                    QSTileViewImpl(themedContext, QSIconViewImpl(themedContext))
                 )
             )
 
-        val mediaView = FrameLayout(context)
-        mediaView.addView(View(context), MATCH_PARENT, 800)
+        val mediaView = FrameLayout(themedContext)
+        mediaView.addView(View(themedContext), MATCH_PARENT, 800)
 
         qsPanel.setUsingHorizontalLayout(/* horizontal */ true, mediaView, /* force */ true)
         qsPanel.measure(
@@ -135,12 +142,12 @@
         qsPanel.tileLayout?.addTile(
             QSPanelControllerBase.TileRecord(
                 mock(QSTile::class.java),
-                QSTileViewImpl(context, QSIconViewImpl(context))
+                QSTileViewImpl(themedContext, QSIconViewImpl(themedContext))
             )
         )
 
-        val mediaView = FrameLayout(context)
-        mediaView.addView(View(context), MATCH_PARENT, 800)
+        val mediaView = FrameLayout(themedContext)
+        mediaView.addView(View(themedContext), MATCH_PARENT, 800)
 
         qsPanel.setUsingHorizontalLayout(/* horizontal */ true, mediaView, /* force */ true)
         qsPanel.measure(
@@ -161,7 +168,10 @@
     @Test
     fun testBottomPadding() {
         val padding = 10
-        context.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_bottom, padding)
+        themedContext.orCreateTestableResources.addOverride(
+                R.dimen.qs_panel_padding_bottom,
+                padding
+        )
         qsPanel.updatePadding()
         assertThat(qsPanel.paddingBottom).isEqualTo(padding)
     }
@@ -170,8 +180,11 @@
     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, paddingCombined)
+        themedContext.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_top, padding)
+        themedContext.orCreateTestableResources.addOverride(
+                R.dimen.qs_panel_padding_top,
+                paddingCombined
+        )
 
         qsPanel.updatePadding()
         assertThat(qsPanel.paddingTop).isEqualTo(paddingCombined)
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 587c49d..9781baa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -25,6 +25,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -67,8 +68,8 @@
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.settings.UserFileManager;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.phone.AutoTileManager;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.FakeSharedPreferences;
 import com.android.systemui.util.concurrency.FakeExecutor;
@@ -86,7 +87,6 @@
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.List;
-import java.util.Optional;
 import java.util.concurrent.Executor;
 
 import javax.inject.Provider;
@@ -108,9 +108,9 @@
     @Mock
     private TunerService mTunerService;
     @Mock
-    private Provider<AutoTileManager> mAutoTiles;
+    private AutoTileManager mAutoTiles;
     @Mock
-    private CentralSurfaces mCentralSurfaces;
+    private ShadeController mShadeController;
     @Mock
     private QSLogger mQSLogger;
     @Mock
@@ -141,6 +141,7 @@
         mFeatureFlags = new FakeFeatureFlags();
 
         mFeatureFlags.set(Flags.QS_PIPELINE_NEW_HOST, false);
+        mFeatureFlags.set(Flags.QS_PIPELINE_AUTO_ADD, false);
 
         mMainExecutor = new FakeExecutor(new FakeSystemClock());
 
@@ -161,9 +162,10 @@
         mSecureSettings = new FakeSettings();
         saveSetting("");
         mQSTileHost = new TestQSTileHost(mContext, mDefaultFactory, mMainExecutor,
-                mPluginManager, mTunerService, mAutoTiles, mCentralSurfaces,
+                mPluginManager, mTunerService, () -> mAutoTiles, mShadeController,
                 mQSLogger, mUserTracker, mSecureSettings, mCustomTileStatePersister,
                 mTileLifecycleManagerFactory, mUserFileManager, mFeatureFlags);
+        mMainExecutor.runAllReady();
 
         mSecureSettings.registerContentObserverForUser(SETTING, new ContentObserver(null) {
             @Override
@@ -682,6 +684,17 @@
         assertEquals(CUSTOM_TILE.getClassName(), proto.tiles[1].getComponentName().className);
     }
 
+    @Test
+    public void testUserChange_flagOn_autoTileManagerNotified() {
+        mFeatureFlags.set(Flags.QS_PIPELINE_NEW_HOST, true);
+        int currentUser = mUserTracker.getUserId();
+        clearInvocations(mAutoTiles);
+        when(mUserTracker.getUserId()).thenReturn(currentUser + 1);
+
+        mQSTileHost.onTuningChanged(SETTING, "a,b");
+        verify(mAutoTiles).changeUser(UserHandle.of(currentUser + 1));
+    }
+
     private SharedPreferences getSharedPreferencesForUser(int user) {
         return mUserFileManager.getSharedPreferences(QSTileHost.TILES, 0, user);
     }
@@ -691,13 +704,13 @@
                 QSFactory defaultFactory, Executor mainExecutor,
                 PluginManager pluginManager, TunerService tunerService,
                 Provider<AutoTileManager> autoTiles,
-                CentralSurfaces centralSurfaces, QSLogger qsLogger,
+                ShadeController shadeController, QSLogger qsLogger,
                 UserTracker userTracker, SecureSettings secureSettings,
                 CustomTileStatePersister customTileStatePersister,
                 TileLifecycleManager.Factory tileLifecycleManagerFactory,
                 UserFileManager userFileManager, FeatureFlags featureFlags) {
             super(context, defaultFactory, mainExecutor, pluginManager,
-                    tunerService, autoTiles,  Optional.of(centralSurfaces), qsLogger,
+                    tunerService, autoTiles,  shadeController, qsLogger,
                     userTracker, secureSettings, customTileStatePersister,
                     tileLifecycleManagerFactory, userFileManager, featureFlags);
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
index 8789253..f55ef65 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
@@ -26,12 +26,14 @@
 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.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.TestableLooper;
+import android.view.ContextThemeWrapper;
 import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
 
@@ -56,14 +58,17 @@
     private Resources mResources;
     private int mLayoutSizeForOneTile;
     private TileLayout mTileLayout; // under test
+    private Context mSpyContext;
+
 
     @Before
     public void setUp() throws Exception {
-        Context context = Mockito.spy(mContext);
-        mResources = Mockito.spy(context.getResources());
-        Mockito.when(mContext.getResources()).thenReturn(mResources);
+        mSpyContext = Mockito.spy(
+                new ContextThemeWrapper(mContext, R.style.Theme_SystemUI_QuickSettings));
+        mResources = Mockito.spy(mSpyContext.getResources());
+        when(mSpyContext.getResources()).thenReturn(mResources);
 
-        mTileLayout = new TileLayout(context);
+        mTileLayout = new TileLayout(mSpyContext);
         // Layout needs to leave space for the tile margins. Three times the margin size is
         // sufficient for any number of columns.
         mLayoutSizeForOneTile =
@@ -73,7 +78,7 @@
     private QSPanelControllerBase.TileRecord createTileRecord() {
         return new QSPanelControllerBase.TileRecord(
                 mock(QSTile.class),
-                spy(new QSTileViewImpl(mContext, new QSIconViewImpl(mContext))));
+                spy(new QSTileViewImpl(mSpyContext, new QSIconViewImpl(mSpyContext))));
     }
 
     @Test
@@ -161,7 +166,7 @@
                 .layout(left2.capture(), top2.capture(), right2.capture(), bottom2.capture());
 
         // We assume two tiles will always fit side-by-side.
-        assertTrue(mContext.getResources().getInteger(R.integer.quick_settings_num_columns) > 1);
+        assertTrue(mSpyContext.getResources().getInteger(R.integer.quick_settings_num_columns) > 1);
 
         // left <= right, top <= bottom
         assertTrue(left1.getValue() <= right1.getValue());
@@ -218,16 +223,16 @@
 
     @Test
     public void resourcesChanged_updateResources_returnsTrue() {
-        Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(1);
+        when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(1);
         mTileLayout.updateResources(); // setup with 1
-        Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(2);
+        when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(2);
 
         assertEquals(true, mTileLayout.updateResources());
     }
 
     @Test
     public void resourcesSame_updateResources_returnsFalse() {
-        Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(1);
+        when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(1);
         mTileLayout.updateResources(); // setup with 1
 
         assertEquals(false, mTileLayout.updateResources());
@@ -250,7 +255,7 @@
         QSPanelControllerBase.TileRecord tileRecord = createTileRecord();
         mTileLayout.addTile(tileRecord);
 
-        FakeTileView tileView = new FakeTileView(mContext);
+        FakeTileView tileView = new FakeTileView(mSpyContext);
         QSTile.State state = new QSTile.State();
         state.label = "TEST LABEL";
         state.secondaryLabel = "TEST SECONDARY LABEL";
@@ -276,9 +281,10 @@
     }
 
     private void changeFontScaling(float scale) {
-        Configuration configuration = new Configuration(mContext.getResources().getConfiguration());
+        Configuration configuration =
+                new Configuration(mSpyContext.getResources().getConfiguration());
         configuration.fontScale = scale;
         // updateConfiguration could help update on both resource configuration and displayMetrics
-        mContext.getResources().updateConfiguration(configuration, null, null);
+        mSpyContext.getResources().updateConfiguration(configuration, null, null);
     }
 }
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 198ed4a..41240e5 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,7 +35,7 @@
 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.animation.view.LaunchableFrameLayout
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.qs.QSTile
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
index 2cc6709..d647d6a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
@@ -21,6 +21,7 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
+import android.view.ContextThemeWrapper
 import androidx.test.filters.SmallTest
 import com.android.settingslib.Utils
 import com.android.settingslib.drawable.UserIconDrawable
@@ -63,6 +64,8 @@
     private val testScope = TestScope()
     private lateinit var utils: FooterActionsTestUtils
 
+    private val themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
+
     @Before
     fun setUp() {
         utils = FooterActionsTestUtils(context, TestableLooper.get(this), testScope.testScheduler)
@@ -84,8 +87,14 @@
                     ContentDescription.Resource(R.string.accessibility_quick_settings_settings)
                 )
             )
-        assertThat(settings.backgroundColor).isEqualTo(R.attr.offStateColor)
-        assertThat(settings.iconTint).isNull()
+        assertThat(settings.backgroundColor).isEqualTo(R.attr.shadeInactive)
+        assertThat(settings.iconTint)
+            .isEqualTo(
+                Utils.getColorAttrDefaultColor(
+                    themedContext,
+                    R.attr.onShadeInactiveVariant,
+                )
+            )
     }
 
     @Test
@@ -105,12 +114,12 @@
                     ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu)
                 )
             )
-        assertThat(power.backgroundColor).isEqualTo(com.android.internal.R.attr.colorAccent)
+        assertThat(power.backgroundColor).isEqualTo(R.attr.shadeActive)
         assertThat(power.iconTint)
             .isEqualTo(
                 Utils.getColorAttrDefaultColor(
-                    context,
-                    com.android.internal.R.attr.textColorOnAccent,
+                    themedContext,
+                    R.attr.onShadeActive,
                 ),
             )
     }
@@ -170,7 +179,7 @@
         assertThat(userSwitcher).isNotNull()
         assertThat(userSwitcher!!.icon)
             .isEqualTo(Icon.Loaded(picture, ContentDescription.Loaded("Signed in as foo")))
-        assertThat(userSwitcher.backgroundColor).isEqualTo(R.attr.offStateColor)
+        assertThat(userSwitcher.backgroundColor).isEqualTo(R.attr.shadeInactive)
 
         // Change the current user name.
         userSwitcherControllerWrapper.currentUserName = "bar"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
index 18f3837..dc0fae5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
@@ -38,6 +38,7 @@
 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.nullable
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -75,6 +76,9 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        whenever(context.createContextAsUser(any(), anyInt())).thenReturn(context)
+        whenever(context.packageManager).thenReturn(packageManager)
+
         // Use the default value set in the ServiceInfo
         whenever(packageManager.getComponentEnabledSetting(any()))
             .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
@@ -86,7 +90,6 @@
         underTest =
             InstalledTilesComponentRepositoryImpl(
                 context,
-                packageManager,
                 testDispatcher,
             )
     }
@@ -224,6 +227,52 @@
             assertThat(componentNames).isEmpty()
         }
 
+    @Test
+    fun packageOnlyInSecondaryUser_noException() =
+        testScope.runTest {
+            val userId = 10
+            val secondaryUserContext: Context = mock()
+            whenever(context.userId).thenReturn(0) // System context
+            whenever(context.createContextAsUser(eq(UserHandle.of(userId)), anyInt()))
+                .thenReturn(secondaryUserContext)
+
+            val secondaryUserPackageManager: PackageManager = mock()
+            whenever(secondaryUserContext.packageManager).thenReturn(secondaryUserPackageManager)
+
+            // Use the default value set in the ServiceInfo
+            whenever(secondaryUserPackageManager.getComponentEnabledSetting(any()))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
+            // System User package manager throws exception if the component doesn't exist for that
+            // user
+            whenever(packageManager.getComponentEnabledSetting(TEST_COMPONENT))
+                .thenThrow(IllegalArgumentException()) // The package is not in the system user
+
+            val resolveInfo =
+                ResolveInfo(TEST_COMPONENT, hasPermission = true, defaultEnabled = true)
+            // Both package manager should return the same (because the query is for the secondary
+            // user)
+            whenever(
+                    secondaryUserPackageManager.queryIntentServicesAsUser(
+                        matchIntent(),
+                        matchFlags(),
+                        eq(userId)
+                    )
+                )
+                .thenReturn(listOf(resolveInfo))
+            whenever(
+                    packageManager.queryIntentServicesAsUser(
+                        matchIntent(),
+                        matchFlags(),
+                        eq(userId)
+                    )
+                )
+                .thenReturn(listOf(resolveInfo))
+
+            val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+
+            assertThat(componentNames).containsExactly(TEST_COMPONENT)
+        }
+
     private fun getRegisteredReceiver(): BroadcastReceiver {
         verify(context)
             .registerReceiverAsUser(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingListTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingListTest.kt
new file mode 100644
index 0000000..817ac61
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingListTest.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.qs.pipeline.domain.autoaddable
+
+import android.content.ComponentName
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class AutoAddableSettingListTest : SysuiTestCase() {
+
+    private val factory =
+        object : AutoAddableSetting.Factory {
+            override fun create(setting: String, spec: TileSpec): AutoAddableSetting {
+                return AutoAddableSetting(
+                    mock(),
+                    mock(),
+                    setting,
+                    spec,
+                )
+            }
+        }
+
+    @Test
+    fun correctLines_correctAutoAddables() {
+        val setting1 = "setting1"
+        val setting2 = "setting2"
+        val spec1 = TileSpec.create("spec1")
+        val spec2 = TileSpec.create(ComponentName("pkg", "cls"))
+
+        context.orCreateTestableResources.addOverride(
+            R.array.config_quickSettingsAutoAdd,
+            arrayOf(toStringLine(setting1, spec1), toStringLine(setting2, spec2))
+        )
+
+        val autoAddables = AutoAddableSettingList.parseSettingsResource(context.resources, factory)
+
+        assertThat(autoAddables)
+            .containsExactly(factory.create(setting1, spec1), factory.create(setting2, spec2))
+    }
+
+    @Test
+    fun malformedLine_ignored() {
+        val setting = "setting"
+        val spec = TileSpec.create("spec")
+
+        context.orCreateTestableResources.addOverride(
+            R.array.config_quickSettingsAutoAdd,
+            arrayOf(toStringLine(setting, spec), "bad_line")
+        )
+
+        val autoAddables = AutoAddableSettingList.parseSettingsResource(context.resources, factory)
+
+        assertThat(autoAddables).containsExactly(factory.create(setting, spec))
+    }
+
+    @Test
+    fun invalidSpec_ignored() {
+        val setting = "setting"
+        val spec = TileSpec.create("spec")
+
+        context.orCreateTestableResources.addOverride(
+            R.array.config_quickSettingsAutoAdd,
+            arrayOf(toStringLine(setting, spec), "invalid:")
+        )
+
+        val autoAddables = AutoAddableSettingList.parseSettingsResource(context.resources, factory)
+
+        assertThat(autoAddables).containsExactly(factory.create(setting, spec))
+    }
+
+    companion object {
+        private fun toStringLine(setting: String, spec: TileSpec) =
+            "$setting$SETTINGS_SEPARATOR${spec.spec}"
+        private const val SETTINGS_SEPARATOR = ":"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt
new file mode 100644
index 0000000..36c3c9d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt
@@ -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.systemui.qs.pipeline.domain.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+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.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class AutoAddableSettingTest : SysuiTestCase() {
+
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    private val secureSettings = FakeSettings()
+    private val underTest =
+        AutoAddableSetting(
+            secureSettings,
+            testDispatcher,
+            SETTING,
+            SPEC,
+        )
+
+    @Test
+    fun settingNotSet_noSignal() =
+        testScope.runTest {
+            val userId = 0
+            val signal by collectLastValue(underTest.autoAddSignal(userId))
+
+            assertThat(signal).isNull() // null means no emitted value
+        }
+
+    @Test
+    fun settingSetTo0_noSignal() =
+        testScope.runTest {
+            val userId = 0
+            val signal by collectLastValue(underTest.autoAddSignal(userId))
+
+            secureSettings.putIntForUser(SETTING, 0, userId)
+
+            assertThat(signal).isNull() // null means no emitted value
+        }
+
+    @Test
+    fun settingSetToNon0_signal() =
+        testScope.runTest {
+            val userId = 0
+            val signal by collectLastValue(underTest.autoAddSignal(userId))
+
+            secureSettings.putIntForUser(SETTING, 42, userId)
+
+            assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+        }
+
+    @Test
+    fun settingSetForUser_onlySignalInThatUser() =
+        testScope.runTest {
+            val signal0 by collectLastValue(underTest.autoAddSignal(0))
+            val signal1 by collectLastValue(underTest.autoAddSignal(1))
+
+            secureSettings.putIntForUser(SETTING, /* value */ 42, /* userHandle */ 1)
+
+            assertThat(signal0).isNull()
+            assertThat(signal1).isEqualTo(AutoAddSignal.Add(SPEC))
+        }
+
+    @Test
+    fun multipleNonZeroChanges_onlyOneSignal() =
+        testScope.runTest {
+            val userId = 0
+            val signals by collectValues(underTest.autoAddSignal(userId))
+
+            secureSettings.putIntForUser(SETTING, 1, userId)
+            secureSettings.putIntForUser(SETTING, 2, userId)
+
+            assertThat(signals.size).isEqualTo(1)
+        }
+
+    @Test
+    fun strategyIfNotAdded() {
+        assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.IfNotAdded(SPEC))
+    }
+
+    companion object {
+        private const val SETTING = "setting"
+        private val SPEC = TileSpec.create("spec")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddableTest.kt
new file mode 100644
index 0000000..afb43c7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddableTest.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.pipeline.domain.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.statusbar.policy.CallbackController
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class CallbackControllerAutoAddableTest : SysuiTestCase() {
+
+    @Test
+    fun callbackAddedAndRemoved() = runTest {
+        val controller = TestableController()
+        val callback = object : TestableController.Callback {}
+        val underTest =
+            object :
+                CallbackControllerAutoAddable<TestableController.Callback, TestableController>(
+                    controller
+                ) {
+                override val description: String = ""
+                override val spec: TileSpec
+                    get() = SPEC
+
+                override fun ProducerScope<AutoAddSignal>.getCallback():
+                    TestableController.Callback {
+                    return callback
+                }
+            }
+
+        val job = launch { underTest.autoAddSignal(0).collect {} }
+        runCurrent()
+        assertThat(controller.callbacks).containsExactly(callback)
+        job.cancel()
+        runCurrent()
+        assertThat(controller.callbacks).isEmpty()
+    }
+
+    @Test
+    fun sendAddFromCallback() = runTest {
+        val controller = TestableController()
+        val underTest =
+            object :
+                CallbackControllerAutoAddable<TestableController.Callback, TestableController>(
+                    controller
+                ) {
+                override val description: String = ""
+
+                override val spec: TileSpec
+                    get() = SPEC
+
+                override fun ProducerScope<AutoAddSignal>.getCallback():
+                    TestableController.Callback {
+                    return object : TestableController.Callback {
+                        override fun change() {
+                            sendAdd()
+                        }
+                    }
+                }
+            }
+
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        assertThat(signal).isNull()
+
+        controller.callbacks.first().change()
+
+        assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+    }
+
+    @Test
+    fun strategyIfNotAdded() {
+        val underTest =
+            object :
+                CallbackControllerAutoAddable<TestableController.Callback, TestableController>(
+                    TestableController()
+                ) {
+                override val description: String = ""
+                override val spec: TileSpec
+                    get() = SPEC
+
+                override fun ProducerScope<AutoAddSignal>.getCallback():
+                    TestableController.Callback {
+                    return object : TestableController.Callback {}
+                }
+            }
+
+        assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.IfNotAdded(SPEC))
+    }
+
+    private class TestableController : CallbackController<TestableController.Callback> {
+
+        val callbacks = mutableSetOf<Callback>()
+
+        override fun addCallback(listener: Callback) {
+            callbacks.add(listener)
+        }
+
+        override fun removeCallback(listener: Callback) {
+            callbacks.remove(listener)
+        }
+
+        interface Callback {
+            fun change() {}
+        }
+    }
+
+    companion object {
+        private val SPEC = TileSpec.create("test")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddableTest.kt
new file mode 100644
index 0000000..a357dad
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddableTest.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.pipeline.domain.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.CastTile
+import com.android.systemui.statusbar.policy.CastController
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+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.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class CastAutoAddableTest : SysuiTestCase() {
+
+    @Mock private lateinit var castController: CastController
+    @Captor private lateinit var callbackCaptor: ArgumentCaptor<CastController.Callback>
+
+    private lateinit var underTest: CastAutoAddable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest = CastAutoAddable(castController)
+    }
+
+    @Test
+    fun onCastDevicesChanged_noDevices_noSignal() = runTest {
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        runCurrent()
+
+        verify(castController).addCallback(capture(callbackCaptor))
+
+        callbackCaptor.value.onCastDevicesChanged()
+
+        assertThat(signal).isNull()
+    }
+
+    @Test
+    fun onCastDevicesChanged_deviceNotConnectedOrConnecting_noSignal() = runTest {
+        val device =
+            CastController.CastDevice().apply {
+                state = CastController.CastDevice.STATE_DISCONNECTED
+            }
+        whenever(castController.castDevices).thenReturn(listOf(device))
+
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        runCurrent()
+
+        verify(castController).addCallback(capture(callbackCaptor))
+
+        callbackCaptor.value.onCastDevicesChanged()
+
+        assertThat(signal).isNull()
+    }
+
+    @Test
+    fun onCastDevicesChanged_someDeviceConnecting_addSignal() = runTest {
+        val disconnectedDevice =
+            CastController.CastDevice().apply {
+                state = CastController.CastDevice.STATE_DISCONNECTED
+            }
+        val connectingDevice =
+            CastController.CastDevice().apply { state = CastController.CastDevice.STATE_CONNECTING }
+        whenever(castController.castDevices)
+            .thenReturn(listOf(disconnectedDevice, connectingDevice))
+
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        runCurrent()
+
+        verify(castController).addCallback(capture(callbackCaptor))
+
+        callbackCaptor.value.onCastDevicesChanged()
+
+        assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+    }
+
+    @Test
+    fun onCastDevicesChanged_someDeviceConnected_addSignal() = runTest {
+        val disconnectedDevice =
+            CastController.CastDevice().apply {
+                state = CastController.CastDevice.STATE_DISCONNECTED
+            }
+        val connectedDevice =
+            CastController.CastDevice().apply { state = CastController.CastDevice.STATE_CONNECTED }
+        whenever(castController.castDevices).thenReturn(listOf(disconnectedDevice, connectedDevice))
+
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        runCurrent()
+
+        verify(castController).addCallback(capture(callbackCaptor))
+
+        callbackCaptor.value.onCastDevicesChanged()
+
+        assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+    }
+
+    companion object {
+        private val SPEC = TileSpec.create(CastTile.TILE_SPEC)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddableTest.kt
new file mode 100644
index 0000000..098ffc3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddableTest.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.qs.pipeline.domain.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.DataSaverTile
+import com.android.systemui.statusbar.policy.DataSaverController
+import com.android.systemui.util.mockito.capture
+import com.google.common.truth.Truth.assertThat
+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.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DataSaverAutoAddableTest : SysuiTestCase() {
+
+    @Mock private lateinit var dataSaverController: DataSaverController
+    @Captor private lateinit var callbackCaptor: ArgumentCaptor<DataSaverController.Listener>
+
+    private lateinit var underTest: DataSaverAutoAddable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest = DataSaverAutoAddable(dataSaverController)
+    }
+
+    @Test
+    fun dataSaverNotEnabled_NoSignal() = runTest {
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        runCurrent()
+
+        verify(dataSaverController).addCallback(capture(callbackCaptor))
+
+        callbackCaptor.value.onDataSaverChanged(false)
+
+        assertThat(signal).isNull()
+    }
+
+    @Test
+    fun dataSaverEnabled_addSignal() = runTest {
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        runCurrent()
+
+        verify(dataSaverController).addCallback(capture(callbackCaptor))
+
+        callbackCaptor.value.onDataSaverChanged(true)
+
+        assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+    }
+
+    companion object {
+        private val SPEC = TileSpec.create(DataSaverTile.TILE_SPEC)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddableTest.kt
new file mode 100644
index 0000000..a2e3538
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddableTest.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.qs.pipeline.domain.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.DeviceControlsTile
+import com.android.systemui.statusbar.policy.DeviceControlsController
+import com.android.systemui.util.mockito.capture
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+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.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DeviceControlsAutoAddableTest : SysuiTestCase() {
+
+    @Mock private lateinit var deviceControlsController: DeviceControlsController
+    @Captor private lateinit var callbackCaptor: ArgumentCaptor<DeviceControlsController.Callback>
+
+    private lateinit var underTest: DeviceControlsAutoAddable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest = DeviceControlsAutoAddable(deviceControlsController)
+    }
+
+    @Test
+    fun strategyAlways() {
+        assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Always)
+    }
+
+    @Test
+    fun onControlsUpdate_position_addSignal() = runTest {
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        val position = 5
+        runCurrent()
+
+        verify(deviceControlsController).setCallback(capture(callbackCaptor))
+        callbackCaptor.value.onControlsUpdate(position)
+
+        assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC, position))
+        verify(deviceControlsController).removeCallback()
+    }
+
+    @Test
+    fun onControlsUpdate_nullPosition_noAddSignal() = runTest {
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        runCurrent()
+
+        verify(deviceControlsController).setCallback(capture(callbackCaptor))
+        callbackCaptor.value.onControlsUpdate(null)
+
+        assertThat(signal).isNull()
+        verify(deviceControlsController).removeCallback()
+    }
+
+    @Test
+    fun onRemoveControlsAutoTracker_removeSignal() = runTest {
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        runCurrent()
+
+        verify(deviceControlsController).setCallback(capture(callbackCaptor))
+        callbackCaptor.value.removeControlsAutoTracker()
+
+        assertThat(signal).isEqualTo(AutoAddSignal.Remove(SPEC))
+    }
+
+    @Test
+    fun flowCancelled_removeCallback() = runTest {
+        val job = launch { underTest.autoAddSignal(0).collect() }
+        runCurrent()
+
+        job.cancel()
+        runCurrent()
+        verify(deviceControlsController).removeCallback()
+    }
+
+    companion object {
+        private val SPEC = TileSpec.create(DeviceControlsTile.TILE_SPEC)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddableTest.kt
new file mode 100644
index 0000000..ee96b47
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddableTest.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.qs.pipeline.domain.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.HotspotTile
+import com.android.systemui.statusbar.policy.HotspotController
+import com.android.systemui.util.mockito.capture
+import com.google.common.truth.Truth.assertThat
+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.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class HotspotAutoAddableTest : SysuiTestCase() {
+
+    @Mock private lateinit var hotspotController: HotspotController
+    @Captor private lateinit var callbackCaptor: ArgumentCaptor<HotspotController.Callback>
+
+    private lateinit var underTest: HotspotAutoAddable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest = HotspotAutoAddable(hotspotController)
+    }
+
+    @Test
+    fun enabled_addSignal() = runTest {
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        runCurrent()
+
+        verify(hotspotController).addCallback(capture(callbackCaptor))
+        callbackCaptor.value.onHotspotChanged(/* enabled = */ true, /* numDevices = */ 5)
+
+        assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+    }
+
+    @Test
+    fun notEnabled_noSignal() = runTest {
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        runCurrent()
+
+        verify(hotspotController).addCallback(capture(callbackCaptor))
+        callbackCaptor.value.onHotspotChanged(/* enabled = */ false, /* numDevices = */ 0)
+
+        assertThat(signal).isNull()
+    }
+
+    companion object {
+        private val SPEC = TileSpec.create(HotspotTile.TILE_SPEC)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddableTest.kt
new file mode 100644
index 0000000..e03072a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddableTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.pipeline.domain.autoaddable
+
+import android.hardware.display.NightDisplayListener
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.NightDisplayTile
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestResult
+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.Answers
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NightDisplayAutoAddableTest : SysuiTestCase() {
+
+    @Mock(answer = Answers.RETURNS_SELF)
+    private lateinit var nightDisplayListenerBuilder: NightDisplayListenerModule.Builder
+    @Mock private lateinit var nightDisplayListener: NightDisplayListener
+    @Captor private lateinit var callbackCaptor: ArgumentCaptor<NightDisplayListener.Callback>
+
+    private lateinit var underTest: NightDisplayAutoAddable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(nightDisplayListenerBuilder.build()).thenReturn(nightDisplayListener)
+    }
+
+    @Test
+    fun disabled_strategyDisabled() =
+        testWithFeatureAvailability(enabled = false) {
+            assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Disabled)
+        }
+
+    @Test
+    fun enabled_strategyIfNotAdded() = testWithFeatureAvailability {
+        assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.IfNotAdded(SPEC))
+    }
+
+    @Test
+    fun listenerCreatedForCorrectUser() = testWithFeatureAvailability {
+        val user = 42
+        backgroundScope.launch { underTest.autoAddSignal(user).collect() }
+        runCurrent()
+
+        val inOrder = inOrder(nightDisplayListenerBuilder)
+        inOrder.verify(nightDisplayListenerBuilder).setUser(user)
+        inOrder.verify(nightDisplayListenerBuilder).build()
+    }
+
+    @Test
+    fun onCancelFlow_removeCallback() = testWithFeatureAvailability {
+        val job = launch { underTest.autoAddSignal(0).collect() }
+        runCurrent()
+        job.cancel()
+        runCurrent()
+        verify(nightDisplayListener).setCallback(null)
+    }
+
+    @Test
+    fun onActivatedTrue_addSignal() = testWithFeatureAvailability {
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        runCurrent()
+
+        verify(nightDisplayListener).setCallback(capture(callbackCaptor))
+        callbackCaptor.value.onActivated(true)
+
+        assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+    }
+
+    private fun testWithFeatureAvailability(
+        enabled: Boolean = true,
+        body: suspend TestScope.() -> TestResult
+    ) = runTest {
+        context.orCreateTestableResources.addOverride(
+            com.android.internal.R.bool.config_nightDisplayAvailable,
+            enabled
+        )
+        underTest = NightDisplayAutoAddable(nightDisplayListenerBuilder, context)
+        body()
+    }
+
+    companion object {
+        private val SPEC = TileSpec.create(NightDisplayTile.TILE_SPEC)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddableTest.kt
new file mode 100644
index 0000000..7b4a55e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddableTest.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.qs.pipeline.domain.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.ReduceBrightColorsController
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.ReduceBrightColorsTile
+import com.android.systemui.util.mockito.capture
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestResult
+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.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ReduceBrightColorsAutoAddableTest : SysuiTestCase() {
+
+    @Mock private lateinit var reduceBrightColorsController: ReduceBrightColorsController
+    @Captor
+    private lateinit var reduceBrightColorsListenerCaptor:
+        ArgumentCaptor<ReduceBrightColorsController.Listener>
+
+    private lateinit var underTest: ReduceBrightColorsAutoAddable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun notAvailable_strategyDisabled() =
+        testWithFeatureAvailability(available = false) {
+            assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Disabled)
+        }
+
+    @Test
+    fun available_strategyIfNotAdded() =
+        testWithFeatureAvailability(available = true) {
+            assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.IfNotAdded(SPEC))
+        }
+
+    @Test
+    fun activated_addSignal() = testWithFeatureAvailability {
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        runCurrent()
+
+        verify(reduceBrightColorsController).addCallback(capture(reduceBrightColorsListenerCaptor))
+
+        reduceBrightColorsListenerCaptor.value.onActivated(true)
+
+        assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+    }
+
+    @Test
+    fun notActivated_noSignal() = testWithFeatureAvailability {
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+        runCurrent()
+
+        verify(reduceBrightColorsController).addCallback(capture(reduceBrightColorsListenerCaptor))
+
+        reduceBrightColorsListenerCaptor.value.onActivated(false)
+
+        assertThat(signal).isNull()
+    }
+
+    private fun testWithFeatureAvailability(
+        available: Boolean = true,
+        body: suspend TestScope.() -> TestResult
+    ) = runTest {
+        underTest = ReduceBrightColorsAutoAddable(reduceBrightColorsController, available)
+        body()
+    }
+
+    companion object {
+        private val SPEC = TileSpec.create(ReduceBrightColorsTile.TILE_SPEC)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddableTest.kt
new file mode 100644
index 0000000..fb35a3a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddableTest.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.qs.pipeline.domain.autoaddable
+
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import android.testing.AndroidTestingRunner
+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.coroutines.collectValues
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.statusbar.policy.SafetyController
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+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
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class SafetyCenterAutoAddableTest : SysuiTestCase() {
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    @Mock private lateinit var safetyController: SafetyController
+    @Mock private lateinit var packageManager: PackageManager
+    @Captor
+    private lateinit var safetyControllerListenerCaptor: ArgumentCaptor<SafetyController.Listener>
+
+    private lateinit var underTest: SafetyCenterAutoAddable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        context.ensureTestableResources()
+
+        // Set these by default, will also test special cases
+        context.orCreateTestableResources.addOverride(
+            R.string.safety_quick_settings_tile_class,
+            SAFETY_TILE_CLASS_NAME
+        )
+        whenever(packageManager.permissionControllerPackageName)
+            .thenReturn(PERMISSION_CONTROLLER_PACKAGE_NAME)
+
+        underTest =
+            SafetyCenterAutoAddable(
+                safetyController,
+                packageManager,
+                context.resources,
+                testDispatcher,
+            )
+    }
+
+    @Test
+    fun strategyAlwaysTrack() =
+        testScope.runTest {
+            assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Always)
+        }
+
+    @Test
+    fun tileAlwaysAdded() =
+        testScope.runTest {
+            val signal by collectLastValue(underTest.autoAddSignal(0))
+
+            assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+        }
+
+    @Test
+    fun safetyCenterDisabled_removeSignal() =
+        testScope.runTest {
+            val signal by collectLastValue(underTest.autoAddSignal(0))
+            runCurrent()
+
+            verify(safetyController).addCallback(capture(safetyControllerListenerCaptor))
+            safetyControllerListenerCaptor.value.onSafetyCenterEnableChanged(false)
+
+            assertThat(signal).isEqualTo(AutoAddSignal.Remove(SPEC))
+        }
+
+    @Test
+    fun safetyCenterEnabled_newAddSignal() =
+        testScope.runTest {
+            val signals by collectValues(underTest.autoAddSignal(0))
+            runCurrent()
+
+            verify(safetyController).addCallback(capture(safetyControllerListenerCaptor))
+            safetyControllerListenerCaptor.value.onSafetyCenterEnableChanged(true)
+
+            assertThat(signals.size).isEqualTo(2)
+            assertThat(signals.last()).isEqualTo(AutoAddSignal.Add(SPEC))
+        }
+
+    @Test
+    fun flowCancelled_removeListener() =
+        testScope.runTest {
+            val job = launch { underTest.autoAddSignal(0).collect() }
+            runCurrent()
+
+            verify(safetyController).addCallback(capture(safetyControllerListenerCaptor))
+
+            job.cancel()
+            runCurrent()
+            verify(safetyController).removeCallback(safetyControllerListenerCaptor.value)
+        }
+
+    @Test
+    fun emptyClassName_noSignals() =
+        testScope.runTest {
+            context.orCreateTestableResources.addOverride(
+                R.string.safety_quick_settings_tile_class,
+                ""
+            )
+            val signal by collectLastValue(underTest.autoAddSignal(0))
+            runCurrent()
+
+            verify(safetyController, never()).addCallback(any())
+
+            assertThat(signal).isNull()
+        }
+
+    companion object {
+        private const val SAFETY_TILE_CLASS_NAME = "cls"
+        private const val PERMISSION_CONTROLLER_PACKAGE_NAME = "pkg"
+        private val SPEC =
+            TileSpec.create(
+                ComponentName(PERMISSION_CONTROLLER_PACKAGE_NAME, SAFETY_TILE_CLASS_NAME)
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddableTest.kt
new file mode 100644
index 0000000..6b250f4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddableTest.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.qs.pipeline.domain.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.QuickAccessWalletTile
+import com.android.systemui.statusbar.policy.WalletController
+import com.android.systemui.util.mockito.whenever
+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.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class WalletAutoAddableTest : SysuiTestCase() {
+
+    @Mock private lateinit var walletController: WalletController
+
+    private lateinit var underTest: WalletAutoAddable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest = WalletAutoAddable(walletController)
+    }
+
+    @Test
+    fun strategyIfNotAdded() {
+        assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.IfNotAdded(SPEC))
+    }
+
+    @Test
+    fun walletPositionNull_noSignal() = runTest {
+        whenever(walletController.getWalletPosition()).thenReturn(null)
+
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+
+        assertThat(signal).isNull()
+    }
+
+    @Test
+    fun walletPositionNumber_addedInThatPosition() = runTest {
+        val position = 4
+        whenever(walletController.getWalletPosition()).thenReturn(4)
+
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+
+        assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC, position))
+    }
+
+    companion object {
+        private val SPEC = TileSpec.create(QuickAccessWalletTile.TILE_SPEC)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddableTest.kt
new file mode 100644
index 0000000..e9f7c8ab
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddableTest.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.pipeline.domain.autoaddable
+
+import android.content.pm.UserInfo
+import android.content.pm.UserInfo.FLAG_FULL
+import android.content.pm.UserInfo.FLAG_MANAGED_PROFILE
+import android.content.pm.UserInfo.FLAG_PRIMARY
+import android.content.pm.UserInfo.FLAG_PROFILE
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.WorkModeTile
+import com.android.systemui.settings.FakeUserTracker
+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.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class WorkTileAutoAddableTest : SysuiTestCase() {
+
+    private lateinit var userTracker: FakeUserTracker
+
+    private lateinit var underTest: WorkTileAutoAddable
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        userTracker =
+            FakeUserTracker(
+                _userId = USER_INFO_0.id,
+                _userInfo = USER_INFO_0,
+                _userProfiles = listOf(USER_INFO_0)
+            )
+
+        underTest = WorkTileAutoAddable(userTracker)
+    }
+
+    @Test
+    fun changeInProfiles_hasManagedProfile_sendsAddSignal() = runTest {
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+
+        userTracker.set(listOf(USER_INFO_0, USER_INFO_WORK), selectedUserIndex = 0)
+
+        assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+    }
+
+    @Test
+    fun changeInProfiles_noManagedProfile_sendsRemoveSignal() = runTest {
+        userTracker.set(listOf(USER_INFO_0, USER_INFO_WORK), selectedUserIndex = 0)
+
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+
+        userTracker.set(listOf(USER_INFO_0), selectedUserIndex = 0)
+
+        assertThat(signal).isEqualTo(AutoAddSignal.Remove(SPEC))
+    }
+
+    @Test
+    fun startingWithManagedProfile_sendsAddSignal() = runTest {
+        userTracker.set(listOf(USER_INFO_0, USER_INFO_WORK), selectedUserIndex = 0)
+
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+
+        assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+    }
+
+    @Test
+    fun userChangeToUserWithProfile_noSignalForOriginalUser() = runTest {
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+
+        userTracker.set(listOf(USER_INFO_1, USER_INFO_WORK), selectedUserIndex = 0)
+
+        assertThat(signal).isNotEqualTo(AutoAddSignal.Add(SPEC))
+    }
+
+    @Test
+    fun userChangeToUserWithoutProfile_noSignalForOriginalUser() = runTest {
+        userTracker.set(listOf(USER_INFO_0, USER_INFO_WORK), selectedUserIndex = 0)
+        val signal by collectLastValue(underTest.autoAddSignal(0))
+
+        userTracker.set(listOf(USER_INFO_1), selectedUserIndex = 0)
+
+        assertThat(signal).isNotEqualTo(AutoAddSignal.Remove(SPEC))
+    }
+
+    @Test
+    fun strategyAlways() {
+        assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Always)
+    }
+
+    companion object {
+        private val SPEC = TileSpec.create(WorkModeTile.TILE_SPEC)
+        private val USER_INFO_0 = UserInfo(0, "", FLAG_PRIMARY or FLAG_FULL)
+        private val USER_INFO_1 = UserInfo(1, "", FLAG_FULL)
+        private val USER_INFO_WORK = UserInfo(10, "", FLAG_PROFILE or FLAG_MANAGED_PROFILE)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
new file mode 100644
index 0000000..f924b35
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.pipeline.domain.interactor
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.qs.pipeline.data.repository.FakeAutoAddRepository
+import com.android.systemui.qs.pipeline.domain.autoaddable.FakeAutoAddable
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+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.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class AutoAddInteractorTest : SysuiTestCase() {
+    private val testScope = TestScope()
+
+    private val autoAddRepository = FakeAutoAddRepository()
+
+    @Mock private lateinit var dumpManager: DumpManager
+    @Mock private lateinit var currentTilesInteractor: CurrentTilesInteractor
+    @Mock private lateinit var logger: QSPipelineLogger
+    private lateinit var underTest: AutoAddInteractor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(currentTilesInteractor.userId).thenReturn(MutableStateFlow(USER))
+    }
+
+    @Test
+    fun autoAddable_alwaysTrack_addSignal_tileAddedAndMarked() =
+        testScope.runTest {
+            val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.Always)
+            val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
+
+            underTest = createInteractor(setOf(fakeAutoAddable))
+
+            val position = 3
+            fakeAutoAddable.sendAddSignal(USER, position)
+            runCurrent()
+
+            verify(currentTilesInteractor).addTile(SPEC, position)
+            assertThat(autoAddedTiles).contains(SPEC)
+        }
+
+    @Test
+    fun autoAddable_alwaysTrack_addThenRemoveSignal_tileAddedAndRemoved() =
+        testScope.runTest {
+            val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.Always)
+            val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
+
+            underTest = createInteractor(setOf(fakeAutoAddable))
+
+            val position = 3
+            fakeAutoAddable.sendAddSignal(USER, position)
+            runCurrent()
+            fakeAutoAddable.sendRemoveSignal(USER)
+            runCurrent()
+
+            val inOrder = inOrder(currentTilesInteractor)
+            inOrder.verify(currentTilesInteractor).addTile(SPEC, position)
+            inOrder.verify(currentTilesInteractor).removeTiles(setOf(SPEC))
+            assertThat(autoAddedTiles).doesNotContain(SPEC)
+        }
+
+    @Test
+    fun autoAddable_alwaysTrack_addSignalWhenAddedPreviously_noop() =
+        testScope.runTest {
+            val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.Always)
+            autoAddRepository.markTileAdded(USER, SPEC)
+            runCurrent()
+
+            underTest = createInteractor(setOf(fakeAutoAddable))
+
+            val position = 3
+            fakeAutoAddable.sendAddSignal(USER, position)
+            runCurrent()
+
+            verify(currentTilesInteractor, never()).addTile(SPEC, position)
+        }
+
+    @Test
+    fun autoAddable_disabled_noInteractionsWithCurrentTilesInteractor() =
+        testScope.runTest {
+            val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.Disabled)
+            val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
+
+            underTest = createInteractor(setOf(fakeAutoAddable))
+
+            val position = 3
+            fakeAutoAddable.sendAddSignal(USER, position)
+            runCurrent()
+            fakeAutoAddable.sendRemoveSignal(USER)
+            runCurrent()
+
+            verify(currentTilesInteractor, never()).addTile(any(), anyInt())
+            verify(currentTilesInteractor, never()).removeTiles(any())
+            assertThat(autoAddedTiles).doesNotContain(SPEC)
+        }
+
+    @Test
+    fun autoAddable_trackIfNotAdded_removeSignal_noop() =
+        testScope.runTest {
+            val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.IfNotAdded(SPEC))
+            runCurrent()
+
+            underTest = createInteractor(setOf(fakeAutoAddable))
+
+            fakeAutoAddable.sendRemoveSignal(USER)
+            runCurrent()
+
+            verify(currentTilesInteractor, never()).addTile(any(), anyInt())
+            verify(currentTilesInteractor, never()).removeTiles(any())
+        }
+
+    @Test
+    fun autoAddable_trackIfNotAdded_addSignalWhenPreviouslyAdded_noop() =
+        testScope.runTest {
+            val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.IfNotAdded(SPEC))
+            autoAddRepository.markTileAdded(USER, SPEC)
+            runCurrent()
+
+            underTest = createInteractor(setOf(fakeAutoAddable))
+
+            fakeAutoAddable.sendAddSignal(USER)
+            runCurrent()
+
+            verify(currentTilesInteractor, never()).addTile(any(), anyInt())
+            verify(currentTilesInteractor, never()).removeTiles(any())
+        }
+
+    @Test
+    fun autoAddable_trackIfNotAdded_addSignal_addedAndMarked() =
+        testScope.runTest {
+            val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.IfNotAdded(SPEC))
+            val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
+
+            underTest = createInteractor(setOf(fakeAutoAddable))
+
+            val position = 3
+            fakeAutoAddable.sendAddSignal(USER, position)
+            runCurrent()
+
+            verify(currentTilesInteractor).addTile(SPEC, position)
+            assertThat(autoAddedTiles).contains(SPEC)
+        }
+
+    private fun createInteractor(autoAddables: Set<AutoAddable>): AutoAddInteractor {
+        return AutoAddInteractor(
+                autoAddables,
+                autoAddRepository,
+                dumpManager,
+                logger,
+                testScope.backgroundScope
+            )
+            .apply { init(currentTilesInteractor) }
+    }
+
+    companion object {
+        private val SPEC = TileSpec.create("spec")
+        private val USER = 10
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index e7ad489..30cea2d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -100,6 +100,7 @@
         MockitoAnnotations.initMocks(this)
 
         featureFlags.set(Flags.QS_PIPELINE_NEW_HOST, true)
+        featureFlags.set(Flags.QS_PIPELINE_AUTO_ADD, true)
 
         userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1))
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorImplTest.kt
index 45783ab..6556cfd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/PanelInteractorImplTest.kt
@@ -18,8 +18,7 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.phone.CentralSurfaces
-import java.util.Optional
+import com.android.systemui.shade.ShadeController
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -31,7 +30,7 @@
 @SmallTest
 class PanelInteractorImplTest : SysuiTestCase() {
 
-    @Mock private lateinit var centralSurfaces: CentralSurfaces
+    @Mock private lateinit var shadeController: ShadeController
 
     @Before
     fun setup() {
@@ -40,37 +39,28 @@
 
     @Test
     fun openPanels_callsCentralSurfaces() {
-        val underTest = PanelInteractorImpl(Optional.of(centralSurfaces))
+        val underTest = PanelInteractorImpl(shadeController)
 
         underTest.openPanels()
 
-        verify(centralSurfaces).postAnimateOpenPanels()
+        verify(shadeController).postAnimateExpandQs()
     }
 
     @Test
     fun collapsePanels_callsCentralSurfaces() {
-        val underTest = PanelInteractorImpl(Optional.of(centralSurfaces))
+        val underTest = PanelInteractorImpl(shadeController)
 
         underTest.collapsePanels()
 
-        verify(centralSurfaces).postAnimateCollapsePanels()
+        verify(shadeController).postAnimateCollapseShade()
     }
 
     @Test
     fun forceCollapsePanels_callsCentralSurfaces() {
-        val underTest = PanelInteractorImpl(Optional.of(centralSurfaces))
+        val underTest = PanelInteractorImpl(shadeController)
 
         underTest.forceCollapsePanels()
 
-        verify(centralSurfaces).postAnimateForceCollapsePanels()
-    }
-
-    @Test
-    fun whenOptionalEmpty_doesnThrow() {
-        val underTest = PanelInteractorImpl(Optional.empty())
-
-        underTest.openPanels()
-        underTest.collapsePanels()
-        underTest.forceCollapsePanels()
+        verify(shadeController).postAnimateForceCollapseShade()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
index f55d262..180fed9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
@@ -155,6 +155,28 @@
         inOrder.verify(d).stop();
     }
 
+    @Test
+    public void testAnimatorCallbackRemovedOnOldDrawable() {
+        ImageView iv = new ImageView(mContext);
+        AnimatedVectorDrawable d1 = mock(AnimatedVectorDrawable.class);
+        when(d1.getConstantState()).thenReturn(fakeConstantState(d1));
+        AnimatedVectorDrawable d2 = mock(AnimatedVectorDrawable.class);
+        when(d2.getConstantState()).thenReturn(fakeConstantState(d2));
+        State s = new State();
+        s.isTransient = true;
+
+        // When set Animatable2 d1
+        s.icon = new QSTileImpl.DrawableIcon(d1);
+        mIconView.updateIcon(iv, s, true);
+
+        // And then set Animatable2 d2
+        s.icon = new QSTileImpl.DrawableIcon(d2);
+        mIconView.updateIcon(iv, s, true);
+
+        // Then d1 has its callback cleared
+        verify(d1).clearAnimationCallbacks();
+    }
+
     private static Drawable.ConstantState fakeConstantState(Drawable otherDrawable) {
         return new Drawable.ConstantState() {
             @Override
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 962b537..22b1c7b 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
@@ -204,6 +204,19 @@
     }
 
     @Test
+    public void testLongClick_falsing() {
+        mFalsingManager.setFalseLongTap(true);
+        mTile.longClick(null /* view */);
+        mTestableLooper.processAllMessages();
+        assertThat(mTile.mLongClicked).isFalse();
+
+        mFalsingManager.setFalseLongTap(false);
+        mTile.longClick(null /* view */);
+        mTestableLooper.processAllMessages();
+        assertThat(mTile.mLongClicked).isTrue();
+    }
+
+    @Test
     public void testSecondaryClick_Metrics() {
         mTile.secondaryClick(null /* view */);
         verify(mMetricsLogger).write(argThat(new TileLogMatcher(ACTION_QS_SECONDARY_CLICK)));
@@ -518,6 +531,7 @@
     }
     private static class TileImpl extends QSTileImpl<QSTile.BooleanState> {
         boolean mClicked;
+        boolean mLongClicked;
         int mRefreshes = 0;
 
         protected TileImpl(
@@ -551,6 +565,11 @@
         }
 
         @Override
+        protected void handleLongClick(@Nullable View view) {
+            mLongClicked = true;
+        }
+
+        @Override
         protected void handleUpdateState(BooleanState state, Object arg) {
             mRefreshes++;
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
index 28aeba4..3c66772 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
@@ -22,6 +22,7 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.text.TextUtils
+import android.view.ContextThemeWrapper
 import android.view.View
 import android.view.accessibility.AccessibilityNodeInfo
 import android.widget.TextView
@@ -386,7 +387,11 @@
         context: Context,
         icon: QSIconView,
         collapsed: Boolean
-    ) : QSTileViewImpl(context, icon, collapsed) {
+    ) : QSTileViewImpl(
+            ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings),
+            icon,
+            collapsed
+    ) {
         fun changeState(state: QSTile.State) {
             handleStateChanged(state)
         }
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 f0e4e3a..77a4436 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
@@ -25,6 +25,7 @@
 import android.provider.Settings.Global.ZEN_MODE_OFF
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import android.view.ContextThemeWrapper
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.MetricsLogger
@@ -110,7 +111,9 @@
 
         whenever(qsHost.userId).thenReturn(DEFAULT_USER)
 
-        val wrappedContext = object : ContextWrapper(context) {
+        val wrappedContext = object : ContextWrapper(
+                ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
+        ) {
             override fun getSharedPreferences(file: File?, mode: Int): SharedPreferences {
                 return sharedPreferences
             }
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
index ddbfca5..cbc3553 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
@@ -33,6 +33,8 @@
 import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -44,8 +46,11 @@
 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.anyBoolean
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
@@ -61,6 +66,8 @@
     @Mock private lateinit var qsLogger: QSLogger
     @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
     @Mock private lateinit var uiEventLogger: QsEventLogger
+    @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
 
     private lateinit var testableLooper: TestableLooper
     private lateinit var systemClock: FakeSystemClock
@@ -69,6 +76,8 @@
 
     val featureFlags = FakeFeatureFlags()
 
+    @Captor private lateinit var argumentCaptor: ArgumentCaptor<Runnable>
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -88,11 +97,13 @@
                 statusBarStateController,
                 activityStarter,
                 qsLogger,
+                keyguardStateController,
                 dialogLaunchAnimator,
                 FakeSettings(),
                 FakeSettings(),
                 FakeSystemClock(),
                 featureFlags,
+                userTracker,
                 backgroundDelayableExecutor,
             )
         fontScalingTile.initialize()
@@ -124,15 +135,45 @@
     }
 
     @Test
-    fun clickTile_showDialog() {
+    fun clickTile_screenUnlocked_showDialogAnimationFromView() {
+        `when`(keyguardStateController.isShowing).thenReturn(false)
         val view = View(context)
         fontScalingTile.click(view)
         testableLooper.processAllMessages()
 
+        verify(activityStarter)
+            .executeRunnableDismissingKeyguard(
+                argumentCaptor.capture(),
+                eq(null),
+                eq(true),
+                eq(true),
+                eq(false)
+            )
+        argumentCaptor.value.run()
         verify(dialogLaunchAnimator).showFromView(any(), eq(view), nullable(), anyBoolean())
     }
 
     @Test
+    fun clickTile_onLockScreen_neverShowDialogAnimationFromView() {
+        `when`(keyguardStateController.isShowing).thenReturn(true)
+        val view = View(context)
+        fontScalingTile.click(view)
+        testableLooper.processAllMessages()
+
+        verify(activityStarter)
+            .executeRunnableDismissingKeyguard(
+                argumentCaptor.capture(),
+                eq(null),
+                eq(true),
+                eq(true),
+                eq(false)
+            )
+        argumentCaptor.value.run()
+        verify(dialogLaunchAnimator, never())
+            .showFromView(any(), eq(view), nullable(), anyBoolean())
+    }
+
+    @Test
     fun getLongClickIntent_getExpectedIntent() {
         val intent: Intent? = fontScalingTile.getLongClickIntent()
 
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 6614392..5b30687 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
@@ -968,6 +968,7 @@
         assertThat(mInternetDialogController.mSubIdTelephonyManagerMap.get(SUB_ID)).isEqualTo(
                 mTelephonyManager);
         assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.get(SUB_ID)).isNotNull();
+        assertThat(mInternetDialogController.mCallback).isNotNull();
 
         mInternetDialogController.onStop();
 
@@ -980,6 +981,7 @@
         verify(mAccessPointController).removeAccessPointCallback(mInternetDialogController);
         verify(mConnectivityManager).unregisterNetworkCallback(
                 any(ConnectivityManager.NetworkCallback.class));
+        assertThat(mInternetDialogController.mCallback).isNull();
     }
 
     private String getResourcesString(String name) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index 105387d..c85c8ba 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -27,7 +27,6 @@
 import com.android.systemui.scene.shared.model.SceneModel
 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.Test
@@ -39,8 +38,8 @@
 @RunWith(JUnit4::class)
 class QuickSettingsSceneViewModelTest : SysuiTestCase() {
 
-    private val testScope = TestScope()
-    private val utils = SceneTestUtils(this, testScope)
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
     private val sceneInteractor = utils.sceneInteractor()
     private val authenticationInteractor =
         utils.authenticationInteractor(
@@ -70,8 +69,10 @@
     fun onContentClicked_deviceUnlocked_switchesToGone() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.unlockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(true)
             runCurrent()
 
             underTest.onContentClicked()
@@ -83,8 +84,10 @@
     fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(false)
             runCurrent()
 
             underTest.onContentClicked()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
index 8744aa3..59b5953 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
@@ -16,9 +16,11 @@
 
 package com.android.systemui.screenrecord;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
@@ -32,6 +34,7 @@
 import android.os.Binder;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.testing.AndroidTestingRunner;
 
 import androidx.test.filters.SmallTest;
@@ -41,7 +44,9 @@
 import com.android.systemui.media.MediaProjectionCaptureTarget;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.settings.UserContextProvider;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -77,20 +82,38 @@
     private UserContextProvider mUserContextTracker;
     @Captor
     private ArgumentCaptor<Runnable> mRunnableCaptor;
-    private KeyguardDismissUtil mKeyguardDismissUtil = new KeyguardDismissUtil() {
-        public void executeWhenUnlocked(ActivityStarter.OnDismissAction action,
-                boolean requiresShadeOpen) {
-            action.onDismiss();
-        }
-    };
+    @Mock
+    private KeyguardStateController mKeyguardStateController;
+    @Mock
+    private SysuiStatusBarStateController mStatusBarStateController;
+    @Mock
+    private ActivityStarter mActivityStarter;
+
+    private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
+
+    private KeyguardDismissUtil mKeyguardDismissUtil = new KeyguardDismissUtil(
+            mKeyguardStateController, mStatusBarStateController, mActivityStarter);
 
     private RecordingService mRecordingService;
 
+    private class RecordingServiceTestable extends RecordingService {
+        RecordingServiceTestable(
+                RecordingController controller, Executor executor,
+                Handler handler, UiEventLogger uiEventLogger,
+                NotificationManager notificationManager,
+                UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil) {
+            super(controller, executor, handler,
+                    uiEventLogger, notificationManager, userContextTracker, keyguardDismissUtil);
+            attachBaseContext(mContext);
+        }
+    }
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mRecordingService = Mockito.spy(new RecordingService(mController, mExecutor, mHandler,
-                mUiEventLogger, mNotificationManager, mUserContextTracker, mKeyguardDismissUtil));
+        mRecordingService = Mockito.spy(new RecordingServiceTestable(mController, mExecutor,
+                mHandler, mUiEventLogger, mNotificationManager,
+                mUserContextTracker, mKeyguardDismissUtil));
 
         // Return actual context info
         doReturn(mContext).when(mRecordingService).getApplicationContext();
@@ -160,8 +183,7 @@
         Intent startIntent = RecordingService.getStartIntent(mContext, 0, 0, false, null);
         mRecordingService.onStartCommand(startIntent, 0, 0);
 
-        // Then the state is set to not recording
-        verify(mController).updateState(false);
+        assertUpdateState(false);
     }
 
     @Test
@@ -179,7 +201,7 @@
 
         mRecordingService.onStopped();
 
-        verify(mController).updateState(false);
+        assertUpdateState(false);
     }
 
     @Test
@@ -235,8 +257,21 @@
         verify(mExecutor).execute(mRunnableCaptor.capture());
         mRunnableCaptor.getValue().run();
 
-        // Then the state is set to not recording and we cancel the notification
-        verify(mController).updateState(false);
+        assertUpdateState(false);
         verify(mNotificationManager).cancelAsUser(any(), anyInt(), any());
     }
+
+    private void assertUpdateState(boolean state) {
+        // Then the state is set to not recording, and we cancel the notification
+        // non SYSTEM user doesn't have the reference to the correct controller,
+        // so a broadcast is sent in case of non SYSTEM user.
+        if (UserHandle.USER_SYSTEM == mContext.getUserId()) {
+            verify(mController).updateState(state);
+        } else {
+            ArgumentCaptor<Intent> argumentCaptor = ArgumentCaptor.forClass(Intent.class);
+            verify(mRecordingService).sendBroadcast(argumentCaptor.capture(), eq(PERMISSION_SELF));
+            assertEquals(RecordingController.INTENT_UPDATE_STATE,
+                    argumentCaptor.getValue().getAction());
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java
index 67b1099..d8897e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java
@@ -16,14 +16,17 @@
 
 package com.android.systemui.screenshot.appclips;
 
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
+
 import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.admin.DevicePolicyManager;
@@ -31,8 +34,6 @@
 import android.content.Intent;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.os.UserHandle;
-import android.os.UserManager;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -46,7 +47,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.util.Optional;
@@ -63,7 +63,6 @@
     @Mock private Optional<Bubbles> mOptionalBubbles;
     @Mock private Bubbles mBubbles;
     @Mock private DevicePolicyManager mDevicePolicyManager;
-    @Mock private UserManager mUserManager;
 
     private AppClipsService mAppClipsService;
 
@@ -81,51 +80,84 @@
     }
 
     @Test
+    public void flagOff_internal_shouldReturnFailed() throws RemoteException {
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(false);
+
+        assertThat(getInterfaceWithRealContext()
+                .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID))
+                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+    }
+
+    @Test
     public void emptyBubbles_shouldReturnFalse() throws RemoteException {
-        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
-        when(mOptionalBubbles.isEmpty()).thenReturn(true);
+        mockForEmptyBubbles();
 
         assertThat(getInterfaceWithRealContext()
                 .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
     }
 
     @Test
+    public void emptyBubbles_internal_shouldReturnFailed() throws RemoteException {
+        mockForEmptyBubbles();
+
+        assertThat(getInterfaceWithRealContext()
+                .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID))
+                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+    }
+
+    @Test
     public void taskIdNotAppBubble_shouldReturnFalse() throws RemoteException {
-        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
-        when(mOptionalBubbles.isEmpty()).thenReturn(false);
-        when(mOptionalBubbles.get()).thenReturn(mBubbles);
-        when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(false);
+        mockForTaskIdNotAppBubble();
 
         assertThat(getInterfaceWithRealContext()
                 .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
     }
 
     @Test
+    public void taskIdNotAppBubble_internal_shouldReturnWindowUnsupported() throws RemoteException {
+        mockForTaskIdNotAppBubble();
+
+        assertThat(getInterfaceWithRealContext()
+                .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID))
+                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED);
+    }
+
+    @Test
     public void dpmScreenshotBlocked_shouldReturnFalse() throws RemoteException {
-        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
-        when(mOptionalBubbles.isEmpty()).thenReturn(false);
-        when(mOptionalBubbles.get()).thenReturn(mBubbles);
-        when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
-        when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true);
+        mockForScreenshotBlocked();
 
         assertThat(getInterfaceWithRealContext()
                 .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
     }
 
     @Test
+    public void dpmScreenshotBlocked_internal_shouldReturnBlockedByAdmin() throws RemoteException {
+        mockForScreenshotBlocked();
+
+        assertThat(getInterfaceWithRealContext()
+                .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID))
+                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN);
+    }
+
+    @Test
     public void configComponentNameNotValid_shouldReturnFalse() throws RemoteException {
-        when(mMockContext.getString(anyInt())).thenReturn(EMPTY);
-        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
-        when(mOptionalBubbles.isEmpty()).thenReturn(false);
-        when(mOptionalBubbles.get()).thenReturn(mBubbles);
-        when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
-        when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false);
+        mockForInvalidConfigComponentName();
 
         assertThat(getInterfaceWithMockContext()
                 .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
     }
 
     @Test
+    public void configComponentNameNotValid_internal_shouldReturnFailed() throws RemoteException {
+        mockForInvalidConfigComponentName();
+
+        assertThat(getInterfaceWithMockContext()
+                .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID))
+                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+    }
+
+
+    @Test
     public void allPrerequisitesSatisfy_shouldReturnTrue() throws RemoteException {
         mockToSatisfyAllPrerequisites();
 
@@ -134,28 +166,44 @@
     }
 
     @Test
-    public void isManagedProfile_shouldUseProxyConnection() throws RemoteException {
-        when(mUserManager.isManagedProfile()).thenReturn(true);
-        when(mUserManager.getMainUser()).thenReturn(UserHandle.SYSTEM);
-        IAppClipsService service = getInterfaceWithRealContext();
-        mAppClipsService.mProxyConnectorToMainProfile =
-                Mockito.spy(mAppClipsService.mProxyConnectorToMainProfile);
+    public void allPrerequisitesSatisfy_internal_shouldReturnSuccess() throws RemoteException {
+        mockToSatisfyAllPrerequisites();
 
-        service.canLaunchCaptureContentActivityForNote(FAKE_TASK_ID);
-
-        verify(mAppClipsService.mProxyConnectorToMainProfile).postForResult(any());
+        assertThat(getInterfaceWithRealContext()
+                .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID))
+                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
     }
 
-    @Test
-    public void isManagedProfile_noMainUser_shouldReturnFalse() {
-        when(mUserManager.isManagedProfile()).thenReturn(true);
-        when(mUserManager.getMainUser()).thenReturn(null);
-
-        getInterfaceWithRealContext();
-
-        assertThat(mAppClipsService.mProxyConnectorToMainProfile).isNull();
+    private void mockForEmptyBubbles() {
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+        when(mOptionalBubbles.isEmpty()).thenReturn(true);
     }
 
+    private void mockForTaskIdNotAppBubble() {
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+        when(mOptionalBubbles.isEmpty()).thenReturn(false);
+        when(mOptionalBubbles.get()).thenReturn(mBubbles);
+        when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(false);
+    }
+
+    private void mockForScreenshotBlocked() {
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+        when(mOptionalBubbles.isEmpty()).thenReturn(false);
+        when(mOptionalBubbles.get()).thenReturn(mBubbles);
+        when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
+        when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true);
+    }
+
+    private void mockForInvalidConfigComponentName() {
+        when(mMockContext.getString(anyInt())).thenReturn(EMPTY);
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+        when(mOptionalBubbles.isEmpty()).thenReturn(false);
+        when(mOptionalBubbles.get()).thenReturn(mBubbles);
+        when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
+        when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false);
+    }
+
+
     private void mockToSatisfyAllPrerequisites() {
         when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
         when(mOptionalBubbles.isEmpty()).thenReturn(false);
@@ -166,13 +214,13 @@
 
     private IAppClipsService getInterfaceWithRealContext() {
         mAppClipsService = new AppClipsService(getContext(), mFeatureFlags,
-                mOptionalBubbles, mDevicePolicyManager, mUserManager);
+                mOptionalBubbles, mDevicePolicyManager);
         return getInterfaceFromService(mAppClipsService);
     }
 
     private IAppClipsService getInterfaceWithMockContext() {
         mAppClipsService = new AppClipsService(mMockContext, mFeatureFlags,
-                mOptionalBubbles, mDevicePolicyManager, mUserManager);
+                mOptionalBubbles, mDevicePolicyManager);
         return getInterfaceFromService(mAppClipsService);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
index e9007ff..7fad972 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
@@ -24,22 +24,19 @@
 import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
 import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE;
 
-import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
+import static com.android.internal.infra.AndroidFuture.completedFuture;
 import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED;
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI;
-import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_USE_WP_USER;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assume.assumeFalse;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.Activity;
-import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -52,20 +49,22 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.UserHandle;
-import android.os.UserManager;
 import android.testing.AndroidTestingRunner;
 
 import androidx.test.rule.ActivityTestRule;
 import androidx.test.runner.intercepting.SingleActivityFactory;
 
+import com.android.internal.infra.ServiceConnector;
 import com.android.internal.logging.UiEventLogger;
+import com.android.internal.statusbar.IAppClipsService;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.notetask.NoteTaskController;
-import com.android.systemui.settings.UserTracker;
-import com.android.wm.shell.bubbles.Bubbles;
+
+import com.google.common.util.concurrent.MoreExecutors;
 
 import org.junit.After;
 import org.junit.Before;
@@ -75,8 +74,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.List;
-import java.util.Optional;
+import java.util.concurrent.Executor;
 
 @RunWith(AndroidTestingRunner.class)
 public final class AppClipsTrampolineActivityTest extends SysuiTestCase {
@@ -86,25 +84,19 @@
     private static final int TEST_UID = 42;
     private static final String TEST_CALLING_PACKAGE = "test-calling-package";
 
-    @Mock
-    private DevicePolicyManager mDevicePolicyManager;
-    @Mock
-    private FeatureFlags mFeatureFlags;
-    @Mock
-    private Optional<Bubbles> mOptionalBubbles;
-    @Mock
-    private Bubbles mBubbles;
+    @Mock private ServiceConnector<IAppClipsService> mServiceConnector;
     @Mock
     private NoteTaskController mNoteTaskController;
     @Mock
     private PackageManager mPackageManager;
     @Mock
-    private UserTracker mUserTracker;
-    @Mock
     private UiEventLogger mUiEventLogger;
     @Mock
-    private UserManager mUserManager;
-
+    private BroadcastSender mBroadcastSender;
+    @Background
+    private Executor mBgExecutor;
+    @Main
+    private Executor mMainExecutor;
     @Main
     private Handler mMainHandler;
 
@@ -114,9 +106,9 @@
             new SingleActivityFactory<>(AppClipsTrampolineActivityTestable.class) {
                 @Override
                 protected AppClipsTrampolineActivityTestable create(Intent unUsed) {
-                    return new AppClipsTrampolineActivityTestable(mDevicePolicyManager,
-                            mFeatureFlags, mOptionalBubbles, mNoteTaskController, mPackageManager,
-                            mUserTracker, mUiEventLogger, mUserManager, mMainHandler);
+                    return new AppClipsTrampolineActivityTestable(mServiceConnector,
+                            mNoteTaskController, mPackageManager, mUiEventLogger, mBroadcastSender,
+                            mBgExecutor, mMainExecutor, mMainHandler);
                 }
             };
 
@@ -133,6 +125,8 @@
             mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH));
 
         MockitoAnnotations.initMocks(this);
+        mBgExecutor = MoreExecutors.directExecutor();
+        mMainExecutor = MoreExecutors.directExecutor();
         mMainHandler = mContext.getMainThreadHandler();
 
         mActivityIntent = new Intent(mContext, AppClipsTrampolineActivityTestable.class);
@@ -169,19 +163,9 @@
     }
 
     @Test
-    public void flagOff_shouldFinishWithResultCancel() {
-        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(false);
-
-        mActivityRule.launchActivity(mActivityIntent);
-
-        assertThat(mActivityRule.getActivityResult().getResultCode())
-                .isEqualTo(Activity.RESULT_CANCELED);
-    }
-
-    @Test
-    public void bubblesEmpty_shouldFinishWithFailed() {
-        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
-        when(mOptionalBubbles.isEmpty()).thenReturn(true);
+    public void queryService_returnedFailed_shouldFinishWithFailed() {
+        when(mServiceConnector.postForResult(any()))
+                .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_FAILED));
 
         mActivityRule.launchActivity(mActivityIntent);
 
@@ -189,14 +173,13 @@
         assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
         assertThat(getStatusCodeExtra(actualResult.getResultData()))
                 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+        assertThat(mActivityRule.getActivity().isFinishing()).isTrue();
     }
 
     @Test
-    public void taskIdNotAppBubble_shouldFinishWithWindowModeUnsupported() {
-        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
-        when(mOptionalBubbles.isEmpty()).thenReturn(false);
-        when(mOptionalBubbles.get()).thenReturn(mBubbles);
-        when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(false);
+    public void queryService_returnedWindowModeUnsupported_shouldFinishWithWindowModeUnsupported() {
+        when(mServiceConnector.postForResult(any()))
+                .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED));
 
         mActivityRule.launchActivity(mActivityIntent);
 
@@ -204,15 +187,13 @@
         assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
         assertThat(getStatusCodeExtra(actualResult.getResultData()))
                 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED);
+        assertThat(mActivityRule.getActivity().isFinishing()).isTrue();
     }
 
     @Test
-    public void dpmScreenshotBlocked_shouldFinishWithBlockedByAdmin() {
-        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
-        when(mOptionalBubbles.isEmpty()).thenReturn(false);
-        when(mOptionalBubbles.get()).thenReturn(mBubbles);
-        when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true);
-        when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true);
+    public void queryService_returnedScreenshotBlocked_shouldFinishWithBlockedByAdmin() {
+        when(mServiceConnector.postForResult(any()))
+                .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN));
 
         mActivityRule.launchActivity(mActivityIntent);
 
@@ -220,6 +201,7 @@
         assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
         assertThat(getStatusCodeExtra(actualResult.getResultData()))
                 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN);
+        assertThat(mActivityRule.getActivity().isFinishing()).isTrue();
     }
 
     @Test
@@ -240,6 +222,7 @@
         assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
         assertThat(getStatusCodeExtra(actualResult.getResultData()))
                 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED);
+        assertThat(mActivityRule.getActivity().isFinishing()).isTrue();
     }
 
     @Test
@@ -261,6 +244,7 @@
         assertThat(getStatusCodeExtra(actualResult.getResultData()))
                 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
         assertThat(actualResult.getResultData().getData()).isEqualTo(TEST_URI);
+        assertThat(mActivityRule.getActivity().isFinishing()).isTrue();
     }
 
     @Test
@@ -274,48 +258,9 @@
         verify(mUiEventLogger).log(SCREENSHOT_FOR_NOTE_TRIGGERED, TEST_UID, TEST_CALLING_PACKAGE);
     }
 
-    @Test
-    public void startAppClipsActivity_throughWPUser_shouldStartMainUserActivity()
-            throws NameNotFoundException {
-        when(mUserManager.isManagedProfile()).thenReturn(true);
-        when(mUserManager.getMainUser()).thenReturn(UserHandle.SYSTEM);
-        mockToSatisfyAllPrerequisites();
-
-        AppClipsTrampolineActivityTestable activity = mActivityRule.launchActivity(mActivityIntent);
-        waitForIdleSync();
-
-        Intent actualIntent = activity.mStartedIntent;
-        assertThat(actualIntent.getComponent()).isEqualTo(
-                new ComponentName(mContext, AppClipsTrampolineActivity.class));
-        assertThat(actualIntent.getFlags()).isEqualTo(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
-        assertThat(actualIntent.getBooleanExtra(EXTRA_USE_WP_USER, false)).isTrue();
-        assertThat(activity.mStartingUser).isEqualTo(UserHandle.SYSTEM);
-    }
-
-    @Test
-    public void startAppClipsActivity_throughWPUser_noMainUser_shouldFinishWithFailed()
-            throws NameNotFoundException {
-        when(mUserManager.isManagedProfile()).thenReturn(true);
-        when(mUserManager.getMainUser()).thenReturn(null);
-
-        mockToSatisfyAllPrerequisites();
-
-        mActivityRule.launchActivity(mActivityIntent);
-        waitForIdleSync();
-
-        ActivityResult actualResult = mActivityRule.getActivityResult();
-        assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
-        assertThat(getStatusCodeExtra(actualResult.getResultData()))
-                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
-    }
-
     private void mockToSatisfyAllPrerequisites() throws NameNotFoundException {
-        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
-        when(mOptionalBubbles.isEmpty()).thenReturn(false);
-        when(mOptionalBubbles.get()).thenReturn(mBubbles);
-        when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true);
-        when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false);
-        when(mUserTracker.getUserProfiles()).thenReturn(List.of());
+        when(mServiceConnector.postForResult(any()))
+                .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_SUCCESS));
 
         ApplicationInfo testApplicationInfo = new ApplicationInfo();
         testApplicationInfo.uid = TEST_UID;
@@ -330,17 +275,14 @@
         Intent mStartedIntent;
         UserHandle mStartingUser;
 
-        public AppClipsTrampolineActivityTestable(DevicePolicyManager devicePolicyManager,
-                FeatureFlags flags,
-                Optional<Bubbles> optionalBubbles,
-                NoteTaskController noteTaskController,
-                PackageManager packageManager,
-                UserTracker userTracker,
-                UiEventLogger uiEventLogger,
-                UserManager userManager,
+        public AppClipsTrampolineActivityTestable(
+                ServiceConnector<IAppClipsService> serviceServiceConnector,
+                NoteTaskController noteTaskController, PackageManager packageManager,
+                UiEventLogger uiEventLogger, BroadcastSender broadcastSender,
+                @Background Executor bgExecutor, @Main Executor mainExecutor,
                 @Main Handler mainHandler) {
-            super(devicePolicyManager, flags, optionalBubbles, noteTaskController, packageManager,
-                    userTracker, uiEventLogger, userManager, mainHandler);
+            super(serviceServiceConnector, noteTaskController, packageManager, uiEventLogger,
+                    broadcastSender, bgExecutor, mainExecutor, mainHandler);
         }
 
         @Override
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 7646193..16751c9 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
@@ -16,7 +16,6 @@
 
 package com.android.systemui.settings.brightness
 
-import android.content.Intent
 import android.graphics.Rect
 import android.os.Handler
 import android.testing.AndroidTestingRunner
@@ -25,9 +24,9 @@
 import android.view.ViewGroup
 import androidx.test.filters.SmallTest
 import androidx.test.rule.ActivityTestRule
-import androidx.test.runner.intercepting.SingleActivityFactory
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.activity.SingleActivityFactory
 import com.android.systemui.settings.FakeDisplayTracker
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.mockito.any
@@ -59,19 +58,17 @@
     @JvmField
     var activityRule =
         ActivityTestRule(
-            object : SingleActivityFactory<TestDialog>(TestDialog::class.java) {
-                override fun create(intent: Intent?): TestDialog {
-                    return TestDialog(
-                        userTracker,
-                        displayTracker,
-                        brightnessSliderControllerFactory,
-                        mainExecutor,
-                        backgroundHandler
-                    )
-                }
+            /* activityFactory= */ SingleActivityFactory {
+                TestDialog(
+                    userTracker,
+                    displayTracker,
+                    brightnessSliderControllerFactory,
+                    mainExecutor,
+                    backgroundHandler
+                )
             },
-            false,
-            false
+            /* initialTouchMode= */ false,
+            /* launchActivity= */ false,
         )
 
     @Before
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 3706859..0a1eca6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
@@ -62,9 +62,9 @@
             assertThat(getConstraint(R.id.clock).layout.startToStart).isEqualTo(R.id.begin_guide)
             assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f)
 
-            assertThat(getConstraint(R.id.batteryRemainingIcon).layout.endToEnd)
+            assertThat(getConstraint(R.id.shade_header_system_icons).layout.endToEnd)
                 .isEqualTo(R.id.end_guide)
-            assertThat(getConstraint(R.id.batteryRemainingIcon).layout.horizontalBias)
+            assertThat(getConstraint(R.id.shade_header_system_icons).layout.horizontalBias)
                 .isEqualTo(1f)
 
             assertThat(getConstraint(R.id.privacy_container).layout.endToEnd)
@@ -95,9 +95,9 @@
             assertThat(getConstraint(R.id.date).layout.startToStart).isEqualTo(PARENT_ID)
             assertThat(getConstraint(R.id.date).layout.horizontalBias).isEqualTo(0.5f)
 
-            assertThat(getConstraint(R.id.batteryRemainingIcon).layout.endToEnd)
+            assertThat(getConstraint(R.id.shade_header_system_icons).layout.endToEnd)
                 .isEqualTo(PARENT_ID)
-            assertThat(getConstraint(R.id.batteryRemainingIcon).layout.horizontalBias)
+            assertThat(getConstraint(R.id.shade_header_system_icons).layout.horizontalBias)
                 .isEqualTo(0.5f)
 
             assertThat(getConstraint(R.id.privacy_container).layout.endToEnd)
@@ -133,18 +133,15 @@
         changes()
 
         with(qqsConstraint) {
-            assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
-            assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
+            assertThat(systemIconsAlphaConstraint).isEqualTo(1f)
         }
 
         with(qsConstraint) {
-            assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
-            assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
+            assertThat(systemIconsAlphaConstraint).isEqualTo(1f)
         }
 
         with(largeScreenConstraint) {
-            assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
-            assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
+            assertThat(systemIconsAlphaConstraint).isEqualTo(1f)
         }
     }
 
@@ -155,18 +152,15 @@
         changes()
 
         with(qqsConstraint) {
-            assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(0f)
-            assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(0f)
+            assertThat(systemIconsAlphaConstraint).isEqualTo(0f)
         }
 
         with(qsConstraint) {
-            assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
-            assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
+            assertThat(systemIconsAlphaConstraint).isEqualTo(1f)
         }
 
         with(largeScreenConstraint) {
-            assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
-            assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
+            assertThat(systemIconsAlphaConstraint).isEqualTo(1f)
         }
     }
 
@@ -181,12 +175,13 @@
 
         with(qqsConstraint) {
             // In this case, the date is constrained on the end by a Barrier determined by either
-            // privacy or statusIcons
+            // privacy or clickableIcons
             assertThat(getConstraint(R.id.date).layout.endToStart).isEqualTo(R.id.barrier)
-            assertThat(getConstraint(R.id.statusIcons).layout.startToEnd).isEqualTo(R.id.date)
+            assertThat(getConstraint(R.id.shade_header_system_icons).layout.startToEnd)
+                .isEqualTo(R.id.date)
             assertThat(getConstraint(R.id.privacy_container).layout.startToEnd).isEqualTo(R.id.date)
             assertThat(getConstraint(R.id.barrier).layout.mReferenceIds).asList().containsExactly(
-                R.id.statusIcons,
+                R.id.shade_header_system_icons,
                 R.id.privacy_container
             )
             assertThat(getConstraint(R.id.barrier).layout.mBarrierDirection).isEqualTo(START)
@@ -272,7 +267,7 @@
             assertThat(getConstraint(R.id.center_left).layout.guideBegin).isEqualTo(offsetFromEdge)
             assertThat(getConstraint(R.id.center_right).layout.guideEnd).isEqualTo(offsetFromEdge)
             assertThat(getConstraint(R.id.date).layout.endToStart).isEqualTo(R.id.center_left)
-            assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
+            assertThat(getConstraint(R.id.shade_header_system_icons).layout.startToEnd)
                 .isEqualTo(R.id.center_right)
             assertThat(getConstraint(R.id.privacy_container).layout.startToEnd)
                 .isEqualTo(R.id.center_right)
@@ -285,9 +280,9 @@
             assertThat(getConstraint(R.id.date).layout.endToStart).isNotEqualTo(R.id.center_left)
             assertThat(getConstraint(R.id.date).layout.endToStart).isNotEqualTo(R.id.center_right)
 
-            assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
+            assertThat(getConstraint(R.id.shade_header_system_icons).layout.startToEnd)
                 .isNotEqualTo(R.id.center_left)
-            assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
+            assertThat(getConstraint(R.id.shade_header_system_icons).layout.startToEnd)
                 .isNotEqualTo(R.id.center_right)
 
             assertThat(getConstraint(R.id.privacy_container).layout.startToEnd)
@@ -311,7 +306,7 @@
             assertThat(getConstraint(R.id.center_left).layout.guideEnd).isEqualTo(offsetFromEdge)
             assertThat(getConstraint(R.id.center_right).layout.guideBegin).isEqualTo(offsetFromEdge)
             assertThat(getConstraint(R.id.date).layout.endToStart).isEqualTo(R.id.center_right)
-            assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
+            assertThat(getConstraint(R.id.shade_header_system_icons).layout.startToEnd)
                 .isEqualTo(R.id.center_left)
             assertThat(getConstraint(R.id.privacy_container).layout.startToEnd)
                 .isEqualTo(R.id.center_left)
@@ -324,9 +319,9 @@
             assertThat(getConstraint(R.id.date).layout.endToStart).isNotEqualTo(R.id.center_left)
             assertThat(getConstraint(R.id.date).layout.endToStart).isNotEqualTo(R.id.center_right)
 
-            assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
+            assertThat(getConstraint(R.id.shade_header_system_icons).layout.startToEnd)
                 .isNotEqualTo(R.id.center_left)
-            assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
+            assertThat(getConstraint(R.id.shade_header_system_icons).layout.startToEnd)
                 .isNotEqualTo(R.id.center_right)
 
             assertThat(getConstraint(R.id.privacy_container).layout.startToEnd)
@@ -382,7 +377,8 @@
         CombinedShadeHeadersConstraintManagerImpl.emptyCutoutConstraints()()
 
         assertThat(qqsConstraint.getConstraint(R.id.date).layout.constrainedWidth).isTrue()
-        assertThat(qqsConstraint.getConstraint(R.id.statusIcons).layout.constrainedWidth).isTrue()
+        val shadeHeaderConstraint = qqsConstraint.getConstraint(R.id.shade_header_system_icons)
+        assertThat(shadeHeaderConstraint.layout.constrainedWidth).isTrue()
     }
 
     @Test
@@ -390,9 +386,13 @@
         CombinedShadeHeadersConstraintManagerImpl.centerCutoutConstraints(false, 10)()
 
         assertThat(qqsConstraint.getConstraint(R.id.date).layout.constrainedWidth).isTrue()
-        assertThat(qqsConstraint.getConstraint(R.id.statusIcons).layout.constrainedWidth).isTrue()
+        val shadeHeaderConstraint = qqsConstraint.getConstraint(R.id.shade_header_system_icons)
+        assertThat(shadeHeaderConstraint.layout.constrainedWidth).isTrue()
     }
 
+    private val ConstraintSet.systemIconsAlphaConstraint
+        get() = getConstraint(R.id.shade_header_system_icons).propertySet.alpha
+
     private operator fun ConstraintsChanges.invoke() {
         qqsConstraintsChanges?.invoke(qqsConstraint)
         qsConstraintsChanges?.invoke(qsConstraint)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index fe89a14..9188293 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -80,6 +80,7 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.common.ui.view.LongPressHandlingView;
@@ -89,12 +90,12 @@
 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.KeyguardViewConfigurator;
 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.KeyguardFaceAuthInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
@@ -116,6 +117,7 @@
 import com.android.systemui.qs.QSFragment;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shade.data.repository.ShadeRepository;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.shade.transition.ShadeTransitionController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -167,6 +169,7 @@
 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.kotlin.JavaAdapter;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.util.time.SystemClock;
 import com.android.wm.shell.animation.FlingAnimationUtils;
@@ -253,6 +256,7 @@
     @Mock protected UserManager mUserManager;
     @Mock protected UiEventLogger mUiEventLogger;
     @Mock protected LockIconViewController mLockIconViewController;
+    @Mock protected KeyguardViewConfigurator mKeyguardViewConfigurator;
     @Mock protected KeyguardMediaController mKeyguardMediaController;
     @Mock protected NavigationModeController mNavigationModeController;
     @Mock protected NavigationBarController mNavigationBarController;
@@ -309,10 +313,13 @@
     @Mock protected ActivityStarter mActivityStarter;
     @Mock protected KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor;
     @Mock protected ShadeRepository mShadeRepository;
+    @Mock private ShadeInteractor mShadeInteractor;
+    @Mock private JavaAdapter mJavaAdapter;
     @Mock private CastController mCastController;
 
     protected final int mMaxUdfpsBurnInOffsetY = 5;
     protected KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
+    protected FakeKeyguardRepository mFakeKeyguardRepository;
     protected KeyguardInteractor mKeyguardInteractor;
     protected NotificationPanelViewController.TouchHandler mTouchHandler;
     protected ConfigurationController mConfigurationController;
@@ -340,10 +347,12 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
         mMainDispatcher = getMainDispatcher();
-        mKeyguardBottomAreaInteractor = new KeyguardBottomAreaInteractor(
-                new FakeKeyguardRepository());
-        mKeyguardInteractor = new KeyguardInteractor(new FakeKeyguardRepository(), mCommandQueue,
-                mFeatureFlags, new FakeKeyguardBouncerRepository());
+        KeyguardInteractorFactory.WithDependencies keyguardInteractorDeps =
+                KeyguardInteractorFactory.create();
+        mFakeKeyguardRepository = keyguardInteractorDeps.getRepository();
+        mKeyguardBottomAreaInteractor = new KeyguardBottomAreaInteractor(mFakeKeyguardRepository);
+        mKeyguardInteractor = keyguardInteractorDeps.getKeyguardInteractor();
+
         SystemClock systemClock = new FakeSystemClock();
         mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger, mDumpManager,
                 mInteractionJankMonitor, mShadeExpansionStateManager);
@@ -594,7 +603,6 @@
                 mNotificationStackSizeCalculator,
                 mUnlockedScreenOffAnimationController,
                 mShadeTransitionController,
-                mInteractionJankMonitor,
                 systemClock,
                 mKeyguardBottomAreaViewModel,
                 mKeyguardBottomAreaInteractor,
@@ -611,6 +619,7 @@
                 mKeyuardLongPressViewModel,
                 mKeyguardInteractor,
                 mActivityStarter,
+                mKeyguardViewConfigurator,
                 mKeyguardFaceAuthInteractor);
         mNotificationPanelViewController.initDependencies(
                 mCentralSurfaces,
@@ -620,7 +629,7 @@
                 mHeadsUpManager);
         mNotificationPanelViewController.setTrackingStartedListener(() -> {});
         mNotificationPanelViewController.setOpenCloseListener(
-                new NotificationPanelViewController.OpenCloseListener() {
+                new OpenCloseListener() {
                     @Override
                     public void onClosingFinished() {}
 
@@ -682,6 +691,8 @@
                 mDumpManager,
                 mKeyguardFaceAuthInteractor,
                 mShadeRepository,
+                mShadeInteractor,
+                mJavaAdapter,
                 mCastController
         );
     }
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 987e09c..eb4ae1a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -34,6 +34,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
@@ -58,6 +59,9 @@
 import com.android.keyguard.FaceAuthApiRequestReason;
 import com.android.systemui.DejankUtils;
 import com.android.systemui.R;
+import com.android.systemui.keyguard.shared.model.WakeSleepReason;
+import com.android.systemui.keyguard.shared.model.WakefulnessModel;
+import com.android.systemui.keyguard.shared.model.WakefulnessState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener;
@@ -741,6 +745,47 @@
     }
 
     @Test
+    public void onQsSetExpansionHeightCalled_qsFullyExpandedOnKeyguard_showNSSL() {
+        // GIVEN
+        mStatusBarStateController.setState(KEYGUARD);
+        when(mKeyguardBypassController.getBypassEnabled()).thenReturn(false);
+        when(mQsController.getFullyExpanded()).thenReturn(true);
+        when(mQsController.getExpanded()).thenReturn(true);
+
+        // WHEN
+        int transitionDistance = mNotificationPanelViewController.getMaxPanelTransitionDistance();
+        mNotificationPanelViewController.setExpandedHeight(transitionDistance);
+
+        // THEN
+        // We are interested in the last value of the stack alpha.
+        ArgumentCaptor<Float> alphaCaptor = ArgumentCaptor.forClass(Float.class);
+        verify(mNotificationStackScrollLayoutController, atLeastOnce())
+                .setAlpha(alphaCaptor.capture());
+        assertThat(alphaCaptor.getValue()).isEqualTo(1.0f);
+    }
+
+    @Test
+    public void onQsSetExpansionHeightCalled_qsFullyExpandedOnKeyguard_hideNSSL() {
+        // GIVEN
+        mStatusBarStateController.setState(KEYGUARD);
+        when(mKeyguardBypassController.getBypassEnabled()).thenReturn(false);
+        when(mQsController.getFullyExpanded()).thenReturn(false);
+        when(mQsController.getExpanded()).thenReturn(true);
+
+        // WHEN
+        int transitionDistance = mNotificationPanelViewController
+                .getMaxPanelTransitionDistance() / 2;
+        mNotificationPanelViewController.setExpandedHeight(transitionDistance);
+
+        // THEN
+        // We are interested in the last value of the stack alpha.
+        ArgumentCaptor<Float> alphaCaptor = ArgumentCaptor.forClass(Float.class);
+        verify(mNotificationStackScrollLayoutController, atLeastOnce())
+                .setAlpha(alphaCaptor.capture());
+        assertThat(alphaCaptor.getValue()).isEqualTo(0.0f);
+    }
+
+    @Test
     public void testSwitchesToBigClockInSplitShadeOnAodAnimateDisabled() {
         when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(false);
         mStatusBarStateController.setState(KEYGUARD);
@@ -1180,4 +1225,43 @@
         when(mUnlockedScreenOffAnimationController.isAnimationPlaying()).thenReturn(true);
         assertThat(mNotificationPanelViewController.isExpanded()).isTrue();
     }
+
+    @Test
+    public void getFalsingThreshold_deviceNotInteractive_isQsThreshold() {
+        mFakeKeyguardRepository.setWakefulnessModel(
+                new WakefulnessModel(
+                        WakefulnessState.ASLEEP,
+                        /* lastWakeReason= */ WakeSleepReason.TAP,
+                        /* lastSleepReason= */ WakeSleepReason.POWER_BUTTON)
+        );
+        when(mQsController.getFalsingThreshold()).thenReturn(14);
+
+        assertThat(mNotificationPanelViewController.getFalsingThreshold()).isEqualTo(14);
+    }
+
+    @Test
+    public void getFalsingThreshold_lastWakeNotDueToTouch_isQsThreshold() {
+        mFakeKeyguardRepository.setWakefulnessModel(
+                new WakefulnessModel(
+                        WakefulnessState.AWAKE,
+                        /* lastWakeReason= */ WakeSleepReason.POWER_BUTTON,
+                        /* lastSleepReason= */ WakeSleepReason.POWER_BUTTON)
+        );
+        when(mQsController.getFalsingThreshold()).thenReturn(14);
+
+        assertThat(mNotificationPanelViewController.getFalsingThreshold()).isEqualTo(14);
+    }
+
+    @Test
+    public void getFalsingThreshold_lastWakeDueToTouch_greaterThanQsThreshold() {
+        mFakeKeyguardRepository.setWakefulnessModel(
+                new WakefulnessModel(
+                        WakefulnessState.AWAKE,
+                        /* lastWakeReason= */ WakeSleepReason.TAP,
+                        /* lastSleepReason= */ WakeSleepReason.POWER_BUTTON)
+        );
+        when(mQsController.getFalsingThreshold()).thenReturn(14);
+
+        assertThat(mNotificationPanelViewController.getFalsingThreshold()).isGreaterThan(14);
+    }
 }
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 526dc8d..cde6ac0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -126,7 +126,7 @@
                     }
             };
         mNotificationShadeWindowController.setScrimsVisibilityListener((visibility) -> {});
-        mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
+        mNotificationShadeWindowController.setWindowRootView(mNotificationShadeWindowView);
 
         mNotificationShadeWindowController.attach();
         verify(mWindowManager).addView(eq(mNotificationShadeWindowView), any());
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 3995e71..5fb3a79 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -21,26 +21,35 @@
 import android.view.MotionEvent
 import android.view.ViewGroup
 import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardMessageAreaController
 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.back.domain.interactor.BackActionInteractor
+import com.android.systemui.bouncer.data.factory.BouncerMessageFactory
+import com.android.systemui.bouncer.data.repository.FakeBouncerMessageRepository
+import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
+import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil
+import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
 import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.dock.DockManager
+import com.android.systemui.dump.logcatLogBuffer
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 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.log.BouncerLogger
 import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
 import com.android.systemui.multishade.data.repository.MultiShadeRepository
 import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
 import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.NotificationInsetsController
@@ -54,6 +63,7 @@
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
@@ -70,9 +80,9 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 import java.util.Optional
+import org.mockito.Mockito.`when` as whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -83,6 +93,8 @@
     @Mock private lateinit var view: NotificationShadeWindowView
     @Mock private lateinit var sysuiStatusBarStateController: SysuiStatusBarStateController
     @Mock private lateinit var centralSurfaces: CentralSurfaces
+    @Mock private lateinit var backActionInteractor: BackActionInteractor
+    @Mock private lateinit var powerInteractor: PowerInteractor
     @Mock private lateinit var dockManager: DockManager
     @Mock private lateinit var notificationPanelViewController: NotificationPanelViewController
     @Mock private lateinit var notificationShadeDepthController: NotificationShadeDepthController
@@ -134,6 +146,7 @@
         featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
         featureFlags.set(Flags.DUAL_SHADE, false)
         featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
+        featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
 
         val inputProxy = MultiShadeInputProxy()
         testScope = TestScope()
@@ -162,6 +175,8 @@
                 statusBarWindowStateController,
                 lockIconViewController,
                 centralSurfaces,
+                backActionInteractor,
+                powerInteractor,
                 notificationShadeWindowController,
                 unfoldTransitionProgressProvider,
                 keyguardUnlockAnimationController,
@@ -170,6 +185,7 @@
                 pulsingGestureListener,
                 keyguardBouncerViewModel,
                 keyguardBouncerComponentFactory,
+                mock(KeyguardMessageAreaController.Factory::class.java),
                 keyguardTransitionInteractor,
                 primaryBouncerToGoneTransitionViewModel,
                 featureFlags,
@@ -182,14 +198,17 @@
                         multiShadeInteractor = multiShadeInteractor,
                         featureFlags = featureFlags,
                         keyguardTransitionInteractor =
-                            KeyguardTransitionInteractor(
-                                repository = FakeKeyguardTransitionRepository(),
-                                scope = testScope.backgroundScope
-                            ),
+                            KeyguardTransitionInteractorFactory.create(
+                                    scope = TestScope().backgroundScope,
+                            ).keyguardTransitionInteractor,
                         falsingManager = FalsingManagerFake(),
                         shadeController = shadeController,
                     )
                 },
+                BouncerMessageInteractor(FakeBouncerMessageRepository(),
+                        mock(BouncerMessageFactory::class.java),
+                        FakeUserRepository(), CountDownTimerUtil(), featureFlags),
+                BouncerLogger(logcatLogBuffer("BouncerLog"))
             )
         underTest.setupExpandedStatusBar()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index fe95742..544137e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -21,25 +21,34 @@
 import android.view.MotionEvent
 import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardMessageAreaController
 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.back.domain.interactor.BackActionInteractor
+import com.android.systemui.bouncer.data.factory.BouncerMessageFactory
+import com.android.systemui.bouncer.data.repository.FakeBouncerMessageRepository
+import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
+import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil
+import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
 import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.dock.DockManager
+import com.android.systemui.dump.logcatLogBuffer
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
+import com.android.systemui.log.BouncerLogger
 import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
 import com.android.systemui.multishade.data.repository.MultiShadeRepository
 import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
 import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
 import com.android.systemui.statusbar.DragDownHelper
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -54,6 +63,7 @@
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
@@ -70,6 +80,7 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito
 import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
@@ -84,6 +95,8 @@
     @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
     @Mock private lateinit var shadeController: ShadeController
     @Mock private lateinit var centralSurfaces: CentralSurfaces
+    @Mock private lateinit var backActionInteractor: BackActionInteractor
+    @Mock private lateinit var powerInteractor: PowerInteractor
     @Mock private lateinit var dockManager: DockManager
     @Mock private lateinit var notificationPanelViewController: NotificationPanelViewController
     @Mock private lateinit var notificationStackScrollLayout: NotificationStackScrollLayout
@@ -147,6 +160,7 @@
         featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
         featureFlags.set(Flags.DUAL_SHADE, false)
         featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
+        featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
         val inputProxy = MultiShadeInputProxy()
         testScope = TestScope()
         val multiShadeInteractor =
@@ -174,6 +188,8 @@
                 statusBarWindowStateController,
                 lockIconViewController,
                 centralSurfaces,
+                backActionInteractor,
+                powerInteractor,
                 notificationShadeWindowController,
                 unfoldTransitionProgressProvider,
                 keyguardUnlockAnimationController,
@@ -182,6 +198,7 @@
                 pulsingGestureListener,
                 keyguardBouncerViewModel,
                 keyguardBouncerComponentFactory,
+                Mockito.mock(KeyguardMessageAreaController.Factory::class.java),
                 keyguardTransitionInteractor,
                 primaryBouncerToGoneTransitionViewModel,
                 featureFlags,
@@ -194,14 +211,22 @@
                         multiShadeInteractor = multiShadeInteractor,
                         featureFlags = featureFlags,
                         keyguardTransitionInteractor =
-                            KeyguardTransitionInteractor(
-                                repository = FakeKeyguardTransitionRepository(),
-                                scope = testScope.backgroundScope
-                            ),
+                            KeyguardTransitionInteractorFactory.create(
+                                    scope = TestScope().backgroundScope,
+                                )
+                                .keyguardTransitionInteractor,
                         falsingManager = FalsingManagerFake(),
                         shadeController = shadeController,
                     )
                 },
+                BouncerMessageInteractor(
+                    FakeBouncerMessageRepository(),
+                    Mockito.mock(BouncerMessageFactory::class.java),
+                    FakeUserRepository(),
+                    CountDownTimerUtil(),
+                    featureFlags
+                ),
+                BouncerLogger(logcatLogBuffer("BouncerLog"))
             )
 
         controller.setupExpandedStatusBar()
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 d7c06a7..a4fab1d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
@@ -25,22 +25,24 @@
 import android.view.MotionEvent
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.phone.CentralSurfaces
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.tuner.TunerService
 import com.android.systemui.tuner.TunerService.Tunable
 import com.android.systemui.util.mockito.eq
+import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.anyLong
-import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
@@ -52,14 +54,12 @@
 @SmallTest
 class PulsingGestureListenerTest : SysuiTestCase() {
     @Mock
-    private lateinit var view: NotificationShadeWindowView
-    @Mock
-    private lateinit var centralSurfaces: CentralSurfaces
-    @Mock
     private lateinit var dockManager: DockManager
     @Mock
     private lateinit var falsingManager: FalsingManager
     @Mock
+    private lateinit var falsingCollector: FalsingCollector
+    @Mock
     private lateinit var ambientDisplayConfiguration: AmbientDisplayConfiguration
     @Mock
     private lateinit var tunerService: TunerService
@@ -71,7 +71,10 @@
     private lateinit var shadeLogger: ShadeLogger
     @Mock
     private lateinit var userTracker: UserTracker
+    @Mock
+    private lateinit var screenOffAnimationController: ScreenOffAnimationController
 
+    private lateinit var powerRepository: FakePowerRepository
     private lateinit var tunableCaptor: ArgumentCaptor<Tunable>
     private lateinit var underTest: PulsingGestureListener
 
@@ -79,11 +82,17 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        powerRepository = FakePowerRepository()
+
         underTest = PulsingGestureListener(
-                view,
                 falsingManager,
                 dockManager,
-                centralSurfaces,
+                PowerInteractor(
+                    powerRepository,
+                    falsingCollector,
+                    screenOffAnimationController,
+                    statusBarStateController,
+                ),
                 ambientDisplayConfiguration,
                 statusBarStateController,
                 shadeLogger,
@@ -92,6 +101,7 @@
                 dumpManager
         )
         whenever(dockManager.isDocked).thenReturn(false)
+        whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true)
     }
 
     @Test
@@ -110,8 +120,8 @@
         underTest.onSingleTapUp(upEv)
 
         // THEN wake up device if dozing
-        verify(centralSurfaces).wakeUpIfDozing(
-                anyLong(), anyString(), eq(PowerManager.WAKE_REASON_TAP))
+        assertThat(powerRepository.lastWakeWhy).isNotNull()
+        assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_TAP)
     }
 
     @Test
@@ -130,8 +140,8 @@
         underTest.onDoubleTapEvent(upEv)
 
         // THEN wake up device if dozing
-        verify(centralSurfaces).wakeUpIfDozing(
-                anyLong(), anyString(), eq(PowerManager.WAKE_REASON_TAP))
+        assertThat(powerRepository.lastWakeWhy).isNotNull()
+        assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_TAP)
     }
 
     @Test
@@ -162,8 +172,8 @@
         underTest.onSingleTapUp(upEv)
 
         // THEN the device doesn't wake up
-        verify(centralSurfaces, never()).wakeUpIfDozing(
-                anyLong(), anyString(), anyInt())
+        assertThat(powerRepository.lastWakeWhy).isNull()
+        assertThat(powerRepository.lastWakeReason).isNull()
     }
 
     @Test
@@ -210,8 +220,8 @@
         underTest.onDoubleTapEvent(upEv)
 
         // THEN the device doesn't wake up
-        verify(centralSurfaces, never()).wakeUpIfDozing(
-                anyLong(), anyString(), anyInt())
+        assertThat(powerRepository.lastWakeWhy).isNull()
+        assertThat(powerRepository.lastWakeReason).isNull()
     }
 
     @Test
@@ -230,8 +240,8 @@
         underTest.onSingleTapUp(upEv)
 
         // THEN the device doesn't wake up
-        verify(centralSurfaces, never()).wakeUpIfDozing(
-                anyLong(), anyString(), anyInt())
+        assertThat(powerRepository.lastWakeWhy).isNull()
+        assertThat(powerRepository.lastWakeReason).isNull()
     }
 
     @Test
@@ -250,8 +260,8 @@
         underTest.onDoubleTapEvent(upEv)
 
         // THEN the device doesn't wake up
-        verify(centralSurfaces, never()).wakeUpIfDozing(
-                anyLong(), anyString(), anyInt())
+        assertThat(powerRepository.lastWakeWhy).isNull()
+        assertThat(powerRepository.lastWakeReason).isNull()
     }
 
     fun updateSettings() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
new file mode 100644
index 0000000..a2c2912
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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 org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityManager;
+
+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.keyguard.TestScopeProvider;
+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.keyguard.data.repository.FakeKeyguardRepository;
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
+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.data.repository.ShadeRepository;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
+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.disableflags.data.repository.FakeDisableFlagsRepository;
+import com.android.systemui.statusbar.notification.stack.AmbientState;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
+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.LightBarController;
+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.pipeline.mobile.data.repository.FakeUserSetupRepository;
+import com.android.systemui.statusbar.policy.CastController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.user.domain.interactor.UserInteractor;
+import com.android.systemui.util.kotlin.JavaAdapter;
+
+import dagger.Lazy;
+
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import kotlinx.coroutines.test.TestScope;
+
+public class QuickSettingsControllerBaseTest extends SysuiTestCase {
+    protected static final float QS_FRAME_START_X = 0f;
+    protected static final int QS_FRAME_WIDTH = 1000;
+    protected static final int QS_FRAME_TOP = 0;
+    protected static final int QS_FRAME_BOTTOM = 1000;
+    protected static final int DEFAULT_HEIGHT = 1000;
+    // In split shade min = max
+    protected static final int DEFAULT_MIN_HEIGHT_SPLIT_SHADE = DEFAULT_HEIGHT;
+    protected static final int DEFAULT_MIN_HEIGHT = 300;
+
+    protected QuickSettingsController mQsController;
+
+    protected TestScope mTestScope = TestScopeProvider.getTestScope();
+
+    @Mock
+    protected Resources mResources;
+    @Mock protected KeyguardBottomAreaView mQsFrame;
+    @Mock protected KeyguardStatusBarView mKeyguardStatusBar;
+    @Mock protected QS mQs;
+    @Mock protected QSFragment mQSFragment;
+    @Mock protected Lazy<NotificationPanelViewController> mPanelViewControllerLazy;
+    @Mock protected NotificationPanelViewController mNotificationPanelViewController;
+    @Mock protected NotificationPanelView mPanelView;
+    @Mock protected ViewGroup mQsHeader;
+    @Mock protected ViewParent mPanelViewParent;
+    @Mock protected QsFrameTranslateController mQsFrameTranslateController;
+    @Mock protected ShadeTransitionController mShadeTransitionController;
+    @Mock protected PulseExpansionHandler mPulseExpansionHandler;
+    @Mock protected NotificationRemoteInputManager mNotificationRemoteInputManager;
+    @Mock protected StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    @Mock protected LightBarController mLightBarController;
+    @Mock protected NotificationStackScrollLayoutController
+            mNotificationStackScrollLayoutController;
+    @Mock protected LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+    @Mock protected NotificationShadeDepthController mNotificationShadeDepthController;
+    @Mock protected ShadeHeaderController mShadeHeaderController;
+    @Mock protected StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
+    @Mock protected KeyguardStateController mKeyguardStateController;
+    @Mock protected KeyguardBypassController mKeyguardBypassController;
+    @Mock protected KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @Mock protected ScrimController mScrimController;
+    @Mock protected MediaDataManager mMediaDataManager;
+    @Mock protected MediaHierarchyManager mMediaHierarchyManager;
+    @Mock protected AmbientState mAmbientState;
+    @Mock protected RecordingController mRecordingController;
+    @Mock protected FalsingManager mFalsingManager;
+    @Mock protected FalsingCollector mFalsingCollector;
+    @Mock protected AccessibilityManager mAccessibilityManager;
+    @Mock protected LockscreenGestureLogger mLockscreenGestureLogger;
+    @Mock protected MetricsLogger mMetricsLogger;
+    @Mock protected FeatureFlags mFeatureFlags;
+    @Mock protected InteractionJankMonitor mInteractionJankMonitor;
+    @Mock protected ShadeLogger mShadeLogger;
+    @Mock protected DumpManager mDumpManager;
+    @Mock protected UiEventLogger mUiEventLogger;
+    @Mock protected CastController mCastController;
+    @Mock protected DeviceProvisionedController mDeviceProvisionedController;
+    @Mock protected UserInteractor mUserInteractor;
+    protected FakeDisableFlagsRepository mDisableFlagsRepository =
+            new FakeDisableFlagsRepository();
+    protected FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository();
+
+    protected SysuiStatusBarStateController mStatusBarStateController;
+    protected ShadeInteractor mShadeInteractor;
+
+    protected Handler mMainHandler;
+    protected LockscreenShadeTransitionController.Callback mLockscreenShadeTransitionCallback;
+
+    protected final ShadeExpansionStateManager mShadeExpansionStateManager =
+            new ShadeExpansionStateManager();
+
+    protected FragmentHostManager.FragmentListener mFragmentListener;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mPanelViewControllerLazy.get()).thenReturn(mNotificationPanelViewController);
+        mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger, mDumpManager,
+                mInteractionJankMonitor, mShadeExpansionStateManager);
+
+        when(mDeviceProvisionedController.isDeviceProvisioned()).thenReturn(true);
+        mShadeInteractor =
+                new ShadeInteractor(
+                        mTestScope.getBackgroundScope(),
+                        mDisableFlagsRepository,
+                        mKeyguardRepository,
+                        new FakeUserSetupRepository(),
+                        mDeviceProvisionedController,
+                        mUserInteractor
+                );
+
+        KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
+        keyguardStatusView.setId(R.id.keyguard_status_view);
+
+        when(mResources.getDimensionPixelSize(
+                R.dimen.lockscreen_shade_qs_transition_distance)).thenReturn(DEFAULT_HEIGHT);
+        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,
+                mLightBarController,
+                mNotificationStackScrollLayoutController,
+                mLockscreenShadeTransitionController,
+                mNotificationShadeDepthController,
+                mShadeHeaderController,
+                mStatusBarTouchableRegionManager,
+                mKeyguardStateController,
+                mKeyguardBypassController,
+                mKeyguardUpdateMonitor,
+                mScrimController,
+                mMediaDataManager,
+                mMediaHierarchyManager,
+                mAmbientState,
+                mRecordingController,
+                mFalsingManager,
+                mFalsingCollector,
+                mAccessibilityManager,
+                mLockscreenGestureLogger,
+                mMetricsLogger,
+                mFeatureFlags,
+                mInteractionJankMonitor,
+                mShadeLogger,
+                mDumpManager,
+                mock(KeyguardFaceAuthInteractor.class),
+                mock(ShadeRepository.class),
+                mShadeInteractor,
+                new JavaAdapter(mTestScope.getBackgroundScope()),
+                mCastController
+        );
+        mQsController.init();
+
+        mFragmentListener = mQsController.getQsFragmentListener();
+    }
+
+    @After
+    public void tearDown() {
+        mMainHandler.removeCallbacksAndMessages(null);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java
index ff047aa..6d288e1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java
@@ -28,238 +28,32 @@
 
 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.atLeastOnce;
-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.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
-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.data.repository.ShadeRepository;
-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.KeyguardBottomAreaView;
-import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
-import com.android.systemui.statusbar.phone.LightBarController;
-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.CastController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
 
-import dagger.Lazy;
-
-import org.junit.After;
-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.List;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class QuickSettingsControllerTest extends SysuiTestCase {
-
-    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 static final int DEFAULT_HEIGHT = 1000;
-    // In split shade min = max
-    private static final int DEFAULT_MIN_HEIGHT_SPLIT_SHADE = DEFAULT_HEIGHT;
-    private static final int DEFAULT_MIN_HEIGHT = 300;
-
-    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 LightBarController mLightBarController;
-    @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 UiEventLogger mUiEventLogger;
-    @Mock private CastController mCastController;
-
-    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(mResources.getDimensionPixelSize(
-                R.dimen.lockscreen_shade_qs_transition_distance)).thenReturn(DEFAULT_HEIGHT);
-        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,
-                mLightBarController,
-                mNotificationStackScrollLayoutController,
-                mLockscreenShadeTransitionController,
-                mNotificationShadeDepthController,
-                mShadeHeaderController,
-                mStatusBarTouchableRegionManager,
-                mKeyguardStateController,
-                mKeyguardBypassController,
-                mKeyguardUpdateMonitor,
-                mScrimController,
-                mMediaDataManager,
-                mMediaHierarchyManager,
-                mAmbientState,
-                mRecordingController,
-                mFalsingManager,
-                mFalsingCollector,
-                mAccessibilityManager,
-                mLockscreenGestureLogger,
-                mMetricsLogger,
-                mFeatureFlags,
-                mInteractionJankMonitor,
-                mShadeLogger,
-                mDumpManager,
-                mock(KeyguardFaceAuthInteractor.class),
-                mock(ShadeRepository.class),
-                mCastController
-        );
-
-        mFragmentListener = mQsController.getQsFragmentListener();
-    }
-
-    @After
-    public void tearDown() {
-        mMainHandler.removeCallbacksAndMessages(null);
-    }
+public class QuickSettingsControllerTest extends QuickSettingsControllerBaseTest {
 
     @Test
     public void testCloseQsSideEffects() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerWithCoroutinesTest.kt
new file mode 100644
index 0000000..cc4a063
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerWithCoroutinesTest.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.shade
+
+import android.app.StatusBarManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class QuickSettingsControllerWithCoroutinesTest : QuickSettingsControllerBaseTest() {
+
+    @Test
+    fun isExpansionEnabled_dozing_false() =
+        mTestScope.runTest {
+            mKeyguardRepository.setIsDozing(true)
+            runCurrent()
+
+            assertThat(mQsController.isExpansionEnabled).isFalse()
+        }
+
+    @Test
+    fun isExpansionEnabled_notDozing_true() =
+        mTestScope.runTest {
+            mKeyguardRepository.setIsDozing(false)
+            runCurrent()
+
+            assertThat(mQsController.isExpansionEnabled).isTrue()
+        }
+
+    @Test
+    fun isExpansionEnabled_qsDisabled_false() =
+        mTestScope.runTest {
+            mDisableFlagsRepository.disableFlags.value =
+                DisableFlagsModel(
+                    StatusBarManager.DISABLE_NONE,
+                    StatusBarManager.DISABLE2_QUICK_SETTINGS
+                )
+            runCurrent()
+
+            assertThat(mQsController.isExpansionEnabled).isFalse()
+        }
+
+    @Test
+    fun isExpansionEnabled_shadeDisabled_false() =
+        mTestScope.runTest {
+            mDisableFlagsRepository.disableFlags.value =
+                DisableFlagsModel(
+                    StatusBarManager.DISABLE_NONE,
+                    StatusBarManager.DISABLE2_NOTIFICATION_SHADE
+                )
+            runCurrent()
+
+            assertThat(mQsController.isExpansionEnabled).isFalse()
+        }
+
+    @Test
+    fun isExpansionEnabled_qsAndShadeEnabled_true() =
+        mTestScope.runTest {
+            mDisableFlagsRepository.disableFlags.value =
+                DisableFlagsModel(StatusBarManager.DISABLE_NONE, StatusBarManager.DISABLE2_NONE)
+            runCurrent()
+
+            assertThat(mQsController.isExpansionEnabled).isTrue()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt
new file mode 100644
index 0000000..729c4a9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.view.Display
+import android.view.WindowManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.assist.AssistManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.notification.row.NotificationGutsManager
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import dagger.Lazy
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class ShadeControllerImplTest : SysuiTestCase() {
+    @Mock private lateinit var commandQueue: CommandQueue
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
+    @Mock private lateinit var statusBarWindowController: StatusBarWindowController
+    @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+    @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
+    @Mock private lateinit var windowManager: WindowManager
+    @Mock private lateinit var assistManager: AssistManager
+    @Mock private lateinit var gutsManager: NotificationGutsManager
+    @Mock private lateinit var shadeViewController: ShadeViewController
+    @Mock private lateinit var nswvc: NotificationShadeWindowViewController
+    @Mock private lateinit var display: Display
+
+    private lateinit var shadeController: ShadeControllerImpl
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(windowManager.defaultDisplay).thenReturn(display)
+        whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
+        shadeController =
+            ShadeControllerImpl(
+                commandQueue,
+                FakeExecutor(FakeSystemClock()),
+                keyguardStateController,
+                statusBarStateController,
+                statusBarKeyguardViewManager,
+                statusBarWindowController,
+                deviceProvisionedController,
+                notificationShadeWindowController,
+                windowManager,
+                Lazy { assistManager },
+                Lazy { gutsManager },
+            )
+        shadeController.setNotificationShadeWindowViewController(nswvc)
+        shadeController.setShadeViewController(shadeViewController)
+    }
+
+    @Test
+    fun testDisableNotificationShade() {
+        whenever(commandQueue.panelsEnabled()).thenReturn(false)
+
+        // Trying to open it does nothing.
+        shadeController.animateExpandShade()
+        verify(shadeViewController, never()).expandToNotifications()
+        shadeController.animateExpandQs()
+        verify(shadeViewController, never()).expand(ArgumentMatchers.anyBoolean())
+    }
+
+    @Test
+    fun testEnableNotificationShade() {
+        whenever(commandQueue.panelsEnabled()).thenReturn(true)
+
+        // Can now be opened.
+        shadeController.animateExpandShade()
+        verify(shadeViewController).expandToNotifications()
+        shadeController.animateExpandQs()
+        verify(shadeViewController).expandToQs()
+    }
+
+    @Test
+    fun cancelExpansionAndCollapseShade_callsCancelCurrentTouch() {
+        // GIVEN the shade is tracking a touch
+        whenever(shadeViewController.isTracking).thenReturn(true)
+
+        // WHEN cancelExpansionAndCollapseShade is called
+        shadeController.cancelExpansionAndCollapseShade()
+
+        // VERIFY that cancelCurrentTouch is called
+        verify(nswvc).cancelCurrentTouch()
+    }
+
+    @Test
+    fun cancelExpansionAndCollapseShade_doesNotCallAnimateCollapseShade_whenCollapsed() {
+        // GIVEN the shade is tracking a touch
+        whenever(shadeViewController.isTracking).thenReturn(false)
+
+        // WHEN cancelExpansionAndCollapseShade is called
+        shadeController.cancelExpansionAndCollapseShade()
+
+        // VERIFY that cancelCurrentTouch is NOT called
+        verify(nswvc, never()).cancelCurrentTouch()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
index 2da2e92..f542ab0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
@@ -237,11 +237,22 @@
         whenever(mShadeCarrierGroupController.isSingleCarrier).thenReturn(false)
 
         makeShadeVisible()
+        shadeHeaderController.qsExpandedFraction = 1.0f
 
         verify(statusIcons).addIgnoredSlots(carrierIconSlots)
     }
 
     @Test
+    fun dualCarrier_enablesCarrierIconsInStatusIcons_qsExpanded() {
+        whenever(mShadeCarrierGroupController.isSingleCarrier).thenReturn(false)
+
+        makeShadeVisible()
+        shadeHeaderController.qsExpandedFraction = 0.0f
+
+        verify(statusIcons, times(2)).removeIgnoredSlots(carrierIconSlots)
+    }
+
+    @Test
     fun disableQS_notDisabled_visible() {
         makeShadeVisible()
         shadeHeaderController.disable(0, 0, false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierTest.java
index 4461310..dae9c97 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierTest.java
@@ -20,8 +20,10 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import android.content.Context;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.TextView;
@@ -48,7 +50,9 @@
     @Before
     public void setUp() throws Exception {
         mTestableLooper = TestableLooper.get(this);
-        LayoutInflater inflater = LayoutInflater.from(mContext);
+        Context themedContext =
+                new ContextThemeWrapper(mContext, R.style.Theme_SystemUI_QuickSettings);
+        LayoutInflater inflater = LayoutInflater.from(themedContext);
         mContext.ensureTestableResources();
         mTestableLooper.runWithLooper(() ->
                 mShadeCarrier = (ShadeCarrier) inflater.inflate(R.layout.shade_carrier, null));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
new file mode 100644
index 0000000..3ea8f54
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.data.repository
+
+import android.app.ActivityManager
+import android.app.StatusBarManager.DISABLE2_NONE
+import android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE
+import android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.keyguard.KeyguardUpdateMonitor
+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.domain.interactor.KeyguardInteractorFactory
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
+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.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.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class ShadeInteractorTest : SysuiTestCase() {
+    private lateinit var underTest: ShadeInteractor
+
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+    private val featureFlags = FakeFeatureFlags()
+    private val userSetupRepository = FakeUserSetupRepository()
+    private val userRepository = FakeUserRepository()
+    private val disableFlagsRepository = FakeDisableFlagsRepository()
+    private val keyguardRepository = FakeKeyguardRepository()
+
+    @Mock private lateinit var manager: UserManager
+    @Mock private lateinit var headlessSystemUserMode: HeadlessSystemUserMode
+    @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+    @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var activityManager: ActivityManager
+    @Mock private lateinit var uiEventLogger: UiEventLogger
+    @Mock private lateinit var guestInteractor: GuestUserInteractor
+
+    private lateinit var userInteractor: UserInteractor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        featureFlags.set(Flags.FACE_AUTH_REFACTOR, false)
+        featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+
+        val refreshUsersScheduler =
+            RefreshUsersScheduler(
+                applicationScope = testScope.backgroundScope,
+                mainDispatcher = testDispatcher,
+                repository = userRepository,
+            )
+        userInteractor =
+            UserInteractor(
+                applicationContext = context,
+                repository = userRepository,
+                activityStarter = activityStarter,
+                keyguardInteractor =
+                    KeyguardInteractorFactory.create(featureFlags = featureFlags)
+                        .keyguardInteractor,
+                featureFlags = featureFlags,
+                manager = manager,
+                headlessSystemUserMode = headlessSystemUserMode,
+                applicationScope = testScope.backgroundScope,
+                telephonyInteractor =
+                    TelephonyInteractor(
+                        repository = FakeTelephonyRepository(),
+                    ),
+                broadcastDispatcher = fakeBroadcastDispatcher,
+                keyguardUpdateMonitor = keyguardUpdateMonitor,
+                backgroundDispatcher = testDispatcher,
+                activityManager = activityManager,
+                refreshUsersScheduler = refreshUsersScheduler,
+                guestUserInteractor = guestInteractor,
+                uiEventLogger = uiEventLogger,
+            )
+        underTest =
+            ShadeInteractor(
+                testScope.backgroundScope,
+                disableFlagsRepository,
+                keyguardRepository,
+                userSetupRepository,
+                deviceProvisionedController,
+                userInteractor,
+            )
+    }
+
+    @Test
+    fun isShadeEnabled_matchesDisableFlagsRepo() =
+        testScope.runTest {
+            val actual by collectLastValue(underTest.isShadeEnabled)
+
+            disableFlagsRepository.disableFlags.value =
+                DisableFlagsModel(disable2 = DISABLE2_NOTIFICATION_SHADE)
+            assertThat(actual).isFalse()
+
+            disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE)
+
+            assertThat(actual).isTrue()
+        }
+
+    @Test
+    fun isExpandToQsEnabled_deviceNotProvisioned_false() =
+        testScope.runTest {
+            whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(false)
+
+            val actual by collectLastValue(underTest.isExpandToQsEnabled)
+
+            assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun isExpandToQsEnabled_userNotSetupAndSimpleUserSwitcher_false() =
+        testScope.runTest {
+            whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+
+            userSetupRepository.setUserSetup(false)
+            userRepository.setSettings(UserSwitcherSettingsModel(isSimpleUserSwitcher = true))
+
+            val actual by collectLastValue(underTest.isExpandToQsEnabled)
+
+            assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun isExpandToQsEnabled_shadeNotEnabled_false() =
+        testScope.runTest {
+            whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+            userSetupRepository.setUserSetup(true)
+
+            disableFlagsRepository.disableFlags.value =
+                DisableFlagsModel(
+                    disable2 = DISABLE2_NOTIFICATION_SHADE,
+                )
+
+            val actual by collectLastValue(underTest.isExpandToQsEnabled)
+
+            assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun isExpandToQsEnabled_quickSettingsNotEnabled_false() =
+        testScope.runTest {
+            whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+            userSetupRepository.setUserSetup(true)
+
+            disableFlagsRepository.disableFlags.value =
+                DisableFlagsModel(
+                    disable2 = DISABLE2_QUICK_SETTINGS,
+                )
+            val actual by collectLastValue(underTest.isExpandToQsEnabled)
+
+            assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun isExpandToQsEnabled_dozing_false() =
+        testScope.runTest {
+            whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+            userSetupRepository.setUserSetup(true)
+            disableFlagsRepository.disableFlags.value =
+                DisableFlagsModel(
+                    disable2 = DISABLE2_NONE,
+                )
+
+            keyguardRepository.setIsDozing(true)
+
+            val actual by collectLastValue(underTest.isExpandToQsEnabled)
+
+            assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun isExpandToQsEnabled_userSetup_true() =
+        testScope.runTest {
+            whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+            keyguardRepository.setIsDozing(false)
+            disableFlagsRepository.disableFlags.value =
+                DisableFlagsModel(
+                    disable2 = DISABLE2_NONE,
+                )
+
+            userSetupRepository.setUserSetup(true)
+
+            val actual by collectLastValue(underTest.isExpandToQsEnabled)
+
+            assertThat(actual).isTrue()
+        }
+
+    @Test
+    fun isExpandToQsEnabled_notSimpleUserSwitcher_true() =
+        testScope.runTest {
+            whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+            keyguardRepository.setIsDozing(false)
+            disableFlagsRepository.disableFlags.value =
+                DisableFlagsModel(
+                    disable2 = DISABLE2_NONE,
+                )
+
+            userRepository.setSettings(UserSwitcherSettingsModel(isSimpleUserSwitcher = false))
+
+            val actual by collectLastValue(underTest.isExpandToQsEnabled)
+
+            assertThat(actual).isTrue()
+        }
+
+    @Test
+    fun isExpandToQsEnabled_respondsToDozingUpdates() =
+        testScope.runTest {
+            whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+            keyguardRepository.setIsDozing(false)
+            disableFlagsRepository.disableFlags.value =
+                DisableFlagsModel(
+                    disable2 = DISABLE2_NONE,
+                )
+            userSetupRepository.setUserSetup(true)
+
+            val actual by collectLastValue(underTest.isExpandToQsEnabled)
+
+            assertThat(actual).isTrue()
+
+            // WHEN dozing starts
+            keyguardRepository.setIsDozing(true)
+
+            // THEN expand is disabled
+            assertThat(actual).isFalse()
+
+            // WHEN dozing stops
+            keyguardRepository.setIsDozing(false)
+
+            // THEN expand is enabled
+            assertThat(actual).isTrue()
+        }
+
+    @Test
+    fun isExpandToQsEnabled_respondsToDisableUpdates() =
+        testScope.runTest {
+            whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+            keyguardRepository.setIsDozing(false)
+            disableFlagsRepository.disableFlags.value =
+                DisableFlagsModel(
+                    disable2 = DISABLE2_NONE,
+                )
+            userSetupRepository.setUserSetup(true)
+
+            val actual by collectLastValue(underTest.isExpandToQsEnabled)
+
+            assertThat(actual).isTrue()
+
+            // WHEN QS is disabled
+            disableFlagsRepository.disableFlags.value =
+                DisableFlagsModel(
+                    disable2 = DISABLE2_QUICK_SETTINGS,
+                )
+            // THEN expand is disabled
+            assertThat(actual).isFalse()
+
+            // WHEN QS is enabled
+            disableFlagsRepository.disableFlags.value =
+                DisableFlagsModel(
+                    disable2 = DISABLE2_NONE,
+                )
+            // THEN expand is enabled
+            assertThat(actual).isTrue()
+        }
+
+    @Test
+    fun isExpandToQsEnabled_respondsToUserUpdates() =
+        testScope.runTest {
+            whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+            keyguardRepository.setIsDozing(false)
+            disableFlagsRepository.disableFlags.value =
+                DisableFlagsModel(
+                    disable2 = DISABLE2_NONE,
+                )
+            userSetupRepository.setUserSetup(true)
+
+            val actual by collectLastValue(underTest.isExpandToQsEnabled)
+
+            assertThat(actual).isTrue()
+
+            // WHEN the user is no longer setup
+            userSetupRepository.setUserSetup(false)
+            userRepository.setSettings(UserSwitcherSettingsModel(isSimpleUserSwitcher = true))
+
+            // THEN expand is disabled
+            assertThat(actual).isFalse()
+
+            // WHEN the user is setup again
+            userSetupRepository.setUserSetup(true)
+
+            // THEN expand is enabled
+            assertThat(actual).isTrue()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt
index cbf5485..a5bd2ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt
@@ -2,26 +2,16 @@
 
 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.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-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.shade.ShadeExpansionChangeEvent
-import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.policy.FakeConfigurationController
-import com.android.systemui.statusbar.policy.HeadsUpManager
 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.`when` as whenever
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
@@ -30,10 +20,6 @@
 
     @Mock private lateinit var scrimController: ScrimController
     @Mock private lateinit var dumpManager: DumpManager
-    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
-    @Mock private lateinit var headsUpManager: HeadsUpManager
-    @Mock private lateinit var featureFlags: FeatureFlags
-    private val configurationController = FakeConfigurationController()
 
     private lateinit var controller: ScrimShadeTransitionController
 
@@ -41,131 +27,25 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         context.ensureTestableResources()
-        controller =
-            ScrimShadeTransitionController(
-                configurationController,
-                dumpManager,
-                scrimController,
-                context.resources,
-                statusBarStateController,
-                headsUpManager,
-                featureFlags)
+        controller = ScrimShadeTransitionController(dumpManager, scrimController)
 
         controller.onPanelStateChanged(STATE_OPENING)
     }
 
     @Test
-    fun onPanelExpansionChanged_inSingleShade_setsFractionEqualToEventFraction() {
-        setSplitShadeEnabled(false)
-
+    fun onPanelExpansionChanged_setsFractionEqualToEventFraction() {
         controller.onPanelExpansionChanged(EXPANSION_EVENT)
 
         verify(scrimController).setRawPanelExpansionFraction(EXPANSION_EVENT.fraction)
     }
 
-    @Test
-    fun onPanelExpansionChanged_inSplitShade_unlockedShade_setsFractionBasedOnDragDownAmount() {
-        whenever(statusBarStateController.currentOrUpcomingState).thenReturn(StatusBarState.SHADE)
-        val scrimShadeTransitionDistance =
-            context.resources.getDimensionPixelSize(R.dimen.split_shade_scrim_transition_distance)
-        setSplitShadeEnabled(true)
-
-        controller.onPanelExpansionChanged(EXPANSION_EVENT)
-
-        val expectedFraction = EXPANSION_EVENT.dragDownPxAmount / scrimShadeTransitionDistance
-        verify(scrimController).setRawPanelExpansionFraction(expectedFraction)
-    }
-
-    @Test
-    fun onPanelExpansionChanged_inSplitShade_largeDragDownAmount_fractionIsNotGreaterThan1() {
-        whenever(statusBarStateController.currentOrUpcomingState).thenReturn(StatusBarState.SHADE)
-        val scrimShadeTransitionDistance =
-            context.resources.getDimensionPixelSize(R.dimen.split_shade_scrim_transition_distance)
-        setSplitShadeEnabled(true)
-
-        controller.onPanelExpansionChanged(
-            EXPANSION_EVENT.copy(dragDownPxAmount = 100f * scrimShadeTransitionDistance))
-
-        verify(scrimController).setRawPanelExpansionFraction(1f)
-    }
-
-    @Test
-    fun onPanelExpansionChanged_inSplitShade_negativeDragDownAmount_fractionIsNotLessThan0() {
-        whenever(statusBarStateController.currentOrUpcomingState).thenReturn(StatusBarState.SHADE)
-        setSplitShadeEnabled(true)
-
-        controller.onPanelExpansionChanged(EXPANSION_EVENT.copy(dragDownPxAmount = -100f))
-
-        verify(scrimController).setRawPanelExpansionFraction(0f)
-    }
-
-    @Test
-    fun onPanelExpansionChanged_inSplitShade_onLockedShade_setsFractionEqualToEventFraction() {
-        whenever(statusBarStateController.currentOrUpcomingState)
-            .thenReturn(StatusBarState.SHADE_LOCKED)
-        setSplitShadeEnabled(true)
-
-        controller.onPanelExpansionChanged(EXPANSION_EVENT)
-
-        verify(scrimController).setRawPanelExpansionFraction(EXPANSION_EVENT.fraction)
-    }
-
-    @Test
-    fun onPanelExpansionChanged_inSplitShade_flagTrue_setsFractionEqualToEventFraction() {
-        whenever(featureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION))
-                .thenReturn(true)
-        whenever(statusBarStateController.currentOrUpcomingState)
-            .thenReturn(StatusBarState.SHADE)
-        setSplitShadeEnabled(true)
-
-        controller.onPanelExpansionChanged(EXPANSION_EVENT)
-
-        verify(scrimController).setRawPanelExpansionFraction(EXPANSION_EVENT.fraction)
-    }
-
-    @Test
-    fun onPanelExpansionChanged_inSplitShade_onKeyguard_setsFractionEqualToEventFraction() {
-        whenever(statusBarStateController.currentOrUpcomingState)
-            .thenReturn(StatusBarState.KEYGUARD)
-        setSplitShadeEnabled(true)
-
-        controller.onPanelExpansionChanged(EXPANSION_EVENT)
-
-        verify(scrimController).setRawPanelExpansionFraction(EXPANSION_EVENT.fraction)
-    }
-
-    @Test
-    fun onPanelExpansionChanged_inSplitShade_panelOpen_setsFractionEqualToEventFraction() {
-        controller.onPanelStateChanged(STATE_OPEN)
-        whenever(statusBarStateController.currentOrUpcomingState)
-            .thenReturn(StatusBarState.KEYGUARD)
-        setSplitShadeEnabled(true)
-
-        controller.onPanelExpansionChanged(EXPANSION_EVENT)
-
-        verify(scrimController).setRawPanelExpansionFraction(EXPANSION_EVENT.fraction)
-    }
-
-    @Test
-    fun onPanelExpansionChanged_inSplitShade_panelClosed_setsFractionEqualToEventFraction() {
-        controller.onPanelStateChanged(STATE_CLOSED)
-        whenever(statusBarStateController.currentOrUpcomingState)
-            .thenReturn(StatusBarState.KEYGUARD)
-        setSplitShadeEnabled(true)
-
-        controller.onPanelExpansionChanged(EXPANSION_EVENT)
-
-        verify(scrimController).setRawPanelExpansionFraction(EXPANSION_EVENT.fraction)
-    }
-
-    private fun setSplitShadeEnabled(enabled: Boolean) {
-        overrideResource(R.bool.config_use_split_notification_shade, enabled)
-        configurationController.notifyConfigurationChanged()
-    }
-
     companion object {
         val EXPANSION_EVENT =
             ShadeExpansionChangeEvent(
-                fraction = 0.5f, expanded = true, tracking = true, dragDownPxAmount = 10f)
+                fraction = 0.5f,
+                expanded = true,
+                tracking = true,
+                dragDownPxAmount = 10f
+            )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 69d03d9..5d2d192 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -27,7 +27,6 @@
 import com.android.systemui.scene.shared.model.SceneModel
 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.Test
@@ -39,8 +38,8 @@
 @RunWith(JUnit4::class)
 class ShadeSceneViewModelTest : SysuiTestCase() {
 
-    private val testScope = TestScope()
-    private val utils = SceneTestUtils(this, testScope)
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
     private val sceneInteractor = utils.sceneInteractor()
     private val authenticationInteractor =
         utils.authenticationInteractor(
@@ -71,8 +70,10 @@
     fun upTransitionSceneKey_deviceLocked_lockScreen() =
         testScope.runTest {
             val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(false)
 
             assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Lockscreen)
         }
@@ -81,8 +82,10 @@
     fun upTransitionSceneKey_deviceUnlocked_gone() =
         testScope.runTest {
             val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.unlockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(true)
 
             assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone)
         }
@@ -91,8 +94,10 @@
     fun onContentClicked_deviceUnlocked_switchesToGone() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.unlockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(true)
             runCurrent()
 
             underTest.onContentClicked()
@@ -104,8 +109,10 @@
     fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
-            authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
-            authenticationInteractor.lockDevice()
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin(1234)
+            )
+            utils.authenticationRepository.setUnlocked(false)
             runCurrent()
 
             underTest.onContentClicked()
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 04c93cb..9495fdd 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
@@ -21,6 +21,8 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags.TRANSIT_CLOCK
 import com.android.systemui.plugins.ClockController
 import com.android.systemui.plugins.ClockId
 import com.android.systemui.plugins.ClockMetadata
@@ -68,6 +70,7 @@
     private lateinit var fakeDefaultProvider: FakeClockPlugin
     private lateinit var pluginListener: PluginListener<ClockProviderPlugin>
     private lateinit var registry: ClockRegistry
+    private val featureFlags = FakeFeatureFlags()
 
     companion object {
         private fun failFactory(clockId: ClockId): ClockController {
@@ -448,4 +451,44 @@
         val actual = ClockSettings.serialize(ClockSettings("ID", null))
         assertEquals(expected, actual)
     }
+
+    @Test
+    fun testTransitClockEnabled_hasTransitClock() {
+        testTransitClockFlag(true)
+    }
+
+    @Test
+    fun testTransitClockDisabled_noTransitClock() {
+        testTransitClockFlag(false)
+    }
+
+    private fun testTransitClockFlag(flag: Boolean) {
+        featureFlags.set(TRANSIT_CLOCK, flag)
+        registry.isTransitClockEnabled = featureFlags.isEnabled(TRANSIT_CLOCK)
+        val mockPluginLifecycle = mock<PluginLifecycleManager<ClockProviderPlugin>>()
+        val plugin = FakeClockPlugin()
+                .addClock("clock_1", "clock 1")
+                .addClock("DIGITAL_CLOCK_METRO", "metro clock")
+        pluginListener.onPluginLoaded(plugin, mockContext, mockPluginLifecycle)
+
+        val list = registry.getClocks()
+        if (flag) {
+            assertEquals(
+                    setOf(
+                            ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME),
+                            ClockMetadata("clock_1", "clock 1"),
+                            ClockMetadata("DIGITAL_CLOCK_METRO", "metro clock")
+                    ),
+                    list.toSet()
+            )
+        } else {
+            assertEquals(
+                    setOf(
+                            ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME),
+                            ClockMetadata("clock_1", "clock 1")
+                    ),
+                    list.toSet()
+            )
+        }
+    }
 }
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 b724175..0b1753f 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
@@ -555,60 +555,6 @@
     }
 
     /**
-     * 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
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java
index 109f185..22c9e45 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java
@@ -76,5 +76,6 @@
         mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID);
 
         verify(mWindowManager).requestAppKeyboardShortcuts(any(), anyInt());
+        verify(mWindowManager).requestImeKeyboardShortcuts(any(), anyInt());
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java
index ea822aa..a3ecde0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java
@@ -75,5 +75,6 @@
         mKeyboardShortcuts.toggle(mContext, DEVICE_ID);
 
         verify(mWindowManager).requestAppKeyboardShortcuts(any(), anyInt());
+        verify(mWindowManager).requestImeKeyboardShortcuts(any(), anyInt());
     }
 }
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 cd0c1352..b1f5dde 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -72,6 +72,7 @@
 import android.content.pm.UserInfo;
 import android.graphics.Color;
 import android.hardware.biometrics.BiometricFaceConstants;
+import android.hardware.biometrics.BiometricFingerprintConstants;
 import android.hardware.biometrics.BiometricSourceType;
 import android.hardware.fingerprint.FingerprintManager;
 import android.os.BatteryManager;
@@ -98,13 +99,15 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.biometrics.FaceHelpMessageDeferral;
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.flags.FakeFeatureFlags;
 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.keyguard.util.IndicationHelper;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.settings.UserTracker;
@@ -212,6 +215,7 @@
     private StatusBarStateController.StateListener mStatusBarStateListener;
     private ScreenLifecycle.Observer mScreenObserver;
     private BroadcastReceiver mBroadcastReceiver;
+    private IndicationHelper mIndicationHelper;
     private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
     private TestableLooper mTestableLooper;
     private final int mCurrentUserId = 1;
@@ -261,13 +265,14 @@
         when(mDevicePolicyManager.getDeviceOwnerType(DEVICE_OWNER_COMPONENT))
                 .thenReturn(DEVICE_OWNER_TYPE_DEFAULT);
 
-
         when(mDevicePolicyResourcesManager.getString(anyString(), any()))
                 .thenReturn(mDisclosureGeneric);
         when(mDevicePolicyResourcesManager.getString(anyString(), any(), anyString()))
                 .thenReturn(mDisclosureWithOrganization);
         when(mUserTracker.getUserId()).thenReturn(mCurrentUserId);
 
+        mIndicationHelper = new IndicationHelper(mKeyguardUpdateMonitor);
+
         mWakeLock = new WakeLockFake();
         mWakeLockBuilder = new WakeLockFake.Builder(mContext);
         mWakeLockBuilder.setWakeLock(mWakeLock);
@@ -302,7 +307,9 @@
                 mAlternateBouncerInteractor,
                 mAlarmManager,
                 mUserTracker,
-                flags
+                mock(BouncerMessageInteractor.class),
+                flags,
+                mIndicationHelper
         );
         mController.init();
         mController.setIndicationArea(mIndicationArea);
@@ -803,36 +810,22 @@
     }
 
     @Test
-    public void transientIndication_visibleWhenDozing_ignoresFingerprintCancellation() {
+    public void transientIndication_visibleWhenDozing_ignoresFingerprintErrorMsg() {
         createController();
-
         mController.setVisible(true);
         reset(mRotateTextViewController);
+
+        // WHEN a fingerprint error user cancelled message is received
         mController.getKeyguardCallback().onBiometricError(
-                FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED, "foo",
-                BiometricSourceType.FINGERPRINT);
-        mController.getKeyguardCallback().onBiometricError(
-                FingerprintManager.FINGERPRINT_ERROR_CANCELED, "bar",
+                BiometricFingerprintConstants.FINGERPRINT_ERROR_USER_CANCELED, "foo",
                 BiometricSourceType.FINGERPRINT);
 
+        // THEN no message is shown
         verifyNoMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE);
         verifyNoMessage(INDICATION_TYPE_TRANSIENT);
     }
 
     @Test
-    public void transientIndication_visibleWhenDozing_ignoresPowerPressed() {
-        createController();
-
-        mController.setVisible(true);
-        reset(mRotateTextViewController);
-        mController.getKeyguardCallback().onBiometricError(
-                FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED, "foo",
-                BiometricSourceType.FINGERPRINT);
-
-        verifyNoMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE);
-    }
-
-    @Test
     public void transientIndication_swipeUpToRetry() {
         createController();
         String message = mContext.getString(R.string.keyguard_retry);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index 2351f760..ff2f106 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -1,5 +1,6 @@
 package com.android.systemui.statusbar
 
+import android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
@@ -8,14 +9,21 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.qs.QS
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper
 import com.android.systemui.statusbar.notification.stack.AmbientState
@@ -25,7 +33,13 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.phone.LSShadeTransitionLogger
 import com.android.systemui.statusbar.phone.ScrimController
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
 import com.android.systemui.statusbar.policy.FakeConfigurationController
+import com.android.systemui.util.mockito.mock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import org.junit.After
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotNull
@@ -56,8 +70,11 @@
 @SmallTest
 @RunWithLooper(setAsMainLooper = true)
 @RunWith(AndroidTestingRunner::class)
+@OptIn(ExperimentalCoroutinesApi::class)
 class LockscreenShadeTransitionControllerTest : SysuiTestCase() {
 
+    private val testScope = TestScope(StandardTestDispatcher())
+
     lateinit var transitionController: LockscreenShadeTransitionController
     lateinit var row: ExpandableNotificationRow
     @Mock lateinit var statusbarStateController: SysuiStatusBarStateController
@@ -83,12 +100,31 @@
     @Mock lateinit var qsTransitionController: LockscreenShadeQsTransitionController
     @Mock lateinit var activityStarter: ActivityStarter
     @Mock lateinit var transitionControllerCallback: LockscreenShadeTransitionController.Callback
+    private val disableFlagsRepository = FakeDisableFlagsRepository()
+    private val shadeInteractor = ShadeInteractor(
+        testScope.backgroundScope,
+        disableFlagsRepository,
+        keyguardRepository = FakeKeyguardRepository(),
+        userSetupRepository = FakeUserSetupRepository(),
+        deviceProvisionedController = mock(),
+        userInteractor = mock(),
+    )
+    private val powerInteractor = PowerInteractor(
+        FakePowerRepository(),
+        FalsingCollectorFake(),
+        screenOffAnimationController = mock(),
+        statusBarStateController = mock(),
+    )
     @JvmField @Rule val mockito = MockitoJUnit.rule()
 
     private val configurationController = FakeConfigurationController()
 
     @Before
     fun setup() {
+        // By default, have the shade enabled
+        disableFlagsRepository.disableFlags.value = DisableFlagsModel()
+        testScope.runCurrent()
+
         val helper = NotificationTestHelper(
                 mContext,
                 mDependency,
@@ -129,6 +165,8 @@
                 qsTransitionControllerFactory = { qsTransitionController },
                 activityStarter = activityStarter,
                 shadeRepository = FakeShadeRepository(),
+                shadeInteractor = shadeInteractor,
+                powerInteractor = powerInteractor,
             )
         transitionController.addCallback(transitionControllerCallback)
         whenever(nsslController.view).thenReturn(stackscroller)
@@ -203,7 +241,10 @@
 
     @Test
     fun testDontGoWhenShadeDisabled() {
-        whenever(mCentralSurfaces.isShadeDisabled).thenReturn(true)
+        disableFlagsRepository.disableFlags.value = DisableFlagsModel(
+            disable2 = DISABLE2_NOTIFICATION_SHADE,
+        )
+        testScope.runCurrent()
         transitionController.goToLockedShade(null)
         verify(statusbarStateController, never()).setState(anyInt())
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
index ced0734..305f48b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
@@ -33,25 +33,21 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.RemoteInputControllerLogger;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.policy.RemoteInputUriController;
 
-import dagger.Lazy;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.Optional;
-
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
@@ -69,6 +65,7 @@
     @Mock private RemoteInputUriController mRemoteInputUriController;
     @Mock private NotificationClickNotifier mClickNotifier;
     @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
+    @Mock private PowerInteractor mPowerInteractor;
 
     private TestableNotificationRemoteInputManager mRemoteInputManager;
     private NotificationEntry mEntry;
@@ -82,7 +79,7 @@
                 mLockscreenUserManager,
                 mSmartReplyController,
                 mVisibilityProvider,
-                () -> Optional.of(mock(CentralSurfaces.class)),
+                mPowerInteractor,
                 mStateController,
                 mRemoteInputUriController,
                 mock(RemoteInputControllerLogger.class),
@@ -140,7 +137,7 @@
                 NotificationLockscreenUserManager lockscreenUserManager,
                 SmartReplyController smartReplyController,
                 NotificationVisibilityProvider visibilityProvider,
-                Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
+                PowerInteractor powerInteractor,
                 StatusBarStateController statusBarStateController,
                 RemoteInputUriController remoteInputUriController,
                 RemoteInputControllerLogger remoteInputControllerLogger,
@@ -153,7 +150,7 @@
                     lockscreenUserManager,
                     smartReplyController,
                     visibilityProvider,
-                    centralSurfacesOptionalLazy,
+                    powerInteractor,
                     statusBarStateController,
                     remoteInputUriController,
                     remoteInputControllerLogger,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/CommandParserTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/CommandParserTest.kt
new file mode 100644
index 0000000..cfbe8e3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/CommandParserTest.kt
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.commandline
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertThrows
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SmallTest
+class CommandParserTest : SysuiTestCase() {
+    private val parser = CommandParser()
+
+    @Test
+    fun registerToken_cannotReuseNames() {
+        parser.flag("-f")
+        assertThrows(IllegalArgumentException::class.java) { parser.flag("-f") }
+    }
+
+    @Test
+    fun unknownToken_throws() {
+        assertThrows(ArgParseError::class.java) { parser.parse(listOf("unknown-token")) }
+    }
+
+    @Test
+    fun parseSingleFlag_present() {
+        val flag by parser.flag("-f")
+        parser.parse(listOf("-f"))
+        assertTrue(flag)
+    }
+
+    @Test
+    fun parseSingleFlag_notPresent() {
+        val flag by parser.flag("-f")
+        parser.parse(listOf())
+        assertFalse(flag)
+    }
+
+    @Test
+    fun parseSingleOptionalParam_present() {
+        val param by parser.param("-p", valueParser = Type.Int)
+        parser.parse(listOf("-p", "123"))
+        assertThat(param).isEqualTo(123)
+    }
+
+    @Test
+    fun parseSingleOptionalParam_notPresent() {
+        val param by parser.param("-p", valueParser = Type.Int)
+        parser.parse(listOf())
+        assertThat(param).isNull()
+    }
+
+    @Test
+    fun parseSingleOptionalParam_missingArg_throws() {
+        val param by parser.param("-p", valueParser = Type.Int)
+        assertThrows(ArgParseError::class.java) { parser.parse(listOf("-p")) }
+    }
+
+    @Test
+    fun parseSingleRequiredParam_present() {
+        val param by parser.require(parser.param("-p", valueParser = Type.Int))
+        parser.parse(listOf("-p", "123"))
+        assertThat(param).isEqualTo(123)
+    }
+
+    @Test
+    fun parseSingleRequiredParam_notPresent_failsValidation() {
+        val param by parser.require(parser.param("-p", valueParser = Type.Int))
+        assertFalse(parser.parse(listOf()))
+    }
+
+    @Test
+    fun parseSingleRequiredParam_missingArg_throws() {
+        val param by parser.require(parser.param("-p", valueParser = Type.Int))
+        assertThrows(ArgParseError::class.java) { parser.parse(listOf("-p")) }
+    }
+
+    @Test
+    fun parseAsSubCommand_singleFlag_present() {
+        val flag by parser.flag("-f")
+        val args = listOf("-f").listIterator()
+        parser.parseAsSubCommand(args)
+
+        assertTrue(flag)
+    }
+
+    @Test
+    fun parseAsSubCommand_singleFlag_notPresent() {
+        val flag by parser.flag("-f")
+        val args = listOf("--other-flag").listIterator()
+        parser.parseAsSubCommand(args)
+
+        assertFalse(flag)
+    }
+
+    @Test
+    fun parseAsSubCommand_singleOptionalParam_present() {
+        val param by parser.param("-p", valueParser = Type.Int)
+        parser.parseAsSubCommand(listOf("-p", "123", "--other-arg", "321").listIterator())
+        assertThat(param).isEqualTo(123)
+    }
+
+    @Test
+    fun parseAsSubCommand_singleOptionalParam_notPresent() {
+        val param by parser.param("-p", valueParser = Type.Int)
+        parser.parseAsSubCommand(listOf("--other-arg", "321").listIterator())
+        assertThat(param).isNull()
+    }
+
+    @Test
+    fun parseAsSubCommand_singleRequiredParam_present() {
+        val param by parser.require(parser.param("-p", valueParser = Type.Int))
+        parser.parseAsSubCommand(listOf("-p", "123", "--other-arg", "321").listIterator())
+        assertThat(param).isEqualTo(123)
+    }
+
+    @Test
+    fun parseAsSubCommand_singleRequiredParam_notPresent() {
+        parser.require(parser.param("-p", valueParser = Type.Int))
+        assertFalse(parser.parseAsSubCommand(listOf("--other-arg", "321").listIterator()))
+    }
+
+    @Test
+    fun parseCommandWithSubCommand_required_provided() {
+        val topLevelFlag by parser.flag("flag", shortName = "-f")
+
+        val cmd =
+            object : ParseableCommand("test") {
+                val flag by flag("flag1")
+                override fun execute(pw: PrintWriter) {}
+            }
+
+        parser.require(parser.subCommand(cmd))
+        parser.parse(listOf("-f", "test", "--flag1"))
+
+        assertTrue(topLevelFlag)
+        assertThat(cmd).isNotNull()
+        assertTrue(cmd.flag)
+    }
+
+    @Test
+    fun parseCommandWithSubCommand_required_notProvided() {
+        val topLevelFlag by parser.flag("-f")
+
+        val cmd =
+            object : ParseableCommand("test") {
+                val flag by parser.flag("flag1")
+                override fun execute(pw: PrintWriter) {}
+            }
+
+        parser.require(parser.subCommand(cmd))
+
+        assertFalse(parser.parse(listOf("-f")))
+    }
+
+    @Test
+    fun flag_requiredParam_optionalParam_allProvided_failsValidation() {
+        val flag by parser.flag("-f")
+        val optionalParam by parser.param("-p", valueParser = Type.Int)
+        val requiredParam by parser.require(parser.param("-p2", valueParser = Type.Boolean))
+
+        parser.parse(
+            listOf(
+                "-f",
+                "-p",
+                "123",
+                "-p2",
+                "false",
+            )
+        )
+
+        assertTrue(flag)
+        assertThat(optionalParam).isEqualTo(123)
+        assertFalse(requiredParam)
+    }
+
+    @Test
+    fun flag_requiredParam_optionalParam_optionalExcluded() {
+        val flag by parser.flag("-f")
+        val optionalParam by parser.param("-p", valueParser = Type.Int)
+        val requiredParam by parser.require(parser.param("-p2", valueParser = Type.Boolean))
+
+        parser.parse(
+            listOf(
+                "-p2",
+                "true",
+            )
+        )
+
+        assertFalse(flag)
+        assertThat(optionalParam).isNull()
+        assertTrue(requiredParam)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/ParametersTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/ParametersTest.kt
new file mode 100644
index 0000000..e391d6b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/ParametersTest.kt
@@ -0,0 +1,55 @@
+package com.android.systemui.statusbar.commandline
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertThrows
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SmallTest
+class ParametersTest : SysuiTestCase() {
+    @Test
+    fun singleArgOptional_returnsNullBeforeParse() {
+        val optional by SingleArgParamOptional(longName = "longName", valueParser = Type.Int)
+        assertThat(optional).isNull()
+    }
+
+    @Test
+    fun singleArgOptional_returnsParsedValue() {
+        val param = SingleArgParamOptional(longName = "longName", valueParser = Type.Int)
+        param.parseArgsFromIter(listOf("3").listIterator())
+        val optional by param
+        assertThat(optional).isEqualTo(3)
+    }
+
+    @Test
+    fun singleArgRequired_throwsBeforeParse() {
+        val req by SingleArgParam(longName = "param", valueParser = Type.Boolean)
+        assertThrows(IllegalStateException::class.java) { req }
+    }
+
+    @Test
+    fun singleArgRequired_returnsParsedValue() {
+        val param = SingleArgParam(longName = "param", valueParser = Type.Boolean)
+        param.parseArgsFromIter(listOf("true").listIterator())
+        val req by param
+        assertTrue(req)
+    }
+
+    @Test
+    fun param_handledAfterParse() {
+        val optParam = SingleArgParamOptional(longName = "string1", valueParser = Type.String)
+        val reqParam = SingleArgParam(longName = "string2", valueParser = Type.Float)
+
+        assertFalse(optParam.handled)
+        assertFalse(reqParam.handled)
+
+        optParam.parseArgsFromIter(listOf("test").listIterator())
+        reqParam.parseArgsFromIter(listOf("1.23").listIterator())
+
+        assertTrue(optParam.handled)
+        assertTrue(reqParam.handled)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/ParseableCommandTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/ParseableCommandTest.kt
new file mode 100644
index 0000000..86548d0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/ParseableCommandTest.kt
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.commandline
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import org.junit.Assert.assertThrows
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class ParseableCommandTest : SysuiTestCase() {
+    @Mock private lateinit var pw: PrintWriter
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    /**
+     * A little change-detector-y, but this is just a general assertion that building up a command
+     * parser via its wrapper works as expected.
+     */
+    @Test
+    fun testFactoryMethods() {
+        val mySubCommand =
+            object : ParseableCommand("subCommand") {
+                val flag by flag("flag")
+                override fun execute(pw: PrintWriter) {}
+            }
+
+        val mySubCommand2 =
+            object : ParseableCommand("subCommand2") {
+                val flag by flag("flag")
+                override fun execute(pw: PrintWriter) {}
+            }
+
+        // Verify that the underlying parser contains the correct types
+        val myCommand =
+            object : ParseableCommand("testName") {
+                val flag by flag("flag", shortName = "f")
+                val requiredParam by
+                    param(longName = "required-param", shortName = "r", valueParser = Type.String)
+                        .required()
+                val optionalParam by
+                    param(longName = "optional-param", shortName = "o", valueParser = Type.Boolean)
+                val optionalSubCommand by subCommand(mySubCommand)
+                val requiredSubCommand by subCommand(mySubCommand2).required()
+
+                override fun execute(pw: PrintWriter) {}
+            }
+
+        val flags = myCommand.parser.flags
+        val params = myCommand.parser.params
+        val subCommands = myCommand.parser.subCommands
+
+        assertThat(flags).hasSize(2)
+        assertThat(flags[0]).isInstanceOf(Flag::class.java)
+        assertThat(flags[1]).isInstanceOf(Flag::class.java)
+
+        assertThat(params).hasSize(2)
+        val req = params.filter { it is SingleArgParam<*> }
+        val opt = params.filter { it is SingleArgParamOptional<*> }
+        assertThat(req).hasSize(1)
+        assertThat(opt).hasSize(1)
+
+        val reqSub = subCommands.filter { it is RequiredSubCommand<*> }
+        val optSub = subCommands.filter { it is OptionalSubCommand<*> }
+        assertThat(reqSub).hasSize(1)
+        assertThat(optSub).hasSize(1)
+    }
+
+    @Test
+    fun factoryMethods_enforceShortNameRules() {
+        // Short names MUST be one character long
+        assertThrows(IllegalArgumentException::class.java) {
+            val myCommand =
+                object : ParseableCommand("test-command") {
+                    val flag by flag("longName", "invalidShortName")
+
+                    override fun execute(pw: PrintWriter) {}
+                }
+        }
+
+        assertThrows(IllegalArgumentException::class.java) {
+            val myCommand =
+                object : ParseableCommand("test-command") {
+                    val param by param("longName", "invalidShortName", valueParser = Type.String)
+
+                    override fun execute(pw: PrintWriter) {}
+                }
+        }
+    }
+
+    @Test
+    fun factoryMethods_enforceLongNames_notPrefixed() {
+        // Long names must not start with "-", since they will be added
+        assertThrows(IllegalArgumentException::class.java) {
+            val myCommand =
+                object : ParseableCommand("test-command") {
+                    val flag by flag("--invalid")
+
+                    override fun execute(pw: PrintWriter) {}
+                }
+        }
+
+        assertThrows(IllegalArgumentException::class.java) {
+            val myCommand =
+                object : ParseableCommand("test-command") {
+                    val param by param("-invalid", valueParser = Type.String)
+
+                    override fun execute(pw: PrintWriter) {}
+                }
+        }
+    }
+
+    @Test
+    fun executeDoesNotPropagateExceptions() {
+        val cmd =
+            object : ParseableCommand("test-command") {
+                val flag by flag("flag")
+                override fun execute(pw: PrintWriter) {}
+            }
+
+        val throwingCommand = listOf("unknown-token")
+
+        // Given a command that would cause an ArgParseError
+        assertThrows(ArgParseError::class.java) { cmd.parser.parse(throwingCommand) }
+
+        // The parser consumes that error
+        cmd.execute(pw, throwingCommand)
+    }
+
+    @Test
+    fun executeFailingCommand_callsOnParseFailed() {
+        val cmd =
+            object : ParseableCommand("test-command") {
+                val flag by flag("flag")
+
+                var onParseFailedCalled = false
+
+                override fun execute(pw: PrintWriter) {}
+                override fun onParseFailed(error: ArgParseError) {
+                    onParseFailedCalled = true
+                }
+            }
+
+        val throwingCommand = listOf("unknown-token")
+        cmd.execute(pw, throwingCommand)
+
+        assertTrue(cmd.onParseFailedCalled)
+    }
+
+    @Test
+    fun baseCommand() {
+        val myCommand = MyCommand()
+        myCommand.execute(pw, baseCommand)
+
+        assertThat(myCommand.flag1).isFalse()
+        assertThat(myCommand.singleParam).isNull()
+    }
+
+    @Test
+    fun commandWithFlags() {
+        val command = MyCommand()
+        command.execute(pw, cmdWithFlags)
+
+        assertThat(command.flag1).isTrue()
+        assertThat(command.flag2).isTrue()
+    }
+
+    @Test
+    fun commandWithArgs() {
+        val cmd = MyCommand()
+        cmd.execute(pw, cmdWithSingleArgParam)
+
+        assertThat(cmd.singleParam).isEqualTo("single_param")
+    }
+
+    @Test
+    fun commandWithRequiredParam_provided() {
+        val cmd =
+            object : ParseableCommand(name) {
+                val singleRequiredParam: String by
+                    param(
+                            longName = "param1",
+                            shortName = "p",
+                            valueParser = Type.String,
+                        )
+                        .required()
+
+                override fun execute(pw: PrintWriter) {}
+            }
+
+        val cli = listOf("-p", "value")
+        cmd.execute(pw, cli)
+
+        assertThat(cmd.singleRequiredParam).isEqualTo("value")
+    }
+
+    @Test
+    fun commandWithRequiredParam_not_provided_throws() {
+        val cmd =
+            object : ParseableCommand(name) {
+                val singleRequiredParam by
+                    param(shortName = "p", longName = "param1", valueParser = Type.String)
+                        .required()
+
+                override fun execute(pw: PrintWriter) {}
+
+                override fun execute(pw: PrintWriter, args: List<String>) {
+                    parser.parse(args)
+                    execute(pw)
+                }
+            }
+
+        val cli = listOf("")
+        assertThrows(ArgParseError::class.java) { cmd.execute(pw, cli) }
+    }
+
+    @Test
+    fun commandWithSubCommand() {
+        val subName = "sub-command"
+        val subCmd =
+            object : ParseableCommand(subName) {
+                val singleOptionalParam: String? by param("param", valueParser = Type.String)
+
+                override fun execute(pw: PrintWriter) {}
+            }
+
+        val cmd =
+            object : ParseableCommand(name) {
+                val subCmd by subCommand(subCmd)
+                override fun execute(pw: PrintWriter) {}
+            }
+
+        cmd.execute(pw, listOf("sub-command", "--param", "test"))
+        assertThat(cmd.subCmd?.singleOptionalParam).isEqualTo("test")
+    }
+
+    @Test
+    fun complexCommandWithSubCommands_reusedNames() {
+        val commandLine = "-f --param1 arg1 sub-command1 -f -p arg2 --param2 arg3".split(" ")
+
+        val subName = "sub-command1"
+        val subCmd =
+            object : ParseableCommand(subName) {
+                val flag1 by flag("flag", shortName = "f")
+                val param1: String? by param("param1", shortName = "p", valueParser = Type.String)
+
+                override fun execute(pw: PrintWriter) {}
+            }
+
+        val myCommand =
+            object : ParseableCommand(name) {
+                val flag1 by flag(longName = "flag", shortName = "f")
+                val param1 by param("param1", shortName = "p", valueParser = Type.String).required()
+                val param2: String? by param(longName = "param2", valueParser = Type.String)
+                val subCommand by subCommand(subCmd)
+
+                override fun execute(pw: PrintWriter) {}
+            }
+
+        myCommand.execute(pw, commandLine)
+
+        assertThat(myCommand.flag1).isTrue()
+        assertThat(myCommand.param1).isEqualTo("arg1")
+        assertThat(myCommand.param2).isEqualTo("arg3")
+        assertThat(myCommand.subCommand).isNotNull()
+        assertThat(myCommand.subCommand?.flag1).isTrue()
+        assertThat(myCommand.subCommand?.param1).isEqualTo("arg2")
+    }
+
+    class MyCommand(
+        private val onExecute: ((MyCommand) -> Unit)? = null,
+    ) : ParseableCommand(name) {
+
+        val flag1 by flag(shortName = "f", longName = "flag1", description = "flag 1 for test")
+        val flag2 by flag(shortName = "g", longName = "flag2", description = "flag 2 for test")
+        val singleParam: String? by
+            param(
+                shortName = "a",
+                longName = "arg1",
+                valueParser = Type.String,
+            )
+
+        override fun execute(pw: PrintWriter) {
+            onExecute?.invoke(this)
+        }
+    }
+
+    companion object {
+        const val name = "my_command"
+        val baseCommand = listOf("")
+        val cmdWithFlags = listOf("-f", "--flag2")
+        val cmdWithSingleArgParam = listOf("--arg1", "single_param")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/ValueParserTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/ValueParserTest.kt
new file mode 100644
index 0000000..759f0bc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/ValueParserTest.kt
@@ -0,0 +1,61 @@
+package com.android.systemui.statusbar.commandline
+
+import android.graphics.Rect
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SmallTest
+class ValueParserTest : SysuiTestCase() {
+    @Test
+    fun parseString() {
+        assertThat(Type.String.parseValue("test")).isEqualTo(Result.success("test"))
+    }
+
+    @Test
+    fun parseInt() {
+        assertThat(Type.Int.parseValue("123")).isEqualTo(Result.success(123))
+
+        assertTrue(Type.Int.parseValue("not an Int").isFailure)
+    }
+
+    @Test
+    fun parseFloat() {
+        assertThat(Type.Float.parseValue("1.23")).isEqualTo(Result.success(1.23f))
+
+        assertTrue(Type.Int.parseValue("not a Float").isFailure)
+    }
+
+    @Test
+    fun parseBoolean() {
+        assertThat(Type.Boolean.parseValue("true")).isEqualTo(Result.success(true))
+        assertThat(Type.Boolean.parseValue("false")).isEqualTo(Result.success(false))
+
+        assertTrue(Type.Boolean.parseValue("not a Boolean").isFailure)
+    }
+
+    @Test
+    fun mapToComplexType() {
+        val parseSquare = Type.Int.map { Rect(it, it, it, it) }
+
+        assertThat(parseSquare.parseValue("10")).isEqualTo(Result.success(Rect(10, 10, 10, 10)))
+    }
+
+    @Test
+    fun mapToFallibleComplexType() {
+        val fallibleParseSquare =
+            Type.Int.map {
+                if (it > 0) {
+                    Rect(it, it, it, it)
+                } else {
+                    null
+                }
+            }
+
+        assertThat(fallibleParseSquare.parseValue("10"))
+            .isEqualTo(Result.success(Rect(10, 10, 10, 10)))
+        assertTrue(fallibleParseSquare.parseValue("-10").isFailure)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/DisableFlagsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/DisableFlagsLoggerTest.kt
index 4b5d0a3..f1c7956 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/DisableFlagsLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/DisableFlagsLoggerTest.kt
@@ -37,73 +37,8 @@
     private val disableFlagsLogger = DisableFlagsLogger(disable1Flags, disable2Flags)
 
     @Test
-    fun getDisableFlagsString_oldAndNewSame_newAndUnchangedLoggedOldNotLogged() {
-        val state = DisableFlagsLogger.DisableState(
-                0b111, // ABC
-                0b01 // mN
-        )
-
-        val result = disableFlagsLogger.getDisableFlagsString(state, state)
-
-        assertThat(result).doesNotContain("Old")
-        assertThat(result).contains("ABC.mN")
-        assertThat(result).contains("(unchanged)")
-    }
-
-    @Test
-    fun getDisableFlagsString_oldAndNewDifferent_statesAndDiffLogged() {
-        val result = disableFlagsLogger.getDisableFlagsString(
-                DisableFlagsLogger.DisableState(
-                        0b111, // ABC
-                        0b01, // mN
-                ),
-                DisableFlagsLogger.DisableState(
-                        0b001, // abC
-                        0b10 // Mn
-                )
-        )
-
-        assertThat(result).contains("Old: ABC.mN")
-        assertThat(result).contains("New: abC.Mn")
-        assertThat(result).contains("(changed: ab.Mn)")
-    }
-
-    @Test
-    fun getDisableFlagsString_onlyDisable2Different_diffLoggedCorrectly() {
-        val result = disableFlagsLogger.getDisableFlagsString(
-                DisableFlagsLogger.DisableState(
-                        0b001, // abC
-                        0b01, // mN
-                ),
-                DisableFlagsLogger.DisableState(
-                        0b001, // abC
-                        0b00 // mn
-                )
-        )
-
-        assertThat(result).contains("(changed: .n)")
-    }
-
-    @Test
-    fun getDisableFlagsString_nullOld_onlyNewStateLogged() {
-        val result = disableFlagsLogger.getDisableFlagsString(
-            old = null,
-            new = DisableFlagsLogger.DisableState(
-                0b001, // abC
-                0b01, // mN
-            ),
-        )
-
-        assertThat(result).doesNotContain("Old")
-        assertThat(result).contains("abC.mN")
-        assertThat(result).doesNotContain("(")
-        assertThat(result).doesNotContain(")")
-    }
-
-    @Test
     fun getDisableFlagsString_nullLocalModification_localModNotLogged() {
         val result = disableFlagsLogger.getDisableFlagsString(
-                DisableFlagsLogger.DisableState(0, 0),
                 DisableFlagsLogger.DisableState(1, 1),
                 newAfterLocalModification = null
         )
@@ -118,9 +53,7 @@
                 0b10 // mn
         )
 
-        val result = disableFlagsLogger.getDisableFlagsString(
-                DisableFlagsLogger.DisableState(0, 0), newState, newState
-        )
+        val result = disableFlagsLogger.getDisableFlagsString(newState, newState)
 
         assertThat(result).doesNotContain("local modification")
     }
@@ -128,7 +61,6 @@
     @Test
     fun getDisableFlagsString_newAfterLocalModificationDifferent_localModAndDiffLogged() {
         val result = disableFlagsLogger.getDisableFlagsString(
-                old = DisableFlagsLogger.DisableState(0, 0),
                 new = DisableFlagsLogger.DisableState(
                         0b000, // abc
                         0b00 // mn
@@ -143,6 +75,22 @@
     }
 
     @Test
+    fun getDisableFlagsString_onlyDisable2Different_diffLoggedCorrectly() {
+        val result = disableFlagsLogger.getDisableFlagsString(
+            DisableFlagsLogger.DisableState(
+                0b001, // abC
+                0b01, // mN
+            ),
+            DisableFlagsLogger.DisableState(
+                0b001, // abC
+                0b00 // mn
+            )
+        )
+
+        assertThat(result).contains("(changed: .n)")
+    }
+
+    @Test
     fun constructor_defaultDisableFlags_noException() {
         // Just creating the logger with the default params will trigger the exception if there
         // is one.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt
new file mode 100644
index 0000000..580463a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.disableflags.data.repository
+
+import android.app.StatusBarManager.DISABLE2_NONE
+import android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE
+import android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS
+import android.app.StatusBarManager.DISABLE_CLOCK
+import android.app.StatusBarManager.DISABLE_NONE
+import android.app.StatusBarManager.DISABLE_NOTIFICATION_ALERTS
+import android.content.res.Configuration
+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.log.LogBufferFactory
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
+import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.verify
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class DisableFlagsRepositoryTest : SysuiTestCase() {
+
+    private lateinit var underTest: DisableFlagsRepository
+
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+    private val commandQueue: CommandQueue = mock()
+    private val configurationController: ConfigurationController = mock()
+    private val remoteInputQuickSettingsDisabler =
+        RemoteInputQuickSettingsDisabler(
+            context,
+            commandQueue,
+            configurationController,
+        )
+    private val logBuffer = LogBufferFactory(DumpManager(), mock()).create("buffer", 10)
+    private val disableFlagsLogger = DisableFlagsLogger()
+
+    @Before
+    fun setUp() {
+        underTest =
+            DisableFlagsRepositoryImpl(
+                commandQueue,
+                DISPLAY_ID,
+                testScope.backgroundScope,
+                remoteInputQuickSettingsDisabler,
+                logBuffer,
+                disableFlagsLogger,
+            )
+    }
+
+    @Test
+    fun disableFlags_initialValue_none() {
+        assertThat(underTest.disableFlags.value)
+            .isEqualTo(DisableFlagsModel(DISABLE_NONE, DISABLE2_NONE))
+    }
+
+    @Test
+    fun disableFlags_noSubscribers_callbackStillRegistered() =
+        testScope.runTest { verify(commandQueue).addCallback(any()) }
+
+    @Test
+    fun disableFlags_notifAlertsNotDisabled_notifAlertsEnabledTrue() =
+        testScope.runTest {
+            getCommandQueueCallback()
+                .disable(DISPLAY_ID, DISABLE_NONE, DISABLE2_NONE, /* animate= */ false)
+
+            assertThat(underTest.disableFlags.value.areNotificationAlertsEnabled()).isTrue()
+        }
+
+    @Test
+    fun disableFlags_notifAlertsDisabled_notifAlertsEnabledFalse() =
+        testScope.runTest {
+            getCommandQueueCallback()
+                .disable(
+                    DISPLAY_ID,
+                    DISABLE_NOTIFICATION_ALERTS,
+                    DISABLE2_NONE,
+                    /* animate= */ false,
+                )
+
+            assertThat(underTest.disableFlags.value.areNotificationAlertsEnabled()).isFalse()
+        }
+
+    @Test
+    fun disableFlags_notifAlertsDisabled_differentDisplay_notifAlertsEnabledTrue() =
+        testScope.runTest {
+            val wrongDisplayId = DISPLAY_ID + 10
+
+            getCommandQueueCallback()
+                .disable(
+                    wrongDisplayId,
+                    DISABLE_NOTIFICATION_ALERTS,
+                    DISABLE2_NONE,
+                    /* animate= */ false,
+                )
+
+            // THEN our repo reports them as still enabled
+            assertThat(underTest.disableFlags.value.areNotificationAlertsEnabled()).isTrue()
+        }
+
+    @Test
+    fun disableFlags_shadeNotDisabled_shadeEnabledTrue() =
+        testScope.runTest {
+            getCommandQueueCallback()
+                .disable(DISPLAY_ID, DISABLE_NONE, DISABLE2_NONE, /* animate= */ false)
+
+            assertThat(underTest.disableFlags.value.isShadeEnabled()).isTrue()
+        }
+
+    @Test
+    fun disableFlags_shadeDisabled_shadeEnabledFalse() =
+        testScope.runTest {
+            getCommandQueueCallback()
+                .disable(
+                    DISPLAY_ID,
+                    DISABLE_NONE,
+                    DISABLE2_NOTIFICATION_SHADE,
+                    /* animate= */ false,
+                )
+
+            assertThat(underTest.disableFlags.value.isShadeEnabled()).isFalse()
+        }
+
+    @Test
+    fun disableFlags_shadeDisabled_differentDisplay_shadeEnabledTrue() =
+        testScope.runTest {
+            val wrongDisplayId = DISPLAY_ID + 10
+
+            getCommandQueueCallback()
+                .disable(
+                    wrongDisplayId,
+                    DISABLE_NONE,
+                    DISABLE2_NOTIFICATION_SHADE,
+                    /* animate= */ false,
+                )
+
+            // THEN our repo reports them as still enabled
+            assertThat(underTest.disableFlags.value.isShadeEnabled()).isTrue()
+        }
+
+    @Test
+    fun disableFlags_quickSettingsNotDisabled_quickSettingsEnabledTrue() =
+        testScope.runTest {
+            getCommandQueueCallback()
+                .disable(DISPLAY_ID, DISABLE_NONE, DISABLE2_NONE, /* animate= */ false)
+
+            assertThat(underTest.disableFlags.value.isQuickSettingsEnabled()).isTrue()
+        }
+
+    @Test
+    fun disableFlags_quickSettingsDisabled_quickSettingsEnabledFalse() =
+        testScope.runTest {
+            getCommandQueueCallback()
+                .disable(
+                    DISPLAY_ID,
+                    DISABLE_NONE,
+                    DISABLE2_QUICK_SETTINGS,
+                    /* animate= */ false,
+                )
+
+            assertThat(underTest.disableFlags.value.isQuickSettingsEnabled()).isFalse()
+        }
+
+    @Test
+    fun disableFlags_quickSettingsDisabled_differentDisplay_quickSettingsEnabledTrue() =
+        testScope.runTest {
+            val wrongDisplayId = DISPLAY_ID + 10
+
+            getCommandQueueCallback()
+                .disable(
+                    wrongDisplayId,
+                    DISABLE_NONE,
+                    DISABLE2_QUICK_SETTINGS,
+                    /* animate= */ false,
+                )
+
+            // THEN our repo reports them as still enabled
+            assertThat(underTest.disableFlags.value.isQuickSettingsEnabled()).isTrue()
+        }
+
+    @Test
+    fun disableFlags_remoteInputActive_quickSettingsEnabledFalse() =
+        testScope.runTest {
+            // WHEN remote input is set up to be active
+            val configuration = Configuration(mContext.resources.configuration)
+            configuration.orientation = Configuration.ORIENTATION_LANDSCAPE
+            mContext.orCreateTestableResources.addOverride(
+                R.bool.config_use_split_notification_shade,
+                /* value= */ false
+            )
+            remoteInputQuickSettingsDisabler.setRemoteInputActive(true)
+            remoteInputQuickSettingsDisabler.onConfigChanged(configuration)
+
+            getCommandQueueCallback()
+                .disable(
+                    DISPLAY_ID,
+                    DISABLE_NONE,
+                    DISABLE2_NONE,
+                    /* animate= */ false,
+                )
+
+            // THEN quick settings is disabled (even if the disable flags don't say so)
+            assertThat(underTest.disableFlags.value.isQuickSettingsEnabled()).isFalse()
+        }
+
+    @Test
+    fun disableFlags_reactsToChanges() =
+        testScope.runTest {
+            getCommandQueueCallback()
+                .disable(
+                    DISPLAY_ID,
+                    DISABLE_NOTIFICATION_ALERTS,
+                    DISABLE2_NONE,
+                    /* animate= */ false,
+                )
+            assertThat(underTest.disableFlags.value.areNotificationAlertsEnabled()).isFalse()
+
+            getCommandQueueCallback()
+                .disable(
+                    DISPLAY_ID,
+                    DISABLE_CLOCK, // Unrelated to notifications
+                    DISABLE2_NONE,
+                    /* animate= */ false,
+                )
+            assertThat(underTest.disableFlags.value.areNotificationAlertsEnabled()).isTrue()
+
+            getCommandQueueCallback()
+                .disable(
+                    DISPLAY_ID,
+                    DISABLE_NOTIFICATION_ALERTS,
+                    DISABLE2_QUICK_SETTINGS or DISABLE2_NOTIFICATION_SHADE,
+                    /* animate= */ false,
+                )
+            assertThat(underTest.disableFlags.value.areNotificationAlertsEnabled()).isFalse()
+            assertThat(underTest.disableFlags.value.isShadeEnabled()).isFalse()
+            assertThat(underTest.disableFlags.value.isQuickSettingsEnabled()).isFalse()
+        }
+
+    private fun getCommandQueueCallback(): CommandQueue.Callbacks {
+        val callbackCaptor = argumentCaptor<CommandQueue.Callbacks>()
+        verify(commandQueue).addCallback(callbackCaptor.capture())
+        return callbackCaptor.value
+    }
+
+    private companion object {
+        const val DISPLAY_ID = 1
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt
new file mode 100644
index 0000000..b66231c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.disableflags.data.repository
+
+import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeDisableFlagsRepository : DisableFlagsRepository {
+    override val disableFlags = MutableStateFlow(DisableFlagsModel())
+}
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
index 914301f..2af0ceb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
@@ -69,6 +69,8 @@
 
     @Mock private lateinit var listener: SystemStatusAnimationCallback
 
+    @Mock private lateinit var logger: SystemStatusAnimationSchedulerLogger
+
     private lateinit var systemClock: FakeSystemClock
     private lateinit var chipAnimationController: SystemEventChipAnimationController
     private lateinit var systemStatusAnimationScheduler: SystemStatusAnimationScheduler
@@ -538,7 +540,8 @@
                 statusBarWindowController,
                 dumpManager,
                 systemClock,
-                CoroutineScope(StandardTestDispatcher(testScope.testScheduler))
+                CoroutineScope(StandardTestDispatcher(testScope.testScheduler)),
+                logger
             )
         // add a mock listener
         systemStatusAnimationScheduler.addCallback(listener)
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
index a37c386..902dd51 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLoggerTest.kt
@@ -20,8 +20,8 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.StatusBarState
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
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 2fbe871..ea70e9e 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
@@ -32,7 +32,6 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
@@ -46,11 +45,14 @@
 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.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.withArgCaptor
 import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
+import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestCoroutineScheduler
@@ -62,9 +64,8 @@
 import org.mockito.ArgumentMatchers.same
 import org.mockito.Mockito.anyString
 import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
-import java.util.function.Consumer
-import kotlin.time.Duration.Companion.seconds
 import org.mockito.Mockito.`when` as whenever
 
 @SmallTest
@@ -75,7 +76,6 @@
     private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock()
     private val keyguardRepository = FakeKeyguardRepository()
     private val keyguardTransitionRepository = FakeKeyguardTransitionRepository()
-    private val notifPipelineFlags: NotifPipelineFlags = mock()
     private val notifPipeline: NotifPipeline = mock()
     private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock()
     private val statusBarStateController: StatusBarStateController = mock()
@@ -136,13 +136,8 @@
             )
             testScheduler.runCurrent()
 
-            // WHEN: The shade is expanded
-            whenever(statusBarStateController.isExpanded).thenReturn(true)
-            statusBarStateListener.onExpandedChanged(true)
-            testScheduler.runCurrent()
-
-            // THEN: The notification is still treated as "unseen" and is not filtered out.
-            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+            // THEN: We are no longer listening for shade expansions
+            verify(statusBarStateController, never()).addCallback(any())
         }
     }
 
@@ -152,6 +147,10 @@
         keyguardRepository.setKeyguardShowing(false)
         whenever(statusBarStateController.isExpanded).thenReturn(false)
         runKeyguardCoordinatorTest {
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+            )
+
             // WHEN: A notification is posted
             val fakeEntry = NotificationEntryBuilder().build()
             collectionListener.onEntryAdded(fakeEntry)
@@ -162,6 +161,9 @@
 
             // WHEN: The keyguard is now showing
             keyguardRepository.setKeyguardShowing(true)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.GONE, to = KeyguardState.AOD)
+            )
             testScheduler.runCurrent()
 
             // THEN: The notification is recognized as "seen" and is filtered out.
@@ -169,6 +171,9 @@
 
             // WHEN: The keyguard goes away
             keyguardRepository.setKeyguardShowing(false)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.AOD, to = KeyguardState.GONE)
+            )
             testScheduler.runCurrent()
 
             // THEN: The notification is shown regardless
@@ -182,9 +187,10 @@
         keyguardRepository.setKeyguardShowing(false)
         whenever(statusBarStateController.isExpanded).thenReturn(true)
         runKeyguardCoordinatorTest {
-            val fakeEntry = NotificationEntryBuilder()
+            val fakeEntry =
+                NotificationEntryBuilder()
                     .setNotification(Notification.Builder(mContext, "id").setOngoing(true).build())
-                .build()
+                    .build()
             collectionListener.onEntryAdded(fakeEntry)
 
             // WHEN: The keyguard is now showing
@@ -202,11 +208,13 @@
         keyguardRepository.setKeyguardShowing(false)
         whenever(statusBarStateController.isExpanded).thenReturn(true)
         runKeyguardCoordinatorTest {
-            val fakeEntry = NotificationEntryBuilder().build().apply {
-                row = mock<ExpandableNotificationRow>().apply {
-                    whenever(isMediaRow).thenReturn(true)
+            val fakeEntry =
+                NotificationEntryBuilder().build().apply {
+                    row =
+                        mock<ExpandableNotificationRow>().apply {
+                            whenever(isMediaRow).thenReturn(true)
+                        }
                 }
-            }
             collectionListener.onEntryAdded(fakeEntry)
 
             // WHEN: The keyguard is now showing
@@ -299,14 +307,12 @@
         runKeyguardCoordinatorTest {
             // WHEN: A new notification is posted
             val fakeSummary = NotificationEntryBuilder().build()
-            val fakeChild = NotificationEntryBuilder()
+            val fakeChild =
+                NotificationEntryBuilder()
                     .setGroup(context, "group")
                     .setGroupSummary(context, false)
                     .build()
-            GroupEntryBuilder()
-                    .setSummary(fakeSummary)
-                    .addChild(fakeChild)
-                    .build()
+            GroupEntryBuilder().setSummary(fakeSummary).addChild(fakeChild).build()
 
             collectionListener.onEntryAdded(fakeSummary)
             collectionListener.onEntryAdded(fakeChild)
@@ -331,6 +337,10 @@
         runKeyguardCoordinatorTest {
             val fakeEntry = NotificationEntryBuilder().build()
             collectionListener.onEntryAdded(fakeEntry)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.AOD, to = KeyguardState.LOCKSCREEN)
+            )
+            testScheduler.runCurrent()
 
             // WHEN: five seconds have passed
             testScheduler.advanceTimeBy(5.seconds)
@@ -338,10 +348,16 @@
 
             // WHEN: Keyguard is no longer showing
             keyguardRepository.setKeyguardShowing(false)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+            )
             testScheduler.runCurrent()
 
             // WHEN: Keyguard is shown again
             keyguardRepository.setKeyguardShowing(true)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.GONE, to = KeyguardState.AOD)
+            )
             testScheduler.runCurrent()
 
             // THEN: The notification is now recognized as "seen" and is filtered out.
@@ -354,11 +370,17 @@
         // GIVEN: Keyguard is showing, unseen notification is present
         keyguardRepository.setKeyguardShowing(true)
         runKeyguardCoordinatorTest {
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+            )
             val fakeEntry = NotificationEntryBuilder().build()
             collectionListener.onEntryAdded(fakeEntry)
 
             // WHEN: Keyguard is no longer showing
             keyguardRepository.setKeyguardShowing(false)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+            )
 
             // WHEN: Keyguard is shown again
             keyguardRepository.setKeyguardShowing(true)
@@ -369,14 +391,212 @@
         }
     }
 
+    @Test
+    fun unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnough() {
+        // GIVEN: Keyguard is showing, not dozing, unseen notification is present
+        keyguardRepository.setKeyguardShowing(true)
+        keyguardRepository.setIsDozing(false)
+        runKeyguardCoordinatorTest {
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+            )
+            val firstEntry = NotificationEntryBuilder().setId(1).build()
+            collectionListener.onEntryAdded(firstEntry)
+            testScheduler.runCurrent()
+
+            // WHEN: one second has passed
+            testScheduler.advanceTimeBy(1.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: another unseen notification is posted
+            val secondEntry = NotificationEntryBuilder().setId(2).build()
+            collectionListener.onEntryAdded(secondEntry)
+            testScheduler.runCurrent()
+
+            // WHEN: four more seconds have passed
+            testScheduler.advanceTimeBy(4.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: the keyguard is no longer showing
+            keyguardRepository.setKeyguardShowing(false)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+            )
+            testScheduler.runCurrent()
+
+            // WHEN: Keyguard is shown again
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+            )
+            testScheduler.runCurrent()
+
+            // THEN: The first notification is considered seen and is filtered out.
+            assertThat(unseenFilter.shouldFilterOut(firstEntry, 0L)).isTrue()
+
+            // THEN: The second notification is still considered unseen and is not filtered out
+            assertThat(unseenFilter.shouldFilterOut(secondEntry, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedAfterThreshold() {
+        // GIVEN: Keyguard is showing, not dozing
+        keyguardRepository.setKeyguardShowing(true)
+        keyguardRepository.setIsDozing(false)
+        runKeyguardCoordinatorTest {
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+            )
+            testScheduler.runCurrent()
+
+            // WHEN: a new notification is posted
+            val entry = NotificationEntryBuilder().setId(1).build()
+            collectionListener.onEntryAdded(entry)
+            testScheduler.runCurrent()
+
+            // WHEN: five more seconds have passed
+            testScheduler.advanceTimeBy(5.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: the notification is removed
+            collectionListener.onEntryRemoved(entry, 0)
+            testScheduler.runCurrent()
+
+            // WHEN: the notification is re-posted
+            collectionListener.onEntryAdded(entry)
+            testScheduler.runCurrent()
+
+            // WHEN: one more second has passed
+            testScheduler.advanceTimeBy(1.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: the keyguard is no longer showing
+            keyguardRepository.setKeyguardShowing(false)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+            )
+            testScheduler.runCurrent()
+
+            // WHEN: Keyguard is shown again
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+            )
+            testScheduler.runCurrent()
+
+            // THEN: The notification is considered unseen and is not filtered out.
+            assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedBeforeThreshold() {
+        // GIVEN: Keyguard is showing, not dozing
+        keyguardRepository.setKeyguardShowing(true)
+        keyguardRepository.setIsDozing(false)
+        runKeyguardCoordinatorTest {
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+            )
+            testScheduler.runCurrent()
+
+            // WHEN: a new notification is posted
+            val entry = NotificationEntryBuilder().setId(1).build()
+            collectionListener.onEntryAdded(entry)
+            testScheduler.runCurrent()
+
+            // WHEN: one second has passed
+            testScheduler.advanceTimeBy(1.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: the notification is removed
+            collectionListener.onEntryRemoved(entry, 0)
+            testScheduler.runCurrent()
+
+            // WHEN: the notification is re-posted
+            collectionListener.onEntryAdded(entry)
+            testScheduler.runCurrent()
+
+            // WHEN: one more second has passed
+            testScheduler.advanceTimeBy(1.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: the keyguard is no longer showing
+            keyguardRepository.setKeyguardShowing(false)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+            )
+            testScheduler.runCurrent()
+
+            // WHEN: Keyguard is shown again
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+            )
+            testScheduler.runCurrent()
+
+            // THEN: The notification is considered unseen and is not filtered out.
+            assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenNotificationOnKeyguardNotMarkedAsSeenIfUpdatedBeforeThreshold() {
+        // GIVEN: Keyguard is showing, not dozing
+        keyguardRepository.setKeyguardShowing(true)
+        keyguardRepository.setIsDozing(false)
+        runKeyguardCoordinatorTest {
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+            )
+            testScheduler.runCurrent()
+
+            // WHEN: a new notification is posted
+            val entry = NotificationEntryBuilder().setId(1).build()
+            collectionListener.onEntryAdded(entry)
+            testScheduler.runCurrent()
+
+            // WHEN: one second has passed
+            testScheduler.advanceTimeBy(1.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: the notification is updated
+            collectionListener.onEntryUpdated(entry)
+            testScheduler.runCurrent()
+
+            // WHEN: four more seconds have passed
+            testScheduler.advanceTimeBy(4.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: the keyguard is no longer showing
+            keyguardRepository.setKeyguardShowing(false)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+            )
+            testScheduler.runCurrent()
+
+            // WHEN: Keyguard is shown again
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+            )
+            testScheduler.runCurrent()
+
+            // THEN: The notification is considered unseen and is not filtered out.
+            assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
+        }
+    }
+
     private fun runKeyguardCoordinatorTest(
         testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit
     ) {
         val testDispatcher = UnconfinedTestDispatcher()
         val testScope = TestScope(testDispatcher)
-        val fakeSettings = FakeSettings().apply {
-            putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
-        }
+        val fakeSettings =
+            FakeSettings().apply {
+                putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
+            }
         val seenNotificationsProvider = SeenNotificationsProviderImpl()
         val keyguardCoordinator =
             KeyguardCoordinator(
@@ -387,7 +607,6 @@
                 keyguardRepository,
                 keyguardTransitionRepository,
                 mock<KeyguardCoordinatorLogger>(),
-                notifPipelineFlags,
                 testScope.backgroundScope,
                 sectionHeaderVisibilityProvider,
                 fakeSettings,
@@ -397,11 +616,12 @@
         keyguardCoordinator.attach(notifPipeline)
         testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) {
             KeyguardCoordinatorTestScope(
-                keyguardCoordinator,
-                testScope,
-                seenNotificationsProvider,
-                fakeSettings,
-            ).testBlock()
+                    keyguardCoordinator,
+                    testScope,
+                    seenNotificationsProvider,
+                    fakeSettings,
+                )
+                .testBlock()
         }
     }
 
@@ -414,10 +634,9 @@
         val testScheduler: TestCoroutineScheduler
             get() = scope.testScheduler
 
-        val onStateChangeListener: Consumer<String> =
-            withArgCaptor {
-                verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
-            }
+        val onStateChangeListener: Consumer<String> = withArgCaptor {
+            verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
+        }
 
         val unseenFilter: NotifFilter
             get() = keyguardCoordinator.unseenNotifFilter
@@ -426,11 +645,11 @@
             verify(notifPipeline).addCollectionListener(capture())
         }
 
-        val onHeadsUpChangedListener: OnHeadsUpChangedListener get() =
-            withArgCaptor { verify(headsUpManager).addListener(capture()) }
+        val onHeadsUpChangedListener: OnHeadsUpChangedListener
+            get() = withArgCaptor { verify(headsUpManager).addListener(capture()) }
 
-        val statusBarStateListener: StatusBarStateController.StateListener get() =
-            withArgCaptor { verify(statusBarStateController).addCallback(capture()) }
+        val statusBarStateListener: StatusBarStateController.StateListener
+            get() = withArgCaptor { verify(statusBarStateController).addCallback(capture()) }
 
         var showOnlyUnseenNotifsOnKeyguardSetting: Boolean
             get() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsInteractorTest.kt
new file mode 100644
index 0000000..fe49016
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsInteractorTest.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.statusbar.notification.domain.interactor
+
+import android.app.StatusBarManager
+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.statusbar.CommandQueue
+import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
+import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepositoryImpl
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class NotificationsInteractorTest : SysuiTestCase() {
+
+    private lateinit var underTest: NotificationsInteractor
+
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+    private val commandQueue: CommandQueue = mock()
+    private val logBuffer = LogBufferFactory(DumpManager(), mock()).create("buffer", 10)
+    private val disableFlagsLogger = DisableFlagsLogger()
+    private lateinit var disableFlagsRepository: DisableFlagsRepository
+
+    @Before
+    fun setUp() {
+        disableFlagsRepository =
+            DisableFlagsRepositoryImpl(
+                commandQueue,
+                DISPLAY_ID,
+                testScope.backgroundScope,
+                mock(),
+                logBuffer,
+                disableFlagsLogger,
+            )
+        underTest = NotificationsInteractor(disableFlagsRepository)
+    }
+
+    @Test
+    fun disableFlags_notifAlertsNotDisabled_notifAlertsEnabledTrue() {
+        val callback = getCommandQueueCallback()
+
+        callback.disable(
+            DISPLAY_ID,
+            StatusBarManager.DISABLE_NONE,
+            StatusBarManager.DISABLE2_NONE,
+            /* animate= */ false
+        )
+
+        assertThat(underTest.areNotificationAlertsEnabled()).isTrue()
+    }
+
+    @Test
+    fun disableFlags_notifAlertsDisabled_notifAlertsEnabledFalse() {
+        val callback = getCommandQueueCallback()
+
+        callback.disable(
+            DISPLAY_ID,
+            StatusBarManager.DISABLE_NOTIFICATION_ALERTS,
+            StatusBarManager.DISABLE2_NONE,
+            /* animate= */ false
+        )
+
+        assertThat(underTest.areNotificationAlertsEnabled()).isFalse()
+    }
+
+    private fun getCommandQueueCallback(): CommandQueue.Callbacks {
+        val callbackCaptor = argumentCaptor<CommandQueue.Callbacks>()
+        verify(commandQueue).addCallback(callbackCaptor.capture())
+        return callbackCaptor.value
+    }
+
+    private companion object {
+        const val DISPLAY_ID = 1
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/StackStateLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/StackStateLoggerTest.kt
new file mode 100644
index 0000000..47c5e5b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/StackStateLoggerTest.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 this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.logging
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.statusbar.notification.stack.StackStateLogger
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class StackStateLoggerTest : SysuiTestCase() {
+    private val logBufferCounter = LogBufferCounter()
+    private lateinit var logger: StackStateLogger
+
+    @Before
+    fun setup() {
+        logger = StackStateLogger(logBufferCounter.logBuffer, logBufferCounter.logBuffer)
+    }
+
+    @Test
+    fun groupChildRemovalEvent() {
+        logger.groupChildRemovalEventProcessed(KEY)
+        verifyDidLog(1)
+        logger.groupChildRemovalAnimationEnded(KEY)
+        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) {
+            Truth.assertThat(recentLogs).hasSize(times)
+            recentLogs.clear()
+        }
+    }
+
+    private fun verifyDidLog(times: Int) {
+        logBufferCounter.verifyDidLog(times)
+    }
+
+    companion object {
+        private val KEY = "PACKAGE_NAME"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
index c2f1f61..2e68cec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.statusbar.notification.logging.NotificationLogger
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer
+import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -107,6 +108,7 @@
                 rivSubComponentFactory,
                 metricsLogger,
                 logBufferLogger,
+                mock<NotificationChildrenContainerLogger>(),
                 listContainer,
                 smartReplyConstants,
                 smartReplyController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
index 915924f1..bdc8135 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
@@ -108,7 +108,7 @@
 
         mRow.doDragCallback(0, 0);
         verify(controller).startDragAndDrop(mRow);
-        verify(mShadeController).animateCollapsePanels(eq(0), eq(true),
+        verify(mShadeController).animateCollapseShade(eq(0), eq(true),
                 eq(false), anyFloat());
         verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any());
     }
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 4c83194..608778e 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
@@ -733,6 +733,36 @@
         verify(lowPriVectorDrawable, times(1)).start();
     }
 
+    @Test
+    public void isExpanded_hideSensitive_sensitiveNotExpanded()
+            throws Exception {
+        // GIVEN
+        final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        row.setUserExpanded(true);
+        row.setOnKeyguard(false);
+        row.setSensitive(/* sensitive= */true, /* hideSensitive= */false);
+        row.setHideSensitive(/* hideSensitive= */true, /* animated= */false,
+                /* delay= */0L, /* duration= */0L);
+
+        // THEN
+        assertThat(row.isExpanded()).isFalse();
+    }
+
+    @Test
+    public void isExpanded_hideSensitive_nonSensitiveExpanded()
+            throws Exception {
+        // GIVEN
+        final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        row.setUserExpanded(true);
+        row.setOnKeyguard(false);
+        row.setSensitive(/* sensitive= */true, /* hideSensitive= */false);
+        row.setHideSensitive(/* hideSensitive= */false, /* animated= */false,
+                /* delay= */0L, /* duration= */0L);
+
+        // THEN
+        assertThat(row.isExpanded()).isTrue();
+    }
+
     private void setDrawableIconsInImageView(CachingIconView icon, Drawable iconDrawable,
             Drawable rightIconDrawable) {
         ImageView iconView = mock(ImageView.class);
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 813bae8..df47071 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
@@ -76,7 +76,7 @@
 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.notification.stack.StackScrollAlgorithm;
+import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger;
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
@@ -581,6 +581,7 @@
                 mock(NotificationGutsManager.class),
                 mDismissibilityProvider,
                 mock(MetricsLogger.class),
+                mock(NotificationChildrenContainerLogger.class),
                 mock(SmartReplyConstants.class),
                 mock(SmartReplyController.class),
                 mFeatureFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt
index 944eb2d..a87dd2d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt
@@ -22,21 +22,23 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
-import com.android.systemui.statusbar.phone.CentralSurfaces
-import com.android.systemui.util.mockito.any
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 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.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyLong
 import org.mockito.Mockito.isNull
 import org.mockito.Mockito.verify
 
@@ -46,15 +48,27 @@
 
     private val keyguardRepository = FakeKeyguardRepository()
     private val deviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository()
-    private val centralSurfaces: CentralSurfaces = mock()
-    private val systemClock = FakeSystemClock()
+
+    private val screenOffAnimationController =
+        mock<ScreenOffAnimationController>().also {
+            whenever(it.allowWakeUpIfDozing()).thenReturn(true)
+        }
+    private val statusBarStateController: StatusBarStateController = mock()
+    private val powerRepository = FakePowerRepository()
+    private val powerInteractor =
+        PowerInteractor(
+            powerRepository,
+            FalsingCollectorFake(),
+            screenOffAnimationController,
+            statusBarStateController,
+        )
+
     private val keyguardTransitionController: LockscreenShadeTransitionController = mock()
     private val underTest =
         NotificationShelfInteractor(
             keyguardRepository,
             deviceEntryFaceAuthRepository,
-            centralSurfaces,
-            systemClock,
+            powerInteractor,
             keyguardTransitionController,
         )
 
@@ -107,10 +121,12 @@
 
     @Test
     fun goToLockedShadeFromShelf_wakesUpFromDoze() {
+        whenever(statusBarStateController.isDozing).thenReturn(true)
+
         underTest.goToLockedShadeFromShelf()
 
-        verify(centralSurfaces)
-            .wakeUpIfDozing(anyLong(), any(), eq(PowerManager.WAKE_REASON_GESTURE))
+        assertThat(powerRepository.lastWakeReason).isNotNull()
+        assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
index e9a8f3f..7ae1502 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
@@ -24,23 +24,26 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.accessibility.data.repository.FakeAccessibilityRepository
 import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor
+import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel
 import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor
-import com.android.systemui.statusbar.phone.CentralSurfaces
-import com.android.systemui.util.mockito.any
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 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.ArgumentMatchers
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.verify
@@ -54,14 +57,23 @@
     @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
 
     // mocks
-    @Mock private lateinit var centralSurfaces: CentralSurfaces
     @Mock private lateinit var keyguardTransitionController: LockscreenShadeTransitionController
+    @Mock private lateinit var screenOffAnimationController: ScreenOffAnimationController
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
 
     // fakes
     private val keyguardRepository = FakeKeyguardRepository()
     private val deviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository()
-    private val systemClock = FakeSystemClock()
     private val a11yRepo = FakeAccessibilityRepository()
+    private val powerRepository = FakePowerRepository()
+    private val powerInteractor by lazy {
+        PowerInteractor(
+            powerRepository,
+            FalsingCollectorFake(),
+            screenOffAnimationController,
+            statusBarStateController,
+        )
+    }
 
     // real impls
     private val a11yInteractor = AccessibilityInteractor(a11yRepo)
@@ -70,13 +82,17 @@
         NotificationShelfInteractor(
             keyguardRepository,
             deviceEntryFaceAuthRepository,
-            centralSurfaces,
-            systemClock,
+            powerInteractor,
             keyguardTransitionController,
         )
     }
     private val underTest by lazy { NotificationShelfViewModel(interactor, activatableViewModel) }
 
+    @Before
+    fun setUp() {
+        whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true)
+    }
+
     @Test
     fun canModifyColorOfNotifications_whenKeyguardNotShowing() = runTest {
         val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications)
@@ -126,10 +142,12 @@
 
     @Test
     fun onClicked_goesToLockedShade() {
+        whenever(statusBarStateController.isDozing).thenReturn(true)
+
         underTest.onShelfClicked()
 
-        verify(centralSurfaces)
-            .wakeUpIfDozing(ArgumentMatchers.anyLong(), any(), eq(PowerManager.WAKE_REASON_GESTURE))
+        assertThat(powerRepository.lastWakeReason).isNotNull()
+        assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE)
         verify(keyguardTransitionController).goToLockedShade(Mockito.isNull(), eq(true))
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index 05b70eb..8d751e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -1,20 +1,20 @@
 package com.android.systemui.statusbar.notification.stack
 
 import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
+import android.view.LayoutInflater
+import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
 import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
+import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
 import com.android.systemui.statusbar.NotificationShelf
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
-import com.android.systemui.statusbar.notification.row.NotificationTestHelper
 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.StackScrollAlgorithmState
 import com.android.systemui.util.mockito.mock
 import junit.framework.Assert.assertEquals
@@ -36,33 +36,32 @@
 @RunWithLooper
 class NotificationShelfTest : SysuiTestCase() {
 
-    @Mock private lateinit var largeScreenShadeInterpolator: LargeScreenShadeInterpolator
-    @Mock private lateinit var flags: FeatureFlags
+    @Mock
+    private lateinit var largeScreenShadeInterpolator: LargeScreenShadeInterpolator
+    @Mock
+    private lateinit var flags: FeatureFlags
+    @Mock
+    private lateinit var ambientState: AmbientState
+    @Mock
+    private lateinit var hostLayoutController: NotificationStackScrollLayoutController
 
-    private val shelf = NotificationShelf(
-            context,
-            /* attrs */ null,
-            /* showNotificationShelf */true
-    )
-    private val shelfState = shelf.viewState as NotificationShelf.ShelfState
-    private val ambientState = mock(AmbientState::class.java)
-    private val hostLayoutController: NotificationStackScrollLayoutController = mock()
-    private val notificationTestHelper by lazy {
-        allowTestableLooperAsMainThread()
-        NotificationTestHelper(
-                mContext,
-                mDependency,
-                TestableLooper.get(this))
-    }
+    private lateinit var shelf: NotificationShelf
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        val root = FrameLayout(context)
+        shelf = LayoutInflater.from(root.context)
+                .inflate(/* resource = */ R.layout.status_bar_notification_shelf,
+                    /* root = */root,
+                    /* attachToRoot = */false) as NotificationShelf
+
         whenever(ambientState.largeScreenShadeInterpolator).thenReturn(largeScreenShadeInterpolator)
         whenever(ambientState.featureFlags).thenReturn(flags)
+        whenever(ambientState.isSmallScreen).thenReturn(true)
+
         shelf.bind(ambientState, /* hostLayoutController */ hostLayoutController)
         shelf.layout(/* left */ 0, /* top */ 0, /* right */ 30, /* bottom */5)
-        whenever(ambientState.isSmallScreen).thenReturn(true)
     }
 
     @Test
@@ -311,9 +310,8 @@
     }
 
     @Test
-    fun updateState_flagTrue_largeScreen_expansionChanging_shelfAlphaUpdated_largeScreenValue() {
+    fun updateState_largeScreen_expansionChanging_shelfAlphaUpdated_largeScreenValue() {
         val expansionFraction = 0.6f
-        whenever(flags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)).thenReturn(true)
         whenever(ambientState.isSmallScreen).thenReturn(false)
         whenever(largeScreenShadeInterpolator.getNotificationContentAlpha(expansionFraction))
             .thenReturn(0.123f)
@@ -325,20 +323,6 @@
     }
 
     @Test
-    fun updateState_flagFalse_largeScreen_expansionChanging_shelfAlphaUpdated_standardValue() {
-        val expansionFraction = 0.6f
-        whenever(flags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)).thenReturn(false)
-        whenever(ambientState.isSmallScreen).thenReturn(false)
-        whenever(largeScreenShadeInterpolator.getNotificationContentAlpha(expansionFraction))
-            .thenReturn(0.123f)
-
-        updateState_expansionChanging_shelfAlphaUpdated(
-            expansionFraction = expansionFraction,
-            expectedAlpha = ShadeInterpolation.getContentAlpha(expansionFraction)
-        )
-    }
-
-    @Test
     fun updateState_expansionChangingWhileBouncerInTransit_shelfAlphaUpdated() {
         whenever(ambientState.isBouncerInTransit).thenReturn(true)
 
@@ -358,6 +342,127 @@
         )
     }
 
+    @Test
+    fun updateState_withNullLastVisibleBackgroundChild_hideShelf() {
+        // GIVEN
+        shelf.setSensitiveRevealAnimEnabled(true)
+        whenever(ambientState.stackY).thenReturn(100f)
+        whenever(ambientState.stackHeight).thenReturn(100f)
+        val paddingBetweenElements =
+            context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
+        val endOfStack = 200f + paddingBetweenElements
+        whenever(ambientState.isShadeExpanded).thenReturn(true)
+        val lastVisibleBackgroundChild = mock<ExpandableView>()
+        val expandableViewState = ExpandableViewState()
+        whenever(lastVisibleBackgroundChild.viewState).thenReturn(expandableViewState)
+        val stackScrollAlgorithmState = StackScrollAlgorithmState()
+        stackScrollAlgorithmState.firstViewInShelf = mock()
+
+        whenever(ambientState.lastVisibleBackgroundChild).thenReturn(null)
+
+        // WHEN
+        shelf.updateState(stackScrollAlgorithmState, ambientState)
+
+        // THEN
+        val shelfState = shelf.viewState as NotificationShelf.ShelfState
+        assertEquals(true, shelfState.hidden)
+        assertEquals(endOfStack, shelfState.yTranslation)
+    }
+
+    @Test
+    fun updateState_withNullFirstViewInShelf_hideShelf() {
+        // GIVEN
+        shelf.setSensitiveRevealAnimEnabled(true)
+        whenever(ambientState.stackY).thenReturn(100f)
+        whenever(ambientState.stackHeight).thenReturn(100f)
+        val paddingBetweenElements =
+            context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
+        val endOfStack = 200f + paddingBetweenElements
+        whenever(ambientState.isShadeExpanded).thenReturn(true)
+        val lastVisibleBackgroundChild = mock<ExpandableView>()
+        val expandableViewState = ExpandableViewState()
+        whenever(lastVisibleBackgroundChild.viewState).thenReturn(expandableViewState)
+        whenever(ambientState.lastVisibleBackgroundChild).thenReturn(lastVisibleBackgroundChild)
+        val stackScrollAlgorithmState = StackScrollAlgorithmState()
+
+        stackScrollAlgorithmState.firstViewInShelf = null
+
+        // WHEN
+        shelf.updateState(stackScrollAlgorithmState, ambientState)
+
+        // THEN
+        val shelfState = shelf.viewState as NotificationShelf.ShelfState
+        assertEquals(true, shelfState.hidden)
+        assertEquals(endOfStack, shelfState.yTranslation)
+    }
+
+    @Test
+    fun updateState_withCollapsedShade_hideShelf() {
+        // GIVEN
+        shelf.setSensitiveRevealAnimEnabled(true)
+        whenever(ambientState.stackY).thenReturn(100f)
+        whenever(ambientState.stackHeight).thenReturn(100f)
+        val paddingBetweenElements =
+            context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
+        val endOfStack = 200f + paddingBetweenElements
+        val lastVisibleBackgroundChild = mock<ExpandableView>()
+        val expandableViewState = ExpandableViewState()
+        whenever(lastVisibleBackgroundChild.viewState).thenReturn(expandableViewState)
+        whenever(ambientState.lastVisibleBackgroundChild).thenReturn(lastVisibleBackgroundChild)
+        val stackScrollAlgorithmState = StackScrollAlgorithmState()
+        stackScrollAlgorithmState.firstViewInShelf = mock()
+
+        whenever(ambientState.isShadeExpanded).thenReturn(false)
+
+        // WHEN
+        shelf.updateState(stackScrollAlgorithmState, ambientState)
+
+        // THEN
+        val shelfState = shelf.viewState as NotificationShelf.ShelfState
+        assertEquals(true, shelfState.hidden)
+        assertEquals(endOfStack, shelfState.yTranslation)
+    }
+
+    @Test
+    fun updateState_withHiddenSectionBeforeShelf_hideShelf() {
+        // GIVEN
+        shelf.setSensitiveRevealAnimEnabled(true)
+        whenever(ambientState.stackY).thenReturn(100f)
+        whenever(ambientState.stackHeight).thenReturn(100f)
+        val paddingBetweenElements =
+            context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
+        val endOfStack = 200f + paddingBetweenElements
+        whenever(ambientState.isShadeExpanded).thenReturn(true)
+        val lastVisibleBackgroundChild = mock<ExpandableView>()
+        val expandableViewState = ExpandableViewState()
+        whenever(lastVisibleBackgroundChild.viewState).thenReturn(expandableViewState)
+        val stackScrollAlgorithmState = StackScrollAlgorithmState()
+        whenever(ambientState.lastVisibleBackgroundChild).thenReturn(lastVisibleBackgroundChild)
+
+        val ssaVisibleChild = mock<ExpandableView>()
+        val ssaVisibleChildState = ExpandableViewState()
+        ssaVisibleChildState.hidden = true
+        whenever(ssaVisibleChild.viewState).thenReturn(ssaVisibleChildState)
+
+        val ssaVisibleChild1 = mock<ExpandableView>()
+        val ssaVisibleChildState1 = ExpandableViewState()
+        ssaVisibleChildState1.hidden = true
+        whenever(ssaVisibleChild1.viewState).thenReturn(ssaVisibleChildState1)
+
+        stackScrollAlgorithmState.visibleChildren.add(ssaVisibleChild)
+        stackScrollAlgorithmState.visibleChildren.add(ssaVisibleChild1)
+        whenever(ambientState.isExpansionChanging).thenReturn(true)
+        stackScrollAlgorithmState.firstViewInShelf = ssaVisibleChild1
+
+        // WHEN
+        shelf.updateState(stackScrollAlgorithmState, ambientState)
+
+        // THEN
+        val shelfState = shelf.viewState as NotificationShelf.ShelfState
+        assertEquals(true, shelfState.hidden)
+        assertEquals(endOfStack, shelfState.yTranslation)
+    }
+
     private fun setFractionToShade(fraction: Float) {
         whenever(ambientState.fractionToShade).thenReturn(fraction)
     }
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 cd0550e..ee8325e 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
@@ -24,6 +24,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
@@ -32,10 +33,14 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+
 import android.content.res.Resources;
 import android.metrics.LogMaker;
 import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
 import android.view.View;
+import android.view.ViewTreeObserver;
 
 import androidx.test.filters.SmallTest;
 
@@ -44,10 +49,17 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dump.DumpManager;
+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.KeyguardTransitionRepository;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.media.controls.ui.KeyguardMediaController;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
@@ -71,12 +83,12 @@
 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.init.NotificationsController;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent;
 import com.android.systemui.statusbar.notification.stack.NotificationSwipeHelper.NotificationCallback;
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
@@ -103,10 +115,12 @@
  * Tests for {@link NotificationStackScrollLayoutController}.
  */
 @SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 @RunWith(AndroidTestingRunner.class)
 public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase {
 
     @Mock private NotificationGutsManager mNotificationGutsManager;
+    @Mock private NotificationsController mNotificationsController;
     @Mock private NotificationVisibilityProvider mVisibilityProvider;
     @Mock private HeadsUpManagerPhone mHeadsUpManager;
     @Mock private NotificationRoundnessManager mNotificationRoundnessManager;
@@ -119,6 +133,8 @@
     @Mock private KeyguardMediaController mKeyguardMediaController;
     @Mock private SysuiStatusBarStateController mSysuiStatusBarStateController;
     @Mock private KeyguardBypassController mKeyguardBypassController;
+    @Mock private KeyguardInteractor mKeyguardInteractor;
+    @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor;
     @Mock private NotificationLockscreenUserManager mNotificationLockscreenUserManager;
     @Mock private MetricsLogger mMetricsLogger;
     @Mock private DumpManager mDumpManager;
@@ -126,7 +142,6 @@
     @Mock(answer = Answers.RETURNS_SELF)
     private NotificationSwipeHelper.Builder mNotificationSwipeHelperBuilder;
     @Mock private NotificationSwipeHelper mNotificationSwipeHelper;
-    @Mock private CentralSurfaces mCentralSurfaces;
     @Mock private ScrimController mScrimController;
     @Mock private GroupExpansionManager mGroupExpansionManager;
     @Mock private SectionHeaderController mSilentHeaderController;
@@ -142,11 +157,12 @@
     @Mock private StackStateLogger mStackLogger;
     @Mock private NotificationStackScrollLogger mLogger;
     @Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator;
-    @Mock private FeatureFlags mFeatureFlags;
+    private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
     @Mock private NotificationTargetsHelper mNotificationTargetsHelper;
     @Mock private SecureSettings mSecureSettings;
     @Mock private NotificationIconAreaController mIconAreaController;
     @Mock private ActivityStarter mActivityStarter;
+    @Mock private KeyguardTransitionRepository mKeyguardTransitionRepo;
 
     @Captor
     private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor;
@@ -158,9 +174,13 @@
 
     @Before
     public void setUp() {
+        allowTestableLooperAsMainThread();
         MockitoAnnotations.initMocks(this);
 
+        mFeatureFlags.set(Flags.USE_REPOS_FOR_BOUNCER_SHOWING, false);
+
         when(mNotificationSwipeHelperBuilder.build()).thenReturn(mNotificationSwipeHelper);
+        when(mKeyguardTransitionRepo.getTransitions()).thenReturn(emptyFlow());
     }
 
     @Test
@@ -259,28 +279,84 @@
     }
 
     @Test
-    public void testUpdateEmptyShadeView_bouncerShowing_hideEmptyView() {
+    public void testUpdateEmptyShadeView_bouncerShowing_flagOff_hideEmptyView() {
         when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
         initController(/* viewIsAttached= */ true);
 
-        when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
+        // WHEN the flag is off and *only* CentralSurfaces has bouncer as showing
+        mFeatureFlags.set(Flags.USE_REPOS_FOR_BOUNCER_SHOWING, false);
+        mController.setBouncerShowingFromCentralSurfaces(true);
+        when(mPrimaryBouncerInteractor.isBouncerShowing()).thenReturn(false);
+
         setupShowEmptyShadeViewState(true);
         reset(mNotificationStackScrollLayout);
         mController.updateShowEmptyShadeView();
+
+        // THEN the CentralSurfaces value is used. Since the bouncer is showing, we hide the empty
+        // view.
         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
                 /* visible= */ false,
                 /* areNotificationsHiddenInShade= */ false);
     }
 
     @Test
-    public void testUpdateEmptyShadeView_bouncerNotShowing_showEmptyView() {
+    public void testUpdateEmptyShadeView_bouncerShowing_flagOn_hideEmptyView() {
         when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
         initController(/* viewIsAttached= */ true);
 
-        when(mCentralSurfaces.isBouncerShowing()).thenReturn(false);
+        // WHEN the flag is on and *only* PrimaryBouncerInteractor has bouncer as showing
+        mFeatureFlags.set(Flags.USE_REPOS_FOR_BOUNCER_SHOWING, true);
+        when(mPrimaryBouncerInteractor.isBouncerShowing()).thenReturn(true);
+        mController.setBouncerShowingFromCentralSurfaces(false);
+
         setupShowEmptyShadeViewState(true);
         reset(mNotificationStackScrollLayout);
         mController.updateShowEmptyShadeView();
+
+        // THEN the PrimaryBouncerInteractor value is used. Since the bouncer is showing, we
+        // hide the empty view.
+        verify(mNotificationStackScrollLayout).updateEmptyShadeView(
+                /* visible= */ false,
+                /* areNotificationsHiddenInShade= */ false);
+    }
+
+    @Test
+    public void testUpdateEmptyShadeView_bouncerNotShowing_flagOff_showEmptyView() {
+        when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
+        initController(/* viewIsAttached= */ true);
+
+        // WHEN the flag is off and *only* CentralSurfaces has bouncer as not showing
+        mFeatureFlags.set(Flags.USE_REPOS_FOR_BOUNCER_SHOWING, false);
+        mController.setBouncerShowingFromCentralSurfaces(false);
+        when(mPrimaryBouncerInteractor.isBouncerShowing()).thenReturn(true);
+
+        setupShowEmptyShadeViewState(true);
+        reset(mNotificationStackScrollLayout);
+        mController.updateShowEmptyShadeView();
+
+        // THEN the CentralSurfaces value is used. Since the bouncer isn't showing, we can show the
+        // empty view.
+        verify(mNotificationStackScrollLayout).updateEmptyShadeView(
+                /* visible= */ true,
+                /* areNotificationsHiddenInShade= */ false);
+    }
+
+    @Test
+    public void testUpdateEmptyShadeView_bouncerNotShowing_flagOn_showEmptyView() {
+        when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
+        initController(/* viewIsAttached= */ true);
+
+        // WHEN the flag is on and *only* PrimaryBouncerInteractor has bouncer as not showing
+        mFeatureFlags.set(Flags.USE_REPOS_FOR_BOUNCER_SHOWING, true);
+        when(mPrimaryBouncerInteractor.isBouncerShowing()).thenReturn(false);
+        mController.setBouncerShowingFromCentralSurfaces(true);
+
+        setupShowEmptyShadeViewState(true);
+        reset(mNotificationStackScrollLayout);
+        mController.updateShowEmptyShadeView();
+
+        // THEN the PrimaryBouncerInteractor value is used. Since the bouncer isn't showing, we
+        // can show the empty view.
         verify(mNotificationStackScrollLayout).updateEmptyShadeView(
                 /* visible= */ true,
                 /* areNotificationsHiddenInShade= */ false);
@@ -540,17 +616,33 @@
                 .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
     }
 
+    @Test
+    public void updateEmptyShadeView_onKeyguardTransitionToAod_hidesView() {
+        initController(/* viewIsAttached= */ true);
+        mController.onKeyguardTransitionChanged(
+                new TransitionStep(
+                        /* from= */ KeyguardState.GONE,
+                        /* to= */ KeyguardState.AOD));
+        verify(mNotificationStackScrollLayout).updateEmptyShadeView(eq(false), anyBoolean());
+    }
+
     private LogMaker logMatcher(int category, int type) {
         return argThat(new LogMatcher(category, type));
     }
 
     private void setupShowEmptyShadeViewState(boolean toShow) {
         if (toShow) {
-            when(mSysuiStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(SHADE);
+            mController.onKeyguardTransitionChanged(
+                    new TransitionStep(
+                            /* from= */ KeyguardState.LOCKSCREEN,
+                            /* to= */ KeyguardState.GONE));
             mController.setQsFullScreen(false);
             mController.getView().removeAllViews();
         } else {
-            when(mSysuiStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD);
+            mController.onKeyguardTransitionChanged(
+                    new TransitionStep(
+                            /* from= */ KeyguardState.GONE,
+                            /* to= */ KeyguardState.AOD));
             mController.setQsFullScreen(true);
             mController.getView().addContainerView(mock(ExpandableNotificationRow.class));
         }
@@ -558,11 +650,14 @@
 
     private void initController(boolean viewIsAttached) {
         when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(viewIsAttached);
-
+        ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
+        when(mNotificationStackScrollLayout.getViewTreeObserver())
+                .thenReturn(viewTreeObserver);
         mController = new NotificationStackScrollLayoutController(
                 mNotificationStackScrollLayout,
                 true,
                 mNotificationGutsManager,
+                mNotificationsController,
                 mVisibilityProvider,
                 mHeadsUpManager,
                 mNotificationRoundnessManager,
@@ -573,6 +668,9 @@
                 mSysuiStatusBarStateController,
                 mKeyguardMediaController,
                 mKeyguardBypassController,
+                mKeyguardInteractor,
+                mPrimaryBouncerInteractor,
+                mKeyguardTransitionRepo,
                 mZenModeController,
                 mNotificationLockscreenUserManager,
                 Optional.<NotificationListViewModel>empty(),
@@ -582,7 +680,6 @@
                 new FalsingManagerFake(),
                 mResources,
                 mNotificationSwipeHelperBuilder,
-                mCentralSurfaces,
                 mScrimController,
                 mGroupExpansionManager,
                 mSilentHeaderController,
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 40e7e8d..8ad271b 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
@@ -79,9 +79,9 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
+import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.FooterView;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -111,7 +111,7 @@
     private AmbientState mAmbientState;
     private TestableResources mTestableResources;
     @Rule public MockitoRule mockito = MockitoJUnit.rule();
-    @Mock private CentralSurfaces mCentralSurfaces;
+    @Mock private NotificationsController mNotificationsController;
     @Mock private SysuiStatusBarStateController mBarState;
     @Mock private GroupMembershipManager mGroupMembershipManger;
     @Mock private GroupExpansionManager mGroupExpansionManager;
@@ -177,7 +177,7 @@
                 mNotificationStackSizeCalculator);
         mStackScroller = spy(mStackScrollerInternal);
         mStackScroller.setShelfController(notificationShelfController);
-        mStackScroller.setCentralSurfaces(mCentralSurfaces);
+        mStackScroller.setNotificationsController(mNotificationsController);
         mStackScroller.setEmptyShadeView(mEmptyShadeView);
         when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true);
         when(mStackScrollLayoutController.getNotificationRoundnessManager())
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 88b1eb3..4c97d20 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
@@ -9,7 +9,6 @@
 import com.android.systemui.animation.ShadeInterpolation.getContentAlpha
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
 import com.android.systemui.statusbar.EmptyShadeView
 import com.android.systemui.statusbar.NotificationShelf
@@ -191,12 +190,10 @@
     }
 
     @Test
-    fun resetViewStates_flagTrue_largeScreen_expansionChanging_alphaUpdated_largeScreenValue() {
+    fun resetViewStates_largeScreen_expansionChanging_alphaUpdated_largeScreenValue() {
         val expansionFraction = 0.6f
         val surfaceAlpha = 123f
         ambientState.isSmallScreen = false
-        whenever(featureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION))
-                .thenReturn(true)
         whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false)
         whenever(largeScreenShadeInterpolator.getNotificationContentAlpha(expansionFraction))
             .thenReturn(surfaceAlpha)
@@ -208,23 +205,6 @@
     }
 
     @Test
-    fun resetViewStates_flagFalse_largeScreen_expansionChanging_alphaUpdated_standardValue() {
-        val expansionFraction = 0.6f
-        val surfaceAlpha = 123f
-        ambientState.isSmallScreen = false
-        whenever(featureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION))
-                .thenReturn(false)
-        whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false)
-        whenever(largeScreenShadeInterpolator.getNotificationContentAlpha(expansionFraction))
-            .thenReturn(surfaceAlpha)
-
-        resetViewStates_expansionChanging_notificationAlphaUpdated(
-            expansionFraction = expansionFraction,
-            expectedAlpha = getContentAlpha(expansionFraction),
-        )
-    }
-
-    @Test
     fun expansionChanging_largeScreen_bouncerInTransit_alphaUpdated_bouncerValues() {
         ambientState.isSmallScreen = false
         whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(true)
@@ -835,6 +815,74 @@
         )
     }
 
+    // region shouldPinHunToBottomOfExpandedQs
+    @Test
+    fun shouldHunBeVisibleWhenScrolled_mustStayOnScreenFalse_false() {
+        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+            /* mustStayOnScreen= */false,
+            /* headsUpIsVisible= */false,
+            /* showingPulsing= */false,
+            /* isOnKeyguard=*/false,
+            /*headsUpOnKeyguard=*/false
+        )).isFalse()
+    }
+
+    @Test
+    fun shouldPinHunToBottomOfExpandedQs_headsUpIsVisible_false() {
+        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+            /* mustStayOnScreen= */true,
+            /* headsUpIsVisible= */true,
+            /* showingPulsing= */false,
+            /* isOnKeyguard=*/false,
+            /*headsUpOnKeyguard=*/false
+        )).isFalse()
+    }
+
+    @Test
+    fun shouldHunBeVisibleWhenScrolled_showingPulsing_false() {
+        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+            /* mustStayOnScreen= */true,
+            /* headsUpIsVisible= */false,
+            /* showingPulsing= */true,
+            /* isOnKeyguard=*/false,
+            /* headsUpOnKeyguard= */false
+        )).isFalse()
+    }
+
+    @Test
+    fun shouldHunBeVisibleWhenScrolled_isOnKeyguard_false() {
+        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+            /* mustStayOnScreen= */true,
+            /* headsUpIsVisible= */false,
+            /* showingPulsing= */false,
+            /* isOnKeyguard=*/true,
+            /* headsUpOnKeyguard= */false
+        )).isFalse()
+    }
+
+    @Test
+    fun shouldHunBeVisibleWhenScrolled_isNotOnKeyguard_true() {
+        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+            /* mustStayOnScreen= */true,
+            /* headsUpIsVisible= */false,
+            /* showingPulsing= */false,
+            /* isOnKeyguard=*/false,
+            /* headsUpOnKeyguard= */false
+        )).isTrue()
+    }
+
+    @Test
+    fun shouldHunBeVisibleWhenScrolled_headsUpOnKeyguard_true() {
+        assertThat(stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
+            /* mustStayOnScreen= */true,
+            /* headsUpIsVisible= */false,
+            /* showingPulsing= */false,
+            /* isOnKeyguard=*/true,
+            /* headsUpOnKeyguard= */true
+        )).isTrue()
+    }
+    // endregion
+
     private fun createHunViewMock(
             isShadeOpen: Boolean,
             fullyVisible: Boolean,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
index 4a30800..442ba09 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.shade.ShadeController
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
@@ -68,6 +69,7 @@
     @Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
     @Mock private lateinit var lockScreenUserManager: NotificationLockscreenUserManager
     @Mock private lateinit var statusBarWindowController: StatusBarWindowController
+    @Mock private lateinit var notifShadeWindowController: NotificationShadeWindowController
     @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
     @Mock private lateinit var keyguardStateController: KeyguardStateController
     @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
@@ -90,6 +92,7 @@
                 Lazy { keyguardViewMediator },
                 Lazy { shadeController },
                 Lazy { statusBarKeyguardViewManager },
+                Lazy { notifShadeWindowController },
                 activityLaunchAnimator,
                 context,
                 lockScreenUserManager,
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 e680a4e..e4a2236 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
@@ -34,6 +34,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.content.ComponentName;
@@ -51,6 +52,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dagger.NightDisplayListenerModule;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.AutoAddTracker;
 import com.android.systemui.qs.QSHost;
@@ -111,6 +113,8 @@
     @Mock private DataSaverController mDataSaverController;
     @Mock private ManagedProfileController mManagedProfileController;
     @Mock private NightDisplayListener mNightDisplayListener;
+    @Mock(answer = Answers.RETURNS_SELF)
+    private NightDisplayListenerModule.Builder mNightDisplayListenerBuilder;
     @Mock private ReduceBrightColorsController mReduceBrightColorsController;
     @Mock private DeviceControlsController mDeviceControlsController;
     @Mock private WalletController mWalletController;
@@ -151,6 +155,7 @@
                 .thenReturn(TEST_CUSTOM_SAFETY_PKG);
         Context context = Mockito.spy(mContext);
         when(context.getPackageManager()).thenReturn(mPackageManager);
+        when(mNightDisplayListenerBuilder.build()).thenReturn(mNightDisplayListener);
 
         mAutoTileManager = createAutoTileManager(context);
         mAutoTileManager.init();
@@ -167,7 +172,7 @@
             HotspotController hotspotController,
             DataSaverController dataSaverController,
             ManagedProfileController managedProfileController,
-            NightDisplayListener nightDisplayListener,
+            NightDisplayListenerModule.Builder nightDisplayListenerBuilder,
             CastController castController,
             ReduceBrightColorsController reduceBrightColorsController,
             DeviceControlsController deviceControlsController,
@@ -180,7 +185,7 @@
                 hotspotController,
                 dataSaverController,
                 managedProfileController,
-                nightDisplayListener,
+                mNightDisplayListenerBuilder,
                 castController,
                 reduceBrightColorsController,
                 deviceControlsController,
@@ -191,7 +196,7 @@
 
     private AutoTileManager createAutoTileManager(Context context) {
         return createAutoTileManager(context, mAutoAddTrackerBuilder, mHotspotController,
-                mDataSaverController, mManagedProfileController, mNightDisplayListener,
+                mDataSaverController, mManagedProfileController, mNightDisplayListenerBuilder,
                 mCastController, mReduceBrightColorsController, mDeviceControlsController,
                 mWalletController, mSafetyController, mIsReduceBrightColorsAvailable);
     }
@@ -204,7 +209,7 @@
         HotspotController hC = mock(HotspotController.class);
         DataSaverController dSC = mock(DataSaverController.class);
         ManagedProfileController mPC = mock(ManagedProfileController.class);
-        NightDisplayListener nDS = mock(NightDisplayListener.class);
+        NightDisplayListenerModule.Builder nDSB = mock(NightDisplayListenerModule.Builder.class);
         CastController cC = mock(CastController.class);
         ReduceBrightColorsController rBC = mock(ReduceBrightColorsController.class);
         DeviceControlsController dCC = mock(DeviceControlsController.class);
@@ -212,14 +217,14 @@
         SafetyController sC = mock(SafetyController.class);
 
         AutoTileManager manager =
-                createAutoTileManager(mock(Context.class), builder, hC, dSC, mPC, nDS, cC, rBC,
+                createAutoTileManager(mock(Context.class), builder, hC, dSC, mPC, nDSB, cC, rBC,
                         dCC, wC, sC, true);
 
         verify(tracker, never()).initialize();
         verify(hC, never()).addCallback(any());
         verify(dSC, never()).addCallback(any());
         verify(mPC, never()).addCallback(any());
-        verify(nDS, never()).setCallback(any());
+        verifyNoMoreInteractions(nDSB);
         verify(cC, never()).addCallback(any());
         verify(rBC, never()).addCallback(any());
         verify(dCC, never()).setCallback(any());
@@ -615,6 +620,15 @@
         createAutoTileManager(mContext).destroy();
     }
 
+    @Test
+    public void testUserChange_newNightDisplayListenerCreated() {
+        UserHandle newUser = UserHandle.of(1000);
+        mAutoTileManager.changeUser(newUser);
+        InOrder inOrder = inOrder(mNightDisplayListenerBuilder);
+        inOrder.verify(mNightDisplayListenerBuilder).setUser(newUser.getIdentifier());
+        inOrder.verify(mNightDisplayListenerBuilder).build();
+    }
+
     // Will only notify if it's listening
     private void changeValue(String key, int value) {
         mSecureSettings.putIntForUser(key, value, USER);
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 3870d99..8545b89 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
@@ -18,9 +18,9 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
@@ -145,38 +145,33 @@
 
     @Test
     public void testDisableNotificationShade() {
-        when(mCentralSurfaces.getDisabled1()).thenReturn(StatusBarManager.DISABLE_NONE);
-        when(mCentralSurfaces.getDisabled2()).thenReturn(StatusBarManager.DISABLE_NONE);
+        // Start with nothing disabled
+        mSbcqCallbacks.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NONE,
+                StatusBarManager.DISABLE2_NONE, false);
+
         when(mCommandQueue.panelsEnabled()).thenReturn(false);
+        // WHEN the new disable flags have the shade disabled
         mSbcqCallbacks.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NONE,
                 StatusBarManager.DISABLE2_NOTIFICATION_SHADE, false);
 
-        verify(mCentralSurfaces).updateQsExpansionEnabled();
+        // THEN the shade is collapsed
         verify(mShadeController).animateCollapseShade();
-
-        // Trying to open it does nothing.
-        mSbcqCallbacks.animateExpandNotificationsPanel();
-        verify(mShadeViewController, never()).expandToNotifications();
-        mSbcqCallbacks.animateExpandSettingsPanel(null);
-        verify(mShadeViewController, never()).expand(anyBoolean());
     }
 
     @Test
     public void testEnableNotificationShade() {
-        when(mCentralSurfaces.getDisabled1()).thenReturn(StatusBarManager.DISABLE_NONE);
-        when(mCentralSurfaces.getDisabled2())
-                .thenReturn(StatusBarManager.DISABLE2_NOTIFICATION_SHADE);
+        // Start with the shade disabled
+        mSbcqCallbacks.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NONE,
+                StatusBarManager.DISABLE2_NOTIFICATION_SHADE, false);
+        reset(mShadeController);
+
         when(mCommandQueue.panelsEnabled()).thenReturn(true);
+        // WHEN the new disable flags have the shade enabled
         mSbcqCallbacks.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NONE,
                 StatusBarManager.DISABLE2_NONE, false);
-        verify(mCentralSurfaces).updateQsExpansionEnabled();
-        verify(mShadeController, never()).animateCollapseShade();
 
-        // Can now be opened.
-        mSbcqCallbacks.animateExpandNotificationsPanel();
-        verify(mShadeViewController).expandToNotifications();
-        mSbcqCallbacks.animateExpandSettingsPanel(null);
-        verify(mShadeViewController).expandToQs();
+        // THEN the shade is not collapsed
+        verify(mShadeController, never()).animateCollapseShade();
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 7702630..88d8dfc 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
@@ -29,7 +29,6 @@
 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.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
@@ -98,7 +97,9 @@
 import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuController;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.assist.AssistManager;
+import com.android.systemui.back.domain.interactor.BackActionInteractor;
 import com.android.systemui.biometrics.AuthRippleController;
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.charging.WiredChargingRippleController;
 import com.android.systemui.classifier.FalsingCollectorFake;
@@ -113,7 +114,6 @@
 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.notetask.NoteTaskController;
@@ -122,6 +122,7 @@
 import com.android.systemui.plugins.PluginDependencyProvider;
 import com.android.systemui.plugins.PluginManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.recents.ScreenPinningRequest;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.settings.brightness.BrightnessSliderController;
@@ -145,6 +146,7 @@
 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.OperatorNameViewController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.StatusBarState;
@@ -259,6 +261,7 @@
     @Mock private AccessibilityFloatingMenuController mAccessibilityFloatingMenuController;
     @Mock private SysuiColorExtractor mColorExtractor;
     private WakefulnessLifecycle mWakefulnessLifecycle;
+    @Mock private PowerInteractor mPowerInteractor;
     @Mock private ColorExtractor.GradientColors mGradientColors;
     @Mock private PulseExpansionHandler mPulseExpansionHandler;
     @Mock private NotificationWakeUpCoordinator mNotificationWakeUpCoordinator;
@@ -273,10 +276,12 @@
     @Mock private NotificationShadeWindowController mNotificationShadeWindowController;
     @Mock private NotificationIconAreaController mNotificationIconAreaController;
     @Mock private NotificationShadeWindowViewController mNotificationShadeWindowViewController;
+    @Mock private NotificationShelfController mNotificationShelfController;
     @Mock private DozeParameters mDozeParameters;
     @Mock private Lazy<LockscreenWallpaper> mLockscreenWallpaperLazy;
     @Mock private LockscreenWallpaper mLockscreenWallpaper;
     @Mock private DozeServiceHost mDozeServiceHost;
+    @Mock private BackActionInteractor mBackActionInteractor;
     @Mock private ViewMediatorCallback mKeyguardVieMediatorCallback;
     @Mock private VolumeComponent mVolumeComponent;
     @Mock private CommandQueue mCommandQueue;
@@ -287,7 +292,6 @@
     @Mock private StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
     @Mock private ScreenPinningRequest mScreenPinningRequest;
     @Mock private PluginDependencyProvider mPluginDependencyProvider;
-    @Mock private KeyguardDismissUtil mKeyguardDismissUtil;
     @Mock private ExtensionController mExtensionController;
     @Mock private UserInfoControllerImpl mUserInfoControllerImpl;
     @Mock private PhoneStatusBarPolicy mPhoneStatusBarPolicy;
@@ -435,16 +439,18 @@
 
         mShadeController = spy(new ShadeControllerImpl(
                 mCommandQueue,
+                mMainExecutor,
                 mKeyguardStateController,
                 mStatusBarStateController,
                 mStatusBarKeyguardViewManager,
                 mStatusBarWindowController,
+                mDeviceProvisionedController,
                 mNotificationShadeWindowController,
                 mContext.getSystemService(WindowManager.class),
                 () -> mAssistManager,
                 () -> mNotificationGutsManager
         ));
-        mShadeController.setNotificationPanelViewController(mNotificationPanelViewController);
+        mShadeController.setShadeViewController(mNotificationPanelViewController);
         mShadeController.setNotificationShadeWindowViewController(
                 mNotificationShadeWindowViewController);
         mShadeController.setNotificationPresenter(mNotificationPresenter);
@@ -484,14 +490,17 @@
                 mMetricsLogger,
                 mShadeLogger,
                 mUiBgExecutor,
+                mNotificationPanelViewController,
                 mNotificationMediaManager,
                 mLockscreenUserManager,
                 mRemoteInputManager,
+                mQuickSettingsController,
                 mUserSwitcherController,
                 mBatteryController,
                 mColorExtractor,
                 mScreenLifecycle,
                 mWakefulnessLifecycle,
+                mPowerInteractor,
                 mStatusBarStateController,
                 Optional.of(mBubbles),
                 () -> mNoteTaskController,
@@ -501,13 +510,17 @@
                 () -> mAssistManager,
                 configurationController,
                 mNotificationShadeWindowController,
+                mNotificationShelfController,
+                mStackScrollerController,
                 mDozeParameters,
                 mScrimController,
                 mLockscreenWallpaperLazy,
                 mBiometricUnlockControllerLazy,
                 mAuthRippleController,
                 mDozeServiceHost,
-                mPowerManager, mScreenPinningRequest,
+                mBackActionInteractor,
+                mPowerManager,
+                mScreenPinningRequest,
                 mDozeScrimController,
                 mVolumeComponent,
                 mCommandQueue,
@@ -519,7 +532,6 @@
                 mInitController,
                 new Handler(TestableLooper.get(this).getLooper()),
                 mPluginDependencyProvider,
-                mKeyguardDismissUtil,
                 mExtensionController,
                 mUserInfoControllerImpl,
                 mPhoneStatusBarPolicy,
@@ -577,13 +589,10 @@
         // TODO: we should be able to call mCentralSurfaces.start() and have all the below values
         // initialized automatically and make NPVC private.
         mCentralSurfaces.mNotificationShadeWindowView = mNotificationShadeWindowView;
-        mCentralSurfaces.mShadeSurface = mNotificationPanelViewController;
-        mCentralSurfaces.mQsController = mQuickSettingsController;
         mCentralSurfaces.mDozeScrimController = mDozeScrimController;
         mCentralSurfaces.mPresenter = mNotificationPresenter;
         mCentralSurfaces.mKeyguardIndicationController = mKeyguardIndicationController;
         mCentralSurfaces.mBarService = mBarService;
-        mCentralSurfaces.mStackScroller = mStackScroller;
         mCentralSurfaces.mGestureWakeLock = mPowerManager.newWakeLock(
                 PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "sysui:GestureWakeLock");
         mCentralSurfaces.startKeyguard();
@@ -821,9 +830,9 @@
                 eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT),
                 mOnBackInvokedCallback.capture());
 
-        when(mNotificationPanelViewController.canBeCollapsed()).thenReturn(true);
+        when(mBackActionInteractor.shouldBackBeHandled()).thenReturn(true);
         mOnBackInvokedCallback.getValue().onBackInvoked();
-        verify(mShadeController).animateCollapseShade();
+        verify(mBackActionInteractor).onBackRequested();
     }
 
     /**
@@ -841,6 +850,7 @@
 
         OnBackAnimationCallback onBackAnimationCallback =
                 (OnBackAnimationCallback) (mOnBackInvokedCallback.getValue());
+        when(mBackActionInteractor.shouldBackBeHandled()).thenReturn(true);
         when(mNotificationPanelViewController.canBeCollapsed()).thenReturn(true);
 
         BackEvent fakeSwipeInFromLeftEdge = new BackEvent(20.0f, 100.0f, 1.0f, BackEvent.EDGE_LEFT);
@@ -863,6 +873,7 @@
 
         OnBackAnimationCallback onBackAnimationCallback =
                 (OnBackAnimationCallback) (mOnBackInvokedCallback.getValue());
+        when(mBackActionInteractor.shouldBackBeHandled()).thenReturn(true);
         when(mNotificationPanelViewController.canBeCollapsed()).thenReturn(true);
 
         BackEvent fakeSwipeInFromLeftEdge = new BackEvent(20.0f, 10.0f, 0.0f, BackEvent.EDGE_LEFT);
@@ -1062,7 +1073,7 @@
         // GIVEN device occluded and panel is NOT expanded
         mCentralSurfaces.setBarStateForTest(SHADE); // occluding on LS has StatusBarState = SHADE
         when(mKeyguardStateController.isOccluded()).thenReturn(true);
-        mCentralSurfaces.mPanelExpanded = false;
+        when(mNotificationPanelViewController.isPanelExpanded()).thenReturn(false);
 
         mCentralSurfaces.updateScrimController();
 
@@ -1076,7 +1087,7 @@
         // GIVEN device occluded and qs IS expanded
         mCentralSurfaces.setBarStateForTest(SHADE); // occluding on LS has StatusBarState = SHADE
         when(mKeyguardStateController.isOccluded()).thenReturn(true);
-        mCentralSurfaces.mPanelExpanded = true;
+        when(mNotificationPanelViewController.isPanelExpanded()).thenReturn(true);
 
         mCentralSurfaces.updateScrimController();
 
@@ -1145,32 +1156,6 @@
     }
 
     @Test
-    public void collapseShade_callsanimateCollapseShade_whenExpanded() {
-        // GIVEN the shade is expanded
-        mCentralSurfaces.onShadeExpansionFullyChanged(true);
-        mCentralSurfaces.setBarStateForTest(SHADE);
-
-        // WHEN collapseShade is called
-        mCentralSurfaces.collapseShade();
-
-        // VERIFY that animateCollapseShade is called
-        verify(mShadeController).animateCollapseShade();
-    }
-
-    @Test
-    public void collapseShade_doesNotCallanimateCollapseShade_whenCollapsed() {
-        // GIVEN the shade is collapsed
-        mCentralSurfaces.onShadeExpansionFullyChanged(false);
-        mCentralSurfaces.setBarStateForTest(SHADE);
-
-        // WHEN collapseShade is called
-        mCentralSurfaces.collapseShade();
-
-        // VERIFY that animateCollapseShade is NOT called
-        verify(mShadeController, never()).animateCollapseShade();
-    }
-
-    @Test
     public void deviceStateChange_unfolded_shadeOpen_setsLeaveOpenOnKeyguardHide() {
         setFoldedStates(FOLD_STATE_FOLDED);
         setGoToSleepStates(FOLD_STATE_FOLDED);
@@ -1266,34 +1251,6 @@
     }
 
     @Test
-    public void dozing_wakeUp() throws RemoteException {
-        // GIVEN can wakeup when dozing & is dozing
-        when(mScreenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true);
-        setDozing(true);
-
-        // WHEN wakeup is requested
-        final int wakeReason = PowerManager.WAKE_REASON_TAP;
-        mCentralSurfaces.wakeUpIfDozing(0, "", wakeReason);
-
-        // THEN power manager receives wakeup
-        verify(mPowerManagerService).wakeUp(eq(0L), eq(wakeReason), anyString(), anyString());
-    }
-
-    @Test
-    public void notDozing_noWakeUp() throws RemoteException {
-        // GIVEN can wakeup when dozing and NOT dozing
-        when(mScreenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true);
-        setDozing(false);
-
-        // WHEN wakeup is requested
-        final int wakeReason = PowerManager.WAKE_REASON_TAP;
-        mCentralSurfaces.wakeUpIfDozing(0, "", wakeReason);
-
-        // THEN power manager receives wakeup
-        verify(mPowerManagerService, never()).wakeUp(anyLong(), anyInt(), anyString(), anyString());
-    }
-
-    @Test
     public void frpLockedDevice_shadeDisabled() {
         when(mDeviceProvisionedController.isFrpActive()).thenReturn(true);
         when(mDozeServiceHost.isPulsing()).thenReturn(true);
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
index 3e90ed9..4aac841 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt
@@ -42,6 +42,8 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
@@ -135,6 +137,19 @@
     }
 
     @Test
+    fun onFaceAuthEnabledChanged_notifiesBypassEnabledListeners() {
+        initKeyguardBypassController()
+        val bypassListener = mock(KeyguardBypassController.OnBypassStateChangedListener::class.java)
+        val callback = ArgumentCaptor.forClass(KeyguardStateController.Callback::class.java)
+
+        keyguardBypassController.registerOnBypassStateChangedListener(bypassListener)
+        verify(keyguardStateController).addCallback(callback.capture())
+
+        callback.value.onFaceAuthEnabledChanged()
+        verify(bypassListener).onBypassStateChanged(anyBoolean())
+    }
+
+    @Test
     fun configDevicePostureClosed_matchState_isPostureAllowedForFaceAuth_returnTrue() {
         defaultConfigPostureClosed()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardDismissUtilTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardDismissUtilTest.java
new file mode 100644
index 0000000..b0aa2d3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardDismissUtilTest.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.systemui.statusbar.phone;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+
+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
+public class KeyguardDismissUtilTest extends SysuiTestCase {
+    @Mock
+    private KeyguardStateController mKeyguardStateController;
+    @Mock
+    private SysuiStatusBarStateController mStatusBarStateController;
+    @Mock
+    private ActivityStarter mActivityStarter;
+    @Mock
+    private ActivityStarter.OnDismissAction mAction;
+
+    private KeyguardDismissUtil mKeyguardDismissUtil;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mKeyguardDismissUtil = new KeyguardDismissUtil(
+                mKeyguardStateController, mStatusBarStateController, mActivityStarter);
+    }
+
+    @Test
+    public void testSetLeaveOpenOnKeyguardHideWhenKeyGuardStateControllerIsShowing() {
+        doReturn(true).when(mKeyguardStateController).isShowing();
+
+        mKeyguardDismissUtil.executeWhenUnlocked(mAction, true /* requiresShadeOpen */,
+                true /* afterKeyguardGone */);
+
+        verify(mStatusBarStateController).setLeaveOpenOnKeyguardHide(true);
+
+        verify(mActivityStarter).dismissKeyguardThenExecute(mAction, null, true);
+
+    }
+
+    @Test
+    public void testSetLeaveOpenOnKeyguardHideWhenKeyGuardStateControllerIsNotShowing() {
+        doReturn(false).when(mKeyguardStateController).isShowing();
+
+        mKeyguardDismissUtil.executeWhenUnlocked(mAction, true /* requiresShadeOpen */,
+                true /* afterKeyguardGone */);
+
+        //no interaction with mStatusBarStateController
+        verify(mStatusBarStateController, times(0)).setLeaveOpenOnKeyguardHide(true);
+
+        verify(mActivityStarter).dismissKeyguardThenExecute(mAction, null, true);
+
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LockscreenWallpaperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LockscreenWallpaperTest.kt
new file mode 100644
index 0000000..47671fb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LockscreenWallpaperTest.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
+ */
+
+package com.android.systemui.statusbar.phone
+
+import android.app.WallpaperManager
+import android.content.pm.UserInfo
+import android.os.Looper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.user.data.model.SelectionStatus
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.kotlin.JavaAdapter
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.utils.os.FakeHandler
+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.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.verify
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class LockscreenWallpaperTest : SysuiTestCase() {
+
+    private lateinit var underTest: LockscreenWallpaper
+
+    private val testScope = TestScope(StandardTestDispatcher())
+    private val userRepository = FakeUserRepository()
+
+    private val wallpaperManager: WallpaperManager = mock()
+
+    @Before
+    fun setUp() {
+        whenever(wallpaperManager.isLockscreenLiveWallpaperEnabled).thenReturn(false)
+        whenever(wallpaperManager.isWallpaperSupported).thenReturn(true)
+        underTest =
+            LockscreenWallpaper(
+                /* wallpaperManager= */ wallpaperManager,
+                /* iWallpaperManager= */ mock(),
+                /* keyguardUpdateMonitor= */ mock(),
+                /* dumpManager= */ mock(),
+                /* mediaManager= */ mock(),
+                /* mainHandler= */ FakeHandler(Looper.getMainLooper()),
+                /* javaAdapter= */ JavaAdapter(testScope.backgroundScope),
+                /* userRepository= */ userRepository,
+                /* userTracker= */ mock(),
+            )
+        underTest.start()
+    }
+
+    @Test
+    fun getBitmap_matchesUserIdFromUserRepo() =
+        testScope.runTest {
+            val info = UserInfo(/* id= */ 5, /* name= */ "id5", /* flags= */ 0)
+            userRepository.setUserInfos(listOf(info))
+            userRepository.setSelectedUserInfo(info)
+
+            underTest.bitmap
+
+            verify(wallpaperManager).getWallpaperFile(any(), eq(5))
+        }
+
+    @Test
+    fun getBitmap_usesOldUserIfNewUserInProgress() =
+        testScope.runTest {
+            val info5 = UserInfo(/* id= */ 5, /* name= */ "id5", /* flags= */ 0)
+            val info6 = UserInfo(/* id= */ 6, /* name= */ "id6", /* flags= */ 0)
+            userRepository.setUserInfos(listOf(info5, info6))
+            userRepository.setSelectedUserInfo(info5)
+
+            // WHEN the selection of user 6 is only in progress
+            userRepository.setSelectedUserInfo(
+                info6,
+                selectionStatus = SelectionStatus.SELECTION_IN_PROGRESS
+            )
+
+            underTest.bitmap
+
+            // THEN we still use user 5 for wallpaper selection
+            verify(wallpaperManager).getWallpaperFile(any(), eq(5))
+        }
+}
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 6b18169..85fbef0 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
@@ -27,6 +27,8 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
+import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State
 import com.android.systemui.privacy.PrivacyItemController
 import com.android.systemui.privacy.logging.PrivacyLogger
 import com.android.systemui.screenrecord.RecordingController
@@ -46,9 +48,17 @@
 import com.android.systemui.statusbar.policy.ZenModeController
 import com.android.systemui.util.RingerModeTracker
 import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.kotlin.JavaAdapter
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.time.DateFormatUtil
 import com.android.systemui.util.time.FakeSystemClock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.test.TestScope
+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
@@ -57,6 +67,8 @@
 import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
@@ -64,11 +76,13 @@
 
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 class PhoneStatusBarPolicyTest : SysuiTestCase() {
 
     companion object {
         private const val ALARM_SLOT = "alarm"
+        private const val CONNECTED_DISPLAY_SLOT = "connected_display"
     }
 
     @Mock private lateinit var iconController: StatusBarIconController
@@ -102,6 +116,9 @@
     private lateinit var alarmCallbackCaptor:
         ArgumentCaptor<NextAlarmController.NextAlarmChangeCallback>
 
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+    private val fakeConnectedDisplayStateProvider = FakeConnectedDisplayStateProvider()
+
     private lateinit var executor: FakeExecutor
     private lateinit var statusBarPolicy: PhoneStatusBarPolicy
     private lateinit var testableLooper: TestableLooper
@@ -164,6 +181,57 @@
         verify(iconController).setIconVisibility(ALARM_SLOT, true)
     }
 
+    @Test
+    fun connectedDisplay_connected_iconShown() =
+        testScope.runTest {
+            statusBarPolicy.init()
+            clearInvocations(iconController)
+
+            fakeConnectedDisplayStateProvider.emit(State.CONNECTED)
+            runCurrent()
+
+            verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, true)
+        }
+
+    @Test
+    fun connectedDisplay_disconnected_iconHidden() =
+        testScope.runTest {
+            statusBarPolicy.init()
+            clearInvocations(iconController)
+
+            fakeConnectedDisplayStateProvider.emit(State.DISCONNECTED)
+
+            verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, false)
+        }
+
+    @Test
+    fun connectedDisplay_disconnectedThenConnected_iconShown() =
+        testScope.runTest {
+            statusBarPolicy.init()
+            clearInvocations(iconController)
+
+            fakeConnectedDisplayStateProvider.emit(State.CONNECTED)
+            fakeConnectedDisplayStateProvider.emit(State.DISCONNECTED)
+            fakeConnectedDisplayStateProvider.emit(State.CONNECTED)
+
+            inOrder(iconController).apply {
+                verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, true)
+                verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, false)
+                verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, true)
+            }
+        }
+
+    @Test
+    fun connectedDisplay_connectSecureDisplay_iconShown() =
+        testScope.runTest {
+            statusBarPolicy.init()
+            clearInvocations(iconController)
+
+            fakeConnectedDisplayStateProvider.emit(State.CONNECTED_SECURE)
+
+            verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, true)
+        }
+
     private fun createAlarmInfo(): AlarmManager.AlarmClockInfo {
         return AlarmManager.AlarmClockInfo(10L, null)
     }
@@ -200,7 +268,16 @@
             dateFormatUtil,
             ringerModeTracker,
             privacyItemController,
-            privacyLogger
+            privacyLogger,
+            fakeConnectedDisplayStateProvider,
+            JavaAdapter(testScope.backgroundScope)
         )
     }
+
+    private class FakeConnectedDisplayStateProvider : ConnectedDisplayInteractor {
+        private val flow = MutableSharedFlow<State>()
+        suspend fun emit(value: State) = flow.emit(value)
+        override val connectedDisplayState: Flow<State>
+            get() = flow
+    }
 }
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 a9ed175..ab80158 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
@@ -61,10 +61,9 @@
 import com.android.systemui.animation.ShadeInterpolation;
 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.KeyguardTransitionInteractor;
-import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants;
+import com.android.systemui.bouncer.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;
@@ -697,9 +696,7 @@
     }
 
     @Test
-    public void transitionToUnlocked_nonClippedQs_flagTrue_followsLargeScreensInterpolator() {
-        when(mFeatureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION))
-                .thenReturn(true);
+    public void transitionToUnlocked_nonClippedQs_followsLargeScreensInterpolator() {
         mScrimController.setClipsQsScrim(false);
         mScrimController.setRawPanelExpansionFraction(0f);
         mScrimController.transitionTo(ScrimState.UNLOCKED);
@@ -737,48 +734,6 @@
                 mScrimBehind, OPAQUE));
     }
 
-
-    @Test
-    public void transitionToUnlocked_nonClippedQs_flagFalse() {
-        when(mFeatureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION))
-                .thenReturn(false);
-        mScrimController.setClipsQsScrim(false);
-        mScrimController.setRawPanelExpansionFraction(0f);
-        mScrimController.transitionTo(ScrimState.UNLOCKED);
-        finishAnimationsImmediately();
-        assertScrimAlpha(Map.of(
-                mScrimInFront, TRANSPARENT,
-                mNotificationsScrim, TRANSPARENT,
-                mScrimBehind, TRANSPARENT));
-
-        assertScrimTinted(Map.of(
-                mNotificationsScrim, false,
-                mScrimInFront, false,
-                mScrimBehind, true
-        ));
-
-        // Back scrim should be visible after start dragging
-        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.31f);
-        assertScrimAlpha(Map.of(
-                mScrimInFront, TRANSPARENT,
-                mNotificationsScrim, SEMI_TRANSPARENT,
-                mScrimBehind, OPAQUE));
-    }
-
     @Test
     public void scrimStateCallback() {
         mScrimController.transitionTo(ScrimState.UNLOCKED);
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 3eea93c..c7143de 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,8 +16,8 @@
 
 package com.android.systemui.statusbar.phone;
 
-import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN;
-import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE;
+import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN;
+import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -60,16 +60,16 @@
 import com.android.keyguard.ViewMediatorCallback;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
+import com.android.systemui.bouncer.ui.BouncerView;
+import com.android.systemui.bouncer.ui.BouncerViewDelegate;
 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;
@@ -135,6 +135,7 @@
     @Mock private BouncerView mBouncerView;
     @Mock private BouncerViewDelegate mBouncerViewDelegate;
     @Mock private OnBackAnimationCallback mBouncerViewDelegateBackCallback;
+    @Mock private NotificationShadeWindowController mNotificationShadeWindowController;
     @Mock private NotificationShadeWindowView mNotificationShadeWindowView;
     @Mock private WindowInsetsController mWindowInsetsController;
     @Mock private TaskbarDelegate mTaskbarDelegate;
@@ -168,7 +169,7 @@
                 .isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM))
                 .thenReturn(true);
 
-        when(mCentralSurfaces.getNotificationShadeWindowView())
+        when(mNotificationShadeWindowController.getWindowRootView())
                 .thenReturn(mNotificationShadeWindowView);
         when(mNotificationShadeWindowView.getWindowInsetsController())
                 .thenReturn(mWindowInsetsController);
@@ -184,7 +185,7 @@
                         mDreamOverlayStateController,
                         mock(NavigationModeController.class),
                         mock(DockManager.class),
-                        mock(NotificationShadeWindowController.class),
+                        mNotificationShadeWindowController,
                         mKeyguardStateController,
                         mock(NotificationMediaManager.class),
                         mKeyguardMessageAreaFactory,
@@ -646,21 +647,21 @@
     @Test
     public void testReportBouncerOnDreamWhenVisible() {
         mBouncerExpansionCallback.onVisibilityChanged(true);
-        verify(mCentralSurfaces).setBouncerShowingOverDream(false);
+        assertFalse(mStatusBarKeyguardViewManager.isBouncerShowingOverDream());
         Mockito.clearInvocations(mCentralSurfaces);
         when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true);
         mBouncerExpansionCallback.onVisibilityChanged(true);
-        verify(mCentralSurfaces).setBouncerShowingOverDream(true);
+        assertTrue(mStatusBarKeyguardViewManager.isBouncerShowingOverDream());
     }
 
     @Test
     public void testReportBouncerOnDreamWhenNotVisible() {
         mBouncerExpansionCallback.onVisibilityChanged(false);
-        verify(mCentralSurfaces).setBouncerShowingOverDream(false);
+        assertFalse(mStatusBarKeyguardViewManager.isBouncerShowingOverDream());
         Mockito.clearInvocations(mCentralSurfaces);
         when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true);
         mBouncerExpansionCallback.onVisibilityChanged(false);
-        verify(mCentralSurfaces).setBouncerShowingOverDream(false);
+        assertFalse(mStatusBarKeyguardViewManager.isBouncerShowingOverDream());
     }
 
     @Test
@@ -736,6 +737,16 @@
     }
 
     @Test
+    public void testResetBouncerAnimatingAway() {
+        reset(mPrimaryBouncerInteractor);
+        when(mPrimaryBouncerInteractor.isAnimatingAway()).thenReturn(true);
+
+        mStatusBarKeyguardViewManager.reset(true);
+
+        verify(mPrimaryBouncerInteractor, never()).hide();
+    }
+
+    @Test
     public void handleDispatchTouchEvent_alternateBouncerNotVisible() {
         mStatusBarKeyguardViewManager.addCallback(mCallback);
 
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 d6ae2b7..d44af88 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
@@ -73,6 +73,7 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -233,6 +234,7 @@
                         mCentralSurfaces,
                         mock(NotificationPresenter.class),
                         mock(ShadeViewController.class),
+                        mock(NotificationShadeWindowController.class),
                         mActivityLaunchAnimator,
                         notificationAnimationProvider,
                         mock(LaunchFullScreenIntentProvider.class),
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 fdfe028..5bd6ff4 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
@@ -37,6 +37,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.QuickSettingsController;
@@ -44,7 +45,6 @@
 import com.android.systemui.shade.ShadeNotificationPresenter;
 import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
@@ -56,6 +56,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
 import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource;
+import com.android.systemui.statusbar.notification.domain.interactor.NotificationsInteractor;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptSuppressor;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -81,6 +82,8 @@
     private FakeMetricsLogger mMetricsLogger;
     private final ShadeController mShadeController = mock(ShadeController.class);
     private final CentralSurfaces mCentralSurfaces = mock(CentralSurfaces.class);
+    private final NotificationsInteractor mNotificationsInteractor =
+            mock(NotificationsInteractor.class);
     private final KeyguardStateController mKeyguardStateController =
             mock(KeyguardStateController.class);
     private final NotifPipelineFlags mNotifPipelineFlags = mock(NotifPipelineFlags.class);
@@ -122,9 +125,10 @@
                 mock(NotificationShadeWindowController.class),
                 mock(DynamicPrivacyController.class),
                 mKeyguardStateController,
-                mock(KeyguardIndicationController.class),
                 mCentralSurfaces,
+                mNotificationsInteractor,
                 mock(LockscreenShadeTransitionController.class),
+                mock(PowerInteractor.class),
                 mCommandQueue,
                 mock(NotificationLockscreenUserManager.class),
                 mock(SysuiStatusBarStateController.class),
@@ -235,9 +239,9 @@
                 .setTag("a")
                 .setNotification(n)
                 .build();
-        when(mCentralSurfaces.areNotificationAlertsDisabled()).thenReturn(true);
+        when(mNotificationsInteractor.areNotificationAlertsEnabled()).thenReturn(false);
 
-        assertTrue("CentralSurfaces alerts disabled shouldn't allow interruptions",
+        assertTrue("When alerts aren't enabled, interruptions are suppressed",
                 mInterruptSuppressor.suppressInterruptions(entry));
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
index ea534bb..2e9a690 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.statusbar.LightRevealScrim
+import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.StatusBarStateControllerImpl
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.mockito.eq
@@ -65,6 +66,8 @@
     @Mock
     private lateinit var shadeViewController: ShadeViewController
     @Mock
+    private lateinit var notifShadeWindowController: NotificationShadeWindowController
+    @Mock
     private lateinit var lightRevealScrim: LightRevealScrim
     @Mock
     private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
@@ -89,6 +92,7 @@
                 keyguardStateController,
                 dagger.Lazy<DozeParameters> { dozeParameters },
                 globalSettings,
+                dagger.Lazy<NotificationShadeWindowController> { notifShadeWindowController },
                 interactionJankMonitor,
                 powerManager,
                 handler = handler
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
index 9bc49ae..87d813c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
@@ -50,7 +50,6 @@
         val actualString = stringWriter.toString()
         val expectedLogString =
             disableFlagsLogger.getDisableFlagsString(
-                old = null,
                 new = state,
                 newAfterLocalModification = null,
             )
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 9b1d93b..5dcb901 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
@@ -18,10 +18,6 @@
 
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN;
-import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_IN;
-import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_OUT;
-import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.IDLE;
-import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.RUNNING_CHIP_ANIM;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -49,7 +45,7 @@
 import android.view.ViewPropertyAnimator;
 import android.widget.FrameLayout;
 
-import androidx.core.animation.Animator;
+import androidx.core.animation.AnimatorTestRule;
 import androidx.test.filters.SmallTest;
 
 import com.android.keyguard.KeyguardUpdateMonitor;
@@ -85,6 +81,7 @@
 import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
+import org.junit.ClassRule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -139,6 +136,8 @@
     private StatusBarWindowStateController mStatusBarWindowStateController;
     @Mock
     private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @ClassRule
+    public static AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
 
     private List<StatusBarWindowStateListener> mStatusBarWindowStateListeners = new ArrayList<>();
 
@@ -172,7 +171,6 @@
 
     @Test
     public void testDisableSystemInfo_systemAnimationIdle_doesHide() {
-        when(mAnimationScheduler.getAnimationState()).thenReturn(IDLE);
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
         fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_SYSTEM_INFO, 0, false);
@@ -192,24 +190,26 @@
     public void testSystemStatusAnimation_startedDisabled_finishedWithAnimator_showsSystemInfo() {
         // GIVEN the status bar hides the system info via disable flags, while there is no event
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
-        when(mAnimationScheduler.getAnimationState()).thenReturn(IDLE);
         fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_SYSTEM_INFO, 0, false);
         assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
 
+        // WHEN the system event animation starts
+        fragment.onSystemEventAnimationBegin().start();
+
+        // THEN the view remains invisible during the animation
+        assertEquals(0f, getEndSideContentView().getAlpha(), 0.01);
+        mAnimatorTestRule.advanceTimeBy(500);
+        assertEquals(0f, getEndSideContentView().getAlpha(), 0.01);
+
         // WHEN the disable flags are cleared during a system event animation
-        when(mAnimationScheduler.getAnimationState()).thenReturn(RUNNING_CHIP_ANIM);
         fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
 
-        // THEN the view is made visible again, but still low alpha
-        assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+        // THEN the view remains invisible
         assertEquals(0, getEndSideContentView().getAlpha(), 0.01);
 
         // WHEN the system event animation finishes
-        when(mAnimationScheduler.getAnimationState()).thenReturn(ANIMATING_OUT);
-        Animator anim = fragment.onSystemEventAnimationFinish(false);
-        anim.start();
-        processAllMessages();
-        anim.end();
+        fragment.onSystemEventAnimationFinish(false).start();
+        mAnimatorTestRule.advanceTimeBy(500);
 
         // THEN the system info is full alpha
         assertEquals(1, getEndSideContentView().getAlpha(), 0.01);
@@ -219,20 +219,15 @@
     public void testSystemStatusAnimation_systemInfoDisabled_staysInvisible() {
         // GIVEN the status bar hides the system info via disable flags, while there is no event
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
-        when(mAnimationScheduler.getAnimationState()).thenReturn(IDLE);
         fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_SYSTEM_INFO, 0, false);
         assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
 
         // WHEN the system event animation finishes
-        when(mAnimationScheduler.getAnimationState()).thenReturn(ANIMATING_OUT);
-        Animator anim = fragment.onSystemEventAnimationFinish(false);
-        anim.start();
-        processAllMessages();
-        anim.end();
+        fragment.onSystemEventAnimationFinish(false).start();
+        mAnimatorTestRule.advanceTimeBy(500);
 
-        // THEN the system info is at full alpha, but still INVISIBLE (since the disable flag is
-        // still set)
-        assertEquals(1, getEndSideContentView().getAlpha(), 0.01);
+        // THEN the system info remains invisible (since the disable flag is still set)
+        assertEquals(0, getEndSideContentView().getAlpha(), 0.01);
         assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
     }
 
@@ -241,15 +236,14 @@
     public void testSystemStatusAnimation_notDisabled_animatesAlphaZero() {
         // GIVEN the status bar is not disabled
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
-        when(mAnimationScheduler.getAnimationState()).thenReturn(ANIMATING_IN);
-        // WHEN the system event animation begins
-        Animator anim = fragment.onSystemEventAnimationBegin();
-        anim.start();
-        processAllMessages();
-        anim.end();
+        assertEquals(1, getEndSideContentView().getAlpha(), 0.01);
 
-        // THEN the system info is visible but alpha 0
-        assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+        // WHEN the system event animation begins
+        fragment.onSystemEventAnimationBegin().start();
+        mAnimatorTestRule.advanceTimeBy(500);
+
+        // THEN the system info is invisible
+        assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
         assertEquals(0, getEndSideContentView().getAlpha(), 0.01);
     }
 
@@ -257,25 +251,21 @@
     public void testSystemStatusAnimation_notDisabled_animatesBackToAlphaOne() {
         // GIVEN the status bar is not disabled
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
-        when(mAnimationScheduler.getAnimationState()).thenReturn(ANIMATING_IN);
-        // WHEN the system event animation begins
-        Animator anim = fragment.onSystemEventAnimationBegin();
-        anim.start();
-        processAllMessages();
-        anim.end();
+        assertEquals(1, getEndSideContentView().getAlpha(), 0.01);
 
-        // THEN the system info is visible but alpha 0
-        assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+        // WHEN the system event animation begins
+        fragment.onSystemEventAnimationBegin().start();
+        mAnimatorTestRule.advanceTimeBy(500);
+
+        // THEN the system info is invisible
+        assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
         assertEquals(0, getEndSideContentView().getAlpha(), 0.01);
 
         // WHEN the system event animation finishes
-        when(mAnimationScheduler.getAnimationState()).thenReturn(ANIMATING_OUT);
-        anim = fragment.onSystemEventAnimationFinish(false);
-        anim.start();
-        processAllMessages();
-        anim.end();
+        fragment.onSystemEventAnimationFinish(false).start();
+        mAnimatorTestRule.advanceTimeBy(500);
 
-        // THEN the syste info is full alpha and VISIBLE
+        // THEN the system info is full alpha and VISIBLE
         assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
         assertEquals(1, getEndSideContentView().getAlpha(), 0.01);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt
new file mode 100644
index 0000000..2617613
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.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.statusbar.phone.fragment
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import androidx.core.animation.AnimatorTestRule
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_SOURCE_1 = 1
+private const val TEST_SOURCE_2 = 2
+private const val TEST_ANIMATION_DURATION = 100L
+private const val INITIAL_ALPHA = 1f
+
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+class MultiSourceMinAlphaControllerTest : SysuiTestCase() {
+
+    private val view = View(context)
+    private val multiSourceMinAlphaController =
+        MultiSourceMinAlphaController(view, initialAlpha = INITIAL_ALPHA)
+
+    @get:Rule val animatorTestRule = AnimatorTestRule()
+
+    @Before
+    fun setup() {
+        multiSourceMinAlphaController.reset()
+    }
+
+    @Test
+    fun testSetAlpha() {
+        multiSourceMinAlphaController.setAlpha(alpha = 0.5f, sourceId = TEST_SOURCE_1)
+        assertEquals(0.5f, view.alpha)
+    }
+
+    @Test
+    fun testAnimateToAlpha() {
+        multiSourceMinAlphaController.animateToAlpha(
+            alpha = 0.5f,
+            sourceId = TEST_SOURCE_1,
+            duration = TEST_ANIMATION_DURATION
+        )
+        animatorTestRule.advanceTimeBy(TEST_ANIMATION_DURATION)
+        assertEquals(0.5f, view.alpha)
+    }
+
+    @Test
+    fun testReset() {
+        multiSourceMinAlphaController.animateToAlpha(
+            alpha = 0.5f,
+            sourceId = TEST_SOURCE_1,
+            duration = TEST_ANIMATION_DURATION
+        )
+        multiSourceMinAlphaController.setAlpha(alpha = 0.7f, sourceId = TEST_SOURCE_2)
+        multiSourceMinAlphaController.reset()
+        // advance time to ensure that animators are cancelled when the controller is reset
+        animatorTestRule.advanceTimeBy(TEST_ANIMATION_DURATION)
+        assertEquals(INITIAL_ALPHA, view.alpha)
+    }
+
+    @Test
+    fun testMinOfTwoSourcesIsApplied() {
+        multiSourceMinAlphaController.setAlpha(alpha = 0f, sourceId = TEST_SOURCE_1)
+        multiSourceMinAlphaController.setAlpha(alpha = 0.5f, sourceId = TEST_SOURCE_2)
+        assertEquals(0f, view.alpha)
+        multiSourceMinAlphaController.setAlpha(alpha = 1f, sourceId = TEST_SOURCE_1)
+        assertEquals(0.5f, view.alpha)
+    }
+
+    @Test
+    fun testSetAlphaForSameSourceCancelsAnimator() {
+        multiSourceMinAlphaController.animateToAlpha(
+            alpha = 0f,
+            sourceId = TEST_SOURCE_1,
+            duration = TEST_ANIMATION_DURATION
+        )
+        animatorTestRule.advanceTimeBy(TEST_ANIMATION_DURATION / 2)
+        multiSourceMinAlphaController.setAlpha(alpha = 1f, sourceId = TEST_SOURCE_1)
+        animatorTestRule.advanceTimeBy(TEST_ANIMATION_DURATION / 2)
+        // verify that animation was cancelled and the setAlpha call overrides the alpha value of
+        // the animation
+        assertEquals(1f, view.alpha)
+    }
+}
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
index 6e3af26..932c4a1 100644
--- 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
@@ -152,13 +152,11 @@
         }
 
     private fun sendConfig(subId: Int) {
-        fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
-            receiver.onReceive(
-                context,
-                Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
-                    .putExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, subId)
-            )
-        }
+        fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+            context,
+            Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
+                .putExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, subId),
+        )
     }
 
     companion object {
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 d1df6e3..cf832b4 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
@@ -379,9 +379,10 @@
             var latest: Int? = null
             val job = underTest.carrierId.onEach { latest = it }.launchIn(this)
 
-            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
-                receiver.onReceive(context, carrierIdIntent(carrierId = 4321))
-            }
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                carrierIdIntent(carrierId = 4321),
+            )
 
             assertThat(latest).isEqualTo(4321)
 
@@ -653,10 +654,7 @@
             val job = underTest.networkName.onEach { latest = it }.launchIn(this)
 
             val intent = spnIntent()
-
-            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
-                receiver.onReceive(context, intent)
-            }
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
 
             assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP))
 
@@ -670,17 +668,13 @@
             val job = underTest.networkName.onEach { latest = it }.launchIn(this)
 
             val intent = spnIntent()
-            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
-                receiver.onReceive(context, intent)
-            }
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(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)
-            }
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, wrongSubIntent)
 
             // THEN the previous intent's name is still used
             assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP))
@@ -695,9 +689,7 @@
             val job = underTest.networkName.onEach { latest = it }.launchIn(this)
 
             val intent = spnIntent()
-            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
-                receiver.onReceive(context, intent)
-            }
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
             assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP))
 
             val intentWithoutInfo =
@@ -706,9 +698,7 @@
                     showPlmn = false,
                 )
 
-            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
-                receiver.onReceive(context, intentWithoutInfo)
-            }
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intentWithoutInfo)
 
             assertThat(latest).isEqualTo(DEFAULT_NAME)
 
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 fd156d8..862eb00 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
@@ -73,7 +73,6 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertThrows
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
@@ -588,11 +587,10 @@
         }
 
     @Test
-    fun testConnectionRepository_invalidSubId_throws() =
+    fun testConnectionRepository_invalidSubId_doesNotThrow() =
         testScope.runTest {
-            assertThrows(IllegalArgumentException::class.java) {
-                underTest.getRepoForSubId(SUB_1_ID)
-            }
+            underTest.getRepoForSubId(SUB_1_ID)
+            // No exception
         }
 
     @Test
@@ -626,23 +624,17 @@
 
             assertThat(latest).isEqualTo(INVALID_SUBSCRIPTION_ID)
 
-            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
-                receiver.onReceive(
-                    context,
-                    Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
-                        .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID)
-                )
-            }
+            val intent2 =
+                Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+                    .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID)
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent2)
 
             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)
-                )
-            }
+            val intent1 =
+                Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+                    .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID)
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent1)
 
             assertThat(latest).isEqualTo(SUB_1_ID)
         }
@@ -1074,13 +1066,10 @@
             assertThat(configFromContext.showAtLeast3G).isTrue()
 
             // WHEN the change event is fired
-            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
-                receiver.onReceive(
-                    context,
-                    Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
-                        .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID)
-                )
-            }
+            val intent =
+                Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+                    .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID)
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
 
             // THEN the config is updated
             assertTrue(latest!!.areEqual(configFromContext))
@@ -1099,12 +1088,10 @@
             assertThat(configFromContext.showAtLeast3G).isTrue()
 
             // WHEN the change event is fired
-            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
-                receiver.onReceive(
-                    context,
-                    Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
-                )
-            }
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED),
+            )
 
             // THEN the config is updated
             assertThat(latest!!.areEqual(configFromContext)).isTrue()
@@ -1123,12 +1110,10 @@
             assertThat(configFromContext.showAtLeast3G).isTrue()
 
             // WHEN the change event is fired
-            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
-                receiver.onReceive(
-                    context,
-                    Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
-                )
-            }
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED),
+            )
 
             // WHEN collection starts AFTER the broadcast is sent out
             val latest by collectLastValue(underTest.defaultDataSubRatConfig)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
index 6301fa0..842d548 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -48,7 +48,11 @@
 
         keyguardTransitionRepository = FakeKeyguardTransitionRepository()
         val interactor =
-            KeyguardTransitionInteractor(keyguardTransitionRepository, testScope.backgroundScope)
+            KeyguardTransitionInteractorFactory.create(
+                    scope = TestScope().backgroundScope,
+                    repository = keyguardTransitionRepository,
+                )
+                .keyguardTransitionInteractor
         underTest = CollapsedStatusBarViewModelImpl(interactor, testScope.backgroundScope)
     }
 
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 dc68180..5ed3a5c 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
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.data.repository.prod
 
+import android.content.Intent
 import android.net.ConnectivityManager
 import android.net.Network
 import android.net.NetworkCapabilities
@@ -33,7 +34,6 @@
 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.data.model.ConnectivitySlots
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
@@ -46,13 +46,10 @@
 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.nullable
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.Executor
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.TestScope
@@ -60,7 +57,6 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
-import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
@@ -73,7 +69,6 @@
 
     private lateinit var underTest: WifiRepositoryImpl
 
-    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
     @Mock private lateinit var logger: WifiInputLogger
     @Mock private lateinit var tableLogger: TableLogBuffer
     @Mock private lateinit var connectivityManager: ConnectivityManager
@@ -86,15 +81,6 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        whenever(
-                broadcastDispatcher.broadcastFlow(
-                    any(),
-                    nullable(),
-                    anyInt(),
-                    nullable(),
-                )
-            )
-            .thenReturn(flowOf(Unit))
         executor = FakeExecutor(FakeSystemClock())
 
         connectivityRepository =
@@ -168,27 +154,23 @@
     @Test
     fun isWifiEnabled_intentsReceived_valueUpdated() =
         testScope.runTest {
-            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)
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(WifiManager.WIFI_STATE_CHANGED_ACTION),
+            )
 
             assertThat(underTest.isWifiEnabled.value).isTrue()
 
             whenever(wifiManager.isWifiEnabled).thenReturn(false)
-            intentFlow.emit(Unit)
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(WifiManager.WIFI_STATE_CHANGED_ACTION),
+            )
 
             assertThat(underTest.isWifiEnabled.value).isFalse()
 
@@ -198,23 +180,16 @@
     @Test
     fun isWifiEnabled_bothIntentAndNetworkUpdates_valueAlwaysUpdated() =
         testScope.runTest {
-            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)
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(WifiManager.WIFI_STATE_CHANGED_ACTION),
+            )
             assertThat(underTest.isWifiEnabled.value).isFalse()
 
             whenever(wifiManager.isWifiEnabled).thenReturn(true)
@@ -227,7 +202,10 @@
             assertThat(underTest.isWifiEnabled.value).isFalse()
 
             whenever(wifiManager.isWifiEnabled).thenReturn(true)
-            intentFlow.emit(Unit)
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(WifiManager.WIFI_STATE_CHANGED_ACTION),
+            )
             assertThat(underTest.isWifiEnabled.value).isTrue()
 
             networkJob.cancel()
@@ -1317,7 +1295,7 @@
 
     private fun createRepo(): WifiRepositoryImpl {
         return WifiRepositoryImpl(
-            broadcastDispatcher,
+            fakeBroadcastDispatcher,
             connectivityManager,
             connectivityRepository,
             logger,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardStateControllerTest.java
index d787ada..5cabcd4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardStateControllerTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.hardware.biometrics.BiometricSourceType;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
@@ -43,6 +44,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -66,6 +68,9 @@
     @Mock
     private KeyguardUpdateMonitorLogger mLogger;
 
+    @Captor
+    private ArgumentCaptor<KeyguardUpdateMonitorCallback> mUpdateCallbackCaptor;
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
@@ -84,6 +89,23 @@
     }
 
     @Test
+    public void testFaceAuthEnabledChanged_calledWhenFaceEnrollmentStateChanges() {
+        KeyguardStateController.Callback callback = mock(KeyguardStateController.Callback.class);
+
+        when(mKeyguardUpdateMonitor.isFaceAuthEnabledForUser(anyInt())).thenReturn(false);
+        verify(mKeyguardUpdateMonitor).registerCallback(mUpdateCallbackCaptor.capture());
+        mKeyguardStateController.addCallback(callback);
+        assertThat(mKeyguardStateController.isFaceAuthEnabled()).isFalse();
+
+        when(mKeyguardUpdateMonitor.isFaceAuthEnabledForUser(anyInt())).thenReturn(true);
+        mUpdateCallbackCaptor.getValue().onBiometricEnrollmentStateChanged(
+                BiometricSourceType.FACE);
+
+        assertThat(mKeyguardStateController.isFaceAuthEnabled()).isTrue();
+        verify(callback).onFaceAuthEnabledChanged();
+    }
+
+    @Test
     public void testIsShowing() {
         assertThat(mKeyguardStateController.isShowing()).isFalse();
         mKeyguardStateController.notifyKeyguardState(true /* showing */, false /* occluded */);
@@ -177,16 +199,14 @@
     @Test
     public void testOnEnabledTrustAgentsChangedCallback() {
         final Random random = new Random();
-        final ArgumentCaptor<KeyguardUpdateMonitorCallback> updateCallbackCaptor =
-                ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
 
-        verify(mKeyguardUpdateMonitor).registerCallback(updateCallbackCaptor.capture());
+        verify(mKeyguardUpdateMonitor).registerCallback(mUpdateCallbackCaptor.capture());
         final KeyguardStateController.Callback stateCallback =
                 mock(KeyguardStateController.Callback.class);
         mKeyguardStateController.addCallback(stateCallback);
 
         when(mLockPatternUtils.isSecure(anyInt())).thenReturn(true);
-        updateCallbackCaptor.getValue().onEnabledTrustAgentsChanged(random.nextInt());
+        mUpdateCallbackCaptor.getValue().onEnabledTrustAgentsChanged(random.nextInt());
         verify(stateCallback).onUnlockedChanged();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
index 3b0d512..ae38958e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
@@ -51,9 +51,11 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.SmartReplyController;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
@@ -107,14 +109,15 @@
 
     private SmartReplyInflaterImpl mSmartReplyInflater;
     private SmartActionInflaterImpl mSmartActionInflater;
+    private KeyguardDismissUtil mKeyguardDismissUtil;
 
     @Mock private SmartReplyConstants mConstants;
     @Mock private ActivityStarter mActivityStarter;
     @Mock private HeadsUpManager mHeadsUpManager;
     @Mock private NotificationRemoteInputManager mNotificationRemoteInputManager;
     @Mock private SmartReplyController mSmartReplyController;
-
-    private final KeyguardDismissUtil mKeyguardDismissUtil = new KeyguardDismissUtil();
+    @Mock private  KeyguardStateController mKeyguardStateController;
+    @Mock private  SysuiStatusBarStateController mStatusBarStateController;
 
     @Before
     public void setUp() {
@@ -122,12 +125,15 @@
         mReceiver = new BlockingQueueIntentReceiver();
         mContext.registerReceiver(mReceiver, new IntentFilter(TEST_ACTION),
                 Context.RECEIVER_EXPORTED_UNAUDITED);
-        mKeyguardDismissUtil.setDismissHandler((action, unused, afterKgGone) -> action.onDismiss());
+
         mDependency.injectMockDependency(KeyguardUpdateMonitor.class);
         mDependency.injectMockDependency(ShadeController.class);
         mDependency.injectMockDependency(NotificationRemoteInputManager.class);
         mDependency.injectTestDependency(ActivityStarter.class, mActivityStarter);
         mDependency.injectTestDependency(SmartReplyConstants.class, mConstants);
+        mDependency.injectTestDependency(KeyguardStateController.class, mKeyguardStateController);
+        mDependency.injectTestDependency(StatusBarStateController.class, mStatusBarStateController);
+
 
         // Any number of replies are fine.
         when(mConstants.getMinNumSystemGeneratedReplies()).thenReturn(0);
@@ -153,6 +159,13 @@
 
         mActionIcon = Icon.createWithResource(mContext, R.drawable.ic_person);
 
+        mKeyguardDismissUtil = new KeyguardDismissUtil(
+                mKeyguardStateController, mStatusBarStateController, mActivityStarter) {
+            public void executeWhenUnlocked(ActivityStarter.OnDismissAction action,
+                    boolean requiresShadeOpen, boolean afterKeyguardGone) {
+                action.onDismiss();
+            }
+        };
         mSmartReplyInflater = new SmartReplyInflaterImpl(
                 mConstants,
                 mKeyguardDismissUtil,
@@ -185,7 +198,17 @@
 
     @Test
     public void testSendSmartReply_keyguardCancelled() throws InterruptedException {
-        mKeyguardDismissUtil.setDismissHandler((action, unused, afterKgGone) -> { });
+        mKeyguardDismissUtil = new KeyguardDismissUtil(
+                mKeyguardStateController, mStatusBarStateController, mActivityStarter) {
+            public void executeWhenUnlocked(ActivityStarter.OnDismissAction action,
+                    boolean requiresShadeOpen, boolean afterKeyguardGone) { }};
+        mSmartReplyInflater = new SmartReplyInflaterImpl(
+                mConstants,
+                mKeyguardDismissUtil,
+                mNotificationRemoteInputManager,
+                mSmartReplyController,
+                mContext);
+
         setSmartReplies(TEST_CHOICES);
 
         mView.getChildAt(2).performClick();
@@ -196,9 +219,20 @@
     @Test
     public void testSendSmartReply_waitsForKeyguard() throws InterruptedException {
         AtomicReference<OnDismissAction> actionRef = new AtomicReference<>();
+        mKeyguardDismissUtil = new KeyguardDismissUtil(
+                mKeyguardStateController, mStatusBarStateController, mActivityStarter) {
+            public void executeWhenUnlocked(ActivityStarter.OnDismissAction action,
+                    boolean requiresShadeOpen, boolean afterKeyguardGone) {
+                actionRef.set(action);
+            }
+        };
+        mSmartReplyInflater = new SmartReplyInflaterImpl(
+                mConstants,
+                mKeyguardDismissUtil,
+                mNotificationRemoteInputManager,
+                mSmartReplyController,
+                mContext);
 
-        mKeyguardDismissUtil.setDismissHandler((action, unused, afterKgGone)
-                -> actionRef.set(action));
         setSmartReplies(TEST_CHOICES);
 
         mView.getChildAt(2).performClick();
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 d9ee081..813597a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
@@ -28,12 +28,10 @@
 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.keyguard.domain.interactor.KeyguardInteractorFactory
 import com.android.systemui.shade.ShadeFoldAnimator
 import com.android.systemui.shade.ShadeViewController
-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
@@ -80,8 +78,6 @@
 
     @Mock lateinit var viewTreeObserver: ViewTreeObserver
 
-    @Mock private lateinit var commandQueue: CommandQueue
-
     @Mock lateinit var shadeFoldAnimator: ShadeFoldAnimator
 
     @Captor private lateinit var foldStateListenerCaptor: ArgumentCaptor<FoldStateListener>
@@ -111,15 +107,10 @@
             onActionStarted.run()
         }
 
-        keyguardRepository = FakeKeyguardRepository()
         val featureFlags = FakeFeatureFlags().apply { set(FACE_AUTH_REFACTOR, true) }
-        val keyguardInteractor =
-            KeyguardInteractor(
-                repository = keyguardRepository,
-                commandQueue = commandQueue,
-                featureFlags = featureFlags,
-                bouncerRepository = FakeKeyguardBouncerRepository(),
-            )
+        val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags)
+        val keyguardInteractor = withDeps.keyguardInteractor
+        keyguardRepository = withDeps.repository
 
         // Needs to be run on the main thread
         runBlocking(IMMEDIATE) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
index b30c20d..b04eb01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
@@ -25,8 +25,8 @@
 import android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS
 import androidx.test.filters.SmallTest
 import androidx.test.rule.ActivityTestRule
-import androidx.test.runner.intercepting.SingleActivityFactory
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.activity.SingleActivityFactory
 import com.google.common.truth.Truth.assertThat
 
 import javax.inject.Inject
@@ -49,19 +49,17 @@
 
     open class UsbPermissionActivityTestable @Inject constructor (
         val message: UsbAudioWarningDialogMessage
-    )
-        : UsbPermissionActivity(UsbAudioWarningDialogMessage())
+    ) : UsbPermissionActivity(UsbAudioWarningDialogMessage())
 
     @Rule
     @JvmField
-    var activityRule = ActivityTestRule<UsbPermissionActivityTestable>(
-            object : SingleActivityFactory<UsbPermissionActivityTestable>(
-                    UsbPermissionActivityTestable::class.java
-            ) {
-                    override fun create(intent: Intent?): UsbPermissionActivityTestable {
-                        return UsbPermissionActivityTestable(mMessage)
-                    }
-            }, false, false)
+    var activityRule = ActivityTestRule(
+            /* activityFactory= */ SingleActivityFactory {
+                UsbPermissionActivityTestable(mMessage)
+            },
+            /* initialTouchMode= */ false,
+            /* launchActivity= */ false,
+    )
 
     private val activityIntent = Intent(mContext, UsbPermissionActivityTestable::class.java)
             .apply {
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 079fbcd..0c28cbb 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
@@ -26,6 +26,8 @@
 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.SelectedUserModel
+import com.android.systemui.user.data.model.SelectionStatus
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
@@ -224,6 +226,40 @@
     }
 
     @Test
+    fun userTrackerCallback_updatesSelectionStatus() = runSelfCancelingTest {
+        underTest = create(this)
+        var selectedUser: SelectedUserModel? = null
+        underTest.selectedUser.onEach { selectedUser = it }.launchIn(this)
+        setUpUsers(count = 2, selectedIndex = 1)
+
+        // WHEN the user is changing
+        tracker.onUserChanging(userId = 1)
+
+        // THEN the selection status is IN_PROGRESS
+        assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_IN_PROGRESS)
+
+        // WHEN the user has finished changing
+        tracker.onUserChanged(userId = 1)
+
+        // THEN the selection status is COMPLETE
+        assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_COMPLETE)
+
+        tracker.onProfileChanged()
+        assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_COMPLETE)
+
+        setUpUsers(count = 2, selectedIndex = 0)
+
+        tracker.onUserChanging(userId = 0)
+        assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_IN_PROGRESS)
+
+        // WHEN a profile change occurs while a user is changing
+        tracker.onProfileChanged()
+
+        // THEN the selection status remains as IN_PROGRESS
+        assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_IN_PROGRESS)
+    }
+
+    @Test
     fun userSwitchingInProgress_registersUserTrackerCallback() = runSelfCancelingTest {
         underTest = create(this)
 
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 ca83d49..89dce61 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
@@ -39,12 +39,10 @@
 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.keyguard.domain.interactor.KeyguardInteractorFactory
 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
@@ -97,7 +95,6 @@
     @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
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
 
     private lateinit var underTest: UserInteractor
@@ -126,8 +123,9 @@
                 set(Flags.FULL_SCREEN_USER_SWITCHER, false)
                 set(Flags.FACE_AUTH_REFACTOR, true)
             }
+        val reply = KeyguardInteractorFactory.create(featureFlags = featureFlags)
+        keyguardRepository = reply.repository
         userRepository = FakeUserRepository()
-        keyguardRepository = FakeKeyguardRepository()
         telephonyRepository = FakeTelephonyRepository()
         val testDispatcher = StandardTestDispatcher()
         testScope = TestScope(testDispatcher)
@@ -142,13 +140,7 @@
                 applicationContext = context,
                 repository = userRepository,
                 activityStarter = activityStarter,
-                keyguardInteractor =
-                    KeyguardInteractor(
-                        repository = keyguardRepository,
-                        commandQueue = commandQueue,
-                        featureFlags = featureFlags,
-                        bouncerRepository = FakeKeyguardBouncerRepository(),
-                    ),
+                keyguardInteractor = reply.keyguardInteractor,
                 manager = manager,
                 headlessSystemUserMode = headlessSystemUserMode,
                 applicationScope = testScope.backgroundScope,
@@ -641,13 +633,11 @@
 
             userRepository.setSelectedUserInfo(userInfos[1])
             runCurrent()
-            fakeBroadcastDispatcher.registeredReceivers.forEach {
-                it.onReceive(
-                    context,
-                    Intent(Intent.ACTION_USER_SWITCHED)
-                        .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id),
-                )
-            }
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(Intent.ACTION_USER_SWITCHED)
+                    .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id),
+            )
             runCurrent()
 
             verify(callback1, atLeastOnce()).onUserStateChanged()
@@ -664,12 +654,10 @@
             userRepository.setSelectedUserInfo(userInfos[0])
             val refreshUsersCallCount = userRepository.refreshUsersCallCount
 
-            fakeBroadcastDispatcher.registeredReceivers.forEach {
-                it.onReceive(
-                    context,
-                    Intent(Intent.ACTION_USER_INFO_CHANGED),
-                )
-            }
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(Intent.ACTION_USER_INFO_CHANGED),
+            )
 
             runCurrent()
 
@@ -684,13 +672,11 @@
             userRepository.setSelectedUserInfo(userInfos[0])
             val refreshUsersCallCount = userRepository.refreshUsersCallCount
 
-            fakeBroadcastDispatcher.registeredReceivers.forEach {
-                it.onReceive(
-                    context,
-                    Intent(Intent.ACTION_USER_UNLOCKED)
-                        .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM),
-                )
-            }
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(Intent.ACTION_USER_UNLOCKED)
+                    .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM),
+            )
             runCurrent()
 
             assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
@@ -704,12 +690,10 @@
             userRepository.setSelectedUserInfo(userInfos[0])
             val refreshUsersCallCount = userRepository.refreshUsersCallCount
 
-            fakeBroadcastDispatcher.registeredReceivers.forEach {
-                it.onReceive(
-                    context,
-                    Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337),
-                )
-            }
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337),
+            )
 
             assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount)
         }
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 fd8c6c7..9cb26e0 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
@@ -32,11 +32,8 @@
 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.keyguard.domain.interactor.KeyguardInteractorFactory
 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
@@ -80,13 +77,11 @@
     @Mock private lateinit var uiEventLogger: UiEventLogger
     @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
     @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
-    @Mock private lateinit var commandQueue: CommandQueue
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
 
     private lateinit var underTest: StatusBarUserChipViewModel
 
     private val userRepository = FakeUserRepository()
-    private val keyguardRepository = FakeKeyguardRepository()
     private lateinit var guestUserInteractor: GuestUserInteractor
     private lateinit var refreshUsersScheduler: RefreshUsersScheduler
 
@@ -250,12 +245,8 @@
                     repository = userRepository,
                     activityStarter = activityStarter,
                     keyguardInteractor =
-                        KeyguardInteractor(
-                            repository = keyguardRepository,
-                            commandQueue = commandQueue,
-                            featureFlags = featureFlags,
-                            bouncerRepository = FakeKeyguardBouncerRepository(),
-                        ),
+                        KeyguardInteractorFactory.create(featureFlags = featureFlags)
+                            .keyguardInteractor,
                     featureFlags = featureFlags,
                     manager = manager,
                     headlessSystemUserMode = headlessSystemUserMode,
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 9155084..e3f9fac 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
@@ -30,11 +30,9 @@
 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.keyguard.domain.interactor.KeyguardInteractorFactory
 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
@@ -79,7 +77,6 @@
     @Mock private lateinit var uiEventLogger: UiEventLogger
     @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
     @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
-    @Mock private lateinit var commandQueue: CommandQueue
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
 
     private lateinit var underTest: UserSwitcherViewModel
@@ -112,7 +109,6 @@
             )
         }
 
-        keyguardRepository = FakeKeyguardRepository()
         val refreshUsersScheduler =
             RefreshUsersScheduler(
                 applicationScope = testScope.backgroundScope,
@@ -140,38 +136,35 @@
                 set(Flags.FULL_SCREEN_USER_SWITCHER, false)
                 set(Flags.FACE_AUTH_REFACTOR, true)
             }
+        val reply = KeyguardInteractorFactory.create(featureFlags = featureFlags)
+        keyguardRepository = reply.repository
+
         underTest =
             UserSwitcherViewModel(
-                    userInteractor =
-                        UserInteractor(
-                            applicationContext = context,
-                            repository = userRepository,
-                            activityStarter = activityStarter,
-                            keyguardInteractor =
-                                KeyguardInteractor(
-                                    repository = keyguardRepository,
-                                    commandQueue = commandQueue,
-                                    featureFlags = featureFlags,
-                                    bouncerRepository = FakeKeyguardBouncerRepository(),
-                                ),
-                            featureFlags = featureFlags,
-                            manager = manager,
-                            headlessSystemUserMode = headlessSystemUserMode,
-                            applicationScope = testScope.backgroundScope,
-                            telephonyInteractor =
-                                TelephonyInteractor(
-                                    repository = FakeTelephonyRepository(),
-                                ),
-                            broadcastDispatcher = fakeBroadcastDispatcher,
-                            keyguardUpdateMonitor = keyguardUpdateMonitor,
-                            backgroundDispatcher = testDispatcher,
-                            activityManager = activityManager,
-                            refreshUsersScheduler = refreshUsersScheduler,
-                            guestUserInteractor = guestUserInteractor,
-                            uiEventLogger = uiEventLogger,
-                        ),
-                    guestUserInteractor = guestUserInteractor,
-                )
+                userInteractor =
+                    UserInteractor(
+                        applicationContext = context,
+                        repository = userRepository,
+                        activityStarter = activityStarter,
+                        keyguardInteractor = reply.keyguardInteractor,
+                        featureFlags = featureFlags,
+                        manager = manager,
+                        headlessSystemUserMode = headlessSystemUserMode,
+                        applicationScope = testScope.backgroundScope,
+                        telephonyInteractor =
+                            TelephonyInteractor(
+                                repository = FakeTelephonyRepository(),
+                            ),
+                        broadcastDispatcher = fakeBroadcastDispatcher,
+                        keyguardUpdateMonitor = keyguardUpdateMonitor,
+                        backgroundDispatcher = testDispatcher,
+                        activityManager = activityManager,
+                        refreshUsersScheduler = refreshUsersScheduler,
+                        guestUserInteractor = guestUserInteractor,
+                        uiEventLogger = uiEventLogger,
+                    ),
+                guestUserInteractor = guestUserInteractor,
+            )
     }
 
     @Test
@@ -323,7 +316,7 @@
             setUsers(count = 2)
             val isFinishRequested = mutableListOf<Boolean>()
             val job =
-                    launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
+                launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
             assertThat(isFinishRequested.last()).isFalse()
 
             underTest.onCancelButtonClicked()
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 47a86b1..a09af00 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -35,6 +35,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
@@ -133,15 +134,19 @@
 import com.android.wm.shell.bubbles.BubbleDataRepository;
 import com.android.wm.shell.bubbles.BubbleEntry;
 import com.android.wm.shell.bubbles.BubbleLogger;
+import com.android.wm.shell.bubbles.BubbleOverflow;
 import com.android.wm.shell.bubbles.BubbleStackView;
 import com.android.wm.shell.bubbles.BubbleViewInfoTask;
+import com.android.wm.shell.bubbles.BubbleViewProvider;
 import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.bubbles.StackEducationViewKt;
+import com.android.wm.shell.bubbles.properties.BubbleProperties;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -151,7 +156,6 @@
 
 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;
@@ -300,6 +304,8 @@
 
     private UserHandle mUser0;
 
+    private FakeBubbleProperties mBubbleProperties;
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -320,7 +326,7 @@
                 mColorExtractor, mDumpManager, mKeyguardStateController,
                 mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager,
                 mShadeWindowLogger);
-        mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
+        mNotificationShadeWindowController.setWindowRootView(mNotificationShadeWindowView);
         mNotificationShadeWindowController.attach();
 
         mAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class);
@@ -361,6 +367,7 @@
                         mock(UserTracker.class)
                 );
         when(mShellTaskOrganizer.getExecutor()).thenReturn(syncExecutor);
+        mBubbleProperties = new FakeBubbleProperties();
         mBubbleController = new TestableBubbleController(
                 mContext,
                 mShellInit,
@@ -385,7 +392,8 @@
                 mock(Handler.class),
                 mTaskViewTransitions,
                 mock(SyncTransactionQueue.class),
-                mock(IWindowManager.class));
+                mock(IWindowManager.class),
+                mBubbleProperties);
         mBubbleController.setExpandListener(mBubbleExpandListener);
         spyOn(mBubbleController);
 
@@ -476,7 +484,7 @@
     public void testAddBubble() {
         mBubbleController.updateBubble(mBubbleEntry);
         assertTrue(mBubbleController.hasBubbles());
-        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
     }
 
     @Test
@@ -484,7 +492,7 @@
         assertFalse(mBubbleController.hasBubbles());
         mBubbleController.updateBubble(mBubbleEntry);
         assertTrue(mBubbleController.hasBubbles());
-        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
     }
 
     @Test
@@ -499,7 +507,7 @@
         assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
         verify(mNotifCallback, times(2)).invalidateNotifications(anyString());
 
-        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
     }
 
     @Test
@@ -562,7 +570,7 @@
         assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey()));
         assertNull(mBubbleData.getBubbleInStackWithKey(mRow2.getKey()));
 
-        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
     }
 
     @Test
@@ -581,7 +589,7 @@
         mBubbleData.setExpanded(true);
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
 
         // Make sure the notif is suppressed
         assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
@@ -590,11 +598,10 @@
         mBubbleController.collapseStack();
         verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
         assertStackCollapsed();
-        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
     }
 
     @Test
-    @Ignore("Currently broken.")
     public void testCollapseAfterChangingExpandedBubble() {
         // Mark it as a bubble and add it explicitly
         mEntryListener.onEntryAdded(mRow);
@@ -613,7 +620,7 @@
         assertStackExpanded();
         verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
                 true, mRow2.getKey());
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
 
         // Last added is the one that is expanded
         assertEquals(mRow2.getKey(), mBubbleData.getSelectedBubble().getKey());
@@ -638,7 +645,7 @@
         // Collapse
         mBubbleController.collapseStack();
         assertStackCollapsed();
-        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
     }
 
     @Test
@@ -658,7 +665,7 @@
         mBubbleData.setExpanded(true);
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
 
         // Notif is suppressed after expansion
         assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
@@ -683,7 +690,7 @@
         mBubbleData.setExpanded(true);
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
 
         // Notif is suppressed after expansion
         assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
@@ -712,7 +719,7 @@
         BubbleStackView stackView = mBubbleController.getStackView();
         mBubbleData.setExpanded(true);
 
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
 
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getKey());
@@ -743,7 +750,7 @@
         // We should be collapsed
         verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
         assertFalse(mBubbleController.hasBubbles());
-        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
     }
 
     @Test
@@ -756,7 +763,7 @@
         BubbleStackView stackView = mBubbleController.getStackView();
         mBubbleData.setExpanded(true);
 
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
         assertStackExpanded();
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey());
 
@@ -771,7 +778,7 @@
         // We should be collapsed
         verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
         assertFalse(mBubbleController.hasBubbles());
-        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
     }
 
 
@@ -789,7 +796,7 @@
         verify(mBubbleExpandListener, never()).onBubbleExpandChanged(false /* expanded */,
                 mRow.getKey());
         assertStackCollapsed();
-        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
     }
 
     @Test
@@ -805,7 +812,7 @@
         verify(mBubbleExpandListener).onBubbleExpandChanged(true /* expanded */,
                 mRow.getKey());
         assertStackExpanded();
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
     }
 
     @Test
@@ -822,7 +829,7 @@
         // Dot + flyout is hidden because notif is suppressed
         assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
         assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showFlyout());
-        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
     }
 
     @Test
@@ -844,7 +851,7 @@
         // Dot + flyout is hidden because notif is suppressed
         assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot());
         assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showFlyout());
-        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
     }
 
     @Test
@@ -1125,7 +1132,7 @@
         assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry2User11.getKey())).isNotNull();
 
         // Would have loaded bubbles twice because of user switch
-        verify(mDataRepository, times(2)).loadBubbles(anyInt(), any());
+        verify(mDataRepository, times(2)).loadBubbles(anyInt(), anyList(), any());
     }
 
     @Test
@@ -1181,7 +1188,7 @@
         mEntryListener.onEntryRemoved(mRow2, REASON_APP_CANCEL);
         assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
 
-        verify(mDataRepository, times(1)).loadBubbles(anyInt(), any());
+        verify(mDataRepository, times(1)).loadBubbles(anyInt(), anyList(), any());
     }
 
     /**
@@ -1249,11 +1256,11 @@
         BubbleStackView stackView = mBubbleController.getStackView();
         mBubbleData.setExpanded(true);
         assertStackExpanded();
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
 
         // Show the menu
         stackView.showManageMenu(true);
-        assertSysuiStates(true /* stackExpanded */, true /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, true /* manageMenuExpanded */);
         assertTrue(stackView.isManageMenuSettingsVisible());
         assertTrue(stackView.isManageMenuDontBubbleVisible());
     }
@@ -1267,11 +1274,11 @@
         BubbleStackView stackView = mBubbleController.getStackView();
         mBubbleData.setExpanded(true);
         assertStackExpanded();
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
 
         // Show the menu
         stackView.showManageMenu(true);
-        assertSysuiStates(true /* stackExpanded */, true /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, true /* manageMenuExpanded */);
         assertFalse(stackView.isManageMenuSettingsVisible());
         assertFalse(stackView.isManageMenuDontBubbleVisible());
     }
@@ -1285,15 +1292,15 @@
         BubbleStackView stackView = mBubbleController.getStackView();
         mBubbleData.setExpanded(true);
         assertStackExpanded();
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
 
         // Show the menu
         stackView.showManageMenu(true);
-        assertSysuiStates(true /* stackExpanded */, true /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, true /* manageMenuExpanded */);
 
         // Hide the menu
         stackView.showManageMenu(false);
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
     }
 
     @Test
@@ -1305,16 +1312,16 @@
         BubbleStackView stackView = mBubbleController.getStackView();
         mBubbleData.setExpanded(true);
         assertStackExpanded();
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
 
         // Show the menu
         stackView.showManageMenu(true);
-        assertSysuiStates(true /* stackExpanded */, true /* mangeMenuExpanded */);
+        assertSysuiStates(true /* stackExpanded */, true /* manageMenuExpanded */);
 
         // Collapse the stack
         mBubbleData.setExpanded(false);
 
-        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
+        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
     }
 
     @Test
@@ -1576,7 +1583,7 @@
 
         NotificationListenerService.RankingMap rankingMap =
                 mock(NotificationListenerService.RankingMap.class);
-        when(rankingMap.getOrderedKeys()).thenReturn(new String[] { mBubbleEntry.getKey() });
+        when(rankingMap.getOrderedKeys()).thenReturn(new String[]{mBubbleEntry.getKey()});
         mBubbleController.onRankingUpdated(rankingMap, entryDataByKey);
 
         // Should no longer be in the stack
@@ -1884,7 +1891,111 @@
         );
     }
 
-        /** Creates a bubble using the userId and package. */
+    @Test
+    public void registerBubbleBarListener_barDisabled_largeScreen_shouldBeIgnored() {
+        mBubbleProperties.mIsBubbleBarEnabled = false;
+        mPositioner.setIsLargeScreen(true);
+        mEntryListener.onEntryAdded(mRow);
+        mBubbleController.updateBubble(mBubbleEntry);
+        assertTrue(mBubbleController.hasBubbles());
+
+        assertStackMode();
+
+        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
+        mBubbleController.registerBubbleStateListener(bubbleStateListener);
+
+        assertStackMode();
+
+        assertThat(mBubbleController.getStackView().getBubbleCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void registerBubbleBarListener_barEnabled_smallScreen_shouldBeIgnored() {
+        mBubbleProperties.mIsBubbleBarEnabled = true;
+        mPositioner.setIsLargeScreen(false);
+        mEntryListener.onEntryAdded(mRow);
+        mBubbleController.updateBubble(mBubbleEntry);
+        assertTrue(mBubbleController.hasBubbles());
+
+        assertStackMode();
+
+        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
+        mBubbleController.registerBubbleStateListener(bubbleStateListener);
+
+        assertStackMode();
+
+        assertThat(mBubbleController.getStackView().getBubbleCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void registerBubbleBarListener_switchToBarAndBackToStack() {
+        mBubbleProperties.mIsBubbleBarEnabled = true;
+        mPositioner.setIsLargeScreen(true);
+        mEntryListener.onEntryAdded(mRow);
+        mBubbleController.updateBubble(mBubbleEntry);
+        assertTrue(mBubbleController.hasBubbles());
+
+        assertStackMode();
+
+        assertThat(mBubbleData.getBubbles()).hasSize(1);
+        assertBubbleIsInflatedForStack(mBubbleData.getBubbles().get(0));
+        assertBubbleIsInflatedForStack(mBubbleData.getOverflow());
+
+        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
+        mBubbleController.registerBubbleStateListener(bubbleStateListener);
+
+        assertBarMode();
+
+        assertThat(mBubbleData.getBubbles()).hasSize(1);
+        assertBubbleIsInflatedForBar(mBubbleData.getBubbles().get(0));
+        assertBubbleIsInflatedForBar(mBubbleData.getOverflow());
+
+        mBubbleController.unregisterBubbleStateListener();
+
+        assertStackMode();
+
+        assertThat(mBubbleData.getBubbles()).hasSize(1);
+        assertBubbleIsInflatedForStack(mBubbleData.getBubbles().get(0));
+        assertBubbleIsInflatedForStack(mBubbleData.getOverflow());
+    }
+
+    @Test
+    public void switchBetweenBarAndStack_noBubbles_shouldBeIgnored() {
+        mBubbleProperties.mIsBubbleBarEnabled = false;
+        mPositioner.setIsLargeScreen(true);
+        assertFalse(mBubbleController.hasBubbles());
+
+        assertNoBubbleContainerViews();
+
+        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
+        mBubbleController.registerBubbleStateListener(bubbleStateListener);
+
+        assertNoBubbleContainerViews();
+
+        mBubbleController.unregisterBubbleStateListener();
+
+        assertNoBubbleContainerViews();
+    }
+
+    @Test
+    public void bubbleBarBubbleExpandedAndCollapsed() {
+        mBubbleProperties.mIsBubbleBarEnabled = true;
+        mPositioner.setIsLargeScreen(true);
+        mEntryListener.onEntryAdded(mRow);
+        mBubbleController.updateBubble(mBubbleEntry);
+
+        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
+        mBubbleController.registerBubbleStateListener(bubbleStateListener);
+        mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(), true);
+
+        assertThat(mBubbleController.getLayerView().isExpanded()).isTrue();
+
+        mBubbleController.collapseStack();
+
+        assertThat(mBubbleController.getLayerView().isExpanded()).isFalse();
+    }
+
+    /** Creates a bubble using the userId and package. */
     private Bubble createBubble(int userId, String pkg) {
         final UserHandle userHandle = new UserHandle(userId);
         NotificationEntry workEntry = new NotificationEntryBuilder()
@@ -2000,6 +2111,44 @@
         assertFalse(mBubbleController.getImplCachedState().isStackExpanded());
     }
 
+    /** Asserts that both the bubble stack and bar views don't exist. */
+    private void assertNoBubbleContainerViews() {
+        assertThat(mBubbleController.getStackView()).isNull();
+        assertThat(mBubbleController.getLayerView()).isNull();
+    }
+
+    /** Asserts that the stack is created and the bar is null. */
+    private void assertStackMode() {
+        assertThat(mBubbleController.getStackView()).isNotNull();
+        assertThat(mBubbleController.getLayerView()).isNull();
+    }
+
+    /** Asserts that the given bubble has the stack expanded view inflated. */
+    private void assertBubbleIsInflatedForStack(BubbleViewProvider b) {
+        assertThat(b.getIconView()).isNotNull();
+        assertThat(b.getExpandedView()).isNotNull();
+        assertThat(b.getBubbleBarExpandedView()).isNull();
+    }
+
+    /** Asserts that the bar is created and the stack is null. */
+    private void assertBarMode() {
+        assertThat(mBubbleController.getStackView()).isNull();
+        assertThat(mBubbleController.getLayerView()).isNotNull();
+    }
+
+    /** Asserts that the given bubble has the bar expanded view inflated. */
+    private void assertBubbleIsInflatedForBar(BubbleViewProvider b) {
+        // the icon view should be inflated for the overflow but not for other bubbles when showing
+        // in the bar
+        if (b instanceof Bubble) {
+            assertThat(b.getIconView()).isNull();
+        } else if (b instanceof BubbleOverflow) {
+            assertThat(b.getIconView()).isNotNull();
+        }
+        assertThat(b.getExpandedView()).isNull();
+        assertThat(b.getBubbleBarExpandedView()).isNotNull();
+    }
+
     /**
      * Asserts that a bubble notification is suppressed from the shade and also validates the cached
      * state is updated.
@@ -2029,4 +2178,19 @@
         assertThat(mSysUiStateBubblesExpanded).isEqualTo(stackExpanded);
         assertThat(mSysUiStateBubblesManageMenuExpanded).isEqualTo(manageMenuExpanded);
     }
+
+    private static class FakeBubbleStateListener implements Bubbles.BubbleStateListener {
+        @Override
+        public void onBubbleStateChange(BubbleBarUpdate update) {
+        }
+    }
+
+    private static class FakeBubbleProperties implements BubbleProperties {
+        boolean mIsBubbleBarEnabled = false;
+
+        @Override
+        public boolean isBubbleBarEnabled() {
+            return mIsBubbleBarEnabled;
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
index 14c3f3c..5855347 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
@@ -31,6 +31,7 @@
 import com.android.wm.shell.bubbles.BubbleDataRepository;
 import com.android.wm.shell.bubbles.BubbleLogger;
 import com.android.wm.shell.bubbles.BubblePositioner;
+import com.android.wm.shell.bubbles.properties.BubbleProperties;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.ShellExecutor;
@@ -74,13 +75,14 @@
             Handler shellMainHandler,
             TaskViewTransitions taskViewTransitions,
             SyncTransactionQueue syncQueue,
-            IWindowManager wmService) {
+            IWindowManager wmService,
+            BubbleProperties bubbleProperties) {
         super(context, shellInit, shellCommandHandler, shellController, data, Runnable::run,
                 floatingContentCoordinator, dataRepository, statusBarService, windowManager,
                 windowManagerShellWrapper, userManager, launcherApps, bubbleLogger,
                 taskStackListener, shellTaskOrganizer, positioner, displayController,
                 oneHandedOptional, dragAndDropController, shellMainExecutor, shellMainHandler,
-                new SyncExecutor(), taskViewTransitions, syncQueue, wmService);
+                new SyncExecutor(), taskViewTransitions, syncQueue, wmService, bubbleProperties);
         setInflateSynchronously(true);
         onInit();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubblePositioner.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubblePositioner.java
index 6edc373..047dc65 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubblePositioner.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubblePositioner.java
@@ -27,6 +27,7 @@
 
 public class TestableBubblePositioner extends BubblePositioner {
     private int mMaxBubbles;
+    private boolean mIsLargeScreen = false;
 
     public TestableBubblePositioner(Context context,
             WindowManager windowManager) {
@@ -46,4 +47,13 @@
     public int getMaxBubbles() {
         return mMaxBubbles;
     }
+
+    public void setIsLargeScreen(boolean isLargeScreen) {
+        mIsLargeScreen = isLargeScreen;
+    }
+
+    @Override
+    public boolean isLargeScreen() {
+        return mIsLargeScreen;
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiBaseFragmentTest.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiBaseFragmentTest.java
index 9179efc..e470406 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiBaseFragmentTest.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiBaseFragmentTest.java
@@ -57,8 +57,7 @@
 
     @Before
     public void sysuiSetup() throws ExecutionException, InterruptedException {
-        SystemUIInitializer initializer =
-                SystemUIInitializerFactory.createFromConfigNoAssert(mContext);
+        SystemUIInitializer initializer = new SystemUIInitializerImpl(mContext);
         initializer.init(true);
         mDependency = new TestableDependency(initializer.getSysUIComponent().createDependency());
         Dependency.setInstance(mDependency);
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 8bbd58d..de177168 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
@@ -90,8 +90,7 @@
         if (isRobolectricTest()) {
             mContext = mContext.createDefaultDisplayContext();
         }
-        SystemUIInitializer initializer =
-                SystemUIInitializerFactory.createFromConfigNoAssert(mContext);
+        SystemUIInitializer initializer = new SystemUIInitializerImpl(mContext);
         initializer.init(true);
         mDependency = new TestableDependency(initializer.getSysUIComponent().createDependency());
         Dependency.setInstance(mDependency);
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/activity/SingleActivityFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/SingleActivityFactory.kt
new file mode 100644
index 0000000..5a92fb1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/SingleActivityFactory.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.activity
+
+import android.app.Activity
+import android.content.Intent
+import androidx.test.runner.intercepting.SingleActivityFactory
+
+/**
+ * Builds a new [SingleActivityFactory] which delegating any call of [SingleActivityFactory.create]
+ * to the [instantiate] parameter.
+ *
+ * For more details, see [SingleActivityFactory].
+ */
+inline fun <reified T : Activity> SingleActivityFactory(
+    crossinline instantiate: (intent: Intent?) -> T,
+): SingleActivityFactory<T> {
+    return object : SingleActivityFactory<T>(T::class.java) {
+        override fun create(intent: Intent?): T = instantiate(intent)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
new file mode 100644
index 0000000..a718f70
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.authentication.data.repository
+
+import com.android.keyguard.KeyguardSecurityModel.SecurityMode
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeAuthenticationRepository(
+    private val delegate: AuthenticationRepository,
+    private val onSecurityModeChanged: (SecurityMode) -> Unit,
+) : AuthenticationRepository by delegate {
+
+    private val _isUnlocked = MutableStateFlow(false)
+    override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow()
+
+    private var authenticationMethod: AuthenticationMethodModel = DEFAULT_AUTHENTICATION_METHOD
+
+    override suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
+        return authenticationMethod
+    }
+
+    fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
+        this.authenticationMethod = authenticationMethod
+        onSecurityModeChanged(authenticationMethod.toSecurityMode())
+    }
+
+    fun setUnlocked(isUnlocked: Boolean) {
+        _isUnlocked.value = isUnlocked
+    }
+
+    companion object {
+        val DEFAULT_AUTHENTICATION_METHOD =
+            AuthenticationMethodModel.Pin(listOf(1, 2, 3, 4), autoConfirm = false)
+
+        fun AuthenticationMethodModel.toSecurityMode(): SecurityMode {
+            return when (this) {
+                is AuthenticationMethodModel.Pin -> SecurityMode.PIN
+                is AuthenticationMethodModel.Password -> SecurityMode.Password
+                is AuthenticationMethodModel.Pattern -> SecurityMode.Pattern
+                is AuthenticationMethodModel.Swipe,
+                is AuthenticationMethodModel.None -> SecurityMode.None
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeBouncerMessageRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeBouncerMessageRepository.kt
new file mode 100644
index 0000000..d9b926d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeBouncerMessageRepository.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.bouncer.data.repository
+
+import com.android.systemui.bouncer.shared.model.BouncerMessageModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class FakeBouncerMessageRepository : BouncerMessageRepository {
+    private val _primaryAuthMessage = MutableStateFlow<BouncerMessageModel?>(null)
+    override val primaryAuthMessage: StateFlow<BouncerMessageModel?>
+        get() = _primaryAuthMessage
+
+    private val _faceAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
+    override val faceAcquisitionMessage: StateFlow<BouncerMessageModel?>
+        get() = _faceAcquisitionMessage
+    private val _fingerprintAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
+    override val fingerprintAcquisitionMessage: StateFlow<BouncerMessageModel?>
+        get() = _fingerprintAcquisitionMessage
+    private val _customMessage = MutableStateFlow<BouncerMessageModel?>(null)
+    override val customMessage: StateFlow<BouncerMessageModel?>
+        get() = _customMessage
+    private val _biometricAuthMessage = MutableStateFlow<BouncerMessageModel?>(null)
+    override val biometricAuthMessage: StateFlow<BouncerMessageModel?>
+        get() = _biometricAuthMessage
+    private val _authFlagsMessage = MutableStateFlow<BouncerMessageModel?>(null)
+    override val authFlagsMessage: StateFlow<BouncerMessageModel?>
+        get() = _authFlagsMessage
+
+    private val _biometricLockedOutMessage = MutableStateFlow<BouncerMessageModel?>(null)
+    override val biometricLockedOutMessage: Flow<BouncerMessageModel?>
+        get() = _biometricLockedOutMessage
+
+    override fun setPrimaryAuthMessage(value: BouncerMessageModel?) {
+        _primaryAuthMessage.value = value
+    }
+
+    override fun setFaceAcquisitionMessage(value: BouncerMessageModel?) {
+        _faceAcquisitionMessage.value = value
+    }
+
+    override fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?) {
+        _fingerprintAcquisitionMessage.value = value
+    }
+
+    override fun setCustomMessage(value: BouncerMessageModel?) {
+        _customMessage.value = value
+    }
+
+    fun setBiometricAuthMessage(value: BouncerMessageModel?) {
+        _biometricAuthMessage.value = value
+    }
+
+    fun setAuthFlagsMessage(value: BouncerMessageModel?) {
+        _authFlagsMessage.value = value
+    }
+
+    fun setBiometricLockedOutMessage(value: BouncerMessageModel?) {
+        _biometricLockedOutMessage.value = value
+    }
+
+    override fun clearMessage() {
+        _primaryAuthMessage.value = null
+        _faceAcquisitionMessage.value = null
+        _fingerprintAcquisitionMessage.value = null
+        _customMessage.value = null
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
new file mode 100644
index 0000000..10529e6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
@@ -0,0 +1,97 @@
+package com.android.systemui.bouncer.data.repository
+
+import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
+import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Fake implementation of [KeyguardRepository] */
+class FakeKeyguardBouncerRepository : KeyguardBouncerRepository {
+    private val _primaryBouncerShow = MutableStateFlow(false)
+    override val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
+    private val _primaryBouncerShowingSoon = MutableStateFlow(false)
+    override val primaryBouncerShowingSoon = _primaryBouncerShowingSoon.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(KeyguardBouncerConstants.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()
+    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 setAlternateVisible(isVisible: Boolean) {
+        _isAlternateBouncerVisible.value = isVisible
+    }
+
+    override fun setAlternateBouncerUIAvailable(isAvailable: Boolean) {
+        _isAlternateBouncerUIAvailable.value = isAvailable
+    }
+
+    override fun setPrimaryShow(isShowing: Boolean) {
+        _primaryBouncerShow.value = isShowing
+    }
+
+    override fun setPrimaryShowingSoon(showingSoon: Boolean) {
+        _primaryBouncerShowingSoon.value = showingSoon
+    }
+
+    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/broadcast/FakeBroadcastDispatcher.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
index af940e4..21a5eb7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
@@ -18,6 +18,7 @@
 
 import android.content.BroadcastReceiver
 import android.content.Context
+import android.content.Intent
 import android.content.IntentFilter
 import android.os.Handler
 import android.os.Looper
@@ -31,6 +32,14 @@
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.Executor
 
+/**
+ * A fake instance of [BroadcastDispatcher] for tests.
+ *
+ * Important: The *real* broadcast dispatcher will only send intents to receivers if the intent
+ * matches the [IntentFilter] that the [BroadcastReceiver] was registered with. This fake class
+ * exposes [sendIntentToMatchingReceiversOnly] to get the same matching behavior as the real
+ * broadcast dispatcher.
+ */
 class FakeBroadcastDispatcher(
     context: SysuiTestableContext,
     mainExecutor: Executor,
@@ -52,7 +61,7 @@
         PendingRemovalStore(logger)
     ) {
 
-    val registeredReceivers: MutableSet<BroadcastReceiver> = ConcurrentHashMap.newKeySet()
+    private val receivers: MutableSet<InternalReceiver> = ConcurrentHashMap.newKeySet()
 
     override fun registerReceiverWithHandler(
         receiver: BroadcastReceiver,
@@ -62,7 +71,7 @@
         @Context.RegisterReceiverFlags flags: Int,
         permission: String?
     ) {
-        registeredReceivers.add(receiver)
+        receivers.add(InternalReceiver(receiver, filter))
     }
 
     override fun registerReceiver(
@@ -73,24 +82,52 @@
         @Context.RegisterReceiverFlags flags: Int,
         permission: String?
     ) {
-        registeredReceivers.add(receiver)
+        receivers.add(InternalReceiver(receiver, filter))
     }
 
     override fun unregisterReceiver(receiver: BroadcastReceiver) {
-        registeredReceivers.remove(receiver)
+        receivers.removeIf { it.receiver == receiver }
     }
 
     override fun unregisterReceiverForUser(receiver: BroadcastReceiver, user: UserHandle) {
-        registeredReceivers.remove(receiver)
+        receivers.removeIf { it.receiver == receiver }
     }
 
-    fun cleanUpReceivers(testName: String) {
-        registeredReceivers.forEach {
-            Log.i(testName, "Receiver not unregistered from dispatcher: $it")
-            if (shouldFailOnLeakedReceiver) {
-                throw IllegalStateException("Receiver not unregistered from dispatcher: $it")
+    /**
+     * Sends the given [intent] to *only* the receivers that were registered with an [IntentFilter]
+     * that matches the intent.
+     */
+    fun sendIntentToMatchingReceiversOnly(context: Context, intent: Intent) {
+        receivers.forEach {
+            if (
+                it.filter.match(
+                    context.contentResolver,
+                    intent,
+                    /* resolve= */ false,
+                    /* logTag= */ "FakeBroadcastDispatcher",
+                ) > 0
+            ) {
+                it.receiver.onReceive(context, intent)
             }
         }
-        registeredReceivers.clear()
     }
+
+    val numReceiversRegistered: Int
+        get() = receivers.size
+
+    fun cleanUpReceivers(testName: String) {
+        receivers.forEach {
+            val receiver = it.receiver
+            Log.i(testName, "Receiver not unregistered from dispatcher: $receiver")
+            if (shouldFailOnLeakedReceiver) {
+                throw IllegalStateException("Receiver not unregistered from dispatcher: $receiver")
+            }
+        }
+        receivers.clear()
+    }
+
+    private data class InternalReceiver(
+        val receiver: BroadcastReceiver,
+        val filter: IntentFilter,
+    )
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBouncerMessageRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBouncerMessageRepository.kt
deleted file mode 100644
index b03b4ba..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBouncerMessageRepository.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF 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.bouncer.data.repository.BouncerMessageRepository
-import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-
-class FakeBouncerMessageRepository : BouncerMessageRepository {
-    private val _primaryAuthMessage = MutableStateFlow<BouncerMessageModel?>(null)
-    override val primaryAuthMessage: StateFlow<BouncerMessageModel?>
-        get() = _primaryAuthMessage
-
-    private val _faceAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
-    override val faceAcquisitionMessage: StateFlow<BouncerMessageModel?>
-        get() = _faceAcquisitionMessage
-    private val _fingerprintAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
-    override val fingerprintAcquisitionMessage: StateFlow<BouncerMessageModel?>
-        get() = _fingerprintAcquisitionMessage
-    private val _customMessage = MutableStateFlow<BouncerMessageModel?>(null)
-    override val customMessage: StateFlow<BouncerMessageModel?>
-        get() = _customMessage
-    private val _biometricAuthMessage = MutableStateFlow<BouncerMessageModel?>(null)
-    override val biometricAuthMessage: StateFlow<BouncerMessageModel?>
-        get() = _biometricAuthMessage
-    private val _authFlagsMessage = MutableStateFlow<BouncerMessageModel?>(null)
-    override val authFlagsMessage: StateFlow<BouncerMessageModel?>
-        get() = _authFlagsMessage
-
-    private val _biometricLockedOutMessage = MutableStateFlow<BouncerMessageModel?>(null)
-    override val biometricLockedOutMessage: Flow<BouncerMessageModel?>
-        get() = _biometricLockedOutMessage
-
-    override fun setPrimaryAuthMessage(value: BouncerMessageModel?) {
-        _primaryAuthMessage.value = value
-    }
-
-    override fun setFaceAcquisitionMessage(value: BouncerMessageModel?) {
-        _faceAcquisitionMessage.value = value
-    }
-
-    override fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?) {
-        _fingerprintAcquisitionMessage.value = value
-    }
-
-    override fun setCustomMessage(value: BouncerMessageModel?) {
-        _customMessage.value = value
-    }
-
-    fun setBiometricAuthMessage(value: BouncerMessageModel?) {
-        _biometricAuthMessage.value = value
-    }
-
-    fun setAuthFlagsMessage(value: BouncerMessageModel?) {
-        _authFlagsMessage.value = value
-    }
-
-    fun setBiometricLockedOutMessage(value: BouncerMessageModel?) {
-        _biometricLockedOutMessage.value = value
-    }
-
-    override fun clearMessage() {
-        _primaryAuthMessage.value = null
-        _faceAcquisitionMessage.value = null
-        _fingerprintAcquisitionMessage.value = null
-        _customMessage.value = null
-    }
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
index 738f09d..2715aaa 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
@@ -41,7 +41,9 @@
     fun setDetectionStatus(status: DetectionStatus) {
         _detectionStatus.value = status
     }
-    override val isLockedOut = MutableStateFlow(false)
+
+    private val _isLockedOut = MutableStateFlow(false)
+    override val isLockedOut = _isLockedOut
     private val _runningAuthRequest = MutableStateFlow<Pair<FaceAuthUiEvent, Boolean>?>(null)
     val runningAuthRequest: StateFlow<Pair<FaceAuthUiEvent, Boolean>?> =
         _runningAuthRequest.asStateFlow()
@@ -56,6 +58,10 @@
         _isAuthRunning.value = true
     }
 
+    fun setLockedOut(value: Boolean) {
+        _isLockedOut.value = value
+    }
+
     override fun cancel() {
         _isAuthRunning.value = false
         _runningAuthRequest.value = null
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
deleted file mode 100644
index 8a6d2aa..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardBouncerRepository.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.keyguard.data.repository
-
-import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
-import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-
-/** Fake implementation of [KeyguardRepository] */
-class FakeKeyguardBouncerRepository : KeyguardBouncerRepository {
-    private val _primaryBouncerShow = MutableStateFlow(false)
-    override val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
-    private val _primaryBouncerShowingSoon = MutableStateFlow(false)
-    override val primaryBouncerShowingSoon = _primaryBouncerShowingSoon.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()
-    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 setAlternateVisible(isVisible: Boolean) {
-        _isAlternateBouncerVisible.value = isVisible
-    }
-
-    override fun setAlternateBouncerUIAvailable(isAvailable: Boolean) {
-        _isAlternateBouncerUIAvailable.value = isAvailable
-    }
-
-    override fun setPrimaryShow(isShowing: Boolean) {
-        _primaryBouncerShow.value = isShowing
-    }
-
-    override fun setPrimaryShowingSoon(showingSoon: Boolean) {
-        _primaryBouncerShowingSoon.value = showingSoon
-    }
-
-    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 b52a768..f6cbb07 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
@@ -81,7 +81,7 @@
         MutableStateFlow(
             WakefulnessModel(WakefulnessState.ASLEEP, WakeSleepReason.OTHER, WakeSleepReason.OTHER)
         )
-    override val wakefulness: Flow<WakefulnessModel> = _wakefulnessModel
+    override val wakefulness = _wakefulnessModel
 
     private val _isUdfpsSupported = MutableStateFlow(false)
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
new file mode 100644
index 0000000..f13c98d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.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.keyguard.domain.interactor
+
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+
+/**
+ * Simply put, I got tired of adding a constructor argument and then having to tweak dozens of
+ * files. This should alleviate some of the burden by providing defaults for testing.
+ */
+object KeyguardInteractorFactory {
+
+    @JvmOverloads
+    @JvmStatic
+    fun create(
+        featureFlags: FakeFeatureFlags = createFakeFeatureFlags(),
+        repository: FakeKeyguardRepository = FakeKeyguardRepository(),
+        commandQueue: FakeCommandQueue = FakeCommandQueue(),
+        bouncerRepository: FakeKeyguardBouncerRepository = FakeKeyguardBouncerRepository(),
+        configurationRepository: FakeConfigurationRepository = FakeConfigurationRepository(),
+    ): WithDependencies {
+        return WithDependencies(
+            repository = repository,
+            commandQueue = commandQueue,
+            featureFlags = featureFlags,
+            bouncerRepository = bouncerRepository,
+            configurationRepository = configurationRepository,
+            KeyguardInteractor(
+                repository = repository,
+                commandQueue = commandQueue,
+                featureFlags = featureFlags,
+                bouncerRepository = bouncerRepository,
+                configurationRepository = configurationRepository,
+            )
+        )
+    }
+
+    /** Provide defaults, otherwise tests will throw an error */
+    fun createFakeFeatureFlags(): FakeFeatureFlags {
+        return FakeFeatureFlags().apply { set(Flags.FACE_AUTH_REFACTOR, false) }
+    }
+
+    data class WithDependencies(
+        val repository: FakeKeyguardRepository,
+        val commandQueue: FakeCommandQueue,
+        val featureFlags: FakeFeatureFlags,
+        val bouncerRepository: FakeKeyguardBouncerRepository,
+        val configurationRepository: FakeConfigurationRepository,
+        val keyguardInteractor: KeyguardInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorFactory.kt
new file mode 100644
index 0000000..312ade5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorFactory.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import kotlinx.coroutines.CoroutineScope
+
+/**
+ * Helper to create a new KeyguardTransitionInteractor in a way that doesn't require modifying 20+
+ * tests whenever we add a constructor param.
+ */
+object KeyguardTransitionInteractorFactory {
+    @JvmOverloads
+    @JvmStatic
+    fun create(
+        scope: CoroutineScope,
+        repository: KeyguardTransitionRepository = FakeKeyguardTransitionRepository(),
+    ): WithDependencies {
+        return WithDependencies(
+            repository = repository,
+            KeyguardTransitionInteractor(
+                scope = scope,
+                repository = repository,
+            )
+        )
+    }
+
+    data class WithDependencies(
+        val repository: KeyguardTransitionRepository,
+        val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/repository/FakePowerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/repository/FakePowerRepository.kt
index 15465f4..3334f3e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/repository/FakePowerRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/repository/FakePowerRepository.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.power.data.repository
 
+import android.os.PowerManager
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -28,7 +29,15 @@
     private val _isInteractive = MutableStateFlow(initialInteractive)
     override val isInteractive: Flow<Boolean> = _isInteractive.asStateFlow()
 
+    var lastWakeWhy: String? = null
+    var lastWakeReason: Int? = null
+
     fun setInteractive(value: Boolean) {
         _isInteractive.value = value
     }
+
+    override fun wakeUp(why: String, @PowerManager.WakeReason wakeReason: Int) {
+        lastWakeWhy = why
+        lastWakeReason = wakeReason
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt
new file mode 100644
index 0000000..9ea079f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.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.qs.pipeline.data.repository
+
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeAutoAddRepository : AutoAddRepository {
+
+    private val autoAddedTilesPerUser = mutableMapOf<Int, MutableStateFlow<Set<TileSpec>>>()
+
+    override fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>> {
+        return getFlow(userId)
+    }
+
+    override suspend fun markTileAdded(userId: Int, spec: TileSpec) {
+        if (spec == TileSpec.Invalid) return
+        with(getFlow(userId)) { value = value.toMutableSet().apply { add(spec) } }
+    }
+
+    override suspend fun unmarkTileAdded(userId: Int, spec: TileSpec) {
+        with(getFlow(userId)) { value = value.toMutableSet().apply { remove(spec) } }
+    }
+
+    private fun getFlow(userId: Int): MutableStateFlow<Set<TileSpec>> =
+        autoAddedTilesPerUser.getOrPut(userId) { MutableStateFlow(emptySet()) }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/autoaddable/FakeAutoAddable.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/autoaddable/FakeAutoAddable.kt
new file mode 100644
index 0000000..ebdd6fd
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/autoaddable/FakeAutoAddable.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.qs.pipeline.domain.autoaddable
+
+import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+
+class FakeAutoAddable(
+    private val spec: TileSpec,
+    override val autoAddTracking: AutoAddTracking,
+) : AutoAddable {
+
+    private val signalsPerUser = mutableMapOf<Int, MutableStateFlow<AutoAddSignal?>>()
+    private fun getFlow(userId: Int): MutableStateFlow<AutoAddSignal?> =
+        signalsPerUser.getOrPut(userId) { MutableStateFlow(null) }
+
+    override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+        return getFlow(userId).asStateFlow().filterNotNull()
+    }
+
+    suspend fun sendRemoveSignal(userId: Int) {
+        getFlow(userId).value = AutoAddSignal.Remove(spec)
+    }
+
+    suspend fun sendAddSignal(userId: Int, position: Int = POSITION_AT_END) {
+        getFlow(userId).value = AutoAddSignal.Add(spec, position)
+    }
+
+    override val description: String
+        get() = "FakeAutoAddable($spec)"
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index be3d54a..0b6e2a2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -16,20 +16,28 @@
 
 package com.android.systemui.scene
 
+import com.android.keyguard.KeyguardSecurityModel.SecurityMode
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.AuthenticationRepository
 import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
+import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository.Companion.toSecurityMode
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
-import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.bouncer.data.repository.BouncerRepository
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor
-import com.android.systemui.scene.data.model.SceneContainerConfig
 import com.android.systemui.scene.data.repository.SceneContainerRepository
 import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.mockito.mock
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
 import kotlinx.coroutines.test.TestScope
 
 /**
@@ -39,9 +47,25 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 class SceneTestUtils(
     test: SysuiTestCase,
-    private val testScope: TestScope? = null,
 ) {
-
+    val testDispatcher: TestDispatcher by lazy { StandardTestDispatcher() }
+    val testScope: TestScope by lazy { TestScope(testDispatcher) }
+    private var securityMode: SecurityMode =
+        FakeAuthenticationRepository.DEFAULT_AUTHENTICATION_METHOD.toSecurityMode()
+    val authenticationRepository: FakeAuthenticationRepository by lazy {
+        FakeAuthenticationRepository(
+            delegate =
+                AuthenticationRepositoryImpl(
+                    applicationScope = applicationScope(),
+                    getSecurityMode = { securityMode },
+                    backgroundDispatcher = testDispatcher,
+                    userRepository = FakeUserRepository(),
+                    lockPatternUtils = mock(),
+                    keyguardRepository = FakeKeyguardRepository(),
+                ),
+            onSecurityModeChanged = { securityMode = it },
+        )
+    }
     private val context = test.context
 
     fun fakeSceneContainerRepository(
@@ -51,7 +75,7 @@
                 fakeSceneContainerConfig(CONTAINER_2),
             )
     ): SceneContainerRepository {
-        return SceneContainerRepository(containerConfigurations)
+        return SceneContainerRepository(containerConfigurations.associateBy { it.name })
     }
 
     fun fakeSceneKeys(): List<SceneKey> {
@@ -82,7 +106,7 @@
     }
 
     fun authenticationRepository(): AuthenticationRepository {
-        return AuthenticationRepositoryImpl()
+        return authenticationRepository
     }
 
     fun authenticationInteractor(
@@ -94,17 +118,6 @@
         )
     }
 
-    private fun applicationScope(): CoroutineScope {
-        return checkNotNull(testScope) {
-                """
-                TestScope not initialized, please create a TestScope and inject it into
-                SceneTestUtils.
-            """
-                    .trimIndent()
-            }
-            .backgroundScope
-    }
-
     fun bouncerInteractor(
         authenticationInteractor: AuthenticationInteractor,
         sceneInteractor: SceneInteractor,
@@ -154,6 +167,10 @@
         )
     }
 
+    private fun applicationScope(): CoroutineScope {
+        return testScope.backgroundScope
+    }
+
     companion object {
         const val CONTAINER_1 = "container1"
         const val CONTAINER_2 = "container2"
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 61e5b5f..51ee0c0 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
@@ -19,18 +19,28 @@
 
 import android.content.pm.UserInfo
 import android.os.UserHandle
+import com.android.systemui.user.data.model.SelectedUserModel
+import com.android.systemui.user.data.model.SelectionStatus
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import java.util.concurrent.atomic.AtomicBoolean
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.yield
 
 class FakeUserRepository : UserRepository {
     companion object {
         // User id to represent a non system (human) user id. We presume this is the main user.
         private const val MAIN_USER_ID = 10
+
+        private val DEFAULT_SELECTED_USER = 0
+        private val DEFAULT_SELECTED_USER_INFO =
+            UserInfo(
+                /* id= */ DEFAULT_SELECTED_USER,
+                /* name= */ "default selected user",
+                /* flags= */ 0,
+            )
     }
 
     private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel())
@@ -40,8 +50,11 @@
     private val _userInfos = MutableStateFlow<List<UserInfo>>(emptyList())
     override val userInfos: Flow<List<UserInfo>> = _userInfos.asStateFlow()
 
-    private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
-    override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
+    override val selectedUser =
+        MutableStateFlow(
+            SelectedUserModel(DEFAULT_SELECTED_USER_INFO, SelectionStatus.SELECTION_COMPLETE)
+        )
+    override val selectedUserInfo: Flow<UserInfo> = selectedUser.map { it.userInfo }
 
     private val _userSwitchingInProgress = MutableStateFlow(false)
     override val userSwitchingInProgress: Flow<Boolean>
@@ -72,7 +85,7 @@
     }
 
     override fun getSelectedUserInfo(): UserInfo {
-        return checkNotNull(_selectedUserInfo.value)
+        return selectedUser.value.userInfo
     }
 
     override fun isSimpleUserSwitcher(): Boolean {
@@ -87,12 +100,15 @@
         _userInfos.value = infos
     }
 
-    suspend fun setSelectedUserInfo(userInfo: UserInfo) {
+    suspend fun setSelectedUserInfo(
+        userInfo: UserInfo,
+        selectionStatus: SelectionStatus = SelectionStatus.SELECTION_COMPLETE,
+    ) {
         check(_userInfos.value.contains(userInfo)) {
             "Cannot select the following user, it is not in the list of user infos: $userInfo!"
         }
 
-        _selectedUserInfo.value = userInfo
+        selectedUser.value = SelectedUserModel(userInfo, selectionStatus)
         yield()
     }
 
diff --git a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
index 308e2cf..3406102 100644
--- a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
+++ b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
@@ -359,11 +359,7 @@
         final File infoStage = new File(filesDir, WALLPAPER_INFO_STAGE);
         final File imageStage = new File(filesDir, SYSTEM_WALLPAPER_STAGE);
         final File lockImageStage = new File(filesDir, LOCK_WALLPAPER_STAGE);
-
-        // If we restored separate lock imagery, the system wallpaper should be
-        // applied as system-only; but if there's no separate lock image, make
-        // sure to apply the restored system wallpaper as both.
-        final int sysWhich = FLAG_SYSTEM | (lockImageStage.exists() ? 0 : FLAG_LOCK);
+        boolean lockImageStageExists = lockImageStage.exists();
 
         try {
             // First parse the live component name so that we know for logging if we care about
@@ -371,14 +367,31 @@
             ComponentName wpService = parseWallpaperComponent(infoStage, "wp");
             mSystemHasLiveComponent = wpService != null;
 
+            ComponentName kwpService = null;
+            boolean lockscreenLiveWallpaper = mWallpaperManager.isLockscreenLiveWallpaperEnabled();
+            if (lockscreenLiveWallpaper) {
+                kwpService = parseWallpaperComponent(infoStage, "kwp");
+            }
+            mLockHasLiveComponent = kwpService != null;
+            boolean separateLockWallpaper = mLockHasLiveComponent || lockImageStage.exists();
+
+            // if there's no separate lock wallpaper, apply the system wallpaper to both screens.
+            final int sysWhich = separateLockWallpaper ? FLAG_SYSTEM : FLAG_SYSTEM | FLAG_LOCK;
+
             // It is valid for the imagery to be absent; it means that we were not permitted
             // to back up the original image on the source device, or there was no user-supplied
             // wallpaper image present.
-            restoreFromStage(imageStage, infoStage, "wp", sysWhich);
-            restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK);
+            if (!lockscreenLiveWallpaper) restoreFromStage(imageStage, infoStage, "wp", sysWhich);
+            if (lockImageStageExists) {
+                restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK);
+            }
+            if (lockscreenLiveWallpaper) restoreFromStage(imageStage, infoStage, "wp", sysWhich);
 
             // And reset to the wallpaper service we should be using
-            updateWallpaperComponent(wpService, !lockImageStage.exists());
+            if (lockscreenLiveWallpaper && mLockHasLiveComponent) {
+                updateWallpaperComponent(kwpService, false, FLAG_LOCK);
+            }
+            updateWallpaperComponent(wpService, !lockImageStageExists, sysWhich);
         } catch (Exception e) {
             Slog.e(TAG, "Unable to restore wallpaper: " + e.getMessage());
             mEventLogger.onRestoreException(e);
@@ -397,9 +410,21 @@
     }
 
     @VisibleForTesting
-    void updateWallpaperComponent(ComponentName wpService, boolean applyToLock) throws IOException {
+    void updateWallpaperComponent(ComponentName wpService, boolean applyToLock, int which)
+            throws IOException {
+        boolean lockscreenLiveWallpaper = mWallpaperManager.isLockscreenLiveWallpaperEnabled();
         if (servicePackageExists(wpService)) {
             Slog.i(TAG, "Using wallpaper service " + wpService);
+            if (lockscreenLiveWallpaper) {
+                mWallpaperManager.setWallpaperComponentWithFlags(wpService, which);
+                if ((which & FLAG_LOCK) != 0) {
+                    mEventLogger.onLockLiveWallpaperRestored(wpService);
+                }
+                if ((which & FLAG_SYSTEM) != 0) {
+                    mEventLogger.onSystemLiveWallpaperRestored(wpService);
+                }
+                return;
+            }
             mWallpaperManager.setWallpaperComponent(wpService);
             if (applyToLock) {
                 // We have a live wallpaper and no static lock image,
@@ -414,7 +439,7 @@
             // in reports from users
             if (wpService != null) {
                 // TODO(b/268471749): Handle delayed case
-                applyComponentAtInstall(wpService, applyToLock);
+                applyComponentAtInstall(wpService, applyToLock, which);
                 Slog.w(TAG, "Wallpaper service " + wpService + " isn't available. "
                         + " Will try to apply later");
             }
@@ -437,7 +462,8 @@
                     // And log the success
                     if ((which & FLAG_SYSTEM) > 0) {
                         mEventLogger.onSystemImageWallpaperRestored();
-                    } else {
+                    }
+                    if ((which & FLAG_LOCK) > 0) {
                         mEventLogger.onLockImageWallpaperRestored();
                     }
                 }
@@ -460,7 +486,8 @@
     private void logRestoreError(int which, String error) {
         if ((which & FLAG_SYSTEM) == FLAG_SYSTEM) {
             mEventLogger.onSystemImageWallpaperRestoreFailed(error);
-        } else if ((which & FLAG_LOCK) == FLAG_LOCK) {
+        }
+        if ((which & FLAG_LOCK) == FLAG_LOCK) {
             mEventLogger.onLockImageWallpaperRestoreFailed(error);
         }
     }
@@ -552,16 +579,21 @@
         // Intentionally blank
     }
 
-    private void applyComponentAtInstall(ComponentName componentName, boolean applyToLock) {
-        PackageMonitor packageMonitor = getWallpaperPackageMonitor(componentName, applyToLock);
+    private void applyComponentAtInstall(ComponentName componentName, boolean applyToLock,
+            int which) {
+        PackageMonitor packageMonitor = getWallpaperPackageMonitor(
+                componentName, applyToLock, which);
         packageMonitor.register(getBaseContext(), null, UserHandle.ALL, true);
     }
 
     @VisibleForTesting
-    PackageMonitor getWallpaperPackageMonitor(ComponentName componentName, boolean applyToLock) {
+    PackageMonitor getWallpaperPackageMonitor(ComponentName componentName, boolean applyToLock,
+            int which) {
         return new PackageMonitor() {
             @Override
             public void onPackageAdded(String packageName, int uid) {
+                boolean lockscreenLiveWallpaper =
+                        mWallpaperManager.isLockscreenLiveWallpaperEnabled();
                 if (!isDeviceInRestore()) {
                     // We don't want to reapply the wallpaper outside a restore.
                     unregister();
@@ -582,16 +614,29 @@
 
                 if (componentName.getPackageName().equals(packageName)) {
                     Slog.d(TAG, "Applying component " + componentName);
-                    boolean sysResult = mWallpaperManager.setWallpaperComponent(componentName);
+                    boolean success = lockscreenLiveWallpaper
+                            ? mWallpaperManager.setWallpaperComponentWithFlags(componentName, which)
+                            : mWallpaperManager.setWallpaperComponent(componentName);
                     WallpaperEventLogger logger = new WallpaperEventLogger(
                             mBackupManager.getDelayedRestoreLogger());
-                    if (sysResult) {
-                        logger.onSystemLiveWallpaperRestored(componentName);
+                    if (success) {
+                        if (!lockscreenLiveWallpaper || (which & FLAG_SYSTEM) != 0) {
+                            logger.onSystemLiveWallpaperRestored(componentName);
+                        }
+                        if (lockscreenLiveWallpaper && (which & FLAG_LOCK) != 0) {
+                            logger.onLockLiveWallpaperRestored(componentName);
+                        }
                     } else {
-                        logger.onSystemLiveWallpaperRestoreFailed(
-                                WallpaperEventLogger.ERROR_SET_COMPONENT_EXCEPTION);
+                        if (!lockscreenLiveWallpaper || (which & FLAG_SYSTEM) != 0) {
+                            logger.onSystemLiveWallpaperRestoreFailed(
+                                    WallpaperEventLogger.ERROR_SET_COMPONENT_EXCEPTION);
+                        }
+                        if (lockscreenLiveWallpaper && (which & FLAG_LOCK) != 0) {
+                            logger.onLockLiveWallpaperRestoreFailed(
+                                    WallpaperEventLogger.ERROR_SET_COMPONENT_EXCEPTION);
+                        }
                     }
-                    if (applyToLock) {
+                    if (applyToLock && !lockscreenLiveWallpaper) {
                         try {
                             mWallpaperManager.clear(FLAG_LOCK);
                             logger.onLockLiveWallpaperRestored(componentName);
diff --git a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
index 9b07ad4..dc1126e 100644
--- a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
+++ b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
@@ -117,6 +117,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
+        when(mWallpaperManager.isLockscreenLiveWallpaperEnabled()).thenReturn(true);
         when(mWallpaperManager.isWallpaperBackupEligible(eq(FLAG_SYSTEM))).thenReturn(true);
         when(mWallpaperManager.isWallpaperBackupEligible(eq(FLAG_LOCK))).thenReturn(true);
 
@@ -364,14 +365,23 @@
         mWallpaperBackupAgent.mIsDeviceInRestore = true;
 
         mWallpaperBackupAgent.updateWallpaperComponent(mWallpaperComponent,
-                /* applyToLock */ true);
+                /* applyToLock */ true, FLAG_LOCK | FLAG_SYSTEM);
 
         // Imitate wallpaper component installation.
         mWallpaperBackupAgent.mWallpaperPackageMonitor.onPackageAdded(TEST_WALLPAPER_PACKAGE,
                 /* uid */0);
-
-        verify(mWallpaperManager, times(1)).setWallpaperComponent(mWallpaperComponent);
-        verify(mWallpaperManager, times(1)).clear(eq(FLAG_LOCK));
+        if (mWallpaperManager.isLockscreenLiveWallpaperEnabled()) {
+            verify(mWallpaperManager, times(1))
+                    .setWallpaperComponentWithFlags(mWallpaperComponent, FLAG_LOCK | FLAG_SYSTEM);
+            verify(mWallpaperManager, never())
+                    .setWallpaperComponentWithFlags(mWallpaperComponent, FLAG_SYSTEM);
+            verify(mWallpaperManager, never())
+                    .setWallpaperComponentWithFlags(mWallpaperComponent, FLAG_LOCK);
+            verify(mWallpaperManager, never()).clear(anyInt());
+        } else {
+            verify(mWallpaperManager, times(1)).setWallpaperComponent(mWallpaperComponent);
+            verify(mWallpaperManager, times(1)).clear(eq(FLAG_LOCK));
+        }
     }
 
     @Test
@@ -380,14 +390,24 @@
         mWallpaperBackupAgent.mIsDeviceInRestore = true;
 
         mWallpaperBackupAgent.updateWallpaperComponent(mWallpaperComponent,
-                /* applyToLock */ false);
+                /* applyToLock */ false, FLAG_SYSTEM);
 
         // Imitate wallpaper component installation.
         mWallpaperBackupAgent.mWallpaperPackageMonitor.onPackageAdded(TEST_WALLPAPER_PACKAGE,
                 /* uid */0);
 
-        verify(mWallpaperManager, times(1)).setWallpaperComponent(mWallpaperComponent);
-        verify(mWallpaperManager, never()).clear(eq(FLAG_LOCK));
+        if (mWallpaperManager.isLockscreenLiveWallpaperEnabled()) {
+            verify(mWallpaperManager, times(1))
+                    .setWallpaperComponentWithFlags(mWallpaperComponent, FLAG_SYSTEM);
+            verify(mWallpaperManager, never())
+                    .setWallpaperComponentWithFlags(mWallpaperComponent, FLAG_LOCK);
+            verify(mWallpaperManager, never())
+                    .setWallpaperComponentWithFlags(mWallpaperComponent, FLAG_LOCK | FLAG_SYSTEM);
+            verify(mWallpaperManager, never()).clear(anyInt());
+        } else {
+            verify(mWallpaperManager, times(1)).setWallpaperComponent(mWallpaperComponent);
+            verify(mWallpaperManager, never()).clear(eq(FLAG_LOCK));
+        }
     }
 
     @Test
@@ -396,7 +416,7 @@
         mWallpaperBackupAgent.mIsDeviceInRestore = false;
 
         mWallpaperBackupAgent.updateWallpaperComponent(mWallpaperComponent,
-                /* applyToLock */ true);
+                /* applyToLock */ true, FLAG_LOCK | FLAG_SYSTEM);
 
         // Imitate wallpaper component installation.
         mWallpaperBackupAgent.mWallpaperPackageMonitor.onPackageAdded(TEST_WALLPAPER_PACKAGE,
@@ -412,7 +432,7 @@
         mWallpaperBackupAgent.mIsDeviceInRestore = false;
 
         mWallpaperBackupAgent.updateWallpaperComponent(mWallpaperComponent,
-                /* applyToLock */ true);
+                /* applyToLock */ true, FLAG_LOCK | FLAG_SYSTEM);
 
         // Imitate "wrong" wallpaper component installation.
         mWallpaperBackupAgent.mWallpaperPackageMonitor.onPackageAdded(/* packageName */"",
@@ -614,6 +634,13 @@
                 mWallpaperBackupAgent.getBackupRestoreEventLogger().getLoggingResults());
         assertThat(result).isNotNull();
         assertThat(result.getSuccessCount()).isEqualTo(1);
+
+        if (mWallpaperManager.isLockscreenLiveWallpaperEnabled()) {
+            result = getLoggingResult(WALLPAPER_IMG_LOCK,
+                    mWallpaperBackupAgent.getBackupRestoreEventLogger().getLoggingResults());
+            assertThat(result).isNotNull();
+            assertThat(result.getSuccessCount()).isEqualTo(1);
+        }
     }
 
     @Test
@@ -649,19 +676,20 @@
     }
 
     @Test
-    public void testOnRestore_lockWallpaperImgMissingAndNoLive_logsFailure() throws Exception {
+    public void testOnRestore_wallpaperImgMissingAndNoLive_logsFailure() throws Exception {
         mockStagedWallpaperFile(WALLPAPER_INFO_STAGE);
-        mockStagedWallpaperFile(SYSTEM_WALLPAPER_STAGE);
         mWallpaperBackupAgent.onCreate(USER_HANDLE, BackupAnnotations.BackupDestination.CLOUD,
                 BackupAnnotations.OperationType.RESTORE);
 
         mWallpaperBackupAgent.onRestoreFinished();
 
-        DataTypeResult result = getLoggingResult(WALLPAPER_IMG_LOCK,
-                mWallpaperBackupAgent.getBackupRestoreEventLogger().getLoggingResults());
-        assertThat(result).isNotNull();
-        assertThat(result.getFailCount()).isEqualTo(1);
-        assertThat(result.getErrors()).containsKey(ERROR_NO_WALLPAPER);
+        for (String wallpaper: List.of(WALLPAPER_IMG_LOCK, WALLPAPER_IMG_SYSTEM)) {
+            DataTypeResult result = getLoggingResult(wallpaper,
+                    mWallpaperBackupAgent.getBackupRestoreEventLogger().getLoggingResults());
+            assertThat(result).isNotNull();
+            assertThat(result.getFailCount()).isEqualTo(1);
+            assertThat(result.getErrors()).containsKey(ERROR_NO_WALLPAPER);
+        }
     }
 
     @Test
@@ -722,13 +750,15 @@
     public void testUpdateWallpaperComponent_delayedRestore_logsSuccess() throws Exception {
         mWallpaperBackupAgent.mIsDeviceInRestore = true;
         when(mWallpaperManager.setWallpaperComponent(any())).thenReturn(true);
+        when(mWallpaperManager.setWallpaperComponentWithFlags(any(), eq(FLAG_LOCK | FLAG_SYSTEM)))
+                .thenReturn(true);
         BackupRestoreEventLogger logger = new BackupRestoreEventLogger(
                 BackupAnnotations.OperationType.RESTORE);
         when(mBackupManager.getDelayedRestoreLogger()).thenReturn(logger);
         mWallpaperBackupAgent.setBackupManagerForTesting(mBackupManager);
 
         mWallpaperBackupAgent.updateWallpaperComponent(mWallpaperComponent,
-                /* applyToLock */ true);
+                /* applyToLock */ true, FLAG_LOCK | FLAG_SYSTEM);
         // Imitate wallpaper component installation.
         mWallpaperBackupAgent.mWallpaperPackageMonitor.onPackageAdded(TEST_WALLPAPER_PACKAGE,
                 /* uid */0);
@@ -752,7 +782,7 @@
         mWallpaperBackupAgent.setBackupManagerForTesting(mBackupManager);
 
         mWallpaperBackupAgent.updateWallpaperComponent(mWallpaperComponent,
-                /* applyToLock */ true);
+                /* applyToLock */ true, FLAG_LOCK | FLAG_SYSTEM);
         // Imitate wallpaper component installation.
         mWallpaperBackupAgent.mWallpaperPackageMonitor.onPackageAdded(TEST_WALLPAPER_PACKAGE,
                 /* uid */0);
@@ -774,7 +804,7 @@
         mWallpaperBackupAgent.setBackupManagerForTesting(mBackupManager);
 
         mWallpaperBackupAgent.updateWallpaperComponent(mWallpaperComponent,
-                /* applyToLock */ true);
+                /* applyToLock */ true, FLAG_LOCK | FLAG_SYSTEM);
 
         // Imitate wallpaper component installation.
         mWallpaperBackupAgent.mWallpaperPackageMonitor.onPackageAdded(TEST_WALLPAPER_PACKAGE,
@@ -909,8 +939,9 @@
 
         @Override
         PackageMonitor getWallpaperPackageMonitor(ComponentName componentName,
-                boolean applyToLock) {
-            mWallpaperPackageMonitor = super.getWallpaperPackageMonitor(componentName, applyToLock);
+                boolean applyToLock, int which) {
+            mWallpaperPackageMonitor = super.getWallpaperPackageMonitor(
+                    componentName, applyToLock, which);
             return mWallpaperPackageMonitor;
         }
 
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index c2ebddf..502ee4d 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -75,6 +75,7 @@
 import android.util.Size;
 import android.view.Surface;
 
+import androidx.annotation.NonNull;
 import androidx.camera.extensions.impl.AutoImageCaptureExtenderImpl;
 import androidx.camera.extensions.impl.AutoPreviewExtenderImpl;
 import androidx.camera.extensions.impl.BeautyImageCaptureExtenderImpl;
@@ -204,7 +205,7 @@
      * A per-process global camera extension manager instance, to track and
      * initialize/release extensions depending on client activity.
      */
-    private static final class CameraExtensionManagerGlobal {
+    private static final class CameraExtensionManagerGlobal implements IBinder.DeathRecipient {
         private static final String TAG = "CameraExtensionManagerGlobal";
         private final int EXTENSION_DELAY_MS = 1000;
 
@@ -212,8 +213,9 @@
         private final HandlerThread mHandlerThread;
         private final Object mLock = new Object();
 
-        private long mCurrentClientCount = 0;
-        private ArraySet<Long> mActiveClients = new ArraySet<>();
+        private ArraySet<IBinder> mActiveClients = new ArraySet<>();
+        private HashMap<IBinder, ArraySet<IBinder.DeathRecipient>> mClientDeathRecipient =
+                new HashMap<>();
         private IInitializeSessionCallback mInitializeCb = null;
 
         // Singleton instance
@@ -314,8 +316,20 @@
             return GLOBAL_CAMERA_MANAGER;
         }
 
-        public long registerClient(Context ctx) {
+        public boolean registerClient(Context ctx, IBinder token) {
             synchronized (mLock) {
+                if (mActiveClients.contains(token)) {
+                    Log.e(TAG, "Failed to register existing client!");
+                    return false;
+                }
+
+                try {
+                    token.linkToDeath(this, 0);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Failed to link to binder token!");
+                    return false;
+                }
+
                 if (INIT_API_SUPPORTED) {
                     if (mActiveClients.isEmpty()) {
                         InitializerFuture status = new InitializerFuture();
@@ -327,43 +341,76 @@
                                     TimeUnit.MILLISECONDS);
                         } catch (TimeoutException e) {
                             Log.e(TAG, "Timed out while initializing camera extensions!");
-                            return -1;
+                            return false;
                         }
                         if (!initSuccess) {
                             Log.e(TAG, "Failed while initializing camera extensions!");
-                            return -1;
+                            return false;
                         }
                     }
                 }
 
-                long ret = mCurrentClientCount;
-                mCurrentClientCount++;
-                if (mCurrentClientCount < 0) {
-                    mCurrentClientCount = 0;
-                }
-                mActiveClients.add(ret);
+                mActiveClients.add(token);
+                mClientDeathRecipient.put(token, new ArraySet<>());
 
-                return ret;
+                return true;
             }
         }
 
-        public void unregisterClient(long clientId) {
+        public void unregisterClient(IBinder token) {
             synchronized (mLock) {
-                if (mActiveClients.remove(clientId) && mActiveClients.isEmpty() &&
-                        INIT_API_SUPPORTED) {
-                    InitializerFuture status = new InitializerFuture();
-                    InitializerImpl.deinit(new ReleaseHandler(status),
-                            new HandlerExecutor(mHandler));
-                    boolean releaseSuccess;
-                    try {
-                        releaseSuccess = status.get(EXTENSION_DELAY_MS, TimeUnit.MILLISECONDS);
-                    } catch (TimeoutException e) {
-                        Log.e(TAG, "Timed out while releasing camera extensions!");
-                        return;
+                if (mActiveClients.remove(token)) {
+                    token.unlinkToDeath(this, 0);
+                    mClientDeathRecipient.remove(token);
+                    if (mActiveClients.isEmpty() && INIT_API_SUPPORTED) {
+                        InitializerFuture status = new InitializerFuture();
+                        InitializerImpl.deinit(new ReleaseHandler(status),
+                                new HandlerExecutor(mHandler));
+                        boolean releaseSuccess;
+                        try {
+                            releaseSuccess = status.get(EXTENSION_DELAY_MS, TimeUnit.MILLISECONDS);
+                        } catch (TimeoutException e) {
+                            Log.e(TAG, "Timed out while releasing camera extensions!");
+                            return;
+                        }
+                        if (!releaseSuccess) {
+                            Log.e(TAG, "Failed while releasing camera extensions!");
+                        }
                     }
-                    if (!releaseSuccess) {
-                        Log.e(TAG, "Failed while releasing camera extensions!");
-                    }
+                }
+            }
+        }
+
+        @Override
+        public void binderDied() {
+            // Do nothing, handled below
+        }
+
+        @Override
+        public void binderDied(@NonNull IBinder who) {
+            synchronized (mLock) {
+                if (mClientDeathRecipient.containsKey(who)) {
+                    mClientDeathRecipient.get(who).stream().forEach(
+                            recipient -> recipient.binderDied(who));
+                }
+                unregisterClient(who);
+            }
+        }
+
+        public void registerDeathRecipient(IBinder token, IBinder.DeathRecipient recipient) {
+            synchronized (mLock) {
+                if (mClientDeathRecipient.containsKey(token)) {
+                    ArraySet<IBinder.DeathRecipient> recipients = mClientDeathRecipient.get(token);
+                    recipients.add(recipient);
+                }
+            }
+        }
+
+        public void unregisterDeathRecipient(IBinder token, IBinder.DeathRecipient recipient) {
+            synchronized (mLock) {
+                if (mClientDeathRecipient.containsKey(token)) {
+                    ArraySet<IBinder.DeathRecipient> recipients = mClientDeathRecipient.get(token);
+                    recipients.remove(recipient);
                 }
             }
         }
@@ -406,21 +453,35 @@
     /**
      * @hide
      */
-    private static long registerClient(Context ctx) {
+    private static boolean registerClient(Context ctx, IBinder token) {
         if (!EXTENSIONS_PRESENT) {
-            return -1;
+            return false;
         }
-        return CameraExtensionManagerGlobal.get().registerClient(ctx);
+        return CameraExtensionManagerGlobal.get().registerClient(ctx, token);
     }
 
     /**
      * @hide
      */
-    public static void unregisterClient(long clientId) {
+    public static void unregisterClient(IBinder token) {
         if (!EXTENSIONS_PRESENT) {
             return;
         }
-        CameraExtensionManagerGlobal.get().unregisterClient(clientId);
+        CameraExtensionManagerGlobal.get().unregisterClient(token);
+    }
+
+    /**
+     * @hide
+     */
+    private static void registerDeathRecipient(IBinder token, IBinder.DeathRecipient recipient) {
+        CameraExtensionManagerGlobal.get().registerDeathRecipient(token, recipient);
+    }
+
+    /**
+     * @hide
+     */
+    private static void unregisterDeathRecipient(IBinder token, IBinder.DeathRecipient recipient) {
+        CameraExtensionManagerGlobal.get().unregisterDeathRecipient(token, recipient);
     }
 
     /**
@@ -649,13 +710,14 @@
 
     private class CameraExtensionsProxyServiceStub extends ICameraExtensionsProxyService.Stub {
         @Override
-        public long registerClient() {
-            return CameraExtensionsProxyService.registerClient(CameraExtensionsProxyService.this);
+        public boolean registerClient(IBinder token) {
+            return CameraExtensionsProxyService.registerClient(CameraExtensionsProxyService.this,
+                    token);
         }
 
         @Override
-        public void unregisterClient(long clientId) {
-            CameraExtensionsProxyService.unregisterClient(clientId);
+        public void unregisterClient(IBinder token) {
+            CameraExtensionsProxyService.unregisterClient(token);
         }
 
         private boolean checkCameraPermission() {
@@ -1192,16 +1254,18 @@
         }
     }
 
-    private class SessionProcessorImplStub extends ISessionProcessorImpl.Stub {
+    private class SessionProcessorImplStub extends ISessionProcessorImpl.Stub implements
+            IBinder.DeathRecipient {
         private final SessionProcessorImpl mSessionProcessor;
         private String mCameraId = null;
+        private IBinder mToken;
 
         public SessionProcessorImplStub(SessionProcessorImpl sessionProcessor) {
             mSessionProcessor = sessionProcessor;
         }
 
         @Override
-        public CameraSessionConfig initSession(String cameraId,
+        public CameraSessionConfig initSession(IBinder token, String cameraId,
                 Map<String, CameraMetadataNative> charsMapNative, OutputSurface previewSurface,
                 OutputSurface imageCaptureSurface, OutputSurface postviewSurface) {
             OutputSurfaceImplStub outputPreviewSurfaceImpl =
@@ -1253,12 +1317,14 @@
             ret.sessionParameter = initializeParcelableMetadata(
                     sessionConfig.getSessionParameters(), cameraId);
             mCameraId = cameraId;
-
+            mToken = token;
+            CameraExtensionsProxyService.registerDeathRecipient(mToken, this);
             return ret;
         }
 
         @Override
-        public void deInitSession() {
+        public void deInitSession(IBinder token) {
+            CameraExtensionsProxyService.unregisterDeathRecipient(mToken, this);
             mSessionProcessor.deInitSession();
         }
 
@@ -1330,6 +1396,11 @@
 
             return null;
         }
+
+        @Override
+        public void binderDied() {
+            mSessionProcessor.deInitSession();
+        }
     }
 
     private class OutputSurfaceConfigurationImplStub implements OutputSurfaceConfigurationImpl {
@@ -1395,24 +1466,31 @@
         }
     }
 
-    private class PreviewExtenderImplStub extends IPreviewExtenderImpl.Stub {
+    private class PreviewExtenderImplStub extends IPreviewExtenderImpl.Stub implements
+            IBinder.DeathRecipient {
         private final PreviewExtenderImpl mPreviewExtender;
         private String mCameraId = null;
+        private boolean mSessionEnabled;
+        private IBinder mToken;
 
         public PreviewExtenderImplStub(PreviewExtenderImpl previewExtender) {
             mPreviewExtender = previewExtender;
         }
 
         @Override
-        public void onInit(String cameraId, CameraMetadataNative cameraCharacteristics) {
+        public void onInit(IBinder token, String cameraId,
+                CameraMetadataNative cameraCharacteristics) {
             mCameraId = cameraId;
             CameraCharacteristics chars = new CameraCharacteristics(cameraCharacteristics);
             mCameraManager.registerDeviceStateListener(chars);
             mPreviewExtender.onInit(cameraId, chars, CameraExtensionsProxyService.this);
+            mToken = token;
+            CameraExtensionsProxyService.registerDeathRecipient(mToken, this);
         }
 
         @Override
-        public void onDeInit() {
+        public void onDeInit(IBinder token) {
+            CameraExtensionsProxyService.unregisterDeathRecipient(mToken, this);
             mPreviewExtender.onDeInit();
         }
 
@@ -1423,11 +1501,13 @@
 
         @Override
         public CaptureStageImpl onEnableSession() {
+            mSessionEnabled = true;
             return initializeParcelable(mPreviewExtender.onEnableSession(), mCameraId);
         }
 
         @Override
         public CaptureStageImpl onDisableSession() {
+            mSessionEnabled = false;
             return initializeParcelable(mPreviewExtender.onDisableSession(), mCameraId);
         }
 
@@ -1516,26 +1596,41 @@
             }
             return null;
         }
+
+        @Override
+        public void binderDied() {
+            if (mSessionEnabled) {
+                mPreviewExtender.onDisableSession();
+            }
+            mPreviewExtender.onDeInit();
+        }
     }
 
-    private class ImageCaptureExtenderImplStub extends IImageCaptureExtenderImpl.Stub {
+    private class ImageCaptureExtenderImplStub extends IImageCaptureExtenderImpl.Stub implements
+            IBinder.DeathRecipient {
         private final ImageCaptureExtenderImpl mImageExtender;
         private String mCameraId = null;
+        private boolean mSessionEnabled;
+        private IBinder mToken;
 
         public ImageCaptureExtenderImplStub(ImageCaptureExtenderImpl imageExtender) {
             mImageExtender = imageExtender;
         }
 
         @Override
-        public void onInit(String cameraId, CameraMetadataNative cameraCharacteristics) {
+        public void onInit(IBinder token, String cameraId,
+                CameraMetadataNative cameraCharacteristics) {
             CameraCharacteristics chars = new CameraCharacteristics(cameraCharacteristics);
             mCameraManager.registerDeviceStateListener(chars);
             mImageExtender.onInit(cameraId, chars, CameraExtensionsProxyService.this);
             mCameraId = cameraId;
+            mToken = token;
+            CameraExtensionsProxyService.registerDeathRecipient(mToken, this);
         }
 
         @Override
-        public void onDeInit() {
+        public void onDeInit(IBinder token) {
+            CameraExtensionsProxyService.unregisterDeathRecipient(mToken, this);
             mImageExtender.onDeInit();
         }
 
@@ -1564,11 +1659,13 @@
 
         @Override
         public CaptureStageImpl onEnableSession() {
+            mSessionEnabled = true;
             return initializeParcelable(mImageExtender.onEnableSession(), mCameraId);
         }
 
         @Override
         public CaptureStageImpl onDisableSession() {
+            mSessionEnabled = false;
             return initializeParcelable(mImageExtender.onDisableSession(), mCameraId);
         }
 
@@ -1737,6 +1834,14 @@
 
             return null;
         }
+
+        @Override
+        public void binderDied() {
+            if (mSessionEnabled) {
+                mImageExtender.onDisableSession();
+            }
+            mImageExtender.onDeInit();
+        }
     }
 
     private class ProcessResultCallback implements ProcessResultImpl {
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
index b1cdc50..ba3d434 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
@@ -319,6 +319,10 @@
                     FullScreenMagnificationController::onUserContextChanged,
                     FullScreenMagnificationController.this, mDisplayId);
             mControllerCtx.getHandler().sendMessage(m);
+
+            synchronized (mLock) {
+                refreshThumbnail();
+            }
         }
 
         @Override
@@ -344,7 +348,7 @@
                     mMagnificationRegion.set(magnified);
                     mMagnificationRegion.getBounds(mMagnificationBounds);
 
-                    refreshThumbnail(getScale(), getCenterX(), getCenterY());
+                    refreshThumbnail();
 
                     // It's possible that our magnification spec is invalid with the new bounds.
                     // Adjust the current spec's offsets if necessary.
@@ -602,13 +606,13 @@
         }
 
         @GuardedBy("mLock")
-        void refreshThumbnail(float scale, float centerX, float centerY) {
+        void refreshThumbnail() {
             if (mMagnificationThumbnail != null) {
                 mMagnificationThumbnail.setThumbnailBounds(
                         mMagnificationBounds,
-                        scale,
-                        centerX,
-                        centerY
+                        getScale(),
+                        getCenterX(),
+                        getCenterY()
                 );
             }
         }
@@ -627,7 +631,7 @@
                 // We call refreshThumbnail when the thumbnail is just created to set current
                 // magnification bounds to thumbnail. It to prevent the thumbnail size has not yet
                 // updated properly and thus shows with huge size. (b/276314641)
-                refreshThumbnail(getScale(), getCenterX(), getCenterY());
+                refreshThumbnail();
             }
         }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
index 87fbee7..f6948e9 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
@@ -502,6 +502,10 @@
     @Override
     public void onSourceBoundsChanged(int displayId, Rect bounds) {
         if (shouldNotifyMagnificationChange(displayId, MAGNIFICATION_MODE_WINDOW)) {
+            // notify sysui the magnification scale changed on window magnifier
+            mWindowMagnificationMgr.onUserMagnificationScaleChanged(
+                    mUserId, displayId, getWindowMagnificationMgr().getScale(displayId));
+
             final MagnificationConfig config = new MagnificationConfig.Builder()
                     .setMode(MAGNIFICATION_MODE_WINDOW)
                     .setActivated(getWindowMagnificationMgr().isWindowMagnifierEnabled(displayId))
@@ -516,6 +520,10 @@
     public void onFullScreenMagnificationChanged(int displayId, @NonNull Region region,
             @NonNull MagnificationConfig config) {
         if (shouldNotifyMagnificationChange(displayId, MAGNIFICATION_MODE_FULLSCREEN)) {
+            // notify sysui the magnification scale changed on fullscreen magnifier
+            mWindowMagnificationMgr.onUserMagnificationScaleChanged(
+                    mUserId, displayId, config.getScale());
+
             mAms.notifyMagnificationChanged(displayId, region, config);
         }
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationScaleProvider.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationScaleProvider.java
index 8e1aa38..41c3dcb 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationScaleProvider.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationScaleProvider.java
@@ -16,6 +16,9 @@
 
 package com.android.server.accessibility.magnification;
 
+import static com.android.internal.accessibility.common.MagnificationConstants.SCALE_MAX_VALUE;
+import static com.android.internal.accessibility.common.MagnificationConstants.SCALE_MIN_VALUE;
+
 import android.content.Context;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -37,8 +40,9 @@
 
     @VisibleForTesting
     protected static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f;
-    public static final float MIN_SCALE = 1.0f;
-    public static final float MAX_SCALE = 8.0f;
+
+    public static final float MIN_SCALE = SCALE_MIN_VALUE;
+    public static final float MAX_SCALE = SCALE_MAX_VALUE;
 
     private final Context mContext;
     // Stores the scale for non-default displays.
@@ -134,6 +138,6 @@
     }
 
     static float constrainScale(float scale) {
-        return MathUtils.constrain(scale, MIN_SCALE, MAX_SCALE);
+        return MathUtils.constrain(scale, SCALE_MIN_VALUE, SCALE_MAX_VALUE);
     }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationThumbnail.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationThumbnail.java
index 03fa93d..a7bdd5a 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationThumbnail.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationThumbnail.java
@@ -99,15 +99,17 @@
             Log.d(LOG_TAG, "setThumbnailBounds " + currentBounds);
         }
         mHandler.post(() -> {
-            mWindowBounds = currentBounds;
-            setBackgroundBounds();
+            refreshBackgroundBounds(currentBounds);
             if (mVisible) {
                 updateThumbnailMainThread(scale, centerX, centerY);
             }
         });
     }
 
-    private void setBackgroundBounds() {
+    @MainThread
+    private void refreshBackgroundBounds(Rect currentBounds) {
+        mWindowBounds = currentBounds;
+
         Point magnificationBoundary = getMagnificationThumbnailPadding(mContext);
         mThumbnailWidth = (int) (mWindowBounds.width() / BG_ASPECT_RATIO);
         mThumbnailHeight = (int) (mWindowBounds.height() / BG_ASPECT_RATIO);
@@ -117,6 +119,10 @@
         mBackgroundParams.height = mThumbnailHeight;
         mBackgroundParams.x = initX;
         mBackgroundParams.y = initY;
+
+        if (mVisible) {
+            mWindowManager.updateViewLayout(mThumbnailLayout, mBackgroundParams);
+        }
     }
 
     @MainThread
@@ -264,21 +270,16 @@
             mThumbnailView.setScaleX(scaleDown);
             mThumbnailView.setScaleY(scaleDown);
         }
-        float thumbnailWidth;
-        float thumbnailHeight;
-        if (mThumbnailView.getWidth() == 0 || mThumbnailView.getHeight() == 0) {
-            // if the thumbnail view size is not updated correctly, we just use the cached values.
-            thumbnailWidth = mThumbnailWidth;
-            thumbnailHeight = mThumbnailHeight;
-        } else {
-            thumbnailWidth = mThumbnailView.getWidth();
-            thumbnailHeight = mThumbnailView.getHeight();
-        }
-        if (!Float.isNaN(centerX)) {
+
+        if (!Float.isNaN(centerX)
+                && !Float.isNaN(centerY)
+                && mThumbnailWidth > 0
+                && mThumbnailHeight > 0
+        ) {
             var padding = mThumbnailView.getPaddingTop();
             var ratio = 1f / BG_ASPECT_RATIO;
-            var centerXScaled = centerX * ratio - (thumbnailWidth / 2f + padding);
-            var centerYScaled = centerY * ratio - (thumbnailHeight / 2f + padding);
+            var centerXScaled = centerX * ratio - (mThumbnailWidth / 2f + padding);
+            var centerYScaled = centerY * ratio - (mThumbnailHeight / 2f + padding);
 
             if (DEBUG) {
                 Log.d(
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java
index 1202cfa..6c6394f 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java
@@ -201,6 +201,22 @@
         return true;
     }
 
+    boolean onUserMagnificationScaleChanged(int userId, int displayId, float scale) {
+        if (mTrace.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MAGNIFICATION_CONNECTION)) {
+            mTrace.logTrace(TAG + ".onMagnificationScaleUpdated",
+                    FLAGS_WINDOW_MAGNIFICATION_CONNECTION, "displayId=" + displayId);
+        }
+        try {
+            mConnection.onUserMagnificationScaleChanged(userId, displayId, scale);
+        } catch (RemoteException e) {
+            if (DBG) {
+                Slog.e(TAG, "Error calling onMagnificationScaleUpdated()", e);
+            }
+            return false;
+        }
+        return true;
+    }
+
     boolean setConnectionCallback(IWindowMagnificationConnectionCallback connectionCallback) {
         if (mTrace.isA11yTracingEnabledForTypes(
                 FLAGS_WINDOW_MAGNIFICATION_CONNECTION
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
index d07db3f..d96682c 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
@@ -827,6 +827,20 @@
     }
 
     /**
+     * Notify System UI the magnification scale on the specified display for userId is changed.
+     *
+     * @param userId the user id.
+     * @param displayId the logical display id.
+     * @param scale magnification scale.
+     */
+    public boolean onUserMagnificationScaleChanged(int userId, int displayId, float scale) {
+        synchronized (mLock) {
+            return mConnectionWrapper != null
+                    && mConnectionWrapper.onUserMagnificationScaleChanged(userId, displayId, scale);
+        }
+    }
+
+    /**
      * Returns the screen-relative X coordinate of the center of the magnified bounds.
      *
      * @param displayId The logical display id
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index 995e557..d5aee92 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -2859,8 +2859,7 @@
             if (DEBUG) {
                 Slog.d(
                         TAG,
-                        addUserIdToLogMessage(
-                                mUserId, "Starting backup confirmation UI, token=" + token));
+                        addUserIdToLogMessage(mUserId, "Starting backup confirmation UI"));
             }
             if (!startConfirmationUi(token, FullBackup.FULL_BACKUP_INTENT_ACTION)) {
                 Slog.e(
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 61fc32d..ca1ab9b 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -50,6 +50,7 @@
 import android.content.pm.ActivityPresentationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ParceledListSlice;
 import android.content.pm.UserInfo;
 import android.database.ContentObserver;
 import android.os.Binder;
@@ -69,6 +70,7 @@
 import android.provider.DeviceConfig.Properties;
 import android.provider.Settings;
 import android.service.contentcapture.ActivityEvent.ActivityEventType;
+import android.service.contentcapture.ContentCaptureServiceInfo;
 import android.service.contentcapture.IDataShareCallback;
 import android.service.contentcapture.IDataShareReadAdapter;
 import android.service.voice.VoiceInteractionManagerInternal;
@@ -79,6 +81,7 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.view.contentcapture.ContentCaptureCondition;
+import android.view.contentcapture.ContentCaptureEvent;
 import android.view.contentcapture.ContentCaptureHelper;
 import android.view.contentcapture.ContentCaptureManager;
 import android.view.contentcapture.DataRemovalRequest;
@@ -88,11 +91,15 @@
 import android.view.contentcapture.IDataShareWriteAdapter;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.infra.AbstractRemoteService;
 import com.android.internal.infra.GlobalWhitelistState;
 import com.android.internal.os.IResultReceiver;
 import com.android.internal.util.DumpUtils;
 import com.android.server.LocalServices;
+import com.android.server.contentprotection.ContentProtectionBlocklistManager;
+import com.android.server.contentprotection.ContentProtectionPackageManager;
+import com.android.server.contentprotection.RemoteContentProtectionService;
 import com.android.server.infra.AbstractMasterSystemService;
 import com.android.server.infra.FrameworkResourcesServiceNameResolver;
 
@@ -117,7 +124,7 @@
  * with other sources to provide contextual data in other areas of the system
  * such as Autofill.
  */
-public final class ContentCaptureManagerService extends
+public class ContentCaptureManagerService extends
         AbstractMasterSystemService<ContentCaptureManagerService, ContentCapturePerUserService> {
 
     private static final String TAG = ContentCaptureManagerService.class.getSimpleName();
@@ -163,13 +170,35 @@
     private boolean mDisabledByDeviceConfig;
 
     // Device-config settings that are cached and passed back to apps
-    @GuardedBy("mLock") int mDevCfgLoggingLevel;
-    @GuardedBy("mLock") int mDevCfgMaxBufferSize;
-    @GuardedBy("mLock") int mDevCfgIdleFlushingFrequencyMs;
-    @GuardedBy("mLock") int mDevCfgTextChangeFlushingFrequencyMs;
-    @GuardedBy("mLock") int mDevCfgLogHistorySize;
-    @GuardedBy("mLock") int mDevCfgIdleUnbindTimeoutMs;
-    @GuardedBy("mLock") boolean mDevCfgDisableFlushForViewTreeAppearing;
+    @GuardedBy("mLock")
+    int mDevCfgLoggingLevel;
+
+    @GuardedBy("mLock")
+    int mDevCfgMaxBufferSize;
+
+    @GuardedBy("mLock")
+    int mDevCfgIdleFlushingFrequencyMs;
+
+    @GuardedBy("mLock")
+    int mDevCfgTextChangeFlushingFrequencyMs;
+
+    @GuardedBy("mLock")
+    int mDevCfgLogHistorySize;
+
+    @GuardedBy("mLock")
+    int mDevCfgIdleUnbindTimeoutMs;
+
+    @GuardedBy("mLock")
+    boolean mDevCfgDisableFlushForViewTreeAppearing;
+
+    @GuardedBy("mLock")
+    boolean mDevCfgEnableContentProtectionReceiver;
+
+    @GuardedBy("mLock")
+    int mDevCfgContentProtectionAppsBlocklistSize;
+
+    @GuardedBy("mLock")
+    int mDevCfgContentProtectionBufferSize;
 
     private final Executor mDataShareExecutor = Executors.newCachedThreadPool();
     private final Handler mHandler = new Handler(Looper.getMainLooper());
@@ -183,6 +212,10 @@
     final GlobalContentCaptureOptions mGlobalContentCaptureOptions =
             new GlobalContentCaptureOptions();
 
+    @Nullable private final ComponentName mContentProtectionServiceComponentName;
+
+    @Nullable private final ContentProtectionBlocklistManager mContentProtectionBlocklistManager;
+
     public ContentCaptureManagerService(@NonNull Context context) {
         super(context, new FrameworkResourcesServiceNameResolver(context,
                 com.android.internal.R.string.config_defaultContentCaptureService),
@@ -220,6 +253,20 @@
                     mServiceNameResolver.getServiceName(userId),
                     mServiceNameResolver.isTemporary(userId));
         }
+
+        if (getEnableContentProtectionReceiverLocked()) {
+            mContentProtectionServiceComponentName = getContentProtectionServiceComponentName();
+            if (mContentProtectionServiceComponentName != null) {
+                mContentProtectionBlocklistManager = createContentProtectionBlocklistManager();
+                mContentProtectionBlocklistManager.updateBlocklist(
+                        mDevCfgContentProtectionAppsBlocklistSize);
+            } else {
+                mContentProtectionBlocklistManager = null;
+            }
+        } else {
+            mContentProtectionServiceComponentName = null;
+            mContentProtectionBlocklistManager = null;
+        }
     }
 
     @Override // from AbstractMasterSystemService
@@ -362,6 +409,11 @@
                 case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT:
                 case ContentCaptureManager
                         .DEVICE_CONFIG_PROPERTY_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING:
+                case ContentCaptureManager
+                        .DEVICE_CONFIG_PROPERTY_ENABLE_CONTENT_PROTECTION_RECEIVER:
+                case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_BUFFER_SIZE:
+                case ContentCaptureManager
+                        .DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_APPS_BLOCKLIST_SIZE:
                     setFineTuneParamsFromDeviceConfig();
                     return;
                 default:
@@ -370,41 +422,84 @@
         }
     }
 
-    private void setFineTuneParamsFromDeviceConfig() {
+    /** @hide */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    protected void setFineTuneParamsFromDeviceConfig() {
         synchronized (mLock) {
-            mDevCfgMaxBufferSize = DeviceConfig.getInt(
-                    DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
-                    ContentCaptureManager.DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE,
-                    ContentCaptureManager.DEFAULT_MAX_BUFFER_SIZE);
-            mDevCfgIdleFlushingFrequencyMs = DeviceConfig.getInt(
-                    DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
-                    ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY,
-                    ContentCaptureManager.DEFAULT_IDLE_FLUSHING_FREQUENCY_MS);
-            mDevCfgTextChangeFlushingFrequencyMs = DeviceConfig.getInt(
-                    DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
-                    ContentCaptureManager.DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY,
-                    ContentCaptureManager.DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS);
-            mDevCfgLogHistorySize = DeviceConfig.getInt(
-                    DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
-                    ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE, 20);
-            mDevCfgIdleUnbindTimeoutMs = DeviceConfig.getInt(
-                    DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
-                    ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT,
-                    (int) AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS);
-            mDevCfgDisableFlushForViewTreeAppearing = DeviceConfig.getBoolean(
-                    DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
-                    ContentCaptureManager
-                        .DEVICE_CONFIG_PROPERTY_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING,
-                    false);
+            mDevCfgMaxBufferSize =
+                    DeviceConfig.getInt(
+                            DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+                            ContentCaptureManager.DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE,
+                            ContentCaptureManager.DEFAULT_MAX_BUFFER_SIZE);
+            mDevCfgIdleFlushingFrequencyMs =
+                    DeviceConfig.getInt(
+                            DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+                            ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY,
+                            ContentCaptureManager.DEFAULT_IDLE_FLUSHING_FREQUENCY_MS);
+            mDevCfgTextChangeFlushingFrequencyMs =
+                    DeviceConfig.getInt(
+                            DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+                            ContentCaptureManager
+                                    .DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY,
+                            ContentCaptureManager.DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS);
+            mDevCfgLogHistorySize =
+                    DeviceConfig.getInt(
+                            DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+                            ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE,
+                            20);
+            mDevCfgIdleUnbindTimeoutMs =
+                    DeviceConfig.getInt(
+                            DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+                            ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT,
+                            (int) AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS);
+            mDevCfgDisableFlushForViewTreeAppearing =
+                    DeviceConfig.getBoolean(
+                            DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+                            ContentCaptureManager
+                                    .DEVICE_CONFIG_PROPERTY_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING,
+                            false);
+            mDevCfgEnableContentProtectionReceiver =
+                    DeviceConfig.getBoolean(
+                            DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+                            ContentCaptureManager
+                                    .DEVICE_CONFIG_PROPERTY_ENABLE_CONTENT_PROTECTION_RECEIVER,
+                            ContentCaptureManager.DEFAULT_ENABLE_CONTENT_PROTECTION_RECEIVER);
+            mDevCfgContentProtectionAppsBlocklistSize =
+                    DeviceConfig.getInt(
+                            DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+                            ContentCaptureManager
+                                    .DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_APPS_BLOCKLIST_SIZE,
+                            ContentCaptureManager.DEFAULT_CONTENT_PROTECTION_APPS_BLOCKLIST_SIZE);
+            // mContentProtectionBlocklistManager.updateBlocklist not called on purpose here to keep
+            // it immutable at this point
+            mDevCfgContentProtectionBufferSize =
+                    DeviceConfig.getInt(
+                            DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+                            ContentCaptureManager
+                                    .DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_BUFFER_SIZE,
+                            ContentCaptureManager.DEFAULT_CONTENT_PROTECTION_BUFFER_SIZE);
             if (verbose) {
-                Slog.v(TAG, "setFineTuneParamsFromDeviceConfig(): "
-                        + "bufferSize=" + mDevCfgMaxBufferSize
-                        + ", idleFlush=" + mDevCfgIdleFlushingFrequencyMs
-                        + ", textFluxh=" + mDevCfgTextChangeFlushingFrequencyMs
-                        + ", logHistory=" + mDevCfgLogHistorySize
-                        + ", idleUnbindTimeoutMs=" + mDevCfgIdleUnbindTimeoutMs
-                        + ", disableFlushForViewTreeAppearing="
-                        + mDevCfgDisableFlushForViewTreeAppearing);
+                Slog.v(
+                        TAG,
+                        "setFineTuneParamsFromDeviceConfig(): "
+                                + "bufferSize="
+                                + mDevCfgMaxBufferSize
+                                + ", idleFlush="
+                                + mDevCfgIdleFlushingFrequencyMs
+                                + ", textFluxh="
+                                + mDevCfgTextChangeFlushingFrequencyMs
+                                + ", logHistory="
+                                + mDevCfgLogHistorySize
+                                + ", idleUnbindTimeoutMs="
+                                + mDevCfgIdleUnbindTimeoutMs
+                                + ", disableFlushForViewTreeAppearing="
+                                + mDevCfgDisableFlushForViewTreeAppearing
+                                + ", enableContentProtectionReceiver="
+                                + mDevCfgEnableContentProtectionReceiver
+                                + ", contentProtectionAppsBlocklistSize="
+                                + mDevCfgContentProtectionAppsBlocklistSize
+                                + ", contentProtectionBufferSize="
+                                + mDevCfgContentProtectionBufferSize);
             }
         }
     }
@@ -645,24 +740,137 @@
 
         final String prefix2 = prefix + "  ";
 
-        pw.print(prefix); pw.print("Users disabled by Settings: "); pw.println(mDisabledBySettings);
-        pw.print(prefix); pw.println("DeviceConfig Settings: ");
-        pw.print(prefix2); pw.print("disabled: "); pw.println(mDisabledByDeviceConfig);
-        pw.print(prefix2); pw.print("loggingLevel: "); pw.println(mDevCfgLoggingLevel);
-        pw.print(prefix2); pw.print("maxBufferSize: "); pw.println(mDevCfgMaxBufferSize);
-        pw.print(prefix2); pw.print("idleFlushingFrequencyMs: ");
+        pw.print(prefix);
+        pw.print("Users disabled by Settings: ");
+        pw.println(mDisabledBySettings);
+        pw.print(prefix);
+        pw.println("DeviceConfig Settings: ");
+        pw.print(prefix2);
+        pw.print("disabled: ");
+        pw.println(mDisabledByDeviceConfig);
+        pw.print(prefix2);
+        pw.print("loggingLevel: ");
+        pw.println(mDevCfgLoggingLevel);
+        pw.print(prefix2);
+        pw.print("maxBufferSize: ");
+        pw.println(mDevCfgMaxBufferSize);
+        pw.print(prefix2);
+        pw.print("idleFlushingFrequencyMs: ");
         pw.println(mDevCfgIdleFlushingFrequencyMs);
-        pw.print(prefix2); pw.print("textChangeFlushingFrequencyMs: ");
+        pw.print(prefix2);
+        pw.print("textChangeFlushingFrequencyMs: ");
         pw.println(mDevCfgTextChangeFlushingFrequencyMs);
-        pw.print(prefix2); pw.print("logHistorySize: "); pw.println(mDevCfgLogHistorySize);
-        pw.print(prefix2); pw.print("idleUnbindTimeoutMs: ");
+        pw.print(prefix2);
+        pw.print("logHistorySize: ");
+        pw.println(mDevCfgLogHistorySize);
+        pw.print(prefix2);
+        pw.print("idleUnbindTimeoutMs: ");
         pw.println(mDevCfgIdleUnbindTimeoutMs);
-        pw.print(prefix2); pw.print("disableFlushForViewTreeAppearing: ");
+        pw.print(prefix2);
+        pw.print("disableFlushForViewTreeAppearing: ");
         pw.println(mDevCfgDisableFlushForViewTreeAppearing);
-        pw.print(prefix); pw.println("Global Options:");
+        pw.print(prefix2);
+        pw.print("enableContentProtectionReceiver: ");
+        pw.println(mDevCfgEnableContentProtectionReceiver);
+        pw.print(prefix2);
+        pw.print("contentProtectionAppsBlocklistSize: ");
+        pw.println(mDevCfgContentProtectionAppsBlocklistSize);
+        pw.print(prefix2);
+        pw.print("contentProtectionBufferSize: ");
+        pw.println(mDevCfgContentProtectionBufferSize);
+        pw.print(prefix);
+        pw.println("Global Options:");
         mGlobalContentCaptureOptions.dump(prefix2, pw);
     }
 
+    /**
+     * Used by the constructor in order to be able to override the value in the tests.
+     *
+     * @hide
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    @GuardedBy("mLock")
+    protected boolean getEnableContentProtectionReceiverLocked() {
+        return mDevCfgEnableContentProtectionReceiver;
+    }
+
+    /** @hide */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    @NonNull
+    protected ContentProtectionBlocklistManager createContentProtectionBlocklistManager() {
+        return new ContentProtectionBlocklistManager(
+                new ContentProtectionPackageManager(getContext()));
+    }
+
+    @Nullable
+    private ComponentName getContentProtectionServiceComponentName() {
+        String flatComponentName = getContentProtectionServiceFlatComponentName();
+        return ComponentName.unflattenFromString(flatComponentName);
+    }
+
+    /** @hide */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    @Nullable
+    protected String getContentProtectionServiceFlatComponentName() {
+        return getContext()
+                .getString(com.android.internal.R.string.config_defaultContentProtectionService);
+    }
+
+    /**
+     * Can also throw runtime exceptions such as {@link SecurityException}.
+     *
+     * @hide
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    @NonNull
+    protected ContentCaptureServiceInfo createContentProtectionServiceInfo(
+            @NonNull ComponentName componentName) throws PackageManager.NameNotFoundException {
+        return new ContentCaptureServiceInfo(
+                getContext(), componentName, /* isTemp= */ false, UserHandle.getCallingUserId());
+    }
+
+    @Nullable
+    private RemoteContentProtectionService createRemoteContentProtectionService() {
+        if (mContentProtectionServiceComponentName == null) {
+            // This case should not be possible but make sure
+            return null;
+        }
+        synchronized (mLock) {
+            if (!mDevCfgEnableContentProtectionReceiver) {
+                return null;
+            }
+        }
+
+        // Check permissions by trying to construct {@link ContentCaptureServiceInfo}
+        try {
+            createContentProtectionServiceInfo(mContentProtectionServiceComponentName);
+        } catch (Exception ex) {
+            // Swallow, exception was already logged
+            return null;
+        }
+
+        return createRemoteContentProtectionService(mContentProtectionServiceComponentName);
+    }
+
+    /** @hide */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    @NonNull
+    protected RemoteContentProtectionService createRemoteContentProtectionService(
+            @NonNull ComponentName componentName) {
+        return new RemoteContentProtectionService(
+                getContext(),
+                componentName,
+                UserHandle.getCallingUserId(),
+                isBindInstantServiceAllowed());
+    }
+
+    /** @hide */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    @NonNull
+    protected ContentCaptureManagerServiceStub getContentCaptureManagerServiceStub() {
+        return mContentCaptureManagerServiceStub;
+    }
+
     final class ContentCaptureManagerServiceStub extends IContentCaptureManager.Stub {
 
         @Override
@@ -896,6 +1104,19 @@
         public void setDefaultServiceEnabled(@UserIdInt int userId, boolean enabled) {
             ContentCaptureManagerService.this.setDefaultServiceEnabled(userId, enabled);
         }
+
+        @Override
+        public void onLoginDetected(@NonNull ParceledListSlice<ContentCaptureEvent> events) {
+            RemoteContentProtectionService service = createRemoteContentProtectionService();
+            if (service == null) {
+                return;
+            }
+            try {
+                service.onLoginDetected(events);
+            } catch (Exception ex) {
+                Slog.e(TAG, "Failed to call remote service", ex);
+            }
+        }
     }
 
     private final class LocalService extends ContentCaptureManagerInternal {
@@ -984,14 +1205,21 @@
         @GuardedBy("mGlobalWhitelistStateLock")
         public ContentCaptureOptions getOptions(@UserIdInt int userId,
                 @NonNull String packageName) {
-            boolean packageWhitelisted;
+            boolean isContentCaptureReceiverEnabled;
+            boolean isContentProtectionReceiverEnabled;
             ArraySet<ComponentName> whitelistedComponents = null;
+
             synchronized (mGlobalWhitelistStateLock) {
-                packageWhitelisted = isWhitelisted(userId, packageName);
-                if (!packageWhitelisted) {
-                    // Full package is not allowlisted: check individual components first
+                isContentCaptureReceiverEnabled =
+                        isContentCaptureReceiverEnabled(userId, packageName);
+                isContentProtectionReceiverEnabled =
+                        isContentProtectionReceiverEnabled(packageName);
+
+                if (!isContentCaptureReceiverEnabled) {
+                    // Full package is not allowlisted: check individual components next
                     whitelistedComponents = getWhitelistedComponents(userId, packageName);
-                    if (whitelistedComponents == null
+                    if (!isContentProtectionReceiverEnabled
+                            && whitelistedComponents == null
                             && packageName.equals(mServicePackages.get(userId))) {
                         // No components allowlisted either, but let it go because it's the
                         // service's own package
@@ -1010,7 +1238,9 @@
                 }
             }
 
-            if (!packageWhitelisted && whitelistedComponents == null) {
+            if (!isContentCaptureReceiverEnabled
+                    && !isContentProtectionReceiverEnabled
+                    && whitelistedComponents == null) {
                 // No can do!
                 if (verbose) {
                     Slog.v(TAG, "getOptionsForPackage(" + packageName + "): not whitelisted");
@@ -1019,11 +1249,19 @@
             }
 
             synchronized (mLock) {
-                final ContentCaptureOptions options = new ContentCaptureOptions(mDevCfgLoggingLevel,
-                        mDevCfgMaxBufferSize, mDevCfgIdleFlushingFrequencyMs,
-                        mDevCfgTextChangeFlushingFrequencyMs, mDevCfgLogHistorySize,
-                        mDevCfgDisableFlushForViewTreeAppearing,
-                        whitelistedComponents);
+                final ContentCaptureOptions options =
+                        new ContentCaptureOptions(
+                                mDevCfgLoggingLevel,
+                                mDevCfgMaxBufferSize,
+                                mDevCfgIdleFlushingFrequencyMs,
+                                mDevCfgTextChangeFlushingFrequencyMs,
+                                mDevCfgLogHistorySize,
+                                mDevCfgDisableFlushForViewTreeAppearing,
+                                isContentCaptureReceiverEnabled || whitelistedComponents != null,
+                                new ContentCaptureOptions.ContentProtectionOptions(
+                                        isContentProtectionReceiverEnabled,
+                                        mDevCfgContentProtectionBufferSize),
+                                whitelistedComponents);
                 if (verbose) Slog.v(TAG, "getOptionsForPackage(" + packageName + "): " + options);
                 return options;
             }
@@ -1042,6 +1280,36 @@
                 }
             }
         }
+
+        @Override // from GlobalWhitelistState
+        public boolean isWhitelisted(@UserIdInt int userId, @NonNull String packageName) {
+            return isContentCaptureReceiverEnabled(userId, packageName)
+                    || isContentProtectionReceiverEnabled(packageName);
+        }
+
+        @Override // from GlobalWhitelistState
+        public boolean isWhitelisted(@UserIdInt int userId, @NonNull ComponentName componentName) {
+            return super.isWhitelisted(userId, componentName)
+                    || isContentProtectionReceiverEnabled(componentName.getPackageName());
+        }
+
+        private boolean isContentCaptureReceiverEnabled(
+                @UserIdInt int userId, @NonNull String packageName) {
+            return super.isWhitelisted(userId, packageName);
+        }
+
+        private boolean isContentProtectionReceiverEnabled(@NonNull String packageName) {
+            if (mContentProtectionServiceComponentName == null
+                    || mContentProtectionBlocklistManager == null) {
+                return false;
+            }
+            synchronized (mLock) {
+                if (!mDevCfgEnableContentProtectionReceiver) {
+                    return false;
+                }
+            }
+            return mContentProtectionBlocklistManager.isAllowed(packageName);
+        }
     }
 
     private static class DataShareCallbackDelegate extends IDataShareCallback.Stub {
diff --git a/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionBlocklistManager.java b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionBlocklistManager.java
new file mode 100644
index 0000000..a0fd28b
--- /dev/null
+++ b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionBlocklistManager.java
@@ -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.server.contentprotection;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.PackageInfo;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Manages whether the content protection is enabled for an app using a blocklist.
+ *
+ * @hide
+ */
+public class ContentProtectionBlocklistManager {
+
+    private static final String TAG = "ContentProtectionBlocklistManager";
+
+    private static final String PACKAGE_NAME_BLOCKLIST_FILENAME =
+            "/product/etc/res/raw/content_protection/package_name_blocklist.txt";
+
+    @NonNull private final ContentProtectionPackageManager mContentProtectionPackageManager;
+
+    @Nullable private Set<String> mPackageNameBlocklist;
+
+    public ContentProtectionBlocklistManager(
+            @NonNull ContentProtectionPackageManager contentProtectionPackageManager) {
+        mContentProtectionPackageManager = contentProtectionPackageManager;
+    }
+
+    public boolean isAllowed(@NonNull String packageName) {
+        if (mPackageNameBlocklist == null) {
+            // List not loaded or failed to load, don't run on anything
+            return false;
+        }
+        if (mPackageNameBlocklist.contains(packageName)) {
+            return false;
+        }
+        PackageInfo packageInfo = mContentProtectionPackageManager.getPackageInfo(packageName);
+        if (packageInfo == null) {
+            return false;
+        }
+        if (!mContentProtectionPackageManager.hasRequestedInternetPermissions(packageInfo)) {
+            return false;
+        }
+        if (mContentProtectionPackageManager.isSystemApp(packageInfo)) {
+            return false;
+        }
+        if (mContentProtectionPackageManager.isUpdatedSystemApp(packageInfo)) {
+            return false;
+        }
+        return true;
+    }
+
+    public void updateBlocklist(int blocklistSize) {
+        Slog.i(TAG, "Blocklist size updating to: " + blocklistSize);
+        mPackageNameBlocklist = readPackageNameBlocklist(blocklistSize);
+    }
+
+    @Nullable
+    private Set<String> readPackageNameBlocklist(int blocklistSize) {
+        if (blocklistSize <= 0) {
+            // Explicitly requested an empty blocklist
+            return Collections.emptySet();
+        }
+        List<String> lines = readLinesFromRawFile(PACKAGE_NAME_BLOCKLIST_FILENAME);
+        if (lines == null) {
+            return null;
+        }
+        return lines.stream().limit(blocklistSize).collect(Collectors.toSet());
+    }
+
+    @VisibleForTesting
+    @Nullable
+    protected List<String> readLinesFromRawFile(@NonNull String filename) {
+        try (FileReader fileReader = new FileReader(filename);
+                BufferedReader bufferedReader = new BufferedReader(fileReader)) {
+            return bufferedReader
+                    .lines()
+                    .map(line -> line.trim())
+                    .filter(line -> !line.isBlank())
+                    .collect(Collectors.toList());
+        } catch (Exception ex) {
+            Slog.e(TAG, "Failed to read: " + filename, ex);
+            return null;
+        }
+    }
+}
diff --git a/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionPackageManager.java b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionPackageManager.java
new file mode 100644
index 0000000..4ebac07
--- /dev/null
+++ b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionPackageManager.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.contentprotection;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManager.PackageInfoFlags;
+import android.util.Slog;
+
+import java.util.Arrays;
+
+/**
+ * Basic package manager for content protection using content capture.
+ *
+ * @hide
+ */
+public class ContentProtectionPackageManager {
+
+    private static final String TAG = "ContentProtectionPackageManager";
+
+    private static final PackageInfoFlags PACKAGE_INFO_FLAGS =
+            PackageInfoFlags.of(PackageManager.GET_PERMISSIONS);
+
+    @NonNull private final PackageManager mPackageManager;
+
+    public ContentProtectionPackageManager(@NonNull Context context) {
+        mPackageManager = context.getPackageManager();
+    }
+
+    @Nullable
+    public PackageInfo getPackageInfo(@NonNull String packageName) {
+        try {
+            return mPackageManager.getPackageInfo(packageName, PACKAGE_INFO_FLAGS);
+        } catch (NameNotFoundException ex) {
+            Slog.w(TAG, "Package info not found for: " + packageName, ex);
+            return null;
+        }
+    }
+
+    public boolean isSystemApp(@NonNull PackageInfo packageInfo) {
+        return packageInfo.applicationInfo != null && isSystemApp(packageInfo.applicationInfo);
+    }
+
+    private boolean isSystemApp(@NonNull ApplicationInfo applicationInfo) {
+        return (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+    }
+
+    public boolean isUpdatedSystemApp(@NonNull PackageInfo packageInfo) {
+        return packageInfo.applicationInfo != null
+                && isUpdatedSystemApp(packageInfo.applicationInfo);
+    }
+
+    private boolean isUpdatedSystemApp(@NonNull ApplicationInfo applicationInfo) {
+        return (applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
+    }
+
+    public boolean hasRequestedInternetPermissions(@NonNull PackageInfo packageInfo) {
+        return packageInfo.requestedPermissions != null
+                && Arrays.asList(packageInfo.requestedPermissions)
+                        .contains(Manifest.permission.INTERNET);
+    }
+}
diff --git a/services/contentcapture/java/com/android/server/contentprotection/RemoteContentProtectionService.java b/services/contentcapture/java/com/android/server/contentprotection/RemoteContentProtectionService.java
new file mode 100644
index 0000000..dd5545d
--- /dev/null
+++ b/services/contentcapture/java/com/android/server/contentprotection/RemoteContentProtectionService.java
@@ -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.server.contentprotection;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ParceledListSlice;
+import android.service.contentcapture.ContentCaptureService;
+import android.service.contentcapture.IContentProtectionService;
+import android.util.Slog;
+import android.view.contentcapture.ContentCaptureEvent;
+
+import com.android.internal.infra.ServiceConnector;
+
+import java.time.Duration;
+
+/**
+ * Connector for the remote content protection service.
+ *
+ * @hide
+ */
+public class RemoteContentProtectionService
+        extends ServiceConnector.Impl<IContentProtectionService> {
+
+    private static final String TAG = RemoteContentProtectionService.class.getSimpleName();
+
+    private static final Duration AUTO_DISCONNECT_TIMEOUT = Duration.ofSeconds(3);
+
+    @NonNull private final ComponentName mComponentName;
+
+    public RemoteContentProtectionService(
+            @NonNull Context context,
+            @NonNull ComponentName componentName,
+            int userId,
+            boolean bindAllowInstant) {
+        super(
+                context,
+                new Intent(ContentCaptureService.PROTECTION_SERVICE_INTERFACE)
+                        .setComponent(componentName),
+                bindAllowInstant ? Context.BIND_ALLOW_INSTANT : 0,
+                userId,
+                IContentProtectionService.Stub::asInterface);
+        mComponentName = componentName;
+    }
+
+    @Override // from ServiceConnector.Impl
+    protected long getAutoDisconnectTimeoutMs() {
+        return AUTO_DISCONNECT_TIMEOUT.toMillis();
+    }
+
+    @Override // from ServiceConnector.Impl
+    protected void onServiceConnectionStatusChanged(
+            @NonNull IContentProtectionService service, boolean isConnected) {
+        Slog.i(
+                TAG,
+                "Connection status for: "
+                        + mComponentName
+                        + " changed to: "
+                        + (isConnected ? "connected" : "disconnected"));
+    }
+
+    public void onLoginDetected(@NonNull ParceledListSlice<ContentCaptureEvent> events) {
+        run(service -> service.onLoginDetected(events));
+    }
+}
diff --git a/services/core/java/com/android/server/WallpaperUpdateReceiver.java b/services/core/java/com/android/server/WallpaperUpdateReceiver.java
index 9917892..2812233 100644
--- a/services/core/java/com/android/server/WallpaperUpdateReceiver.java
+++ b/services/core/java/com/android/server/WallpaperUpdateReceiver.java
@@ -88,7 +88,7 @@
         } else {
             //live wallpaper
             ComponentName currCN = info.getComponent();
-            ComponentName defaultCN = WallpaperManager.getDefaultWallpaperComponent(context);
+            ComponentName defaultCN = WallpaperManager.getCmfDefaultWallpaperComponent(context);
             if (!currCN.equals(defaultCN)) {
                 return true;
             }
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 8c31209..de6522e 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -1027,7 +1027,7 @@
     private static final String KEY_ENABLE_WAIT_FOR_FINISH_ATTACH_APPLICATION =
             "enable_wait_for_finish_attach_application";
 
-    private static final boolean DEFAULT_ENABLE_WAIT_FOR_FINISH_ATTACH_APPLICATION = false;
+    private static final boolean DEFAULT_ENABLE_WAIT_FOR_FINISH_ATTACH_APPLICATION = true;
 
     /** @see #KEY_ENABLE_WAIT_FOR_FINISH_ATTACH_APPLICATION */
     public volatile boolean mEnableWaitForFinishAttachApplication =
@@ -1347,7 +1347,7 @@
         updateForegroundServiceStartsLoggingEnabled();
     }
 
-    private void loadDeviceConfigConstants() {
+    void loadDeviceConfigConstants() {
         mOnDeviceConfigChangedListener.onPropertiesChanged(
                 DeviceConfig.getProperties(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER));
         mOnDeviceConfigChangedForComponentAliasListener.onPropertiesChanged(
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index eb7d5c3..9d8c59a6 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4579,14 +4579,8 @@
         EventLogTags.writeAmProcBound(app.userId, pid, app.processName);
 
         synchronized (mProcLock) {
-            app.mState.setCurAdj(ProcessList.INVALID_ADJ);
-            app.mState.setSetAdj(ProcessList.INVALID_ADJ);
-            app.mState.setVerifiedAdj(ProcessList.INVALID_ADJ);
-            mOomAdjuster.setAttachingSchedGroupLSP(app);
-            app.mState.setForcingToImportant(null);
+            mOomAdjuster.setAttachingProcessStatesLSP(app);
             clearProcessForegroundLocked(app);
-            app.mState.setHasShownUi(false);
-            app.mState.setCached(false);
             app.setDebugging(false);
             app.setKilledByAm(false);
             app.setKilled(false);
@@ -4753,8 +4747,14 @@
                 app.makeActive(thread, mProcessStats);
                 checkTime(startTime, "attachApplicationLocked: immediately after bindApplication");
             }
+            app.setPendingFinishAttach(true);
+
             updateLruProcessLocked(app, false, null);
             checkTime(startTime, "attachApplicationLocked: after updateLruProcessLocked");
+
+            updateOomAdjLocked(app, OOM_ADJ_REASON_PROCESS_BEGIN);
+            checkTime(startTime, "attachApplicationLocked: after updateOomAdjLocked");
+
             final long now = SystemClock.uptimeMillis();
             synchronized (mAppProfiler.mProfilerLock) {
                 app.mProfile.setLastRequestedGc(now);
@@ -4770,8 +4770,6 @@
 
             if (!mConstants.mEnableWaitForFinishAttachApplication) {
                 finishAttachApplicationInner(startSeq, callingUid, pid);
-            } else {
-                app.setPendingFinishAttach(true);
             }
         } catch (Exception e) {
             // We need kill the process group here. (b/148588589)
@@ -19610,7 +19608,7 @@
             for (Display display : allDisplays) {
                 int displayId = display.getDisplayId();
                 // TODO(b/247592632): check other properties like isSecure or proper display type
-                if (display.isValid()
+                if (display.isValid() && ((display.getFlags() & Display.FLAG_PRIVATE) == 0)
                         && (allowOnDefaultDisplay || displayId != Display.DEFAULT_DISPLAY)) {
                     displayIds[numberValidDisplays++] = displayId;
                 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 569f55f..6801d27 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -3699,14 +3699,11 @@
             if (foregroundActivities) {
                 try {
                     int prcState = mIam.getUidProcessState(uid, "android");
-                    ProcessRecord topApp = mInternal.getTopApp();
-                    if (topApp == null) {
-                        mPw.println("No top app found");
+
+                    if (prcState == ProcessStateEnum.TOP) {
+                        mPw.println("New foreground process: " + pid);
                     } else {
-                        int topPid = topApp.getPid();
-                        if (prcState == ProcessStateEnum.TOP && topPid == pid) {
-                            mPw.println("New foreground process: " + pid);
-                        }
+                        mPw.println("No top app found");
                     }
                     mPw.flush();
                 } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index f21ad22..e51fc0a 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -1293,12 +1293,19 @@
         for (int i = numLru - 1; i >= 0; i--) {
             ProcessRecord app = lruList.get(i);
             final ProcessStateRecord state = app.mState;
-            if (!app.isKilledByAm() && app.getThread() != null && !app.isPendingFinishAttach()) {
+            if (!app.isKilledByAm() && app.getThread() != null) {
                 // We don't need to apply the update for the process which didn't get computed
                 if (state.getCompletedAdjSeq() == mAdjSeq) {
                     applyOomAdjLSP(app, true, now, nowElapsed, oomAdjReason);
                 }
 
+                if (app.isPendingFinishAttach()) {
+                    // Avoid trimming processes that are still initializing. If they aren't
+                    // hosting any components yet because they may be unfairly killed.
+                    // We however apply the oom scores set at #setAttachingProcessStatesLSP.
+                    continue;
+                }
+
                 final ProcessServiceRecord psr = app.mServices;
                 // Count the number of process types.
                 switch (state.getCurProcState()) {
@@ -2806,6 +2813,18 @@
             capability &= ~PROCESS_CAPABILITY_BFSL;
         }
 
+
+        if (app.isPendingFinishAttach()) {
+            // If the app is still starting up. We reset the computations to the
+            // hardcoded values in setAttachingProcessStatesLSP. This ensures that the app keeps
+            // hard-coded default 'startup' oom scores while starting up. When it finishes startup,
+            // we'll recompute oom scores based on it's actual hosted compoenents.
+            setAttachingProcessStatesLSP(app);
+            state.setAdjSeq(mAdjSeq);
+            state.setCompletedAdjSeq(state.getAdjSeq());
+            return false;
+        }
+
         // Do final modification to adj.  Everything we do between here and applying
         // the final setAdj must be done in this function, because we will also use
         // it when computing the final cached adj later.  Note that we don't need to
@@ -3243,8 +3262,11 @@
     }
 
     @GuardedBy({"mService", "mProcLock"})
-    void setAttachingSchedGroupLSP(ProcessRecord app) {
+    void setAttachingProcessStatesLSP(ProcessRecord app) {
         int initialSchedGroup = SCHED_GROUP_DEFAULT;
+        int initialProcState = PROCESS_STATE_CACHED_EMPTY;
+        int initialCapability =  PROCESS_CAPABILITY_NONE;
+        boolean initialCached = true;
         final ProcessStateRecord state = app.mState;
         // If the process has been marked as foreground, it is starting as the top app (with
         // Zygote#START_AS_TOP_APP_ARG), so boost the thread priority of its default UI thread.
@@ -3260,13 +3282,24 @@
                     setThreadPriority(app.getPid(), THREAD_PRIORITY_TOP_APP_BOOST);
                 }
                 initialSchedGroup = SCHED_GROUP_TOP_APP;
+                initialProcState = PROCESS_STATE_TOP;
+                initialCapability = PROCESS_CAPABILITY_ALL;
+                initialCached = false;
             } catch (Exception e) {
                 Slog.w(TAG, "Failed to pre-set top priority to " + app + " " + e);
             }
         }
 
-        state.setSetSchedGroup(initialSchedGroup);
         state.setCurrentSchedulingGroup(initialSchedGroup);
+        state.setCurProcState(initialProcState);
+        state.setCurRawProcState(initialProcState);
+        state.setCurCapability(initialCapability);
+        state.setCached(initialCached);
+
+        state.setCurAdj(ProcessList.FOREGROUND_APP_ADJ);
+        state.setCurRawAdj(ProcessList.FOREGROUND_APP_ADJ);
+        state.setForcingToImportant(null);
+        state.setHasShownUi(false);
     }
 
     // ONLY used for unit testing in OomAdjusterTests.java
diff --git a/services/core/java/com/android/server/am/PendingIntentController.java b/services/core/java/com/android/server/am/PendingIntentController.java
index 7ee96aa..a20623c 100644
--- a/services/core/java/com/android/server/am/PendingIntentController.java
+++ b/services/core/java/com/android/server/am/PendingIntentController.java
@@ -26,6 +26,7 @@
 import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.ActivityManagerInternal;
+import android.app.ActivityOptions;
 import android.app.AppGlobals;
 import android.app.PendingIntent;
 import android.app.PendingIntentStats;
@@ -126,6 +127,18 @@
                 }
             }
             Bundle.setDefusable(bOptions, true);
+            ActivityOptions opts = ActivityOptions.fromBundle(bOptions);
+            if (opts != null && opts.getPendingIntentBackgroundActivityStartMode()
+                    != ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
+                Slog.wtf(TAG, "Resetting option setPendingIntentBackgroundActivityStartMode("
+                        + opts.getPendingIntentBackgroundActivityStartMode()
+                        + ") to SYSTEM_DEFINED from the options provided by the pending "
+                        + "intent creator ("
+                        + packageName
+                        + ") because this option is meant for the pending intent sender");
+                opts.setPendingIntentBackgroundActivityStartMode(
+                        ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
+            }
 
             final boolean noCreate = (flags & PendingIntent.FLAG_NO_CREATE) != 0;
             final boolean cancelCurrent = (flags & PendingIntent.FLAG_CANCEL_CURRENT) != 0;
@@ -135,7 +148,7 @@
 
             PendingIntentRecord.Key key = new PendingIntentRecord.Key(type, packageName, featureId,
                     token, resultWho, requestCode, intents, resolvedTypes, flags,
-                    SafeActivityOptions.fromBundle(bOptions), userId);
+                    new SafeActivityOptions(opts), userId);
             WeakReference<PendingIntentRecord> ref;
             ref = mIntentSenderRecords.get(key);
             PendingIntentRecord rec = ref != null ? ref.get() : null;
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 202d407..a0e76f1 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -457,6 +457,20 @@
             // can specify a consistent launch mode even if the PendingIntent is immutable
             final ActivityOptions opts = ActivityOptions.fromBundle(options);
             if (opts != null) {
+                if (opts.getPendingIntentCreatorBackgroundActivityStartMode()
+                        != ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
+                    Slog.wtf(TAG,
+                            "Resetting option "
+                                    + "setPendingIntentCreatorBackgroundActivityStartMode("
+                                    + opts.getPendingIntentCreatorBackgroundActivityStartMode()
+                                    + ") to SYSTEM_DEFINED from the options provided by the "
+                                    + "pending intent sender ("
+                                    + key.packageName
+                                    + ") because this option is meant for the pending intent "
+                                    + "creator");
+                    opts.setPendingIntentCreatorBackgroundActivityStartMode(
+                            ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
+                }
                 finalIntent.addFlags(opts.getPendingIntentLaunchFlags());
             }
 
diff --git a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java
index e498384..753fdae 100644
--- a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java
+++ b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java
@@ -51,11 +51,11 @@
 import com.android.internal.annotations.CompositeRWLock;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.modules.expresslog.Counter;
 import com.android.internal.os.ProcessCpuTracker;
 import com.android.internal.os.TimeoutRecord;
 import com.android.internal.os.anr.AnrLatencyTracker;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.modules.expresslog.Counter;
 import com.android.server.ResourcePressureUtil;
 import com.android.server.criticalevents.CriticalEventLog;
 import com.android.server.stats.pull.ProcfsMemoryUtil.MemorySnapshot;
@@ -456,6 +456,11 @@
         String currentPsiState = ResourcePressureUtil.currentPsiState();
         latencyTracker.currentPsiStateReturned();
         report.append(currentPsiState);
+        // The 'processCpuTracker' variable is a shared resource that might be initialized and
+        // updated in a different thread. In order to prevent thread visibility issues, which
+        // can occur when one thread does not immediately see the changes made to
+        // 'processCpuTracker' by another thread, it is necessary to use synchronization whenever
+        // 'processCpuTracker' is accessed or modified.
         ProcessCpuTracker processCpuTracker = new ProcessCpuTracker(true);
 
         // We push the native pids collection task to the helper thread through
@@ -517,12 +522,16 @@
             }
             mService.updateCpuStatsNow();
             mService.mAppProfiler.printCurrentCpuState(report, anrTime);
-            info.append(processCpuTracker.printCurrentLoad());
+            synchronized (processCpuTracker) {
+                info.append(processCpuTracker.printCurrentLoad());
+            }
             info.append(report);
         }
         report.append(tracesFileException.getBuffer());
 
-        info.append(processCpuTracker.printCurrentState(anrTime));
+        synchronized (processCpuTracker) {
+            info.append(processCpuTracker.printCurrentState(anrTime));
+        }
 
         Slog.e(TAG, info.toString());
         if (tracesFile == null) {
diff --git a/services/core/java/com/android/server/am/StackTracesDumpHelper.java b/services/core/java/com/android/server/am/StackTracesDumpHelper.java
index cf69b53..ba0fd17 100644
--- a/services/core/java/com/android/server/am/StackTracesDumpHelper.java
+++ b/services/core/java/com/android/server/am/StackTracesDumpHelper.java
@@ -464,28 +464,31 @@
             latencyTracker.processCpuTrackerMethodsCalled();
         }
         ArrayList<Integer> extraPids = new ArrayList<>();
-        processCpuTracker.init();
+        synchronized (processCpuTracker) {
+            processCpuTracker.init();
+        }
         try {
             Thread.sleep(200);
         } catch (InterruptedException ignored) {
         }
 
-        processCpuTracker.update();
+        synchronized (processCpuTracker) {
+            processCpuTracker.update();
+            // We'll take the stack crawls of just the top apps using CPU.
+            final int workingStatsNumber = processCpuTracker.countWorkingStats();
+            for (int i = 0; i < workingStatsNumber && extraPids.size() < 2; i++) {
+                ProcessCpuTracker.Stats stats = processCpuTracker.getWorkingStats(i);
+                if (lastPids.indexOfKey(stats.pid) >= 0) {
+                    if (DEBUG_ANR) {
+                        Slog.d(TAG, "Collecting stacks for extra pid " + stats.pid);
+                    }
 
-        // We'll take the stack crawls of just the top apps using CPU.
-        final int workingStatsNumber = processCpuTracker.countWorkingStats();
-        for (int i = 0; i < workingStatsNumber && extraPids.size() < 2; i++) {
-            ProcessCpuTracker.Stats stats = processCpuTracker.getWorkingStats(i);
-            if (lastPids.indexOfKey(stats.pid) >= 0) {
-                if (DEBUG_ANR) {
-                    Slog.d(TAG, "Collecting stacks for extra pid " + stats.pid);
+                    extraPids.add(stats.pid);
+                } else {
+                    Slog.i(TAG,
+                            "Skipping next CPU consuming process, not a java proc: "
+                            + stats.pid);
                 }
-
-                extraPids.add(stats.pid);
-            } else {
-                Slog.i(TAG,
-                        "Skipping next CPU consuming process, not a java proc: "
-                        + stats.pid);
             }
         }
         if (latencyTracker != null) {
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 76a994e..99c2f8a 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -1972,7 +1972,7 @@
     }
 
     private void dismissUserSwitchDialog(Runnable onDismissed) {
-        mInjector.dismissUserSwitchingDialog(onDismissed);
+        mUiHandler.post(() -> mInjector.dismissUserSwitchingDialog(onDismissed));
     }
 
     private void showUserSwitchDialog(Pair<UserInfo, UserInfo> fromToUserPair) {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 84fe12e..e4dd4a4 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -30,6 +30,8 @@
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
+import android.media.AudioPlaybackConfiguration;
+import android.media.AudioRecordingConfiguration;
 import android.media.AudioRoutesInfo;
 import android.media.AudioSystem;
 import android.media.BluetoothProfileConnectionInfo;
@@ -289,37 +291,38 @@
      * @param on
      * @param eventSource for logging purposes
      */
-    /*package*/ void setSpeakerphoneOn(IBinder cb, int pid, boolean on, String eventSource) {
+    /*package*/ void setSpeakerphoneOn(
+            IBinder cb, int uid, boolean on, boolean isPrivileged, String eventSource) {
 
         if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "setSpeakerphoneOn, on: " + on + " pid: " + pid);
+            Log.v(TAG, "setSpeakerphoneOn, on: " + on + " uid: " + uid);
         }
         postSetCommunicationDeviceForClient(new CommunicationDeviceInfo(
-                cb, pid, new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""),
-                on, BtHelper.SCO_MODE_UNDEFINED, eventSource, false));
+                cb, uid, new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""),
+                on, BtHelper.SCO_MODE_UNDEFINED, eventSource, false, isPrivileged));
     }
 
     /**
      * Select device for use for communication use cases.
      * @param cb Client binder for death detection
-     * @param pid Client pid
+     * @param uid Client uid
      * @param device Device selected or null to unselect.
      * @param eventSource for logging purposes
      */
 
     private static final long SET_COMMUNICATION_DEVICE_TIMEOUT_MS = 3000;
 
-    /*package*/ boolean setCommunicationDevice(
-            IBinder cb, int pid, AudioDeviceInfo device, String eventSource) {
+    /*package*/ boolean setCommunicationDevice(IBinder cb, int uid, AudioDeviceInfo device,
+                                               boolean isPrivileged, String eventSource) {
 
         if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "setCommunicationDevice, device: " + device + ", pid: " + pid);
+            Log.v(TAG, "setCommunicationDevice, device: " + device + ", uid: " + uid);
         }
 
         AudioDeviceAttributes deviceAttr =
                 (device != null) ? new AudioDeviceAttributes(device) : null;
-        CommunicationDeviceInfo deviceInfo = new CommunicationDeviceInfo(cb, pid, deviceAttr,
-                device != null, BtHelper.SCO_MODE_UNDEFINED, eventSource, true);
+        CommunicationDeviceInfo deviceInfo = new CommunicationDeviceInfo(cb, uid, deviceAttr,
+                device != null, BtHelper.SCO_MODE_UNDEFINED, eventSource, true, isPrivileged);
         postSetCommunicationDeviceForClient(deviceInfo);
         boolean status;
         synchronized (deviceInfo) {
@@ -353,7 +356,7 @@
             Log.v(TAG, "onSetCommunicationDeviceForClient: " + deviceInfo);
         }
         if (!deviceInfo.mOn) {
-            CommunicationRouteClient client = getCommunicationRouteClientForPid(deviceInfo.mPid);
+            CommunicationRouteClient client = getCommunicationRouteClientForUid(deviceInfo.mUid);
             if (client == null || (deviceInfo.mDevice != null
                     && !deviceInfo.mDevice.equals(client.getDevice()))) {
                 return false;
@@ -361,22 +364,23 @@
         }
 
         AudioDeviceAttributes device = deviceInfo.mOn ? deviceInfo.mDevice : null;
-        setCommunicationRouteForClient(deviceInfo.mCb, deviceInfo.mPid, device,
-                deviceInfo.mScoAudioMode, deviceInfo.mEventSource);
+        setCommunicationRouteForClient(deviceInfo.mCb, deviceInfo.mUid, device,
+                deviceInfo.mScoAudioMode, deviceInfo.mIsPrivileged, deviceInfo.mEventSource);
         return true;
     }
 
     @GuardedBy("mDeviceStateLock")
     /*package*/ void setCommunicationRouteForClient(
-                            IBinder cb, int pid, AudioDeviceAttributes device,
-                            int scoAudioMode, String eventSource) {
+                            IBinder cb, int uid, AudioDeviceAttributes device,
+                            int scoAudioMode, boolean isPrivileged, String eventSource) {
 
         if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "setCommunicationRouteForClient: device: " + device);
+            Log.v(TAG, "setCommunicationRouteForClient: device: " + device
+                    + ", eventSource: " + eventSource);
         }
         AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                                        "setCommunicationRouteForClient for pid: " + pid
-                                        + " device: " + device
+                                        "setCommunicationRouteForClient for uid: " + uid
+                                        + " device: " + device + " isPrivileged: " + isPrivileged
                                         + " from API: " + eventSource)).printLog(TAG));
 
         final boolean wasBtScoRequested = isBluetoothScoRequested();
@@ -385,16 +389,18 @@
 
         // Save previous client route in case of failure to start BT SCO audio
         AudioDeviceAttributes prevClientDevice = null;
-        client = getCommunicationRouteClientForPid(pid);
+        boolean prevPrivileged = false;
+        client = getCommunicationRouteClientForUid(uid);
         if (client != null) {
             prevClientDevice = client.getDevice();
+            prevPrivileged = client.isPrivileged();
         }
 
         if (device != null) {
-            client = addCommunicationRouteClient(cb, pid, device);
+            client = addCommunicationRouteClient(cb, uid, device, isPrivileged);
             if (client == null) {
-                Log.w(TAG, "setCommunicationRouteForClient: could not add client for pid: "
-                        + pid + " and device: " + device);
+                Log.w(TAG, "setCommunicationRouteForClient: could not add client for uid: "
+                        + uid + " and device: " + device);
             }
         } else {
             client = removeCommunicationRouteClient(cb, true);
@@ -406,11 +412,11 @@
         boolean isBtScoRequested = isBluetoothScoRequested();
         if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive())) {
             if (!mBtHelper.startBluetoothSco(scoAudioMode, eventSource)) {
-                Log.w(TAG, "setCommunicationRouteForClient: failure to start BT SCO for pid: "
-                        + pid);
+                Log.w(TAG, "setCommunicationRouteForClient: failure to start BT SCO for uid: "
+                        + uid);
                 // clean up or restore previous client selection
                 if (prevClientDevice != null) {
-                    addCommunicationRouteClient(cb, pid, prevClientDevice);
+                    addCommunicationRouteClient(cb, uid, prevClientDevice, prevPrivileged);
                 } else {
                     removeCommunicationRouteClient(cb, true);
                 }
@@ -447,11 +453,12 @@
     @GuardedBy("mDeviceStateLock")
     private CommunicationRouteClient topCommunicationRouteClient() {
         for (CommunicationRouteClient crc : mCommunicationRouteClients) {
-            if (crc.getPid() == mAudioModeOwner.mPid) {
+            if (crc.getUid() == mAudioModeOwner.mUid) {
                 return crc;
             }
         }
-        if (!mCommunicationRouteClients.isEmpty() && mAudioModeOwner.mPid == 0) {
+        if (!mCommunicationRouteClients.isEmpty() && mAudioModeOwner.mPid == 0
+                && mCommunicationRouteClients.get(0).isActive()) {
             return mCommunicationRouteClients.get(0);
         }
         return null;
@@ -491,14 +498,48 @@
     };
 
     /*package */ static boolean isValidCommunicationDevice(AudioDeviceInfo device) {
+        return isValidCommunicationDeviceType(device.getType());
+    }
+
+    private static boolean isValidCommunicationDeviceType(int deviceType) {
         for (int type : VALID_COMMUNICATION_DEVICE_TYPES) {
-            if (device.getType() == type) {
+            if (deviceType == type) {
                 return true;
             }
         }
         return false;
     }
 
+    /*package */
+    void postCheckCommunicationDeviceRemoval(@NonNull AudioDeviceAttributes device) {
+        if (!isValidCommunicationDeviceType(
+                AudioDeviceInfo.convertInternalDeviceToDeviceType(device.getInternalType()))) {
+            return;
+        }
+        sendLMsgNoDelay(MSG_L_CHECK_COMMUNICATION_DEVICE_REMOVAL, SENDMSG_QUEUE, device);
+    }
+
+    @GuardedBy("mDeviceStateLock")
+    void onCheckCommunicationDeviceRemoval(@NonNull AudioDeviceAttributes device) {
+        if (AudioService.DEBUG_COMM_RTE) {
+            Log.v(TAG, "onCheckCommunicationDeviceRemoval device: " + device.toString());
+        }
+        for (CommunicationRouteClient crc : mCommunicationRouteClients) {
+            if (device.equals(crc.getDevice())) {
+                if (AudioService.DEBUG_COMM_RTE) {
+                    Log.v(TAG, "onCheckCommunicationDeviceRemoval removing client: "
+                            + crc.toString());
+                }
+                // Cancelling the route for this client will remove it from the stack and update
+                // the communication route.
+                CommunicationDeviceInfo deviceInfo = new CommunicationDeviceInfo(
+                        crc.getBinder(), crc.getUid(), device, false,
+                        BtHelper.SCO_MODE_UNDEFINED, "onCheckCommunicationDeviceRemoval",
+                        false, crc.isPrivileged());
+                postSetCommunicationDeviceForClient(deviceInfo);
+            }
+        }
+    }
     /* package */ static List<AudioDeviceInfo> getAvailableCommunicationDevices() {
         ArrayList<AudioDeviceInfo> commDevices = new ArrayList<>();
         AudioDeviceInfo[] allDevices =
@@ -1107,26 +1148,26 @@
         sendLMsgNoDelay(MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE, SENDMSG_QUEUE, info);
     }
 
-    /*package*/ void startBluetoothScoForClient(IBinder cb, int pid, int scoAudioMode,
-                @NonNull String eventSource) {
+    /*package*/ void startBluetoothScoForClient(IBinder cb, int uid, int scoAudioMode,
+                                                boolean isPrivileged, @NonNull String eventSource) {
 
         if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "startBluetoothScoForClient, pid: " + pid);
+            Log.v(TAG, "startBluetoothScoForClient, uid: " + uid);
         }
         postSetCommunicationDeviceForClient(new CommunicationDeviceInfo(
-                cb, pid, new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, ""),
-                true, scoAudioMode, eventSource, false));
+                cb, uid, new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, ""),
+                true, scoAudioMode, eventSource, false, isPrivileged));
     }
 
     /*package*/ void stopBluetoothScoForClient(
-                        IBinder cb, int pid, @NonNull String eventSource) {
+                        IBinder cb, int uid, boolean isPrivileged, @NonNull String eventSource) {
 
         if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "stopBluetoothScoForClient, pid: " + pid);
+            Log.v(TAG, "stopBluetoothScoForClient, uid: " + uid);
         }
         postSetCommunicationDeviceForClient(new CommunicationDeviceInfo(
-                cb, pid, new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, ""),
-                false, BtHelper.SCO_MODE_UNDEFINED, eventSource, false));
+                cb, uid, new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, ""),
+                false, BtHelper.SCO_MODE_UNDEFINED, eventSource, false, isPrivileged));
     }
 
     /*package*/ int setPreferredDevicesForStrategySync(int strategy,
@@ -1367,22 +1408,24 @@
 
     /*package*/ static final class CommunicationDeviceInfo {
         final @NonNull IBinder mCb; // Identifies the requesting client for death handler
-        final int mPid; // Requester process ID
+        final int mUid; // Requester UID
         final @Nullable AudioDeviceAttributes mDevice; // Device being set or reset.
         final boolean mOn; // true if setting, false if resetting
         final int mScoAudioMode; // only used for SCO: requested audio mode
+        final boolean mIsPrivileged; // true if the client app has MODIFY_PHONE_STATE permission
         final @NonNull String mEventSource; // caller identifier for logging
         boolean mWaitForStatus; // true if the caller waits for a completion status (API dependent)
         boolean mStatus = false; // completion status only used if mWaitForStatus is true
 
-        CommunicationDeviceInfo(@NonNull IBinder cb, int pid,
+        CommunicationDeviceInfo(@NonNull IBinder cb, int uid,
                 @Nullable AudioDeviceAttributes device, boolean on, int scoAudioMode,
-                @NonNull String eventSource, boolean waitForStatus) {
+                @NonNull String eventSource, boolean waitForStatus, boolean isPrivileged) {
             mCb = cb;
-            mPid = pid;
+            mUid = uid;
             mDevice = device;
             mOn = on;
             mScoAudioMode = scoAudioMode;
+            mIsPrivileged = isPrivileged;
             mEventSource = eventSource;
             mWaitForStatus = waitForStatus;
         }
@@ -1401,16 +1444,17 @@
             }
 
             return mCb.equals(((CommunicationDeviceInfo) o).mCb)
-                    && mPid == ((CommunicationDeviceInfo) o).mPid;
+                    && mUid == ((CommunicationDeviceInfo) o).mUid;
         }
 
         @Override
         public String toString() {
             return "CommunicationDeviceInfo mCb=" + mCb.toString()
-                    + " mPid=" + mPid
+                    + " mUid=" + mUid
                     + " mDevice=[" + (mDevice != null ? mDevice.toString() : "null") + "]"
                     + " mOn=" + mOn
                     + " mScoAudioMode=" + mScoAudioMode
+                    + " mIsPrivileged=" + mIsPrivileged
                     + " mEventSource=" + mEventSource
                     + " mWaitForStatus=" + mWaitForStatus
                     + " mStatus=" + mStatus;
@@ -1440,7 +1484,7 @@
         }
     }
 
-    /*package*/ boolean handleDeviceConnection(AudioDeviceAttributes attributes,
+    /*package*/ boolean handleDeviceConnection(@NonNull AudioDeviceAttributes attributes,
                                 boolean connect, @Nullable BluetoothDevice btDevice) {
         synchronized (mDeviceStateLock) {
             return mDeviceInventory.handleDeviceConnection(
@@ -1507,8 +1551,7 @@
 
         pw.println("\n" + prefix + "Communication route clients:");
         mCommunicationRouteClients.forEach((cl) -> {
-            pw.println("  " + prefix + "pid: " + cl.getPid() + " device: "
-                        + cl.getDevice() + " cb: " + cl.getBinder()); });
+            pw.println("  " + prefix + cl.toString()); });
 
         pw.println("\n" + prefix + "Computed Preferred communication device: "
                 +  preferredCommunicationDevice());
@@ -1850,6 +1893,15 @@
                     final BluetoothDevice btDevice = (BluetoothDevice) msg.obj;
                     BtHelper.onNotifyPreferredAudioProfileApplied(btDevice);
                 } break;
+
+                case MSG_L_CHECK_COMMUNICATION_DEVICE_REMOVAL: {
+                    synchronized (mSetModeLock) {
+                        synchronized (mDeviceStateLock) {
+                            onCheckCommunicationDeviceRemoval((AudioDeviceAttributes) msg.obj);
+                        }
+                    }
+                } break;
+
                 default:
                     Log.wtf(TAG, "Invalid message " + msg.what);
             }
@@ -1926,6 +1978,7 @@
     private static final int MSG_IL_BTLEAUDIO_TIMEOUT = 49;
 
     private static final int MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED = 52;
+    private static final int MSG_L_CHECK_COMMUNICATION_DEVICE_REMOVAL = 53;
 
     private static boolean isMessageHandledUnderWakelock(int msgId) {
         switch(msgId) {
@@ -2101,13 +2154,20 @@
 
     private class CommunicationRouteClient implements IBinder.DeathRecipient {
         private final IBinder mCb;
-        private final int mPid;
+        private final int mUid;
+        private final boolean mIsPrivileged;
         private AudioDeviceAttributes mDevice;
+        private boolean mPlaybackActive;
+        private boolean mRecordingActive;
 
-        CommunicationRouteClient(IBinder cb, int pid, AudioDeviceAttributes device) {
+        CommunicationRouteClient(IBinder cb, int uid, AudioDeviceAttributes device,
+                                 boolean isPrivileged) {
             mCb = cb;
-            mPid = pid;
+            mUid = uid;
             mDevice = device;
+            mIsPrivileged = isPrivileged;
+            mPlaybackActive = mAudioService.isPlaybackActiveForUid(uid);
+            mRecordingActive = mAudioService.isRecordingActiveForUid(uid);
         }
 
         public boolean registerDeathRecipient() {
@@ -2138,13 +2198,38 @@
             return mCb;
         }
 
-        int getPid() {
-            return mPid;
+        int getUid() {
+            return mUid;
+        }
+
+        boolean isPrivileged() {
+            return mIsPrivileged;
         }
 
         AudioDeviceAttributes getDevice() {
             return mDevice;
         }
+
+        public void setPlaybackActive(boolean active) {
+            mPlaybackActive = active;
+        }
+
+        public void setRecordingActive(boolean active) {
+            mRecordingActive = active;
+        }
+
+        public boolean isActive() {
+            return mIsPrivileged || mRecordingActive || mPlaybackActive;
+        }
+
+        @Override
+        public String toString() {
+            return "[CommunicationRouteClient: mUid: " + mUid
+                    + " mDevice: " + mDevice.toString()
+                    + " mIsPrivileged: " + mIsPrivileged
+                    + " mPlaybackActive: " + mPlaybackActive
+                    + " mRecordingActive: " + mRecordingActive + "]";
+        }
     }
 
     // @GuardedBy("mSetModeLock")
@@ -2154,8 +2239,9 @@
             return;
         }
         Log.w(TAG, "Communication client died");
-        setCommunicationRouteForClient(client.getBinder(), client.getPid(), null,
-                BtHelper.SCO_MODE_UNDEFINED, "onCommunicationRouteClientDied");
+        setCommunicationRouteForClient(client.getBinder(), client.getUid(), null,
+                BtHelper.SCO_MODE_UNDEFINED, client.isPrivileged(),
+                "onCommunicationRouteClientDied");
     }
 
     /**
@@ -2242,8 +2328,8 @@
                     + crc + " eventSource: " + eventSource);
         }
         if (crc != null) {
-            setCommunicationRouteForClient(crc.getBinder(), crc.getPid(), crc.getDevice(),
-                    BtHelper.SCO_MODE_UNDEFINED, eventSource);
+            setCommunicationRouteForClient(crc.getBinder(), crc.getUid(), crc.getDevice(),
+                    BtHelper.SCO_MODE_UNDEFINED, crc.isPrivileged(), eventSource);
         }
     }
 
@@ -2267,6 +2353,7 @@
         dispatchCommunicationDevice();
     }
 
+    @GuardedBy("mDeviceStateLock")
     private CommunicationRouteClient removeCommunicationRouteClient(
                     IBinder cb, boolean unregister) {
         for (CommunicationRouteClient cl : mCommunicationRouteClients) {
@@ -2282,11 +2369,12 @@
     }
 
     @GuardedBy("mDeviceStateLock")
-    private CommunicationRouteClient addCommunicationRouteClient(
-                    IBinder cb, int pid, AudioDeviceAttributes device) {
+    private CommunicationRouteClient addCommunicationRouteClient(IBinder cb, int uid,
+                AudioDeviceAttributes device, boolean isPrivileged) {
         // always insert new request at first position
         removeCommunicationRouteClient(cb, true);
-        CommunicationRouteClient client = new CommunicationRouteClient(cb, pid, device);
+        CommunicationRouteClient client =
+                new CommunicationRouteClient(cb, uid, device, isPrivileged);
         if (client.registerDeathRecipient()) {
             mCommunicationRouteClients.add(0, client);
             return client;
@@ -2295,9 +2383,9 @@
     }
 
     @GuardedBy("mDeviceStateLock")
-    private CommunicationRouteClient getCommunicationRouteClientForPid(int pid) {
+    private CommunicationRouteClient getCommunicationRouteClientForUid(int uid) {
         for (CommunicationRouteClient cl : mCommunicationRouteClients) {
-            if (cl.getPid() == pid) {
+            if (cl.getUid() == uid) {
                 return cl;
             }
         }
@@ -2330,6 +2418,45 @@
         return device;
     }
 
+    void updateCommunicationRouteClientsActivity(
+            List<AudioPlaybackConfiguration> playbackConfigs,
+            List<AudioRecordingConfiguration> recordConfigs) {
+        synchronized (mSetModeLock) {
+            synchronized (mDeviceStateLock) {
+                boolean updateCommunicationRoute = false;
+                for (CommunicationRouteClient crc : mCommunicationRouteClients) {
+                    boolean wasActive = crc.isActive();
+                    if (playbackConfigs != null) {
+                        crc.setPlaybackActive(false);
+                        for (AudioPlaybackConfiguration config : playbackConfigs) {
+                            if (config.getClientUid() == crc.getUid()
+                                    && config.isActive()) {
+                                crc.setPlaybackActive(true);
+                                break;
+                            }
+                        }
+                    }
+                    if (recordConfigs != null) {
+                        crc.setRecordingActive(false);
+                        for (AudioRecordingConfiguration config : recordConfigs) {
+                            if (config.getClientUid() == crc.getUid()
+                                    && !config.isClientSilenced()) {
+                                crc.setRecordingActive(true);
+                                break;
+                            }
+                        }
+                    }
+                    if (wasActive != crc.isActive()) {
+                        updateCommunicationRoute = true;
+                    }
+                }
+                if (updateCommunicationRoute) {
+                    postUpdateCommunicationRouteClient("updateCommunicationRouteClientsActivity");
+                }
+            }
+        }
+    }
+
     @Nullable UUID getDeviceSensorUuid(AudioDeviceAttributes device) {
         synchronized (mDeviceStateLock) {
             return mDeviceInventory.getDeviceSensorUuid(device);
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 0c7f11f..b70e11d 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -1245,8 +1245,9 @@
      * @param btDevice the corresponding Bluetooth device when relevant.
      * @return false if an error was reported by AudioSystem
      */
-    /*package*/ boolean handleDeviceConnection(AudioDeviceAttributes attributes, boolean connect,
-            boolean isForTesting, @Nullable BluetoothDevice btDevice) {
+    /*package*/ boolean handleDeviceConnection(@NonNull AudioDeviceAttributes attributes,
+                                               boolean connect, boolean isForTesting,
+                                               @Nullable BluetoothDevice btDevice) {
         int device = attributes.getInternalType();
         String address = attributes.getAddress();
         String deviceName = attributes.getName();
@@ -1297,6 +1298,7 @@
                         AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT);
                 // always remove even if disconnection failed
                 mConnectedDevices.remove(deviceKey);
+                mDeviceBroker.postCheckCommunicationDeviceRemoval(attributes);
                 status = true;
             }
             if (status) {
@@ -1801,8 +1803,9 @@
 
         // device to remove was visible by APM, update APM
         mDeviceBroker.clearAvrcpAbsoluteVolumeSupported();
-        final int res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(
-                AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address),
+        AudioDeviceAttributes ada = new AudioDeviceAttributes(
+                AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
+        final int res = mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_UNAVAILABLE, a2dpCodec);
 
         if (res != AudioSystem.AUDIO_STATUS_OK) {
@@ -1816,11 +1819,13 @@
                     "A2DP device addr=" + address + " made unavailable")).printLog(TAG));
         }
         mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
+
         // Remove A2DP routes as well
         setCurrentAudioRouteNameIfPossible(null, true /*fromA2dp*/);
         mmi.record();
         updateBluetoothPreferredModes_l(null /*connectedDevice*/);
         purgeDevicesRoles_l();
+        mDeviceBroker.postCheckCommunicationDeviceRemoval(ada);
     }
 
     @GuardedBy("mDevicesLock")
@@ -1855,12 +1860,14 @@
 
     @GuardedBy("mDevicesLock")
     private void makeA2dpSrcUnavailable(String address) {
-        mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(
-                AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address),
+        AudioDeviceAttributes ada = new AudioDeviceAttributes(
+                AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address);
+        mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_UNAVAILABLE,
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
         mConnectedDevices.remove(
                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address));
+        mDeviceBroker.postCheckCommunicationDeviceRemoval(ada);
     }
 
     @GuardedBy("mDevicesLock")
@@ -1893,8 +1900,9 @@
 
     @GuardedBy("mDevicesLock")
     private void makeHearingAidDeviceUnavailable(String address) {
-        mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(
-                AudioSystem.DEVICE_OUT_HEARING_AID, address),
+        AudioDeviceAttributes ada = new AudioDeviceAttributes(
+                AudioSystem.DEVICE_OUT_HEARING_AID, address);
+        mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_UNAVAILABLE,
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
         mConnectedDevices.remove(
@@ -1906,6 +1914,7 @@
                 .set(MediaMetrics.Property.DEVICE,
                         AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HEARING_AID))
                 .record();
+        mDeviceBroker.postCheckCommunicationDeviceRemoval(ada);
     }
 
     /**
@@ -2002,9 +2011,10 @@
 
     @GuardedBy("mDevicesLock")
     private void makeLeAudioDeviceUnavailableNow(String address, int device) {
+        AudioDeviceAttributes ada = null;
         if (device != AudioSystem.DEVICE_NONE) {
-            final int res = AudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(
-                    device, address),
+            ada = new AudioDeviceAttributes(device, address);
+            final int res = AudioSystem.setDeviceConnectionState(ada,
                     AudioSystem.DEVICE_STATE_UNAVAILABLE,
                     AudioSystem.AUDIO_FORMAT_DEFAULT);
 
@@ -2024,6 +2034,9 @@
         setCurrentAudioRouteNameIfPossible(null, false /*fromA2dp*/);
         updateBluetoothPreferredModes_l(null /*connectedDevice*/);
         purgeDevicesRoles_l();
+        if (ada != null) {
+            mDeviceBroker.postCheckCommunicationDeviceRemoval(ada);
+        }
     }
 
     @GuardedBy("mDevicesLock")
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 19d7da4..f80f7d7 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -4261,22 +4261,41 @@
         // When the audio mode owner becomes active, replace any delayed MSG_UPDATE_AUDIO_MODE
         // and request an audio mode update immediately. Upon any other change, queue the message
         // and request an audio mode update after a grace period.
+        updateAudioModeHandlers(
+                configs /* playbackConfigs */, null /* recordConfigs */);
+        mDeviceBroker.updateCommunicationRouteClientsActivity(
+                configs /* playbackConfigs */, null /* recordConfigs */);
+    }
+
+    void updateAudioModeHandlers(List<AudioPlaybackConfiguration> playbackConfigs,
+                                 List<AudioRecordingConfiguration> recordConfigs) {
         synchronized (mDeviceBroker.mSetModeLock) {
             boolean updateAudioMode = false;
             int existingMsgPolicy = SENDMSG_QUEUE;
             int delay = CHECK_MODE_FOR_UID_PERIOD_MS;
             for (SetModeDeathHandler h : mSetModeDeathHandlers) {
                 boolean wasActive = h.isActive();
-                h.setPlaybackActive(false);
-                for (AudioPlaybackConfiguration config : configs) {
-                    final int usage = config.getAudioAttributes().getUsage();
-                    if (config.getClientUid() == h.getUid()
-                            && (usage == AudioAttributes.USAGE_VOICE_COMMUNICATION
+                if (playbackConfigs != null) {
+                    h.setPlaybackActive(false);
+                    for (AudioPlaybackConfiguration config : playbackConfigs) {
+                        final int usage = config.getAudioAttributes().getUsage();
+                        if (config.getClientUid() == h.getUid()
+                                && (usage == AudioAttributes.USAGE_VOICE_COMMUNICATION
                                 || usage == AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
-                            && config.getPlayerState()
-                                == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
-                        h.setPlaybackActive(true);
-                        break;
+                                && config.isActive()) {
+                            h.setPlaybackActive(true);
+                            break;
+                        }
+                    }
+                }
+                if (recordConfigs != null) {
+                    h.setRecordingActive(false);
+                    for (AudioRecordingConfiguration config : recordConfigs) {
+                        if (config.getClientUid() == h.getUid() && !config.isClientSilenced()
+                                && config.getAudioSource() == AudioSource.VOICE_COMMUNICATION) {
+                            h.setRecordingActive(true);
+                            break;
+                        }
                     }
                 }
                 if (wasActive != h.isActive()) {
@@ -4314,38 +4333,10 @@
         // When the audio mode owner becomes active, replace any delayed MSG_UPDATE_AUDIO_MODE
         // and request an audio mode update immediately. Upon any other change, queue the message
         // and request an audio mode update after a grace period.
-        synchronized (mDeviceBroker.mSetModeLock) {
-            boolean updateAudioMode = false;
-            int existingMsgPolicy = SENDMSG_QUEUE;
-            int delay = CHECK_MODE_FOR_UID_PERIOD_MS;
-            for (SetModeDeathHandler h : mSetModeDeathHandlers) {
-                boolean wasActive = h.isActive();
-                h.setRecordingActive(false);
-                for (AudioRecordingConfiguration config : configs) {
-                    if (config.getClientUid() == h.getUid()
-                            && config.getAudioSource() == AudioSource.VOICE_COMMUNICATION) {
-                        h.setRecordingActive(true);
-                        break;
-                    }
-                }
-                if (wasActive != h.isActive()) {
-                    updateAudioMode = true;
-                    if (h.isActive() && h == getAudioModeOwnerHandler()) {
-                        existingMsgPolicy = SENDMSG_REPLACE;
-                        delay = 0;
-                    }
-                }
-            }
-            if (updateAudioMode) {
-                sendMsg(mAudioHandler,
-                        MSG_UPDATE_AUDIO_MODE,
-                        existingMsgPolicy,
-                        AudioSystem.MODE_CURRENT,
-                        android.os.Process.myPid(),
-                        mContext.getPackageName(),
-                        delay);
-            }
-        }
+        updateAudioModeHandlers(
+                null /* playbackConfigs */, configs /* recordConfigs */);
+        mDeviceBroker.updateCommunicationRouteClientsActivity(
+                null /* playbackConfigs */, configs /* recordConfigs */);
     }
 
     private void dumpAudioMode(PrintWriter pw) {
@@ -6298,10 +6289,12 @@
                             ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED)
                     .record();
         }
-
+        final boolean isPrivileged = mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_PHONE_STATE)
+                == PackageManager.PERMISSION_GRANTED;
         final long ident = Binder.clearCallingIdentity();
         try {
-            return mDeviceBroker.setCommunicationDevice(cb, pid, device, eventSource);
+            return mDeviceBroker.setCommunicationDevice(cb, uid, device, isPrivileged, eventSource);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -6347,6 +6340,9 @@
         if (!checkAudioSettingsPermission("setSpeakerphoneOn()")) {
             return;
         }
+        final boolean isPrivileged = mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_PHONE_STATE)
+                == PackageManager.PERMISSION_GRANTED;
 
         // for logging only
         final int uid = Binder.getCallingUid();
@@ -6362,9 +6358,10 @@
                 .set(MediaMetrics.Property.STATE, on
                         ? MediaMetrics.Value.ON : MediaMetrics.Value.OFF)
                 .record();
+
         final long ident = Binder.clearCallingIdentity();
         try {
-            mDeviceBroker.setSpeakerphoneOn(cb, pid, on, eventSource);
+            mDeviceBroker.setSpeakerphoneOn(cb, uid, on, isPrivileged, eventSource);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -6489,7 +6486,7 @@
                 .set(MediaMetrics.Property.SCO_AUDIO_MODE,
                         BtHelper.scoAudioModeToString(scoAudioMode))
                 .record();
-        startBluetoothScoInt(cb, pid, scoAudioMode, eventSource);
+        startBluetoothScoInt(cb, uid, scoAudioMode, eventSource);
 
     }
 
@@ -6512,10 +6509,10 @@
                 .set(MediaMetrics.Property.SCO_AUDIO_MODE,
                         BtHelper.scoAudioModeToString(BtHelper.SCO_MODE_VIRTUAL_CALL))
                 .record();
-        startBluetoothScoInt(cb, pid, BtHelper.SCO_MODE_VIRTUAL_CALL, eventSource);
+        startBluetoothScoInt(cb, uid, BtHelper.SCO_MODE_VIRTUAL_CALL, eventSource);
     }
 
-    void startBluetoothScoInt(IBinder cb, int pid, int scoAudioMode, @NonNull String eventSource) {
+    void startBluetoothScoInt(IBinder cb, int uid, int scoAudioMode, @NonNull String eventSource) {
         MediaMetrics.Item mmi = new MediaMetrics.Item(MediaMetrics.Name.AUDIO_BLUETOOTH)
                 .set(MediaMetrics.Property.EVENT, "startBluetoothScoInt")
                 .set(MediaMetrics.Property.SCO_AUDIO_MODE,
@@ -6526,9 +6523,13 @@
             mmi.set(MediaMetrics.Property.EARLY_RETURN, "permission or systemReady").record();
             return;
         }
+        final boolean isPrivileged = mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_PHONE_STATE)
+                == PackageManager.PERMISSION_GRANTED;
         final long ident = Binder.clearCallingIdentity();
         try {
-            mDeviceBroker.startBluetoothScoForClient(cb, pid, scoAudioMode, eventSource);
+            mDeviceBroker.startBluetoothScoForClient(
+                    cb, uid, scoAudioMode, isPrivileged, eventSource);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -6546,9 +6547,12 @@
         final String eventSource =  new StringBuilder("stopBluetoothSco()")
                 .append(") from u/pid:").append(uid).append("/")
                 .append(pid).toString();
+        final boolean isPrivileged = mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_PHONE_STATE)
+                == PackageManager.PERMISSION_GRANTED;
         final long ident = Binder.clearCallingIdentity();
         try {
-            mDeviceBroker.stopBluetoothScoForClient(cb, pid, eventSource);
+            mDeviceBroker.stopBluetoothScoForClient(cb, uid, isPrivileged, eventSource);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -9283,8 +9287,8 @@
                             break;
                         }
                         boolean wasActive = h.isActive();
-                        h.setPlaybackActive(mPlaybackMonitor.isPlaybackActiveForUid(h.getUid()));
-                        h.setRecordingActive(mRecordMonitor.isRecordingActiveForUid(h.getUid()));
+                        h.setPlaybackActive(isPlaybackActiveForUid(h.getUid()));
+                        h.setRecordingActive(isRecordingActiveForUid(h.getUid()));
                         if (wasActive != h.isActive()) {
                             onUpdateAudioMode(AudioSystem.MODE_CURRENT, android.os.Process.myPid(),
                                     mContext.getPackageName(), false /*force*/);
@@ -12377,6 +12381,16 @@
         }
     }
 
+    /* package */
+    boolean isPlaybackActiveForUid(int uid) {
+        return mPlaybackMonitor.isPlaybackActiveForUid(uid);
+    }
+
+    /* package */
+    boolean isRecordingActiveForUid(int uid) {
+        return mRecordMonitor.isRecordingActiveForUid(uid);
+    }
+
     //======================
     // Audio device management
     //======================
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index 50ffbcb9..3f4f981 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -957,6 +957,7 @@
             players = (HashMap<Integer, AudioPlaybackConfiguration>) mPlayers.clone();
         }
         mFadingManager.unfadeOutUid(uid, players);
+        mDuckingManager.unduckUid(uid, players);
     }
 
     //=================================================================
diff --git a/services/core/java/com/android/server/audio/RecordingActivityMonitor.java b/services/core/java/com/android/server/audio/RecordingActivityMonitor.java
index 652ea52..4332fdd 100644
--- a/services/core/java/com/android/server/audio/RecordingActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/RecordingActivityMonitor.java
@@ -227,8 +227,8 @@
         synchronized (mRecordStates) {
             for (RecordingState state : mRecordStates) {
                 // Note: isActiveConfiguration() == true => state.getConfig() != null
-                if (state.isActiveConfiguration()
-                        && state.getConfig().getClientUid() == uid) {
+                if (state.isActiveConfiguration() && state.getConfig().getClientUid() == uid
+                        && !state.getConfig().isClientSilenced()) {
                     return true;
                 }
             }
diff --git a/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java b/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java
index 969a174..aeb6b6e 100644
--- a/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java
+++ b/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java
@@ -20,7 +20,6 @@
 import android.annotation.Nullable;
 import android.hardware.biometrics.BiometricOverlayConstants;
 import android.hardware.fingerprint.ISidefpsController;
-import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback;
 import android.os.RemoteException;
@@ -44,7 +43,6 @@
 
     @NonNull private final Optional<IUdfpsOverlayController> mUdfpsOverlayController;
     @NonNull private final Optional<ISidefpsController> mSidefpsController;
-    @NonNull private final Optional<IUdfpsOverlay> mUdfpsOverlay;
 
     /**
      * Create an overlay controller for each modality.
@@ -54,11 +52,9 @@
      */
     public SensorOverlays(
             @Nullable IUdfpsOverlayController udfpsOverlayController,
-            @Nullable ISidefpsController sidefpsController,
-            @Nullable IUdfpsOverlay udfpsOverlay) {
+            @Nullable ISidefpsController sidefpsController) {
         mUdfpsOverlayController = Optional.ofNullable(udfpsOverlayController);
         mSidefpsController = Optional.ofNullable(sidefpsController);
-        mUdfpsOverlay = Optional.ofNullable(udfpsOverlay);
     }
 
     /**
@@ -94,14 +90,6 @@
                 Slog.e(TAG, "Remote exception when showing the UDFPS overlay", e);
             }
         }
-
-        if (mUdfpsOverlay.isPresent()) {
-            try {
-                mUdfpsOverlay.get().show(client.getRequestId(), sensorId, reason);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Remote exception when showing the new UDFPS overlay", e);
-            }
-        }
     }
 
     /**
@@ -125,14 +113,6 @@
                 Slog.e(TAG, "Remote exception when hiding the UDFPS overlay", e);
             }
         }
-
-        if (mUdfpsOverlay.isPresent()) {
-            try {
-                mUdfpsOverlay.get().hide(sensorId);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Remote exception when hiding the new udfps overlay", e);
-            }
-        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
index 3fce3cc..35fc43a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
@@ -211,6 +211,8 @@
         // 1) Authenticated == true
         // 2) Error occurred
         // 3) Authenticated == false
+        // 4) onLockout
+        // 5) onLockoutTimed
         mCallback.onClientFinished(this, true /* success */);
     }
 
@@ -300,11 +302,7 @@
         PerformanceTracker.getInstanceForSensorId(getSensorId())
                 .incrementTimedLockoutForUser(getTargetUserId());
 
-        try {
-            getListener().onError(getSensorId(), getCookie(), error, 0 /* vendorCode */);
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Remote exception", e);
-        }
+        onError(error, 0 /* vendorCode */);
     }
 
     @Override
@@ -319,10 +317,6 @@
         PerformanceTracker.getInstanceForSensorId(getSensorId())
                 .incrementPermanentLockoutForUser(getTargetUserId());
 
-        try {
-            getListener().onError(getSensorId(), getCookie(), error, 0 /* vendorCode */);
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Remote exception", e);
-        }
+        onError(error, 0 /* vendorCode */);
     }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
index d6b6f77..33ed63c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -149,6 +149,17 @@
             @NonNull String halInstanceName,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull BiometricContext biometricContext) {
+        this(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher,
+                biometricContext, null /* daemon */);
+    }
+
+    @VisibleForTesting FaceProvider(@NonNull Context context,
+            @NonNull BiometricStateCallback biometricStateCallback,
+            @NonNull SensorProps[] props,
+            @NonNull String halInstanceName,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull BiometricContext biometricContext,
+            IFace daemon) {
         mContext = context;
         mBiometricStateCallback = biometricStateCallback;
         mHalInstanceName = halInstanceName;
@@ -160,6 +171,7 @@
         mTaskStackListener = new BiometricTaskStackListener();
         mBiometricContext = biometricContext;
         mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator();
+        mDaemon = daemon;
 
         for (SensorProps prop : props) {
             final int sensorId = prop.commonProps.sensorId;
@@ -475,7 +487,7 @@
                         BaseClientMonitor clientMonitor,
                         boolean success) {
                     mAuthSessionCoordinator.authEndedFor(userId, Utils.getCurrentStrength(sensorId),
-                            sensorId, requestId, success);
+                            sensorId, requestId, client.wasAuthSuccessful());
                 }
             });
         });
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
index 3d6a156..28cb7d9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
@@ -54,7 +54,6 @@
 import android.hardware.fingerprint.IFingerprintService;
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.hardware.fingerprint.ISidefpsController;
-import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.Binder;
 import android.os.Build;
@@ -913,7 +912,6 @@
             }
             provider.onPointerDown(requestId, sensorId, pc);
         }
-
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
 
@@ -964,16 +962,6 @@
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
-        public void setUdfpsOverlay(@NonNull IUdfpsOverlay controller) {
-            super.setUdfpsOverlay_enforcePermission();
-
-            for (ServiceProvider provider : mRegistry.getProviders()) {
-                provider.setUdfpsOverlay(controller);
-            }
-        }
-
-        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
-        @Override
         public void onPowerPressed() {
             super.onPowerPressed_enforcePermission();
 
@@ -1184,4 +1172,15 @@
             }
         }
     }
+
+    void simulateVhalFingerDown() {
+        if (Utils.isVirtualEnabled(getContext())) {
+            Slog.i(TAG, "Simulate virtual HAL finger down event");
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
+            if (provider != null) {
+                provider.second.simulateVhalFingerDown(UserHandle.getCallingUserId(),
+                        provider.first);
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintShellCommand.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintShellCommand.java
index 636413f..dc6a63f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintShellCommand.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintShellCommand.java
@@ -45,6 +45,8 @@
                     return doHelp();
                 case "sync":
                     return doSync();
+                case "fingerdown":
+                    return doSimulateVhalFingerDown();
                 default:
                     getOutPrintWriter().println("Unrecognized command: " + cmd);
             }
@@ -62,6 +64,8 @@
         pw.println("      Print this help text.");
         pw.println("  sync");
         pw.println("      Sync enrollments now (virtualized sensors only).");
+        pw.println("  fingerdown");
+        pw.println("      Simulate finger down event (virtualized sensors only).");
     }
 
     private int doHelp() {
@@ -73,4 +77,9 @@
         mService.syncEnrollmentsNow();
         return 0;
     }
+
+    private int doSimulateVhalFingerDown() {
+        mService.simulateVhalFingerDown();
+        return 0;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
index 26701c1..a15d1a4 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
@@ -28,7 +28,6 @@
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.hardware.fingerprint.ISidefpsController;
-import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
 
@@ -134,12 +133,6 @@
 
     void setUdfpsOverlayController(@NonNull IUdfpsOverlayController controller);
 
-    /**
-     * Sets udfps overlay
-     * @param controller udfps overlay
-     */
-    void setUdfpsOverlay(@NonNull IUdfpsOverlay controller);
-
     void onPowerPressed();
 
     /**
@@ -157,4 +150,11 @@
      * @param sensorId sensor ID of the associated operation
      */
     default void scheduleWatchdog(int sensorId) {}
+
+    /**
+     * Simulate fingerprint down touch event for virtual HAL
+     * @param userId user ID
+     * @param sensorId sensor ID
+     */
+    default void simulateVhalFingerDown(int userId, int sensorId) {};
 }
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 3fc36b6..54d1faa 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
@@ -30,7 +30,6 @@
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.ISidefpsController;
-import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.Build;
 import android.os.Handler;
@@ -112,7 +111,6 @@
             @NonNull LockoutCache lockoutCache,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
             @Nullable ISidefpsController sidefpsController,
-            @Nullable IUdfpsOverlay udfpsOverlay,
             boolean allowBackgroundAuthentication,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @NonNull Handler handler,
@@ -137,8 +135,7 @@
                 false /* shouldVibrate */,
                 biometricStrength);
         setRequestId(requestId);
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController,
-                sidefpsController, udfpsOverlay);
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
         mSensorProps = sensorProps;
         mALSProbeCallback = getLogger().getAmbientLightProbe(false /* startWithClient */);
         mHandler = handler;
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
index 46f62d3..51a9385 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
@@ -22,7 +22,6 @@
 import android.hardware.biometrics.BiometricOverlayConstants;
 import android.hardware.biometrics.common.ICancellationSignal;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
-import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -59,15 +58,13 @@
             @NonNull FingerprintAuthenticateOptions options,
             @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
-            @Nullable IUdfpsOverlay udfpsOverlay,
             boolean isStrongBiometric) {
         super(context, lazyDaemon, token, listener, options.getUserId(),
                 options.getOpPackageName(), 0 /* cookie */, options.getSensorId(),
                 true /* shouldVibrate */, biometricLogger, biometricContext);
         setRequestId(requestId);
         mIsStrongBiometric = isStrongBiometric;
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController,
-                null /* sideFpsController*/, udfpsOverlay);
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController, null /* sideFpsController*/);
         mOptions = options;
     }
 
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 d35469c..f9e08d6 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
@@ -29,7 +29,6 @@
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.ISidefpsController;
-import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.hardware.keymaster.HardwareAuthToken;
 import android.os.IBinder;
@@ -87,7 +86,6 @@
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
             @Nullable ISidefpsController sidefpsController,
-            @Nullable IUdfpsOverlay udfpsOverlay,
             int maxTemplatesPerUser, @FingerprintManager.EnrollReason int enrollReason) {
         // UDFPS haptics occur when an image is acquired (instead of when the result is known)
         super(context, lazyDaemon, token, listener, userId, hardwareAuthToken, owner, utils,
@@ -95,8 +93,7 @@
                 biometricContext);
         setRequestId(requestId);
         mSensorProps = sensorProps;
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController,
-                sidefpsController, udfpsOverlay);
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
         mMaxTemplatesPerUser = maxTemplatesPerUser;
 
         mALSProbeCallback = getLogger().getAmbientLightProbe(true /* startWithClient */);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index 3e33ef6..0421d78 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -43,7 +43,6 @@
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.hardware.fingerprint.ISidefpsController;
-import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.Binder;
 import android.os.Handler;
@@ -122,7 +121,6 @@
     @Nullable private IFingerprint mDaemon;
     @Nullable private IUdfpsOverlayController mUdfpsOverlayController;
     @Nullable private ISidefpsController mSidefpsController;
-    @Nullable private IUdfpsOverlay mUdfpsOverlay;
     private AuthSessionCoordinator mAuthSessionCoordinator;
 
     private final class BiometricTaskStackListener extends TaskStackListener {
@@ -160,6 +158,17 @@
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
             @NonNull BiometricContext biometricContext) {
+        this(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher,
+                gestureAvailabilityDispatcher, biometricContext, null /* daemon */);
+    }
+
+    @VisibleForTesting FingerprintProvider(@NonNull Context context,
+            @NonNull BiometricStateCallback biometricStateCallback,
+            @NonNull SensorProps[] props, @NonNull String halInstanceName,
+            @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+            @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+            @NonNull BiometricContext biometricContext,
+            IFingerprint daemon) {
         mContext = context;
         mBiometricStateCallback = biometricStateCallback;
         mHalInstanceName = halInstanceName;
@@ -170,6 +179,7 @@
         mTaskStackListener = new BiometricTaskStackListener();
         mBiometricContext = biometricContext;
         mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator();
+        mDaemon = daemon;
 
         final List<SensorLocationInternal> workaroundLocations = getWorkaroundSensorProps(context);
 
@@ -408,7 +418,7 @@
                             BiometricsProtoEnums.CLIENT_UNKNOWN),
                     mBiometricContext,
                     mFingerprintSensors.get(sensorId).getSensorProperties(),
-                    mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay,
+                    mUdfpsOverlayController, mSidefpsController,
                     maxTemplatesPerUser, enrollReason);
             scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
                     mBiometricStateCallback, new ClientMonitorCallback() {
@@ -446,8 +456,7 @@
                     mFingerprintSensors.get(sensorId).getLazySession(), token, id, callback,
                     options,
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
-                    mBiometricContext,
-                    mUdfpsOverlayController, mUdfpsOverlay, isStrongBiometric);
+                    mBiometricContext, mUdfpsOverlayController, isStrongBiometric);
             scheduleForSensor(sensorId, client, mBiometricStateCallback);
         });
 
@@ -471,7 +480,7 @@
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
                     mBiometricContext, isStrongBiometric,
                     mTaskStackListener, mFingerprintSensors.get(sensorId).getLockoutCache(),
-                    mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay,
+                    mUdfpsOverlayController, mSidefpsController,
                     allowBackgroundAuthentication,
                     mFingerprintSensors.get(sensorId).getSensorProperties(), mHandler,
                     Utils.getCurrentStrength(sensorId),
@@ -707,11 +716,6 @@
     }
 
     @Override
-    public void setUdfpsOverlay(@NonNull IUdfpsOverlay controller) {
-        mUdfpsOverlay = controller;
-    }
-
-    @Override
     public void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto,
             boolean clearSchedulerBuffer) {
         if (mFingerprintSensors.contains(sensorId)) {
@@ -845,4 +849,18 @@
         }
         biometricScheduler.startWatchdog();
     }
+
+    @Override
+    public void simulateVhalFingerDown(int userId, int sensorId) {
+        Slog.d(getTag(), "Simulate virtual HAL finger down event");
+        final AidlSession session = mFingerprintSensors.get(sensorId).getSessionForUser(userId);
+        final PointerContext pc = new PointerContext();
+        try {
+            session.getSession().onPointerDownWithContext(pc);
+            session.getSession().onUiReady();
+            session.getSession().onPointerUpWithContext(pc);
+        } catch (RemoteException e) {
+            Slog.e(getTag(), "failed hal operation ", e);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
index 1cbbf89..92b216d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
@@ -40,7 +40,6 @@
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.hardware.fingerprint.ISidefpsController;
-import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.Handler;
 import android.os.IBinder;
@@ -123,7 +122,6 @@
     @NonNull private final HalResultController mHalResultController;
     @Nullable private IUdfpsOverlayController mUdfpsOverlayController;
     @Nullable private ISidefpsController mSidefpsController;
-    @Nullable private IUdfpsOverlay mUdfpsOverlay;
     @NonNull private final BiometricContext mBiometricContext;
     // for requests that do not use biometric prompt
     @NonNull private final AtomicLong mRequestCounter = new AtomicLong(0);
@@ -597,9 +595,7 @@
                     mSensorProperties.sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_ENROLL,
                             BiometricsProtoEnums.CLIENT_UNKNOWN),
-                    mBiometricContext,
-                    mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay,
-                    enrollReason);
+                    mBiometricContext, mUdfpsOverlayController, mSidefpsController, enrollReason);
             mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() {
                 @Override
                 public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
@@ -644,8 +640,7 @@
             final FingerprintDetectClient client = new FingerprintDetectClient(mContext,
                     mLazyDaemon, token, id, listener, options,
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
-                    mBiometricContext, mUdfpsOverlayController, mUdfpsOverlay,
-                    isStrongBiometric);
+                    mBiometricContext, mUdfpsOverlayController, isStrongBiometric);
             mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
         });
 
@@ -668,7 +663,7 @@
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
                     mBiometricContext, isStrongBiometric,
                     mTaskStackListener, mLockoutTracker,
-                    mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay,
+                    mUdfpsOverlayController, mSidefpsController,
                     allowBackgroundAuthentication, mSensorProperties,
                     Utils.getCurrentStrength(mSensorId));
             mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
@@ -856,11 +851,6 @@
     }
 
     @Override
-    public void setUdfpsOverlay(@NonNull IUdfpsOverlay controller) {
-        mUdfpsOverlay = controller;
-    }
-
-    @Override
     public void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto,
             boolean clearSchedulerBuffer) {
         final long sensorToken = proto.start(SensorServiceStateProto.SENSOR_STATES);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
index 2a62338..9966e91 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
@@ -30,7 +30,6 @@
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.ISidefpsController;
-import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -83,7 +82,6 @@
             @NonNull LockoutFrameworkImpl lockoutTracker,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
             @Nullable ISidefpsController sidefpsController,
-            @Nullable IUdfpsOverlay udfpsOverlay,
             boolean allowBackgroundAuthentication,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @Authenticators.Types int sensorStrength) {
@@ -93,8 +91,7 @@
                 false /* shouldVibrate */, sensorStrength);
         setRequestId(requestId);
         mLockoutFrameworkImpl = lockoutTracker;
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController,
-                sidefpsController, udfpsOverlay);
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
         mSensorProps = sensorProps;
         mALSProbeCallback = getLogger().getAmbientLightProbe(false /* startWithClient */);
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
index ed0a201..0d7f9f2 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
@@ -26,7 +26,6 @@
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
 import android.hardware.fingerprint.FingerprintManager;
-import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -67,13 +66,12 @@
             @NonNull FingerprintAuthenticateOptions options,
             @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
-            @Nullable IUdfpsOverlay udfpsOverlay, boolean isStrongBiometric) {
+            boolean isStrongBiometric) {
         super(context, lazyDaemon, token, listener, options.getUserId(),
                 options.getOpPackageName(), 0 /* cookie */, options.getSensorId(),
                 true /* shouldVibrate */, biometricLogger, biometricContext);
         setRequestId(requestId);
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController,
-                null /* sideFpsController */, udfpsOverlay);
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController, null /* sideFpsController */);
         mIsStrongBiometric = isStrongBiometric;
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
index c2b7944..6fee84a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
@@ -27,7 +27,6 @@
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.ISidefpsController;
-import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -69,14 +68,12 @@
             @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
             @Nullable ISidefpsController sidefpsController,
-            @Nullable IUdfpsOverlay udfpsOverlay,
             @FingerprintManager.EnrollReason int enrollReason) {
         super(context, lazyDaemon, token, listener, userId, hardwareAuthToken, owner, utils,
                 timeoutSec, sensorId, true /* shouldVibrate */, biometricLogger,
                 biometricContext);
         setRequestId(requestId);
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController,
-                sidefpsController, udfpsOverlay);
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
 
         mEnrollReason = enrollReason;
         if (enrollReason == FingerprintManager.ENROLL_FIND_SENSOR) {
diff --git a/services/core/java/com/android/server/cpu/CpuInfoReader.java b/services/core/java/com/android/server/cpu/CpuInfoReader.java
index 70d7bde..984ad1d 100644
--- a/services/core/java/com/android/server/cpu/CpuInfoReader.java
+++ b/services/core/java/com/android/server/cpu/CpuInfoReader.java
@@ -203,15 +203,15 @@
                 continue;
             }
             if (dynamicPolicyInfo.curCpuFreqKHz == CpuInfo.MISSING_FREQUENCY
-                    || staticPolicyInfo.maxCpuFreqKHz == CpuInfo.MISSING_FREQUENCY) {
+                    || dynamicPolicyInfo.maxCpuFreqKHz == CpuInfo.MISSING_FREQUENCY) {
                 Slogf.w(TAG, "Current and maximum CPU frequency information mismatch/missing for"
                         + " policy ID %d", policyId);
                 continue;
             }
-            if (dynamicPolicyInfo.curCpuFreqKHz > staticPolicyInfo.maxCpuFreqKHz) {
+            if (dynamicPolicyInfo.curCpuFreqKHz > dynamicPolicyInfo.maxCpuFreqKHz) {
                 Slogf.w(TAG, "Current CPU frequency (%d) is greater than maximum CPU frequency"
                         + " (%d) for policy ID (%d). Skipping CPU frequency policy",
-                        dynamicPolicyInfo.curCpuFreqKHz,  staticPolicyInfo.maxCpuFreqKHz, policyId);
+                        dynamicPolicyInfo.curCpuFreqKHz, dynamicPolicyInfo.maxCpuFreqKHz, policyId);
                 continue;
             }
             for (int coreIdx = 0; coreIdx < staticPolicyInfo.relatedCpuCores.size(); coreIdx++) {
@@ -234,7 +234,8 @@
                 if (dynamicPolicyInfo.affectedCpuCores.indexOf(relatedCpuCore) < 0) {
                     cpuInfoByCpus.append(relatedCpuCore, new CpuInfo(relatedCpuCore,
                             cpusetCategories, /* isOnline= */false, CpuInfo.MISSING_FREQUENCY,
-                            staticPolicyInfo.maxCpuFreqKHz, CpuInfo.MISSING_FREQUENCY, usageStats));
+                            dynamicPolicyInfo.maxCpuFreqKHz, CpuInfo.MISSING_FREQUENCY,
+                            usageStats));
                     continue;
                 }
                 // If a CPU core is online, it must have the usage stats. When the usage stats is
@@ -245,7 +246,7 @@
                     continue;
                 }
                 CpuInfo cpuInfo = new CpuInfo(relatedCpuCore, cpusetCategories, /* isOnline= */true,
-                        dynamicPolicyInfo.curCpuFreqKHz, staticPolicyInfo.maxCpuFreqKHz,
+                        dynamicPolicyInfo.curCpuFreqKHz, dynamicPolicyInfo.maxCpuFreqKHz,
                         dynamicPolicyInfo.avgTimeInStateCpuFreqKHz, usageStats);
                 cpuInfoByCpus.append(relatedCpuCore, cpuInfo);
                 if (DEBUG) {
@@ -423,12 +424,6 @@
         for (int i = 0; i < mCpuFreqPolicyDirsById.size(); i++) {
             int policyId = mCpuFreqPolicyDirsById.keyAt(i);
             File policyDir = mCpuFreqPolicyDirsById.valueAt(i);
-            long maxCpuFreqKHz = readCpuFreqKHz(new File(policyDir, MAX_SCALING_FREQ_FILE));
-            if (maxCpuFreqKHz == CpuInfo.MISSING_FREQUENCY) {
-                Slogf.w(TAG, "Missing max CPU frequency information at %s",
-                        policyDir.getAbsolutePath());
-                continue;
-            }
             File cpuCoresFile = new File(policyDir, RELATED_CPUS_FILE);
             IntArray relatedCpuCores = readCpuCores(cpuCoresFile);
             if (relatedCpuCores == null || relatedCpuCores.size() == 0) {
@@ -436,8 +431,7 @@
                         cpuCoresFile.getAbsolutePath());
                 continue;
             }
-            StaticPolicyInfo staticPolicyInfo = new StaticPolicyInfo(maxCpuFreqKHz,
-                    relatedCpuCores);
+            StaticPolicyInfo staticPolicyInfo = new StaticPolicyInfo(relatedCpuCores);
             mStaticPolicyInfoById.append(policyId, staticPolicyInfo);
             if (DEBUG) {
                 Slogf.d(TAG, "Added static policy info %s for policy id %d", staticPolicyInfo,
@@ -464,8 +458,14 @@
                 Slogf.e(TAG, "Failed to read CPU cores from %s", cpuCoresFile.getAbsolutePath());
                 continue;
             }
+            long maxCpuFreqKHz = readCpuFreqKHz(new File(policyDir, MAX_SCALING_FREQ_FILE));
+            if (maxCpuFreqKHz == CpuInfo.MISSING_FREQUENCY) {
+                Slogf.w(TAG, "Missing max CPU frequency information at %s",
+                        policyDir.getAbsolutePath());
+                continue;
+            }
             DynamicPolicyInfo dynamicPolicyInfo = new DynamicPolicyInfo(curCpuFreqKHz,
-                    avgTimeInStateCpuFreqKHz, affectedCpuCores);
+                    maxCpuFreqKHz, avgTimeInStateCpuFreqKHz, affectedCpuCores);
             dynamicPolicyInfoById.append(policyId, dynamicPolicyInfo);
             if (DEBUG) {
                 Slogf.d(TAG, "Read dynamic policy info %s for policy id %d", dynamicPolicyInfo,
@@ -593,9 +593,12 @@
             List<String> lines = Files.readAllLines(file.toPath());
             IntArray cpuCores = new IntArray(0);
             for (int i = 0; i < lines.size(); i++) {
-                String line = lines.get(i);
-                String[] pairs = line.contains(",") ? line.trim().split(",")
-                        : line.trim().split(" ");
+                String line = lines.get(i).trim();
+                if (line.isEmpty()) {
+                    continue;
+                }
+                String[] pairs = line.contains(",") ? line.split(",")
+                        : line.split(" ");
                 for (int j = 0; j < pairs.length; j++) {
                     String[] minMaxPairs = pairs[j].split("-");
                     if (minMaxPairs.length >= 2) {
@@ -615,6 +618,9 @@
                 }
             }
             return cpuCores;
+        } catch (NumberFormatException e) {
+            Slogf.e(TAG, e, "Failed to read CPU cores from %s due to incorrect file format",
+                    file.getAbsolutePath());
         } catch (Exception e) {
             Slogf.e(TAG, e, "Failed to read CPU cores from %s", file.getAbsolutePath());
         }
@@ -889,29 +895,28 @@
     }
 
     private static final class StaticPolicyInfo {
-        public final long maxCpuFreqKHz;
         public final IntArray relatedCpuCores;
 
-        StaticPolicyInfo(long maxCpuFreqKHz, IntArray relatedCpuCores) {
-            this.maxCpuFreqKHz = maxCpuFreqKHz;
+        StaticPolicyInfo(IntArray relatedCpuCores) {
             this.relatedCpuCores = relatedCpuCores;
         }
 
         @Override
         public String toString() {
-            return "StaticPolicyInfo{maxCpuFreqKHz = " + maxCpuFreqKHz + ", relatedCpuCores = "
-                    + relatedCpuCores + '}';
+            return "StaticPolicyInfo{relatedCpuCores = " + relatedCpuCores + '}';
         }
     }
 
     private static final class DynamicPolicyInfo {
         public final long curCpuFreqKHz;
+        public final long maxCpuFreqKHz;
         public final long avgTimeInStateCpuFreqKHz;
         public final IntArray affectedCpuCores;
 
-        DynamicPolicyInfo(long curCpuFreqKHz, long avgTimeInStateCpuFreqKHz,
+        DynamicPolicyInfo(long curCpuFreqKHz, long maxCpuFreqKHz, long avgTimeInStateCpuFreqKHz,
                 IntArray affectedCpuCores) {
             this.curCpuFreqKHz = curCpuFreqKHz;
+            this.maxCpuFreqKHz = maxCpuFreqKHz;
             this.avgTimeInStateCpuFreqKHz = avgTimeInStateCpuFreqKHz;
             this.affectedCpuCores = affectedCpuCores;
         }
@@ -919,6 +924,7 @@
         @Override
         public String toString() {
             return "DynamicPolicyInfo{curCpuFreqKHz = " + curCpuFreqKHz
+                    + ", maxCpuFreqKHz = " + maxCpuFreqKHz
                     + ", avgTimeInStateCpuFreqKHz = " + avgTimeInStateCpuFreqKHz
                     + ", affectedCpuCores = " + affectedCpuCores + '}';
         }
diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java
index 47cde15..5b11cfe 100644
--- a/services/core/java/com/android/server/display/BrightnessRangeController.java
+++ b/services/core/java/com/android/server/display/BrightnessRangeController.java
@@ -18,29 +18,42 @@
 
 import android.hardware.display.BrightnessInfo;
 import android.os.IBinder;
+import android.provider.DeviceConfigInterface;
+
+import com.android.server.display.feature.DeviceConfigParameterProvider;
 
 import java.io.PrintWriter;
 import java.util.function.BooleanSupplier;
 
 class BrightnessRangeController {
 
-    private static final boolean NBM_FEATURE_FLAG = false;
-
     private final HighBrightnessModeController mHbmController;
     private final NormalBrightnessModeController mNormalBrightnessModeController =
             new NormalBrightnessModeController();
 
     private final Runnable mModeChangeCallback;
+    private final boolean mUseNbmController;
+
 
     BrightnessRangeController(HighBrightnessModeController hbmController,
             Runnable modeChangeCallback) {
-        mHbmController = hbmController;
-        mModeChangeCallback = modeChangeCallback;
+        this(hbmController, modeChangeCallback,
+                new DeviceConfigParameterProvider(DeviceConfigInterface.REAL));
     }
 
+    BrightnessRangeController(HighBrightnessModeController hbmController,
+            Runnable modeChangeCallback, DeviceConfigParameterProvider configParameterProvider) {
+        mHbmController = hbmController;
+        mModeChangeCallback = modeChangeCallback;
+        mUseNbmController = configParameterProvider.isNormalBrightnessControllerFeatureEnabled();
+    }
 
     void dump(PrintWriter pw) {
+        pw.println("BrightnessRangeController:");
+        pw.println("  mUseNormalBrightnessController=" + mUseNbmController);
         mHbmController.dump(pw);
+        mNormalBrightnessModeController.dump(pw);
+
     }
 
     void onAmbientLuxChange(float ambientLux) {
@@ -90,7 +103,7 @@
 
 
     float getCurrentBrightnessMax() {
-        if (NBM_FEATURE_FLAG && mHbmController.getHighBrightnessMode()
+        if (mUseNbmController && mHbmController.getHighBrightnessMode()
                 == BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF) {
             return Math.min(mHbmController.getCurrentBrightnessMax(),
                     mNormalBrightnessModeController.getCurrentBrightnessMax());
@@ -111,7 +124,7 @@
     }
 
     private void applyChanges(BooleanSupplier nbmChangesFunc, Runnable hbmChangesFunc) {
-        if (NBM_FEATURE_FLAG) {
+        if (mUseNbmController) {
             boolean nbmTransitionChanged = nbmChangesFunc.getAsBoolean();
             hbmChangesFunc.run();
             // if nbm transition changed - trigger callback
diff --git a/services/core/java/com/android/server/display/BrightnessSetting.java b/services/core/java/com/android/server/display/BrightnessSetting.java
index de42370..651828b 100644
--- a/services/core/java/com/android/server/display/BrightnessSetting.java
+++ b/services/core/java/com/android/server/display/BrightnessSetting.java
@@ -40,6 +40,7 @@
 
     private final LogicalDisplay mLogicalDisplay;
 
+    private int mUserSerial;
     private final Handler mHandler = new Handler(Looper.getMainLooper()) {
         @Override
         public void handleMessage(Message msg) {
@@ -56,13 +57,15 @@
     @GuardedBy("mSyncRoot")
     private float mBrightness;
 
-    BrightnessSetting(@NonNull PersistentDataStore persistentDataStore,
+    BrightnessSetting(int userSerial,
+            @NonNull PersistentDataStore persistentDataStore,
             @NonNull LogicalDisplay logicalDisplay,
             DisplayManagerService.SyncRoot syncRoot) {
         mPersistentDataStore = persistentDataStore;
         mLogicalDisplay = logicalDisplay;
+        mUserSerial = userSerial;
         mBrightness = mPersistentDataStore.getBrightness(
-                mLogicalDisplay.getPrimaryDisplayDeviceLocked());
+                mLogicalDisplay.getPrimaryDisplayDeviceLocked(), userSerial);
         mSyncRoot = syncRoot;
     }
 
@@ -96,8 +99,13 @@
         mListeners.remove(l);
     }
 
+    /** Sets the user serial for the brightness setting */
+    public void setUserSerial(int userSerial) {
+        mUserSerial = userSerial;
+    }
+
     /**
-     * Sets the brigtness and broadcasts the change to the listeners.
+     * Sets the brightness and broadcasts the change to the listeners.
      * @param brightness The value to which the brightness is to be set.
      */
     public void setBrightness(float brightness) {
@@ -112,7 +120,8 @@
             // changed.
             if (brightness != mBrightness) {
                 mPersistentDataStore.setBrightness(mLogicalDisplay.getPrimaryDisplayDeviceLocked(),
-                        brightness);
+                        brightness, mUserSerial
+                );
             }
             mBrightness = brightness;
             int toSend = Float.floatToIntBits(mBrightness);
diff --git a/services/core/java/com/android/server/display/BrightnessThrottler.java b/services/core/java/com/android/server/display/BrightnessThrottler.java
index cfdcd63..c421ec0 100644
--- a/services/core/java/com/android/server/display/BrightnessThrottler.java
+++ b/services/core/java/com/android/server/display/BrightnessThrottler.java
@@ -22,7 +22,6 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.display.BrightnessInfo;
-import android.hardware.display.DisplayManager;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.IThermalEventListener;
@@ -38,6 +37,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData;
 import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel;
+import com.android.server.display.feature.DeviceConfigParameterProvider;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -63,7 +63,7 @@
     private final Runnable mThrottlingChangeCallback;
     private final SkinThermalStatusObserver mSkinThermalStatusObserver;
     private final DeviceConfigListener mDeviceConfigListener;
-    private final DeviceConfigInterface mDeviceConfig;
+    private final DeviceConfigParameterProvider mConfigParameterProvider;
 
     private int mThrottlingStatus;
 
@@ -118,7 +118,7 @@
         mSkinThermalStatusObserver = new SkinThermalStatusObserver(mInjector, mHandler);
 
         mUniqueDisplayId = uniqueDisplayId;
-        mDeviceConfig = injector.getDeviceConfig();
+        mConfigParameterProvider = new DeviceConfigParameterProvider(injector.getDeviceConfig());
         mDeviceConfigListener = new DeviceConfigListener();
         mThermalBrightnessThrottlingDataId = throttlingDataId;
         mDdcThermalThrottlingDataMap = thermalBrightnessThrottlingDataMap;
@@ -145,7 +145,7 @@
 
     void stop() {
         mSkinThermalStatusObserver.stopObserving();
-        mDeviceConfig.removeOnPropertiesChangedListener(mDeviceConfigListener);
+        mConfigParameterProvider.removeOnPropertiesChangedListener(mDeviceConfigListener);
         // We're asked to stop throttling, so reset brightness restrictions.
         mBrightnessCap = PowerManager.BRIGHTNESS_MAX;
         mBrightnessMaxReason = BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE;
@@ -248,12 +248,6 @@
         mSkinThermalStatusObserver.dump(pw);
     }
 
-    private String getThermalBrightnessThrottlingDataString() {
-        return mDeviceConfig.getString(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
-                DisplayManager.DeviceConfig.KEY_BRIGHTNESS_THROTTLING_DATA,
-                /* defaultValue= */ null);
-    }
-
     // The brightness throttling data id may or may not be specified in the string that is passed
     // in, if there is none specified, we assume it is for the default case. Each string passed in
     // here must be for one display and one throttling id.
@@ -318,7 +312,8 @@
     private void loadThermalBrightnessThrottlingDataFromDeviceConfig() {
         HashMap<String, HashMap<String, ThermalBrightnessThrottlingData>> tempThrottlingData =
                 new HashMap<>(1);
-        mThermalBrightnessThrottlingDataString = getThermalBrightnessThrottlingDataString();
+        mThermalBrightnessThrottlingDataString =
+                mConfigParameterProvider.getBrightnessThrottlingData();
         boolean validConfig = true;
         mThermalBrightnessThrottlingDataOverride.clear();
         if (mThermalBrightnessThrottlingDataString != null) {
@@ -390,8 +385,7 @@
         public Executor mExecutor = new HandlerExecutor(mDeviceConfigHandler);
 
         public void startListening() {
-            mDeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
-                    mExecutor, this);
+            mConfigParameterProvider.addOnPropertiesChangedListener(mExecutor, this);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 7ccfb44..d9cb299 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -515,7 +515,9 @@
     private final SensorData mScreenOffBrightnessSensor = new SensorData();
 
     // The details of the proximity sensor associated with this display.
-    private final SensorData mProximitySensor = new SensorData();
+    // Is null when no sensor should be used for that display
+    @Nullable
+    private SensorData mProximitySensor = new SensorData();
 
     private final List<RefreshRateLimitation> mRefreshRateLimitations =
             new ArrayList<>(2 /*initialCapacity*/);
@@ -1337,7 +1339,8 @@
         return mScreenOffBrightnessSensor;
     }
 
-    SensorData getProximitySensor() {
+    @Nullable
+    public SensorData getProximitySensor() {
         return mProximitySensor;
     }
 
@@ -2563,46 +2566,51 @@
     private void loadAmbientLightSensorFromDdc(DisplayConfiguration config) {
         final SensorDetails sensorDetails = config.getLightSensor();
         if (sensorDetails != null) {
-            mAmbientLightSensor.type = sensorDetails.getType();
-            mAmbientLightSensor.name = sensorDetails.getName();
-            final RefreshRateRange rr = sensorDetails.getRefreshRate();
-            if (rr != null) {
-                mAmbientLightSensor.minRefreshRate = rr.getMinimum().floatValue();
-                mAmbientLightSensor.maxRefreshRate = rr.getMaximum().floatValue();
-            }
+            loadSensorData(sensorDetails, mAmbientLightSensor);
         } else {
             loadAmbientLightSensorFromConfigXml();
         }
     }
 
     private void setProxSensorUnspecified() {
-        mProximitySensor.name = null;
-        mProximitySensor.type = null;
+        mProximitySensor = new SensorData();
     }
 
     private void loadScreenOffBrightnessSensorFromDdc(DisplayConfiguration config) {
         final SensorDetails sensorDetails = config.getScreenOffBrightnessSensor();
         if (sensorDetails != null) {
-            mScreenOffBrightnessSensor.type = sensorDetails.getType();
-            mScreenOffBrightnessSensor.name = sensorDetails.getName();
+            loadSensorData(sensorDetails, mScreenOffBrightnessSensor);
         }
     }
 
     private void loadProxSensorFromDdc(DisplayConfiguration config) {
         SensorDetails sensorDetails = config.getProxSensor();
         if (sensorDetails != null) {
-            mProximitySensor.name = sensorDetails.getName();
-            mProximitySensor.type = sensorDetails.getType();
-            final RefreshRateRange rr = sensorDetails.getRefreshRate();
-            if (rr != null) {
-                mProximitySensor.minRefreshRate = rr.getMinimum().floatValue();
-                mProximitySensor.maxRefreshRate = rr.getMaximum().floatValue();
+            String name = sensorDetails.getName();
+            String type = sensorDetails.getType();
+            if ("".equals(name) && "".equals(type)) {
+                // <proxSensor> with empty values to the config means no sensor should be used
+                mProximitySensor = null;
+            } else {
+                mProximitySensor = new SensorData();
+                loadSensorData(sensorDetails, mProximitySensor);
             }
         } else {
             setProxSensorUnspecified();
         }
     }
 
+    private void loadSensorData(@NonNull SensorDetails sensorDetails,
+            @NonNull SensorData sensorData) {
+        sensorData.name = sensorDetails.getName();
+        sensorData.type = sensorDetails.getType();
+        final RefreshRateRange rr = sensorDetails.getRefreshRate();
+        if (rr != null) {
+            sensorData.minRefreshRate = rr.getMinimum().floatValue();
+            sensorData.maxRefreshRate = rr.getMaximum().floatValue();
+        }
+    }
+
     private void loadBrightnessChangeThresholdsFromXml() {
         loadBrightnessChangeThresholds(/* config= */ null);
     }
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index da8eb23..dbe15b6 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -24,7 +24,6 @@
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE;
 import static android.hardware.display.DisplayManager.EventsMask;
-import static android.hardware.display.DisplayManager.HDR_OUTPUT_CONTROL_FLAG;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD;
@@ -42,7 +41,6 @@
 import static android.hardware.display.HdrConversionMode.HDR_CONVERSION_UNSUPPORTED;
 import static android.os.Process.FIRST_APPLICATION_UID;
 import static android.os.Process.ROOT_UID;
-import static android.provider.DeviceConfig.NAMESPACE_DISPLAY_MANAGER;
 
 import android.Manifest;
 import android.annotation.NonNull;
@@ -116,7 +114,7 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.provider.DeviceConfig;
+import android.provider.DeviceConfigInterface;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.ArraySet;
@@ -152,6 +150,7 @@
 import com.android.server.UiThread;
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 import com.android.server.display.DisplayDeviceConfig.SensorData;
+import com.android.server.display.feature.DeviceConfigParameterProvider;
 import com.android.server.display.layout.Layout;
 import com.android.server.display.mode.DisplayModeDirector;
 import com.android.server.display.utils.SensorUtils;
@@ -506,6 +505,8 @@
 
     private final BrightnessSynchronizer mBrightnessSynchronizer;
 
+    private final DeviceConfigParameterProvider mConfigParameterProvider;
+
     /**
      * Applications use {@link android.view.Display#getRefreshRate} and
      * {@link android.view.Display.Mode#getRefreshRate} to know what is the display refresh rate.
@@ -558,6 +559,7 @@
         mWideColorSpace = colorSpaces[1];
         mOverlayProperties = SurfaceControl.getOverlaySupport();
         mSystemReady = false;
+        mConfigParameterProvider = new DeviceConfigParameterProvider(DeviceConfigInterface.REAL);
     }
 
     public void setupSchedulerPolicies() {
@@ -653,6 +655,12 @@
                             logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId(),
                             userSerial);
                     dpc.setBrightnessConfiguration(config, /* shouldResetShortTermModel= */ true);
+                    // change the brightness value according to the selected user.
+                    final DisplayDevice device = logicalDisplay.getPrimaryDisplayDeviceLocked();
+                    if (device != null) {
+                        dpc.setBrightness(
+                                mPersistentDataStore.getBrightness(device, userSerial), userSerial);
+                    }
                 }
                 dpc.onSwitchUser(newUserId);
             });
@@ -688,11 +696,11 @@
         synchronized (mSyncRoot) {
             mSafeMode = safeMode;
             mSystemReady = true;
-            mIsHdrOutputControlEnabled = isDeviceConfigHdrOutputControlEnabled();
-            DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_DISPLAY_MANAGER,
-                    BackgroundThread.getExecutor(),
+            mIsHdrOutputControlEnabled =
+                    mConfigParameterProvider.isHdrOutputControlFeatureEnabled();
+            mConfigParameterProvider.addOnPropertiesChangedListener(BackgroundThread.getExecutor(),
                     properties -> mIsHdrOutputControlEnabled =
-                            isDeviceConfigHdrOutputControlEnabled());
+                            mConfigParameterProvider.isHdrOutputControlFeatureEnabled());
             // Just in case the top inset changed before the system was ready. At this point, any
             // relevant configuration should be in place.
             recordTopInsetLocked(mLogicalDisplayMapper.getDisplayLocked(Display.DEFAULT_DISPLAY));
@@ -723,12 +731,6 @@
         mContext.registerReceiver(mIdleModeReceiver, filter);
     }
 
-    private boolean isDeviceConfigHdrOutputControlEnabled() {
-        return DeviceConfig.getBoolean(NAMESPACE_DISPLAY_MANAGER,
-                HDR_OUTPUT_CONTROL_FLAG,
-                true);
-    }
-
     @VisibleForTesting
     Handler getDisplayHandler() {
         return mHandler;
@@ -3135,8 +3137,9 @@
             mBrightnessTracker = new BrightnessTracker(mContext, null);
         }
 
-        final BrightnessSetting brightnessSetting = new BrightnessSetting(mPersistentDataStore,
-                display, mSyncRoot);
+        final int userSerial = getUserManager().getUserSerialNumber(mContext.getUserId());
+        final BrightnessSetting brightnessSetting = new BrightnessSetting(userSerial,
+                mPersistentDataStore, display, mSyncRoot);
         final DisplayPowerControllerInterface displayPowerController;
 
         // If display is internal and has a HighBrightnessModeMetadata mapping, use that.
@@ -3151,8 +3154,7 @@
                     + "display: " + display.getDisplayIdLocked());
             return null;
         }
-        if (DeviceConfig.getBoolean("display_manager",
-                "use_newly_structured_display_power_controller", true)) {
+        if (mConfigParameterProvider.isNewPowerControllerFeatureEnabled()) {
             displayPowerController = new DisplayPowerController2(
                     mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler,
                     mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting,
@@ -4253,6 +4255,13 @@
     }
 
     @VisibleForTesting
+    void overrideSensorManager(SensorManager sensorManager) {
+        synchronized (mSyncRoot) {
+            mSensorManager = sensorManager;
+        }
+    }
+
+    @VisibleForTesting
     final class LocalService extends DisplayManagerInternal {
 
         @Override
@@ -4505,7 +4514,7 @@
                 }
                 final DisplayDeviceConfig config = device.getDisplayDeviceConfig();
                 SensorData sensorData = config.getProximitySensor();
-                if (sensorData.matches(sensorName, sensorType)) {
+                if (sensorData != null && sensorData.matches(sensorName, sensorType)) {
                     return new RefreshRateRange(sensorData.minRefreshRate,
                             sensorData.maxRefreshRate);
                 }
@@ -4621,6 +4630,22 @@
         }
 
         @Override
+        public AmbientLightSensorData getAmbientLightSensorData(int displayId) {
+            synchronized (mSyncRoot) {
+                final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(displayId);
+                if (display == null) {
+                    return null;
+                }
+                final DisplayDevice device = display.getPrimaryDisplayDeviceLocked();
+                if (device == null) {
+                    return null;
+                }
+                SensorData data = device.getDisplayDeviceConfig().getAmbientLightSensor();
+                return new AmbientLightSensorData(data.name, data.type);
+            }
+        }
+
+        @Override
         public IntArray getDisplayGroupIds() {
             Set<Integer> visitedIds = new ArraySet<>();
             IntArray displayGroupIds = new IntArray();
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index f6e074f..ffecf2b 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -512,6 +512,9 @@
     // of the lead display that this DPC should follow.
     private float mBrightnessToFollow;
 
+    // Indicates whether we should ramp slowly to the brightness value to follow.
+    private boolean mBrightnessToFollowSlowChange;
+
     // The last auto brightness adjustment that was set by the user and not temporary. Set to
     // Float.NaN when an auto-brightness adjustment hasn't been recorded yet.
     private float mAutoBrightnessAdjustment;
@@ -812,7 +815,8 @@
     }
 
     @Override
-    public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux) {
+    public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
+            boolean slowChange) {
         mBrightnessRangeController.onAmbientLuxChange(ambientLux);
         if (nits < 0) {
             mBrightnessToFollow = leadDisplayBrightness;
@@ -825,6 +829,7 @@
                 mBrightnessToFollow = leadDisplayBrightness;
             }
         }
+        mBrightnessToFollowSlowChange = slowChange;
         sendUpdatePowerState();
     }
 
@@ -842,7 +847,7 @@
             mDisplayBrightnessFollowers.remove(follower.getDisplayId());
             mHandler.postAtTime(() -> follower.setBrightnessToFollow(
                     PowerManager.BRIGHTNESS_INVALID_FLOAT, /* nits= */ -1,
-                    /* ambientLux= */ 0), mClock.uptimeMillis());
+                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
         }
     }
 
@@ -852,7 +857,7 @@
             DisplayPowerControllerInterface follower = mDisplayBrightnessFollowers.valueAt(i);
             mHandler.postAtTime(() -> follower.setBrightnessToFollow(
                     PowerManager.BRIGHTNESS_INVALID_FLOAT, /* nits= */ -1,
-                    /* ambientLux= */ 0), mClock.uptimeMillis());
+                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
         }
         mDisplayBrightnessFollowers.clear();
     }
@@ -1559,6 +1564,7 @@
         final int oldState = mPowerState.getScreenState();
         animateScreenStateChange(state, performScreenOffTransition);
         state = mPowerState.getScreenState();
+        boolean slowChange = false;
 
         if (state == Display.STATE_OFF) {
             brightnessState = PowerManager.BRIGHTNESS_OFF_FLOAT;
@@ -1567,6 +1573,7 @@
 
         if (Float.isNaN(brightnessState) && isValidBrightnessValue(mBrightnessToFollow)) {
             brightnessState = mBrightnessToFollow;
+            slowChange = mBrightnessToFollowSlowChange;
             mBrightnessReasonTemp.setReason(BrightnessReason.REASON_FOLLOWER);
         }
 
@@ -1583,12 +1590,12 @@
                 mAllowAutoBrightnessWhileDozingConfig && Display.isDozeState(state);
         final boolean autoBrightnessEnabled = mUseAutoBrightness
                 && (state == Display.STATE_ON || autoBrightnessEnabledInDoze)
-                && Float.isNaN(brightnessState)
-                && mAutomaticBrightnessController != null
-                && mBrightnessReasonTemp.getReason() != BrightnessReason.REASON_FOLLOWER;
+                && mBrightnessReasonTemp.getReason() != BrightnessReason.REASON_OVERRIDE
+                && mAutomaticBrightnessController != null;
         final boolean autoBrightnessDisabledDueToDisplayOff = mUseAutoBrightness
                 && !(state == Display.STATE_ON || autoBrightnessEnabledInDoze);
         final int autoBrightnessState = autoBrightnessEnabled
+                    && mBrightnessReasonTemp.getReason() != BrightnessReason.REASON_FOLLOWER
                 ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
                 : autoBrightnessDisabledDueToDisplayOff
                         ? AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE
@@ -1648,9 +1655,11 @@
                     mShouldResetShortTermModel);
             mShouldResetShortTermModel = false;
         }
-        mBrightnessRangeController.setAutoBrightnessEnabled(mUseAutoBrightness
+        mBrightnessRangeController.setAutoBrightnessEnabled(autoBrightnessEnabled
                 ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
-                : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
+                : autoBrightnessDisabledDueToDisplayOff
+                        ? AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE
+                        : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
 
         if (mBrightnessTracker != null) {
             mBrightnessTracker.setShouldCollectColorSample(mBrightnessConfiguration != null
@@ -1661,7 +1670,6 @@
         float rawBrightnessState = brightnessState;
 
         // Apply auto-brightness.
-        boolean slowChange = false;
         if (Float.isNaN(brightnessState)) {
             float newAutoBrightnessAdjustment = autoBrightnessAdjustment;
             if (autoBrightnessEnabled) {
@@ -1740,6 +1748,14 @@
             mBrightnessReasonTemp.setReason(BrightnessReason.REASON_MANUAL);
         }
 
+        float ambientLux = mAutomaticBrightnessController == null ? 0
+                : mAutomaticBrightnessController.getAmbientLux();
+        for (int i = 0; i < displayBrightnessFollowers.size(); i++) {
+            DisplayPowerControllerInterface follower = displayBrightnessFollowers.valueAt(i);
+            follower.setBrightnessToFollow(rawBrightnessState, convertToNits(rawBrightnessState),
+                    ambientLux, slowChange);
+        }
+
         // Now that a desired brightness has been calculated, apply brightness throttling. The
         // dimming and low power transformations that follow can only dim brightness further.
         //
@@ -1762,14 +1778,6 @@
             mAppliedThrottling = false;
         }
 
-        float ambientLux = mAutomaticBrightnessController == null ? 0
-                : mAutomaticBrightnessController.getAmbientLux();
-        for (int i = 0; i < displayBrightnessFollowers.size(); i++) {
-            DisplayPowerControllerInterface follower = displayBrightnessFollowers.valueAt(i);
-            follower.setBrightnessToFollow(rawBrightnessState, convertToNits(rawBrightnessState),
-                    ambientLux);
-        }
-
         if (updateScreenBrightnessSetting) {
             // Tell the rest of the system about the new brightness in case we had to change it
             // for things like auto-brightness or high-brightness-mode. Note that we do this
@@ -2157,8 +2165,9 @@
         final DisplayDeviceConfig.HighBrightnessModeData hbmData =
                 ddConfig != null ? ddConfig.getHighBrightnessModeData() : null;
         final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
-        return new HighBrightnessModeController(mHandler, info.width, info.height, displayToken,
-                displayUniqueId, PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, hbmData,
+        return mInjector.getHighBrightnessModeController(mHandler, info.width, info.height,
+                displayToken, displayUniqueId, PowerManager.BRIGHTNESS_MIN,
+                PowerManager.BRIGHTNESS_MAX, hbmData,
                 new HighBrightnessModeController.HdrBrightnessDeviceConfig() {
                     @Override
                     public float getHdrBrightnessFromSdr(
@@ -2294,29 +2303,23 @@
     }
 
     private void loadAmbientLightSensor() {
-        DisplayDeviceConfig.SensorData lightSensor = mDisplayDeviceConfig.getAmbientLightSensor();
         final int fallbackType = mDisplayId == Display.DEFAULT_DISPLAY
                 ? Sensor.TYPE_LIGHT : SensorUtils.NO_FALLBACK;
-        mLightSensor = SensorUtils.findSensor(mSensorManager, lightSensor.type, lightSensor.name,
-                fallbackType);
+        mLightSensor = SensorUtils.findSensor(mSensorManager,
+                mDisplayDeviceConfig.getAmbientLightSensor(), fallbackType);
     }
 
     private void loadScreenOffBrightnessSensor() {
-        DisplayDeviceConfig.SensorData screenOffBrightnessSensor =
-                mDisplayDeviceConfig.getScreenOffBrightnessSensor();
         mScreenOffBrightnessSensor = SensorUtils.findSensor(mSensorManager,
-                screenOffBrightnessSensor.type, screenOffBrightnessSensor.name,
-                SensorUtils.NO_FALLBACK);
+                mDisplayDeviceConfig.getScreenOffBrightnessSensor(), SensorUtils.NO_FALLBACK);
     }
 
     private void loadProximitySensor() {
         if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT || mDisplayId != Display.DEFAULT_DISPLAY) {
             return;
         }
-        final DisplayDeviceConfig.SensorData proxSensor =
-                mDisplayDeviceConfig.getProximitySensor();
-        mProximitySensor = SensorUtils.findSensor(mSensorManager, proxSensor.type, proxSensor.name,
-                Sensor.TYPE_PROXIMITY);
+        mProximitySensor = SensorUtils.findSensor(mSensorManager,
+                mDisplayDeviceConfig.getProximitySensor(), Sensor.TYPE_PROXIMITY);
         if (mProximitySensor != null) {
             mProximityThreshold = Math.min(mProximitySensor.getMaximumRange(),
                     TYPICAL_PROXIMITY_THRESHOLD);
@@ -2666,9 +2669,10 @@
     }
 
     @Override
-    public void setBrightness(float brightnessValue) {
+    public void setBrightness(float brightnessValue, int userSerial) {
         // Update the setting, which will eventually call back into DPC to have us actually update
         // the display with the new value.
+        mBrightnessSetting.setUserSerial(userSerial);
         mBrightnessSetting.setBrightness(brightnessValue);
         if (mDisplayId == Display.DEFAULT_DISPLAY && mPersistBrightnessNitsForDefaultDisplay) {
             float nits = convertToNits(brightnessValue);
@@ -2948,6 +2952,7 @@
                 + mPendingScreenBrightnessSetting);
         pw.println("  mTemporaryScreenBrightness=" + mTemporaryScreenBrightness);
         pw.println("  mBrightnessToFollow=" + mBrightnessToFollow);
+        pw.println("  mBrightnessToFollowSlowChange=" + mBrightnessToFollowSlowChange);
         pw.println("  mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment);
         pw.println("  mBrightnessReason=" + mBrightnessReason);
         pw.println("  mTemporaryAutoBrightnessAdjustment=" + mTemporaryAutoBrightnessAdjustment);
@@ -3527,6 +3532,17 @@
                     brightnessMapper
             );
         }
+
+        HighBrightnessModeController getHighBrightnessModeController(Handler handler, int width,
+                int height, IBinder displayToken, String displayUniqueId, float brightnessMin,
+                float brightnessMax, DisplayDeviceConfig.HighBrightnessModeData hbmData,
+                HighBrightnessModeController.HdrBrightnessDeviceConfig hdrBrightnessCfg,
+                Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata,
+                Context context) {
+            return new HighBrightnessModeController(handler, width, height, displayToken,
+                    displayUniqueId, brightnessMin, brightnessMax, hbmData, hdrBrightnessCfg,
+                    hbmChangeCallback, hbmMetadata, context);
+        }
     }
 
     static class CachedBrightnessInfo {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 2e8c342..7043af8 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -435,6 +435,9 @@
     @Nullable
     private BrightnessMappingStrategy mIdleModeBrightnessMapper;
 
+    // Indicates whether we should ramp slowly to the brightness value to follow.
+    private boolean mBrightnessToFollowSlowChange;
+
     private boolean mIsRbcActive;
 
     // Animators.
@@ -1272,6 +1275,7 @@
         // actual state instead of the desired one.
         animateScreenStateChange(state, mDisplayStateController.shouldPerformScreenOffTransition());
         state = mPowerState.getScreenState();
+        boolean slowChange = false;
         final boolean userSetBrightnessChanged = mDisplayBrightnessController
                 .updateUserSetScreenBrightness();
 
@@ -1281,13 +1285,18 @@
         float rawBrightnessState = displayBrightnessState.getBrightness();
         mBrightnessReasonTemp.set(displayBrightnessState.getBrightnessReason());
 
+        if (displayBrightnessState.getBrightnessReason().getReason()
+                == BrightnessReason.REASON_FOLLOWER) {
+            slowChange = mBrightnessToFollowSlowChange;
+        }
+
         // Take note if the short term model was already active before applying the current
         // request changes.
         final boolean wasShortTermModelActive =
                 mAutomaticBrightnessStrategy.isShortTermModelActive();
         mAutomaticBrightnessStrategy.setAutoBrightnessState(state,
                 mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig(),
-                brightnessState, mBrightnessReasonTemp.getReason(), mPowerRequest.policy,
+                mBrightnessReasonTemp.getReason(), mPowerRequest.policy,
                 mDisplayBrightnessController.getLastUserSetScreenBrightness(),
                 userSetBrightnessChanged);
 
@@ -1297,15 +1306,16 @@
                 && (mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged()
                 || userSetBrightnessChanged);
 
-        mBrightnessRangeController.setAutoBrightnessEnabled(mAutomaticBrightnessStrategy
-                .shouldUseAutoBrightness()
+        mBrightnessRangeController.setAutoBrightnessEnabled(
+                mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()
                 ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
-                : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
+                : mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff()
+                        ? AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE
+                        : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
 
         boolean updateScreenBrightnessSetting = false;
         float currentBrightnessSetting = mDisplayBrightnessController.getCurrentBrightness();
         // Apply auto-brightness.
-        boolean slowChange = false;
         int brightnessAdjustmentFlags = 0;
         if (Float.isNaN(brightnessState)) {
             if (mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()) {
@@ -1322,10 +1332,13 @@
                     brightnessAdjustmentFlags =
                             mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags();
                     updateScreenBrightnessSetting = currentBrightnessSetting != brightnessState;
+                    mAutomaticBrightnessStrategy.setAutoBrightnessApplied(true);
                     mBrightnessReasonTemp.setReason(BrightnessReason.REASON_AUTOMATIC);
                     if (mScreenOffBrightnessSensorController != null) {
                         mScreenOffBrightnessSensorController.setLightSensorEnabled(false);
                     }
+                } else {
+                    mAutomaticBrightnessStrategy.setAutoBrightnessApplied(false);
                 }
             }
         } else {
@@ -1333,6 +1346,7 @@
             // to clamping so that they don't go beyond the current max as specified by HBM
             // Controller.
             brightnessState = clampScreenBrightness(brightnessState);
+            mAutomaticBrightnessStrategy.setAutoBrightnessApplied(false);
         }
 
         // Use default brightness when dozing unless overridden.
@@ -1372,6 +1386,15 @@
             mBrightnessReasonTemp.setReason(BrightnessReason.REASON_MANUAL);
         }
 
+        float ambientLux = mAutomaticBrightnessController == null ? 0
+                : mAutomaticBrightnessController.getAmbientLux();
+        for (int i = 0; i < displayBrightnessFollowers.size(); i++) {
+            DisplayPowerControllerInterface follower = displayBrightnessFollowers.valueAt(i);
+            follower.setBrightnessToFollow(rawBrightnessState,
+                    mDisplayBrightnessController.convertToNits(rawBrightnessState),
+                    ambientLux, slowChange);
+        }
+
         // Now that a desired brightness has been calculated, apply brightness throttling. The
         // dimming and low power transformations that follow can only dim brightness further.
         //
@@ -1394,15 +1417,6 @@
             mAppliedThrottling = false;
         }
 
-        float ambientLux = mAutomaticBrightnessController == null ? 0
-                : mAutomaticBrightnessController.getAmbientLux();
-        for (int i = 0; i < displayBrightnessFollowers.size(); i++) {
-            DisplayPowerControllerInterface follower = displayBrightnessFollowers.valueAt(i);
-            follower.setBrightnessToFollow(rawBrightnessState,
-                    mDisplayBrightnessController.convertToNits(rawBrightnessState),
-                    ambientLux);
-        }
-
         if (updateScreenBrightnessSetting) {
             // Tell the rest of the system about the new brightness in case we had to change it
             // for things like auto-brightness or high-brightness-mode. Note that we do this
@@ -1804,9 +1818,9 @@
         final DisplayDeviceConfig.HighBrightnessModeData hbmData =
                 ddConfig != null ? ddConfig.getHighBrightnessModeData() : null;
         final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
-        return new HighBrightnessModeController(mHandler, info.width, info.height, displayToken,
-                displayUniqueId, PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, hbmData,
-                (sdrBrightness, maxDesiredHdrSdrRatio) ->
+        return mInjector.getHighBrightnessModeController(mHandler, info.width, info.height,
+                displayToken, displayUniqueId, PowerManager.BRIGHTNESS_MIN,
+                PowerManager.BRIGHTNESS_MAX, hbmData, (sdrBrightness, maxDesiredHdrSdrRatio) ->
                         mDisplayDeviceConfig.getHdrBrightnessFromSdr(sdrBrightness,
                                 maxDesiredHdrSdrRatio), modeChangeCallback, hbmMetadata, mContext);
     }
@@ -1936,19 +1950,15 @@
     }
 
     private void loadAmbientLightSensor() {
-        DisplayDeviceConfig.SensorData lightSensor = mDisplayDeviceConfig.getAmbientLightSensor();
         final int fallbackType = mDisplayId == Display.DEFAULT_DISPLAY
                 ? Sensor.TYPE_LIGHT : SensorUtils.NO_FALLBACK;
-        mLightSensor = SensorUtils.findSensor(mSensorManager, lightSensor.type, lightSensor.name,
-                fallbackType);
+        mLightSensor = SensorUtils.findSensor(mSensorManager,
+                mDisplayDeviceConfig.getAmbientLightSensor(), fallbackType);
     }
 
     private void loadScreenOffBrightnessSensor() {
-        DisplayDeviceConfig.SensorData screenOffBrightnessSensor =
-                mDisplayDeviceConfig.getScreenOffBrightnessSensor();
         mScreenOffBrightnessSensor = SensorUtils.findSensor(mSensorManager,
-                screenOffBrightnessSensor.type, screenOffBrightnessSensor.name,
-                SensorUtils.NO_FALLBACK);
+                mDisplayDeviceConfig.getScreenOffBrightnessSensor(), SensorUtils.NO_FALLBACK);
     }
 
     private float clampScreenBrightness(float value) {
@@ -2174,8 +2184,8 @@
     }
 
     @Override
-    public void setBrightness(float brightnessValue) {
-        mDisplayBrightnessController.setBrightness(brightnessValue);
+    public void setBrightness(float brightnessValue, int userSerial) {
+        mDisplayBrightnessController.setBrightness(brightnessValue, userSerial);
     }
 
     @Override
@@ -2189,7 +2199,8 @@
     }
 
     @Override
-    public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux) {
+    public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
+            boolean slowChange) {
         mBrightnessRangeController.onAmbientLuxChange(ambientLux);
         if (nits < 0) {
             mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness);
@@ -2202,6 +2213,7 @@
                 mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness);
             }
         }
+        mBrightnessToFollowSlowChange = slowChange;
         sendUpdatePowerState();
     }
 
@@ -2261,7 +2273,7 @@
             mDisplayBrightnessFollowers.remove(follower.getDisplayId());
             mHandler.postAtTime(() -> follower.setBrightnessToFollow(
                     PowerManager.BRIGHTNESS_INVALID_FLOAT, /* nits= */ -1,
-                    /* ambientLux= */ 0), mClock.uptimeMillis());
+                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
         }
     }
 
@@ -2271,7 +2283,7 @@
             DisplayPowerControllerInterface follower = mDisplayBrightnessFollowers.valueAt(i);
             mHandler.postAtTime(() -> follower.setBrightnessToFollow(
                     PowerManager.BRIGHTNESS_INVALID_FLOAT, /* nits= */ -1,
-                    /* ambientLux= */ 0), mClock.uptimeMillis());
+                    /* ambientLux= */ 0, /* slowChange= */ false), mClock.uptimeMillis());
         }
         mDisplayBrightnessFollowers.clear();
     }
@@ -2341,6 +2353,7 @@
         pw.println("  mReportedToPolicy="
                 + reportedToPolicyToString(mReportedScreenStateToPolicy));
         pw.println("  mIsRbcActive=" + mIsRbcActive);
+        pw.println("  mBrightnessToFollowSlowChange=" + mBrightnessToFollowSlowChange);
         IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "    ");
         mAutomaticBrightnessStrategy.dump(ipw);
 
@@ -2892,6 +2905,17 @@
                     brightnessMapper
             );
         }
+
+        HighBrightnessModeController getHighBrightnessModeController(Handler handler, int width,
+                int height, IBinder displayToken, String displayUniqueId, float brightnessMin,
+                float brightnessMax, DisplayDeviceConfig.HighBrightnessModeData hbmData,
+                HighBrightnessModeController.HdrBrightnessDeviceConfig hdrBrightnessCfg,
+                Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata,
+                Context context) {
+            return new HighBrightnessModeController(handler, width, height, displayToken,
+                    displayUniqueId, brightnessMin, brightnessMax, hbmData, hdrBrightnessCfg,
+                    hbmChangeCallback, hbmMetadata, context);
+        }
     }
 
     static class CachedBrightnessInfo {
diff --git a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
index 73edb97..e3108c9 100644
--- a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
+++ b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
@@ -29,7 +29,7 @@
  * An interface to manage the display's power state and brightness
  */
 public interface DisplayPowerControllerInterface {
-
+    int DEFAULT_USER_SERIAL = -1;
     /**
      * Notified when the display is changed.
      *
@@ -98,7 +98,17 @@
      * Set the screen brightness of the associated display
      * @param brightness The value to which the brightness is to be set
      */
-    void setBrightness(float brightness);
+    default void setBrightness(float brightness) {
+        setBrightness(brightness, DEFAULT_USER_SERIAL);
+    }
+
+    /**
+     * Set the screen brightness of the associated display
+     * @param brightness The value to which the brightness is to be set
+     * @param userSerial The user for which the brightness value is to be set. Use userSerial = -1,
+     * if brightness needs to be updated for the current user.
+     */
+    void setBrightness(float brightness, int userSerial);
 
     /**
      * Checks if the proximity sensor is available
@@ -191,8 +201,10 @@
      * @param nits The brightness value in nits if the device supports nits. Set to a negative
      *             number otherwise.
      * @param ambientLux The lux value that will be passed to {@link HighBrightnessModeController}
+     * @param slowChange Indicates whether we should slowly animate to the given brightness value.
      */
-    void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux);
+    void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
+            boolean slowChange);
 
     /**
      * Add an additional display that will copy the brightness value from this display. This is used
diff --git a/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java b/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java
index c074786..882c02f 100644
--- a/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java
@@ -358,10 +358,8 @@
         if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT || mDisplayId != Display.DEFAULT_DISPLAY) {
             return;
         }
-        final DisplayDeviceConfig.SensorData proxSensor =
-                mDisplayDeviceConfig.getProximitySensor();
-        mProximitySensor = SensorUtils.findSensor(mSensorManager, proxSensor.type, proxSensor.name,
-                Sensor.TYPE_PROXIMITY);
+        mProximitySensor = SensorUtils.findSensor(mSensorManager,
+                mDisplayDeviceConfig.getProximitySensor(), Sensor.TYPE_PROXIMITY);
         if (mProximitySensor != null) {
             mProximityThreshold = Math.min(mProximitySensor.getMaximumRange(),
                     TYPICAL_PROXIMITY_THRESHOLD);
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 7701bc6..89d865e 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -496,7 +496,7 @@
         private void loadDisplayDeviceConfig() {
             // Load display device config
             final Context context = getOverlayContext();
-            mDisplayDeviceConfig = DisplayDeviceConfig.create(context, mPhysicalDisplayId,
+            mDisplayDeviceConfig = mInjector.createDisplayDeviceConfig(context, mPhysicalDisplayId,
                     mIsFirstDisplay);
 
             // Load brightness HWC quirk
@@ -1336,6 +1336,11 @@
         public SurfaceControlProxy getSurfaceControlProxy() {
             return new SurfaceControlProxy();
         }
+
+        public DisplayDeviceConfig createDisplayDeviceConfig(Context context,
+                long physicalDisplayId, boolean isFirstDisplay) {
+            return DisplayDeviceConfig.create(context, physicalDisplayId, isFirstDisplay);
+        }
     }
 
     public interface DisplayEventListener {
diff --git a/services/core/java/com/android/server/display/NormalBrightnessModeController.java b/services/core/java/com/android/server/display/NormalBrightnessModeController.java
index dbabc24..135ebd8 100644
--- a/services/core/java/com/android/server/display/NormalBrightnessModeController.java
+++ b/services/core/java/com/android/server/display/NormalBrightnessModeController.java
@@ -21,6 +21,7 @@
 
 import com.android.server.display.DisplayDeviceConfig.BrightnessLimitMapType;
 
+import java.io.PrintWriter;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -60,6 +61,14 @@
         return recalculateMaxBrightness();
     }
 
+    void dump(PrintWriter pw) {
+        pw.println("NormalBrightnessModeController:");
+        pw.println("  mAutoBrightnessEnabled=" + mAutoBrightnessEnabled);
+        pw.println("  mAmbientLux=" + mAmbientLux);
+        pw.println("  mMaxBrightness=" + mMaxBrightness);
+        pw.println("  mMaxBrightnessLimits=" + mMaxBrightnessLimits);
+    }
+
     private boolean recalculateMaxBrightness() {
         float foundAmbientBoundary = Float.MAX_VALUE;
         float foundMaxBrightness = PowerManager.BRIGHTNESS_MAX;
diff --git a/services/core/java/com/android/server/display/PersistentDataStore.java b/services/core/java/com/android/server/display/PersistentDataStore.java
index 6d6ed72..2d7792d 100644
--- a/services/core/java/com/android/server/display/PersistentDataStore.java
+++ b/services/core/java/com/android/server/display/PersistentDataStore.java
@@ -133,6 +133,7 @@
 
     private static final String TAG_BRIGHTNESS_NITS_FOR_DEFAULT_DISPLAY =
             "brightness-nits-for-default-display";
+    public static final int DEFAULT_USER_ID = -1;
 
     // Remembered Wifi display devices.
     private ArrayList<WifiDisplay> mRememberedWifiDisplays = new ArrayList<WifiDisplay>();
@@ -294,7 +295,7 @@
         return false;
     }
 
-    public float getBrightness(DisplayDevice device) {
+    public float getBrightness(DisplayDevice device, int userSerial) {
         if (device == null || !device.hasStableUniqueId()) {
             return Float.NaN;
         }
@@ -302,10 +303,10 @@
         if (state == null) {
             return Float.NaN;
         }
-        return state.getBrightness();
+        return state.getBrightness(userSerial);
     }
 
-    public boolean setBrightness(DisplayDevice displayDevice, float brightness) {
+    public boolean setBrightness(DisplayDevice displayDevice, float brightness, int userSerial) {
         if (displayDevice == null || !displayDevice.hasStableUniqueId()) {
             return false;
         }
@@ -314,7 +315,7 @@
             return false;
         }
         final DisplayState state = getDisplayState(displayDeviceUniqueId, true);
-        if (state.setBrightness(brightness)) {
+        if (state.setBrightness(brightness, userSerial)) {
             setDirty();
             return true;
         }
@@ -611,6 +612,7 @@
             state.saveToXml(serializer);
             serializer.endTag(null, TAG_DISPLAY);
         }
+
         serializer.endTag(null, TAG_DISPLAY_STATES);
         serializer.startTag(null, TAG_STABLE_DEVICE_VALUES);
         mStableDeviceValues.saveToXml(serializer);
@@ -649,7 +651,8 @@
 
     private static final class DisplayState {
         private int mColorMode;
-        private float mBrightness = Float.NaN;
+
+        private SparseArray<Float> mPerUserBrightness = new SparseArray<>();
         private int mWidth;
         private int mHeight;
         private float mRefreshRate;
@@ -670,16 +673,25 @@
             return mColorMode;
         }
 
-        public boolean setBrightness(float brightness) {
-            if (brightness == mBrightness) {
+        public boolean setBrightness(float brightness, int userSerial) {
+            // Remove the default user brightness, before setting a new user-specific value.
+            // This is a one-time operation, required to restructure the config after user-specific
+            // brightness was introduced.
+            mPerUserBrightness.remove(DEFAULT_USER_ID);
+
+            if (getBrightness(userSerial) == brightness) {
                 return false;
             }
-            mBrightness = brightness;
+            mPerUserBrightness.set(userSerial, brightness);
             return true;
         }
 
-        public float getBrightness() {
-            return mBrightness;
+        public float getBrightness(int userSerial) {
+            float brightness = mPerUserBrightness.get(userSerial, Float.NaN);
+            if (Float.isNaN(brightness)) {
+                brightness = mPerUserBrightness.get(DEFAULT_USER_ID, Float.NaN);
+            }
+            return brightness;
         }
 
         public boolean setBrightnessConfiguration(BrightnessConfiguration configuration,
@@ -729,12 +741,7 @@
                         mColorMode = Integer.parseInt(value);
                         break;
                     case TAG_BRIGHTNESS_VALUE:
-                        String brightness = parser.nextText();
-                        try {
-                            mBrightness = Float.parseFloat(brightness);
-                        } catch (NumberFormatException e) {
-                            mBrightness = Float.NaN;
-                        }
+                        loadBrightnessFromXml(parser);
                         break;
                     case TAG_BRIGHTNESS_CONFIGURATIONS:
                         mDisplayBrightnessConfigurations.loadFromXml(parser);
@@ -760,11 +767,12 @@
             serializer.text(Integer.toString(mColorMode));
             serializer.endTag(null, TAG_COLOR_MODE);
 
-            serializer.startTag(null, TAG_BRIGHTNESS_VALUE);
-            if (!Float.isNaN(mBrightness)) {
-                serializer.text(Float.toString(mBrightness));
+            for (int i = 0; i < mPerUserBrightness.size(); i++) {
+                serializer.startTag(null, TAG_BRIGHTNESS_VALUE);
+                serializer.attributeInt(null, ATTR_USER_SERIAL, mPerUserBrightness.keyAt(i));
+                serializer.text(Float.toString(mPerUserBrightness.valueAt(i)));
+                serializer.endTag(null, TAG_BRIGHTNESS_VALUE);
             }
-            serializer.endTag(null, TAG_BRIGHTNESS_VALUE);
 
             serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATIONS);
             mDisplayBrightnessConfigurations.saveToXml(serializer);
@@ -785,12 +793,33 @@
 
         public void dump(final PrintWriter pw, final String prefix) {
             pw.println(prefix + "ColorMode=" + mColorMode);
-            pw.println(prefix + "BrightnessValue=" + mBrightness);
+            pw.println(prefix + "BrightnessValues: ");
+            for (int i = 0; i < mPerUserBrightness.size(); i++) {
+                pw.println("User: " + mPerUserBrightness.keyAt(i)
+                        + " Value: " + mPerUserBrightness.valueAt(i));
+            }
             pw.println(prefix + "DisplayBrightnessConfigurations: ");
             mDisplayBrightnessConfigurations.dump(pw, prefix);
             pw.println(prefix + "Resolution=" + mWidth + " " + mHeight);
             pw.println(prefix + "RefreshRate=" + mRefreshRate);
         }
+
+        private void loadBrightnessFromXml(TypedXmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            int userSerial;
+            try {
+                userSerial = parser.getAttributeInt(null, ATTR_USER_SERIAL);
+            } catch (NumberFormatException | XmlPullParserException e) {
+                userSerial = DEFAULT_USER_ID;
+                Slog.e(TAG, "Failed to read user serial", e);
+            }
+            String brightness = parser.nextText();
+            try {
+                mPerUserBrightness.set(userSerial, Float.parseFloat(brightness));
+            } catch (NumberFormatException nfe) {
+                Slog.e(TAG, "Failed to read brightness", nfe);
+            }
+        }
     }
 
     private static final class StableDeviceValues {
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
index 7574de8..2f52b70 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
@@ -38,6 +38,8 @@
  * display. Applies the chosen brightness.
  */
 public final class DisplayBrightnessController {
+    private static final int DEFAULT_USER_SERIAL = -1;
+
     // The ID of the display tied to this DisplayBrightnessController
     private final int mDisplayId;
 
@@ -274,8 +276,16 @@
      * Notifies the brightnessSetting to persist the supplied brightness value.
      */
     public void setBrightness(float brightnessValue) {
+        setBrightness(brightnessValue, DEFAULT_USER_SERIAL);
+    }
+
+    /**
+     * Notifies the brightnessSetting to persist the supplied brightness value for a user.
+     */
+    public void setBrightness(float brightnessValue, int userSerial) {
         // Update the setting, which will eventually call back into DPC to have us actually
         // update the display with the new value.
+        mBrightnessSetting.setUserSerial(userSerial);
         mBrightnessSetting.setBrightness(brightnessValue);
         if (mDisplayId == Display.DEFAULT_DISPLAY && mPersistBrightnessNitsForDefaultDisplay) {
             float nits = convertToNits(brightnessValue);
diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
index 0e885dc..bcd5259 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
@@ -73,6 +73,9 @@
     // the user has enabled the auto-brightness from the settings, it is disabled because the
     // display is off
     private boolean mIsAutoBrightnessEnabled = false;
+    // Indicates if auto-brightness is disabled due to the display being off. Needed for metric
+    // purposes.
+    private boolean mAutoBrightnessDisabledDueToDisplayOff;
     // If the auto-brightness model for the last manual changes done by the user.
     private boolean mIsShortTermModelActive = false;
 
@@ -96,24 +99,21 @@
      * AutomaticBrightnessController accounting for any manual changes made by the user.
      */
     public void setAutoBrightnessState(int targetDisplayState,
-            boolean allowAutoBrightnessWhileDozingConfig,
-            float brightnessState, int brightnessReason, int policy,
+            boolean allowAutoBrightnessWhileDozingConfig, int brightnessReason, int policy,
             float lastUserSetScreenBrightness, boolean userSetBrightnessChanged) {
         final boolean autoBrightnessEnabledInDoze =
                 allowAutoBrightnessWhileDozingConfig
                         && Display.isDozeState(targetDisplayState);
         mIsAutoBrightnessEnabled = shouldUseAutoBrightness()
                 && (targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze)
-                && (Float.isNaN(brightnessState)
-                || brightnessReason == BrightnessReason.REASON_TEMPORARY
-                || brightnessReason == BrightnessReason.REASON_BOOST)
-                && mAutomaticBrightnessController != null
-                && brightnessReason != BrightnessReason.REASON_FOLLOWER;
-        final boolean autoBrightnessDisabledDueToDisplayOff = shouldUseAutoBrightness()
+                && brightnessReason != BrightnessReason.REASON_OVERRIDE
+                && mAutomaticBrightnessController != null;
+        mAutoBrightnessDisabledDueToDisplayOff = shouldUseAutoBrightness()
                 && !(targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze);
         final int autoBrightnessState = mIsAutoBrightnessEnabled
+                    && brightnessReason != BrightnessReason.REASON_FOLLOWER
                 ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
-                : autoBrightnessDisabledDueToDisplayOff
+                : mAutoBrightnessDisabledDueToDisplayOff
                         ? AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE
                         : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED;
 
@@ -125,6 +125,10 @@
         return mIsAutoBrightnessEnabled;
     }
 
+    public boolean isAutoBrightnessDisabledDueToDisplayOff() {
+        return mAutoBrightnessDisabledDueToDisplayOff;
+    }
+
     /**
      * Updates the {@link BrightnessConfiguration} that is currently being used by the associated
      * display.
@@ -287,8 +291,6 @@
         mAutoBrightnessAdjustmentReasonsFlags = isTemporaryAutoBrightnessAdjustmentApplied()
                 ? BrightnessReason.ADJUSTMENT_AUTO_TEMP
                 : BrightnessReason.ADJUSTMENT_AUTO;
-        mAppliedAutoBrightness = BrightnessUtils.isValidBrightnessValue(brightnessState)
-                || brightnessState == PowerManager.BRIGHTNESS_OFF_FLOAT;
         float newAutoBrightnessAdjustment =
                 (mAutomaticBrightnessController != null)
                         ? mAutomaticBrightnessController.getAutomaticScreenBrightnessAdjustment()
@@ -345,8 +347,7 @@
     /**
      * Sets if the auto-brightness is applied on the latest brightness change.
      */
-    @VisibleForTesting
-    void setAutoBrightnessApplied(boolean autoBrightnessApplied) {
+    public void setAutoBrightnessApplied(boolean autoBrightnessApplied) {
         mAppliedAutoBrightness = autoBrightnessApplied;
     }
 
diff --git a/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java b/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java
new file mode 100644
index 0000000..feebdf1
--- /dev/null
+++ b/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.feature;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.display.DisplayManager;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfigInterface;
+import android.util.Slog;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Helper class to access all DeviceConfig features for display_manager namespace
+ *
+ **/
+public class DeviceConfigParameterProvider {
+
+    private static final String TAG = "DisplayFeatureProvider";
+
+    private final DeviceConfigInterface mDeviceConfig;
+
+    public DeviceConfigParameterProvider(DeviceConfigInterface deviceConfig) {
+        mDeviceConfig = deviceConfig;
+    }
+
+    // feature: revamping_display_power_controller_feature
+    // parameter: use_newly_structured_display_power_controller
+    public boolean isNewPowerControllerFeatureEnabled() {
+        return mDeviceConfig.getBoolean(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                DisplayManager.DeviceConfig.KEY_NEW_POWER_CONTROLLER, true);
+    }
+
+    // feature: hdr_output_control
+    // parameter: enable_hdr_output_control
+    public boolean isHdrOutputControlFeatureEnabled() {
+        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                DisplayManager.HDR_OUTPUT_CONTROL_FLAG, true);
+    }
+
+    // feature: flexible_brightness_range_feature
+    // parameter: normal_brightness_mode_controller_enabled
+    public boolean isNormalBrightnessControllerFeatureEnabled() {
+        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                DisplayManager.DeviceConfig.KEY_USE_NORMAL_BRIGHTNESS_MODE_CONTROLLER, false);
+    }
+
+    // feature: smooth_display_feature
+    // parameter: peak_refresh_rate_default
+    public float getPeakRefreshRateDefault() {
+        return mDeviceConfig.getFloat(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                DisplayManager.DeviceConfig.KEY_PEAK_REFRESH_RATE_DEFAULT, -1);
+    }
+
+    // Test parameters
+    // usage e.g.: adb shell device_config put display_manager refresh_rate_in_hbm_sunlight 90
+
+    // allows to customize brightness throttling data
+    public String getBrightnessThrottlingData() {
+        return mDeviceConfig.getString(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                DisplayManager.DeviceConfig.KEY_BRIGHTNESS_THROTTLING_DATA, null);
+    }
+
+    public int getRefreshRateInHbmSunlight() {
+        return mDeviceConfig.getInt(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HBM_SUNLIGHT, -1);
+    }
+
+    public int getRefreshRateInHbmHdr() {
+        return mDeviceConfig.getInt(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HBM_HDR, -1);
+    }
+
+
+    public int getRefreshRateInHighZone() {
+        return mDeviceConfig.getInt(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE, -1);
+    }
+
+    public int getRefreshRateInLowZone() {
+        return mDeviceConfig.getInt(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE, -1);
+    }
+
+    /** Return null if no such property or wrong format (not comma separated integers). */
+    @Nullable
+    public int[] getHighAmbientBrightnessThresholds() {
+        return getIntArrayProperty(DisplayManager.DeviceConfig
+                .KEY_FIXED_REFRESH_RATE_HIGH_AMBIENT_BRIGHTNESS_THRESHOLDS);
+    }
+
+    /** Return null if no such property or wrong format (not comma separated integers). */
+    @Nullable
+    public int[] getHighDisplayBrightnessThresholds() {
+        return getIntArrayProperty(DisplayManager.DeviceConfig
+                .KEY_FIXED_REFRESH_RATE_HIGH_DISPLAY_BRIGHTNESS_THRESHOLDS);
+    }
+
+    /** Return null if no such property or wrong format (not comma separated integers). */
+    @Nullable
+    public int[] getLowDisplayBrightnessThresholds() {
+        return getIntArrayProperty(DisplayManager.DeviceConfig
+                .KEY_FIXED_REFRESH_RATE_LOW_DISPLAY_BRIGHTNESS_THRESHOLDS);
+    }
+
+    /** Return null if no such property or wrong format (not comma separated integers). */
+    @Nullable
+    public int[] getLowAmbientBrightnessThresholds() {
+        return getIntArrayProperty(DisplayManager.DeviceConfig
+                .KEY_FIXED_REFRESH_RATE_LOW_AMBIENT_BRIGHTNESS_THRESHOLDS);
+    }
+
+    /** add property change listener to DeviceConfig */
+    public void addOnPropertiesChangedListener(Executor executor,
+            DeviceConfig.OnPropertiesChangedListener listener) {
+        mDeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                executor, listener);
+    }
+
+    /** remove property change listener from DeviceConfig */
+    public void removeOnPropertiesChangedListener(
+            DeviceConfig.OnPropertiesChangedListener listener) {
+        mDeviceConfig.removeOnPropertiesChangedListener(listener);
+    }
+
+    @Nullable
+    private int[] getIntArrayProperty(String prop) {
+        String strArray = mDeviceConfig.getString(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, prop,
+                null);
+
+        if (strArray != null) {
+            return parseIntArray(strArray);
+        }
+        return null;
+    }
+
+    @Nullable
+    private int[] parseIntArray(@NonNull String strArray) {
+        String[] items = strArray.split(",");
+        int[] array = new int[items.length];
+
+        try {
+            for (int i = 0; i < array.length; i++) {
+                array[i] = Integer.parseInt(items[i]);
+            }
+        } catch (NumberFormatException e) {
+            Slog.e(TAG, "Incorrect format for array: '" + strArray + "'", e);
+            array = null;
+        }
+
+        return array;
+    }
+}
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index 1889578..11e35ce 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -20,6 +20,7 @@
 import static android.hardware.display.DisplayManagerInternal.REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE;
 import static android.os.PowerManager.BRIGHTNESS_INVALID;
 
+import android.annotation.IntegerRes;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ContentResolver;
@@ -68,6 +69,7 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.server.LocalServices;
 import com.android.server.display.DisplayDeviceConfig;
+import com.android.server.display.feature.DeviceConfigParameterProvider;
 import com.android.server.display.utils.AmbientFilter;
 import com.android.server.display.utils.AmbientFilterFactory;
 import com.android.server.display.utils.SensorUtils;
@@ -84,6 +86,7 @@
 import java.util.Locale;
 import java.util.Objects;
 import java.util.concurrent.Callable;
+import java.util.function.IntSupplier;
 
 /**
  * The DisplayModeDirector is responsible for determining what modes are allowed to be automatically
@@ -117,7 +120,7 @@
     private final SensorObserver mSensorObserver;
     private final HbmObserver mHbmObserver;
     private final SkinThermalStatusObserver mSkinThermalStatusObserver;
-    private final DeviceConfigInterface mDeviceConfig;
+    private final DeviceConfigParameterProvider mConfigParameterProvider;
     private final DeviceConfigDisplaySettings mDeviceConfigDisplaySettings;
 
     @GuardedBy("mLock")
@@ -157,7 +160,7 @@
         mSupportedModesByDisplay = new SparseArray<>();
         mDefaultModeByDisplay = new SparseArray<>();
         mAppRequestObserver = new AppRequestObserver();
-        mDeviceConfig = injector.getDeviceConfig();
+        mConfigParameterProvider = new DeviceConfigParameterProvider(injector.getDeviceConfig());
         mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings();
         mSettingsObserver = new SettingsObserver(context, handler);
         mBrightnessObserver = new BrightnessObserver(context, handler, injector);
@@ -681,9 +684,9 @@
         synchronized (mLock) {
             mDefaultDisplayDeviceConfig = displayDeviceConfig;
             mSettingsObserver.setRefreshRates(displayDeviceConfig,
-                /* attemptLoadingFromDeviceConfig= */ true);
+                /* attemptReadFromFeatureParams= */ true);
             mBrightnessObserver.updateBlockingZoneThresholds(displayDeviceConfig,
-                /* attemptLoadingFromDeviceConfig= */ true);
+                /* attemptReadFromFeatureParams= */ true);
             mBrightnessObserver.reloadLightSensor(displayDeviceConfig);
             mHbmObserver.setupHdrRefreshRates(displayDeviceConfig);
         }
@@ -1087,7 +1090,7 @@
             // reading from the DeviceConfig is an intensive IO operation and having it in the
             // startup phase where we thrive to keep the latency very low has significant impact.
             setRefreshRates(/* displayDeviceConfig= */ null,
-                /* attemptLoadingFromDeviceConfig= */ false);
+                /* attemptReadFromFeatureParams= */ false);
         }
 
         /**
@@ -1095,8 +1098,8 @@
          * if missing from DisplayDeviceConfig, and finally fallback to config.xml.
          */
         public void setRefreshRates(DisplayDeviceConfig displayDeviceConfig,
-                boolean attemptLoadingFromDeviceConfig) {
-            setDefaultPeakRefreshRate(displayDeviceConfig, attemptLoadingFromDeviceConfig);
+                boolean attemptReadFromFeatureParams) {
+            setDefaultPeakRefreshRate(displayDeviceConfig, attemptReadFromFeatureParams);
             mDefaultRefreshRate =
                     (displayDeviceConfig == null) ? (float) mContext.getResources().getInteger(
                         R.integer.config_defaultRefreshRate)
@@ -1113,9 +1116,9 @@
             cr.registerContentObserver(mMatchContentFrameRateSetting, false /*notifyDescendants*/,
                     this);
 
-            Float deviceConfigDefaultPeakRefresh =
-                    mDeviceConfigDisplaySettings.getDefaultPeakRefreshRate();
-            if (deviceConfigDefaultPeakRefresh != null) {
+            float deviceConfigDefaultPeakRefresh =
+                    mConfigParameterProvider.getPeakRefreshRateDefault();
+            if (deviceConfigDefaultPeakRefresh != -1) {
                 mDefaultPeakRefreshRate = deviceConfigDefaultPeakRefresh;
             }
 
@@ -1137,7 +1140,7 @@
             synchronized (mLock) {
                 if (defaultPeakRefreshRate == null) {
                     setDefaultPeakRefreshRate(mDefaultDisplayDeviceConfig,
-                            /* attemptLoadingFromDeviceConfig= */ false);
+                            /* attemptReadFromFeatureParams= */ false);
                     updateRefreshRateSettingLocked();
                 } else if (mDefaultPeakRefreshRate != defaultPeakRefreshRate) {
                     mDefaultPeakRefreshRate = defaultPeakRefreshRate;
@@ -1171,18 +1174,17 @@
         }
 
         private void setDefaultPeakRefreshRate(DisplayDeviceConfig displayDeviceConfig,
-                boolean attemptLoadingFromDeviceConfig) {
-            Float defaultPeakRefreshRate = null;
+                boolean attemptReadFromFeatureParams) {
+            float defaultPeakRefreshRate = -1;
 
-            if (attemptLoadingFromDeviceConfig) {
+            if (attemptReadFromFeatureParams) {
                 try {
-                    defaultPeakRefreshRate =
-                        mDeviceConfigDisplaySettings.getDefaultPeakRefreshRate();
+                    defaultPeakRefreshRate = mConfigParameterProvider.getPeakRefreshRateDefault();
                 } catch (Exception exception) {
                     // Do nothing
                 }
             }
-            if (defaultPeakRefreshRate == null) {
+            if (defaultPeakRefreshRate == -1) {
                 defaultPeakRefreshRate =
                         (displayDeviceConfig == null) ? (float) mContext.getResources().getInteger(
                                 R.integer.config_defaultPeakRefreshRate)
@@ -1528,7 +1530,7 @@
             mHandler = handler;
             mInjector = injector;
             updateBlockingZoneThresholds(/* displayDeviceConfig= */ null,
-                /* attemptLoadingFromDeviceConfig= */ false);
+                /* attemptReadFromFeatureParams= */ false);
             mRefreshRateInHighZone = context.getResources().getInteger(
                     R.integer.config_fixedRefreshRateInHighZone);
         }
@@ -1537,10 +1539,10 @@
          * This is used to update the blocking zone thresholds from the DeviceConfig, which
          * if missing from DisplayDeviceConfig, and finally fallback to config.xml.
          */
-        public void updateBlockingZoneThresholds(DisplayDeviceConfig displayDeviceConfig,
-                boolean attemptLoadingFromDeviceConfig) {
-            loadLowBrightnessThresholds(displayDeviceConfig, attemptLoadingFromDeviceConfig);
-            loadHighBrightnessThresholds(displayDeviceConfig, attemptLoadingFromDeviceConfig);
+        public void updateBlockingZoneThresholds(@Nullable DisplayDeviceConfig displayDeviceConfig,
+                boolean attemptReadFromFeatureParams) {
+            loadLowBrightnessThresholds(displayDeviceConfig, attemptReadFromFeatureParams);
+            loadHighBrightnessThresholds(displayDeviceConfig, attemptReadFromFeatureParams);
         }
 
         @VisibleForTesting
@@ -1579,20 +1581,20 @@
             return mRefreshRateInLowZone;
         }
 
-        private void loadLowBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig,
-                boolean attemptLoadingFromDeviceConfig) {
-            loadRefreshRateInHighZone(displayDeviceConfig, attemptLoadingFromDeviceConfig);
-            loadRefreshRateInLowZone(displayDeviceConfig, attemptLoadingFromDeviceConfig);
+        private void loadLowBrightnessThresholds(@Nullable DisplayDeviceConfig displayDeviceConfig,
+                boolean attemptReadFromFeatureParams) {
+            loadRefreshRateInHighZone(displayDeviceConfig, attemptReadFromFeatureParams);
+            loadRefreshRateInLowZone(displayDeviceConfig, attemptReadFromFeatureParams);
             mLowDisplayBrightnessThresholds = loadBrightnessThresholds(
-                    () -> mDeviceConfigDisplaySettings.getLowDisplayBrightnessThresholds(),
+                    () -> mConfigParameterProvider.getLowDisplayBrightnessThresholds(),
                     () -> displayDeviceConfig.getLowDisplayBrightnessThresholds(),
                     R.array.config_brightnessThresholdsOfPeakRefreshRate,
-                    displayDeviceConfig, attemptLoadingFromDeviceConfig);
+                    displayDeviceConfig, attemptReadFromFeatureParams);
             mLowAmbientBrightnessThresholds = loadBrightnessThresholds(
-                    () -> mDeviceConfigDisplaySettings.getLowAmbientBrightnessThresholds(),
+                    () -> mConfigParameterProvider.getLowAmbientBrightnessThresholds(),
                     () -> displayDeviceConfig.getLowAmbientBrightnessThresholds(),
                     R.array.config_ambientThresholdsOfPeakRefreshRate,
-                    displayDeviceConfig, attemptLoadingFromDeviceConfig);
+                    displayDeviceConfig, attemptReadFromFeatureParams);
             if (mLowDisplayBrightnessThresholds.length != mLowAmbientBrightnessThresholds.length) {
                 throw new RuntimeException("display low brightness threshold array and ambient "
                         + "brightness threshold array have different length: "
@@ -1604,55 +1606,55 @@
         }
 
         private void loadRefreshRateInLowZone(DisplayDeviceConfig displayDeviceConfig,
-                boolean attemptLoadingFromDeviceConfig) {
-            int refreshRateInLowZone =
-                    (displayDeviceConfig == null) ? mContext.getResources().getInteger(
-                        R.integer.config_defaultRefreshRateInZone)
-                        : displayDeviceConfig.getDefaultLowBlockingZoneRefreshRate();
-            if (attemptLoadingFromDeviceConfig) {
+                boolean attemptReadFromFeatureParams) {
+            int refreshRateInLowZone = -1;
+            if (attemptReadFromFeatureParams) {
                 try {
-                    refreshRateInLowZone = mDeviceConfig.getInt(
-                        DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
-                        DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE,
-                        refreshRateInLowZone);
+                    refreshRateInLowZone = mConfigParameterProvider.getRefreshRateInLowZone();
                 } catch (Exception exception) {
                     // Do nothing
                 }
             }
+            if (refreshRateInLowZone == -1) {
+                refreshRateInLowZone = (displayDeviceConfig == null)
+                        ? mContext.getResources().getInteger(
+                                R.integer.config_defaultRefreshRateInZone)
+                        : displayDeviceConfig.getDefaultLowBlockingZoneRefreshRate();
+            }
             mRefreshRateInLowZone = refreshRateInLowZone;
         }
 
         private void loadRefreshRateInHighZone(DisplayDeviceConfig displayDeviceConfig,
-                boolean attemptLoadingFromDeviceConfig) {
-            int refreshRateInHighZone =
-                    (displayDeviceConfig == null) ? mContext.getResources().getInteger(
-                        R.integer.config_fixedRefreshRateInHighZone) : displayDeviceConfig
-                        .getDefaultHighBlockingZoneRefreshRate();
-            if (attemptLoadingFromDeviceConfig) {
+                boolean attemptReadFromFeatureParams) {
+            int refreshRateInHighZone = -1;
+            if (attemptReadFromFeatureParams) {
                 try {
-                    refreshRateInHighZone = mDeviceConfig.getInt(
-                        DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
-                        DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE,
-                        refreshRateInHighZone);
+                    refreshRateInHighZone = mConfigParameterProvider.getRefreshRateInHighZone();
                 } catch (Exception exception) {
                     // Do nothing
                 }
             }
+            if (refreshRateInHighZone == -1) {
+                refreshRateInHighZone = (displayDeviceConfig == null)
+                        ? mContext.getResources().getInteger(
+                                R.integer.config_fixedRefreshRateInHighZone)
+                        : displayDeviceConfig.getDefaultHighBlockingZoneRefreshRate();
+            }
             mRefreshRateInHighZone = refreshRateInHighZone;
         }
 
         private void loadHighBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig,
-                boolean attemptLoadingFromDeviceConfig) {
+                boolean attemptReadFromFeatureParams) {
             mHighDisplayBrightnessThresholds = loadBrightnessThresholds(
-                    () -> mDeviceConfigDisplaySettings.getHighDisplayBrightnessThresholds(),
+                    () -> mConfigParameterProvider.getHighDisplayBrightnessThresholds(),
                     () -> displayDeviceConfig.getHighDisplayBrightnessThresholds(),
                     R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate,
-                    displayDeviceConfig, attemptLoadingFromDeviceConfig);
+                    displayDeviceConfig, attemptReadFromFeatureParams);
             mHighAmbientBrightnessThresholds = loadBrightnessThresholds(
-                    () -> mDeviceConfigDisplaySettings.getHighAmbientBrightnessThresholds(),
+                    () -> mConfigParameterProvider.getHighAmbientBrightnessThresholds(),
                     () -> displayDeviceConfig.getHighAmbientBrightnessThresholds(),
                     R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate,
-                    displayDeviceConfig, attemptLoadingFromDeviceConfig);
+                    displayDeviceConfig, attemptReadFromFeatureParams);
             if (mHighDisplayBrightnessThresholds.length
                     != mHighAmbientBrightnessThresholds.length) {
                 throw new RuntimeException("display high brightness threshold array and ambient "
@@ -1668,10 +1670,10 @@
                 Callable<int[]> loadFromDeviceConfigDisplaySettingsCallable,
                 Callable<int[]> loadFromDisplayDeviceConfigCallable,
                 int brightnessThresholdOfFixedRefreshRateKey,
-                DisplayDeviceConfig displayDeviceConfig, boolean attemptLoadingFromDeviceConfig) {
+                DisplayDeviceConfig displayDeviceConfig, boolean attemptReadFromFeatureParams) {
             int[] brightnessThresholds = null;
 
-            if (attemptLoadingFromDeviceConfig) {
+            if (attemptReadFromFeatureParams) {
                 try {
                     brightnessThresholds =
                         loadFromDeviceConfigDisplaySettingsCallable.call();
@@ -1715,9 +1717,9 @@
 
             // DeviceConfig is accessible after system ready.
             int[] lowDisplayBrightnessThresholds =
-                    mDeviceConfigDisplaySettings.getLowDisplayBrightnessThresholds();
+                    mConfigParameterProvider.getLowDisplayBrightnessThresholds();
             int[] lowAmbientBrightnessThresholds =
-                    mDeviceConfigDisplaySettings.getLowAmbientBrightnessThresholds();
+                    mConfigParameterProvider.getLowAmbientBrightnessThresholds();
 
             if (lowDisplayBrightnessThresholds != null && lowAmbientBrightnessThresholds != null
                     && lowDisplayBrightnessThresholds.length
@@ -1727,9 +1729,9 @@
             }
 
             int[] highDisplayBrightnessThresholds =
-                    mDeviceConfigDisplaySettings.getHighDisplayBrightnessThresholds();
+                    mConfigParameterProvider.getHighDisplayBrightnessThresholds();
             int[] highAmbientBrightnessThresholds =
-                    mDeviceConfigDisplaySettings.getHighAmbientBrightnessThresholds();
+                    mConfigParameterProvider.getHighAmbientBrightnessThresholds();
 
             if (highDisplayBrightnessThresholds != null && highAmbientBrightnessThresholds != null
                     && highDisplayBrightnessThresholds.length
@@ -1738,14 +1740,12 @@
                 mHighAmbientBrightnessThresholds = highAmbientBrightnessThresholds;
             }
 
-            final int refreshRateInLowZone = mDeviceConfigDisplaySettings
-                    .getRefreshRateInLowZone();
+            final int refreshRateInLowZone = mConfigParameterProvider.getRefreshRateInLowZone();
             if (refreshRateInLowZone != -1) {
                 mRefreshRateInLowZone = refreshRateInLowZone;
             }
 
-            final int refreshRateInHighZone = mDeviceConfigDisplaySettings
-                    .getRefreshRateInHighZone();
+            final int refreshRateInHighZone = mConfigParameterProvider.getRefreshRateInHighZone();
             if (refreshRateInHighZone != -1) {
                 mRefreshRateInHighZone = refreshRateInHighZone;
             }
@@ -1799,15 +1799,15 @@
                     displayDeviceConfig = mDefaultDisplayDeviceConfig;
                 }
                 mLowDisplayBrightnessThresholds = loadBrightnessThresholds(
-                        () -> mDeviceConfigDisplaySettings.getLowDisplayBrightnessThresholds(),
+                        () -> mConfigParameterProvider.getLowDisplayBrightnessThresholds(),
                         () -> displayDeviceConfig.getLowDisplayBrightnessThresholds(),
                         R.array.config_brightnessThresholdsOfPeakRefreshRate,
-                        displayDeviceConfig, /* attemptLoadingFromDeviceConfig= */ false);
+                        displayDeviceConfig, /* attemptReadFromFeatureParams= */ false);
                 mLowAmbientBrightnessThresholds = loadBrightnessThresholds(
-                        () -> mDeviceConfigDisplaySettings.getLowAmbientBrightnessThresholds(),
+                        () -> mConfigParameterProvider.getLowAmbientBrightnessThresholds(),
                         () -> displayDeviceConfig.getLowAmbientBrightnessThresholds(),
                         R.array.config_ambientThresholdsOfPeakRefreshRate,
-                        displayDeviceConfig, /* attemptLoadingFromDeviceConfig= */ false);
+                        displayDeviceConfig, /* attemptReadFromFeatureParams= */ false);
             }
             restartObserver();
         }
@@ -1822,7 +1822,7 @@
                 // from there.
                 synchronized (mLock) {
                     loadRefreshRateInLowZone(mDefaultDisplayDeviceConfig,
-                            /* attemptLoadingFromDeviceConfig= */ false);
+                            /* attemptReadFromFeatureParams= */ false);
                 }
                 restartObserver();
             } else if (refreshRate != mRefreshRateInLowZone) {
@@ -1843,15 +1843,15 @@
                     displayDeviceConfig = mDefaultDisplayDeviceConfig;
                 }
                 mHighDisplayBrightnessThresholds = loadBrightnessThresholds(
-                        () -> mDeviceConfigDisplaySettings.getHighDisplayBrightnessThresholds(),
+                        () -> mConfigParameterProvider.getLowDisplayBrightnessThresholds(),
                         () -> displayDeviceConfig.getHighDisplayBrightnessThresholds(),
                         R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate,
-                        displayDeviceConfig, /* attemptLoadingFromDeviceConfig= */ false);
+                        displayDeviceConfig, /* attemptReadFromFeatureParams= */ false);
                 mHighAmbientBrightnessThresholds = loadBrightnessThresholds(
-                        () -> mDeviceConfigDisplaySettings.getHighAmbientBrightnessThresholds(),
+                        () -> mConfigParameterProvider.getHighAmbientBrightnessThresholds(),
                         () -> displayDeviceConfig.getHighAmbientBrightnessThresholds(),
                         R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate,
-                        displayDeviceConfig, /* attemptLoadingFromDeviceConfig= */ false);
+                        displayDeviceConfig, /* attemptReadFromFeatureParams= */ false);
             }
             restartObserver();
         }
@@ -1866,7 +1866,7 @@
                 // from there.
                 synchronized (mLock) {
                     loadRefreshRateInHighZone(mDefaultDisplayDeviceConfig,
-                            /* attemptLoadingFromDeviceConfig= */ false);
+                            /* attemptReadFromFeatureParams= */ false);
                 }
                 restartObserver();
             } else if (refreshRate != mRefreshRateInHighZone) {
@@ -2675,113 +2675,55 @@
 
     private class DeviceConfigDisplaySettings implements DeviceConfig.OnPropertiesChangedListener {
         public void startListening() {
-            mDeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+            mConfigParameterProvider.addOnPropertiesChangedListener(
                     BackgroundThread.getExecutor(), this);
         }
 
-        /*
-         * Return null if no such property or wrong format (not comma separated integers).
-         */
-        public int[] getLowDisplayBrightnessThresholds() {
-            return getIntArrayProperty(
-                    DisplayManager.DeviceConfig
-                            .KEY_FIXED_REFRESH_RATE_LOW_DISPLAY_BRIGHTNESS_THRESHOLDS);
+        private int getRefreshRateInHbmHdr(DisplayDeviceConfig displayDeviceConfig) {
+            return getRefreshRate(
+                    () -> mConfigParameterProvider.getRefreshRateInHbmHdr(),
+                    () -> displayDeviceConfig.getDefaultRefreshRateInHbmHdr(),
+                    R.integer.config_defaultRefreshRateInHbmHdr,
+                    displayDeviceConfig
+            );
         }
 
-        /*
-         * Return null if no such property or wrong format (not comma separated integers).
-         */
-        public int[] getLowAmbientBrightnessThresholds() {
-            return getIntArrayProperty(
-                    DisplayManager.DeviceConfig
-                            .KEY_FIXED_REFRESH_RATE_LOW_AMBIENT_BRIGHTNESS_THRESHOLDS);
+        private int getRefreshRateInHbmSunlight(DisplayDeviceConfig displayDeviceConfig) {
+            return getRefreshRate(
+                    () -> mConfigParameterProvider.getRefreshRateInHbmSunlight(),
+                    () -> displayDeviceConfig.getDefaultRefreshRateInHbmSunlight(),
+                    R.integer.config_defaultRefreshRateInHbmSunlight,
+                    displayDeviceConfig
+            );
         }
 
-        public int getRefreshRateInLowZone() {
-            return mDeviceConfig.getInt(
-                    DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
-                    DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE, -1);
-
-        }
-
-        /*
-         * Return null if no such property or wrong format (not comma separated integers).
-         */
-        public int[] getHighDisplayBrightnessThresholds() {
-            return getIntArrayProperty(
-                    DisplayManager.DeviceConfig
-                            .KEY_FIXED_REFRESH_RATE_HIGH_DISPLAY_BRIGHTNESS_THRESHOLDS);
-        }
-
-        /*
-         * Return null if no such property or wrong format (not comma separated integers).
-         */
-        public int[] getHighAmbientBrightnessThresholds() {
-            return getIntArrayProperty(
-                    DisplayManager.DeviceConfig
-                            .KEY_FIXED_REFRESH_RATE_HIGH_AMBIENT_BRIGHTNESS_THRESHOLDS);
-        }
-
-        public int getRefreshRateInHighZone() {
-            return mDeviceConfig.getInt(
-                    DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
-                    DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE,
-                    -1);
-        }
-
-        public int getRefreshRateInHbmHdr(DisplayDeviceConfig displayDeviceConfig) {
-            int refreshRate =
-                    (displayDeviceConfig == null) ? mContext.getResources().getInteger(
-                            R.integer.config_defaultRefreshRateInHbmHdr)
-                            : displayDeviceConfig.getDefaultRefreshRateInHbmHdr();
+        private int getRefreshRate(IntSupplier fromConfigPram, IntSupplier fromDisplayDeviceConfig,
+                @IntegerRes int configKey, DisplayDeviceConfig displayDeviceConfig) {
+            int refreshRate = -1;
             try {
-                refreshRate = mDeviceConfig.getInt(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
-                    DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HBM_HDR,
-                    refreshRate);
-            } catch (NullPointerException e) {
+                refreshRate = fromConfigPram.getAsInt();
+            } catch (NullPointerException npe) {
                 // 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
+            if (refreshRate == -1) {
+                refreshRate = (displayDeviceConfig == null)
+                                ? mContext.getResources().getInteger(configKey)
+                                : fromDisplayDeviceConfig.getAsInt();
             }
             return refreshRate;
         }
 
-        /*
-         * Return null if no such property
-         */
-        public Float getDefaultPeakRefreshRate() {
-            float defaultPeakRefreshRate = mDeviceConfig.getFloat(
-                    DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
-                    DisplayManager.DeviceConfig.KEY_PEAK_REFRESH_RATE_DEFAULT, -1);
-
-            if (defaultPeakRefreshRate == -1) {
-                return null;
-            }
-            return defaultPeakRefreshRate;
-        }
-
         @Override
         public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
-            Float defaultPeakRefreshRate = getDefaultPeakRefreshRate();
+            float defaultPeakRefreshRate = mConfigParameterProvider.getPeakRefreshRateDefault();
             mHandler.obtainMessage(MSG_DEFAULT_PEAK_REFRESH_RATE_CHANGED,
-                    defaultPeakRefreshRate).sendToTarget();
+                    defaultPeakRefreshRate == -1 ? null : defaultPeakRefreshRate).sendToTarget();
 
-            int[] lowDisplayBrightnessThresholds = getLowDisplayBrightnessThresholds();
-            int[] lowAmbientBrightnessThresholds = getLowAmbientBrightnessThresholds();
-            final int refreshRateInLowZone = getRefreshRateInLowZone();
+            int[] lowDisplayBrightnessThresholds =
+                    mConfigParameterProvider.getLowDisplayBrightnessThresholds();
+            int[] lowAmbientBrightnessThresholds =
+                    mConfigParameterProvider.getLowAmbientBrightnessThresholds();
+            final int refreshRateInLowZone = mConfigParameterProvider.getRefreshRateInLowZone();
 
             mHandler.obtainMessage(MSG_LOW_BRIGHTNESS_THRESHOLDS_CHANGED,
                     new Pair<>(lowDisplayBrightnessThresholds, lowAmbientBrightnessThresholds))
@@ -2790,9 +2732,11 @@
             mHandler.obtainMessage(MSG_REFRESH_RATE_IN_LOW_ZONE_CHANGED, refreshRateInLowZone,
                     0).sendToTarget();
 
-            int[] highDisplayBrightnessThresholds = getHighDisplayBrightnessThresholds();
-            int[] highAmbientBrightnessThresholds = getHighAmbientBrightnessThresholds();
-            final int refreshRateInHighZone = getRefreshRateInHighZone();
+            int[] highDisplayBrightnessThresholds =
+                    mConfigParameterProvider.getHighDisplayBrightnessThresholds();
+            int[] highAmbientBrightnessThresholds =
+                    mConfigParameterProvider.getHighAmbientBrightnessThresholds();
+            final int refreshRateInHighZone = mConfigParameterProvider.getRefreshRateInHighZone();
 
             mHandler.obtainMessage(MSG_HIGH_BRIGHTNESS_THRESHOLDS_CHANGED,
                     new Pair<>(highDisplayBrightnessThresholds, highAmbientBrightnessThresholds))
@@ -2814,33 +2758,6 @@
                     .sendToTarget();
             }
         }
-
-        private int[] getIntArrayProperty(String prop) {
-            String strArray = mDeviceConfig.getString(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, prop,
-                    null);
-
-            if (strArray != null) {
-                return parseIntArray(strArray);
-            }
-
-            return null;
-        }
-
-        private int[] parseIntArray(@NonNull String strArray) {
-            String[] items = strArray.split(",");
-            int[] array = new int[items.length];
-
-            try {
-                for (int i = 0; i < array.length; i++) {
-                    array[i] = Integer.parseInt(items[i]);
-                }
-            } catch (NumberFormatException e) {
-                Slog.e(TAG, "Incorrect format for array: '" + strArray + "'", e);
-                array = null;
-            }
-
-            return array;
-        }
     }
 
     interface Injector {
diff --git a/services/core/java/com/android/server/display/utils/SensorUtils.java b/services/core/java/com/android/server/display/utils/SensorUtils.java
index 48bc46c..56321cd 100644
--- a/services/core/java/com/android/server/display/utils/SensorUtils.java
+++ b/services/core/java/com/android/server/display/utils/SensorUtils.java
@@ -16,10 +16,13 @@
 
 package com.android.server.display.utils;
 
+import android.annotation.Nullable;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
 import android.text.TextUtils;
 
+import com.android.server.display.DisplayDeviceConfig;
+
 import java.util.List;
 
 /**
@@ -29,17 +32,26 @@
     public static final int NO_FALLBACK = 0;
 
     /**
+     * Finds the specified sensor for SensorData from DisplayDeviceConfig.
+     */
+    @Nullable
+    public static Sensor findSensor(@Nullable SensorManager sensorManager,
+            @Nullable DisplayDeviceConfig.SensorData sensorData, int fallbackType) {
+        if (sensorData == null) {
+            return null;
+        } else {
+            return findSensor(sensorManager, sensorData.type, sensorData.name, fallbackType);
+        }
+    }
+    /**
      * Finds the specified sensor by type and name using SensorManager.
      */
-    public static Sensor findSensor(SensorManager sensorManager, String sensorType,
-            String sensorName, int fallbackType) {
+    @Nullable
+    public static Sensor findSensor(@Nullable SensorManager sensorManager,
+            @Nullable String sensorType, @Nullable String sensorName, int fallbackType) {
         if (sensorManager == null) {
             return null;
         }
-
-        if ("".equals(sensorName) && "".equals(sensorType)) {
-            return null;
-        }
         final boolean isNameSpecified = !TextUtils.isEmpty(sensorName);
         final boolean isTypeSpecified = !TextUtils.isEmpty(sensorType);
         if (isNameSpecified || isTypeSpecified) {
diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java
index 6d70d21..633bf731 100644
--- a/services/core/java/com/android/server/dreams/DreamController.java
+++ b/services/core/java/com/android/server/dreams/DreamController.java
@@ -16,11 +16,11 @@
 
 package com.android.server.dreams;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
 import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
 
 import android.app.ActivityTaskManager;
 import android.app.BroadcastOptions;
+import android.app.IAppTask;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -214,6 +214,27 @@
     }
 
     /**
+     * Provides an appTask for the dream with token {@code dreamToken}, so that the dream controller
+     * can stop the dream task when necessary.
+     */
+    void setDreamAppTask(Binder dreamToken, IAppTask appTask) {
+        if (mCurrentDream == null || mCurrentDream.mToken != dreamToken
+                || mCurrentDream.mAppTask != null) {
+            Slog.e(TAG, "Illegal dream activity start. mCurrentDream.mToken = "
+                    + mCurrentDream.mToken + ", illegal dreamToken = " + dreamToken
+                    + ". Ending this dream activity.");
+            try {
+                appTask.finishAndRemoveTask();
+            } catch (RemoteException | RuntimeException e) {
+                Slog.e(TAG, "Unable to stop illegal dream activity.");
+            }
+            return;
+        }
+
+        mCurrentDream.mAppTask = appTask;
+    }
+
+    /**
      * Stops dreaming.
      *
      * The current dream, if any, and any unstopped previous dreams are stopped. The device stops
@@ -303,8 +324,14 @@
                     mSentStartBroadcast = false;
                 }
 
-                mActivityTaskManager.removeRootTasksWithActivityTypes(
-                        new int[] {ACTIVITY_TYPE_DREAM});
+                if (mCurrentDream != null && mCurrentDream.mAppTask != null) {
+                    // Finish the dream task in case it hasn't finished by itself already.
+                    try {
+                        mCurrentDream.mAppTask.finishAndRemoveTask();
+                    } catch (RemoteException | RuntimeException e) {
+                        Slog.e(TAG, "Unable to stop dream activity.");
+                    }
+                }
 
                 mListener.onDreamStopped(dream.mToken);
             }
@@ -364,6 +391,7 @@
         public final boolean mIsPreviewMode;
         public final boolean mCanDoze;
         public final int mUserId;
+        public IAppTask mAppTask;
 
         public PowerManager.WakeLock mWakeLock;
         public boolean mBound;
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 41651fd..d88fe8a 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -27,6 +27,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
+import android.app.IAppTask;
 import android.app.TaskInfo;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -37,6 +38,7 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManagerInternal;
 import android.content.pm.ServiceInfo;
 import android.database.ContentObserver;
 import android.hardware.display.AmbientDisplayConfiguration;
@@ -116,6 +118,7 @@
     private final PowerManagerInternal mPowerManagerInternal;
     private final PowerManager.WakeLock mDozeWakeLock;
     private final ActivityTaskManagerInternal mAtmInternal;
+    private final PackageManagerInternal mPmInternal;
     private final UserManager mUserManager;
     private final UiEventLogger mUiEventLogger;
     private final DreamUiEventLogger mDreamUiEventLogger;
@@ -203,9 +206,7 @@
 
         @Override
         public void onChange(boolean selfChange, Uri uri) {
-            synchronized (mLock) {
-                updateWhenToDreamSettings();
-            }
+            updateWhenToDreamSettings();
         }
     }
 
@@ -218,6 +219,7 @@
         mPowerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
         mPowerManagerInternal = getLocalService(PowerManagerInternal.class);
         mAtmInternal = getLocalService(ActivityTaskManagerInternal.class);
+        mPmInternal = getLocalService(PackageManagerInternal.class);
         mUserManager = context.getSystemService(UserManager.class);
         mDozeWakeLock = mPowerManager.newWakeLock(PowerManager.DOZE_WAKE_LOCK, DOZE_WAKE_LOCK_TAG);
         mDozeConfig = new AmbientDisplayConfiguration(mContext);
@@ -257,15 +259,7 @@
             if (Build.IS_DEBUGGABLE) {
                 SystemProperties.addChangeCallback(mSystemPropertiesChanged);
             }
-            mContext.registerReceiver(new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    writePulseGestureEnabled();
-                    synchronized (mLock) {
-                        stopDreamLocked(false /*immediate*/, "user switched");
-                    }
-                }
-            }, new IntentFilter(Intent.ACTION_USER_SWITCHED), null, mHandler);
+
             mContext.getContentResolver().registerContentObserver(
                     Settings.Secure.getUriFor(Settings.Secure.DOZE_DOUBLE_TAP_GESTURE), false,
                     mDozeEnabledObserver, UserHandle.USER_ALL);
@@ -303,6 +297,18 @@
         }
     }
 
+    @Override
+    public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
+        updateWhenToDreamSettings();
+
+        mHandler.post(() -> {
+            writePulseGestureEnabled();
+            synchronized (mLock) {
+                stopDreamLocked(false /*immediate*/, "user switched");
+            }
+        });
+    }
+
     private void dumpInternal(PrintWriter pw) {
         synchronized (mLock) {
             pw.println("DREAM MANAGER (dumpsys dreams)");
@@ -318,6 +324,7 @@
             pw.println("mWhenToDream=" + mWhenToDream);
             pw.println("mKeepDreamingWhenUnpluggingDefault=" + mKeepDreamingWhenUnpluggingDefault);
             pw.println("getDozeComponent()=" + getDozeComponent());
+            pw.println("mDreamOverlayServiceName=" + mDreamOverlayServiceName.flattenToString());
             pw.println();
 
             DumpUtils.dumpAsync(mHandler, (pw1, prefix) -> mController.dump(pw1), pw, "", 200);
@@ -1061,6 +1068,64 @@
                 Binder.restoreCallingIdentity(ident);
             }
         }
+
+        @Override // Binder call
+        public void startDreamActivity(@NonNull Intent intent) {
+            final int callingUid = Binder.getCallingUid();
+            final int callingPid = Binder.getCallingPid();
+            // We post here, because startDreamActivity and setDreamAppTask have to run
+            // synchronously and DreamController#setDreamAppTask has to run on mHandler.
+            mHandler.post(() -> {
+                final Binder dreamToken;
+                final String dreamPackageName;
+                synchronized (mLock) {
+                    if (mCurrentDream == null) {
+                        Slog.e(TAG, "Attempt to start DreamActivity, but the device is not "
+                                + "dreaming. Aborting without starting the DreamActivity.");
+                        return;
+                    }
+                    dreamToken = mCurrentDream.token;
+                    dreamPackageName = mCurrentDream.name.getPackageName();
+                }
+
+                if (!canLaunchDreamActivity(dreamPackageName, intent.getPackage(),
+                            callingUid)) {
+                    Slog.e(TAG, "The dream activity can be started only when the device is dreaming"
+                            + " and only by the active dream package.");
+                    return;
+                }
+
+                final IAppTask appTask = mAtmInternal.startDreamActivity(intent, callingUid,
+                        callingPid);
+                if (appTask == null) {
+                    Slog.e(TAG, "Could not start dream activity.");
+                    stopDreamInternal(true, "DreamActivity not started");
+                    return;
+                }
+                mController.setDreamAppTask(dreamToken, appTask);
+            });
+        }
+
+        boolean canLaunchDreamActivity(String dreamPackageName, String packageName,
+                int callingUid) {
+            if (dreamPackageName == null || packageName == null) {
+                Slog.e(TAG, "Cannot launch dream activity due to invalid state. dream component= "
+                        + dreamPackageName + ", packageName=" + packageName);
+                return false;
+            }
+            if (!mPmInternal.isSameApp(packageName, callingUid, UserHandle.getUserId(callingUid))) {
+                Slog.e(TAG, "Cannot launch dream activity because package="
+                        + packageName + " does not match callingUid=" + callingUid);
+                return false;
+            }
+            if (packageName.equals(dreamPackageName)) {
+                return true;
+            }
+            Slog.e(TAG, "Dream packageName does not match active dream. Package " + packageName
+                    + " does not match " + dreamPackageName);
+            return false;
+        }
+
     }
 
     private final class LocalService extends DreamManagerInternal {
diff --git a/services/core/java/com/android/server/input/AmbientKeyboardBacklightController.java b/services/core/java/com/android/server/input/AmbientKeyboardBacklightController.java
new file mode 100644
index 0000000..ce86849
--- /dev/null
+++ b/services/core/java/com/android/server/input/AmbientKeyboardBacklightController.java
@@ -0,0 +1,431 @@
+/*
+ * 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.
+ */
+
+package com.android.server.input;
+
+import android.annotation.MainThread;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManagerInternal;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.util.Slog;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.DisplayInfo;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
+import com.android.server.display.utils.SensorUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A thread-safe component of {@link InputManagerService} responsible for managing the keyboard
+ * backlight based on ambient light sensor.
+ */
+final class AmbientKeyboardBacklightController implements DisplayManager.DisplayListener,
+        SensorEventListener {
+
+    private static final String TAG = "KbdBacklightController";
+
+    // To enable these logs, run:
+    // 'adb shell setprop log.tag.KbdBacklightController DEBUG' (requires restart)
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    // Number of light sensor responses required to overcome temporal hysteresis.
+    @VisibleForTesting
+    public static final int HYSTERESIS_THRESHOLD = 2;
+
+    private static final int MSG_BRIGHTNESS_CALLBACK = 0;
+    private static final int MSG_SETUP_DISPLAY_AND_SENSOR = 1;
+
+    private static final Object sAmbientControllerLock = new Object();
+
+    private final Context mContext;
+    private final Handler mHandler;
+
+    @Nullable
+    @GuardedBy("sAmbientControllerLock")
+    private Sensor mLightSensor;
+    @GuardedBy("sAmbientControllerLock")
+    private String mCurrentDefaultDisplayUniqueId;
+
+    // List of currently registered ambient backlight listeners
+    @GuardedBy("sAmbientControllerLock")
+    private final List<AmbientKeyboardBacklightListener> mAmbientKeyboardBacklightListeners =
+            new ArrayList<>();
+
+    private BrightnessStep[] mBrightnessSteps;
+    private int mCurrentBrightnessStepIndex;
+    private HysteresisState mHysteresisState;
+    private int mHysteresisCount = 0;
+    private float mSmoothingConstant;
+    private int mSmoothedLux;
+    private int mSmoothedLuxAtLastAdjustment;
+
+    private enum HysteresisState {
+        // The most-recent mSmoothedLux matched mSmoothedLuxAtLastAdjustment.
+        STABLE,
+        // The most-recent mSmoothedLux was less than mSmoothedLuxAtLastAdjustment.
+        DECREASING,
+        // The most-recent mSmoothedLux was greater than mSmoothedLuxAtLastAdjustment.
+        INCREASING,
+        // The brightness should be adjusted immediately after the next sensor reading.
+        IMMEDIATE,
+    }
+
+    AmbientKeyboardBacklightController(Context context, Looper looper) {
+        mContext = context;
+        mHandler = new Handler(looper, this::handleMessage);
+        initConfiguration();
+    }
+
+    public void systemRunning() {
+        mHandler.sendEmptyMessage(MSG_SETUP_DISPLAY_AND_SENSOR);
+        DisplayManager displayManager = Objects.requireNonNull(
+                mContext.getSystemService(DisplayManager.class));
+        displayManager.registerDisplayListener(this, mHandler);
+    }
+
+    public void registerAmbientBacklightListener(AmbientKeyboardBacklightListener listener) {
+        synchronized (sAmbientControllerLock) {
+            if (mAmbientKeyboardBacklightListeners.contains(listener)) {
+                throw new IllegalStateException(
+                        "AmbientKeyboardBacklightListener was already registered, listener = "
+                                + listener);
+            }
+            if (mAmbientKeyboardBacklightListeners.isEmpty()) {
+                // Add sensor listener when we add the first ambient backlight listener.
+                addSensorListener(mLightSensor);
+            }
+            mAmbientKeyboardBacklightListeners.add(listener);
+        }
+    }
+
+    public void unregisterAmbientBacklightListener(AmbientKeyboardBacklightListener listener) {
+        synchronized (sAmbientControllerLock) {
+            if (!mAmbientKeyboardBacklightListeners.contains(listener)) {
+                throw new IllegalStateException(
+                        "AmbientKeyboardBacklightListener was never registered, listener = "
+                                + listener);
+            }
+            mAmbientKeyboardBacklightListeners.remove(listener);
+            if (mAmbientKeyboardBacklightListeners.isEmpty()) {
+                removeSensorListener(mLightSensor);
+            }
+        }
+    }
+
+    private void sendBrightnessAdjustment(int brightnessValue) {
+        Message msg = Message.obtain(mHandler, MSG_BRIGHTNESS_CALLBACK, brightnessValue);
+        mHandler.sendMessage(msg);
+    }
+
+    @MainThread
+    private void handleBrightnessCallback(int brightnessValue) {
+        synchronized (sAmbientControllerLock) {
+            for (AmbientKeyboardBacklightListener listener : mAmbientKeyboardBacklightListeners) {
+                listener.onKeyboardBacklightValueChanged(brightnessValue);
+            }
+        }
+    }
+
+    @MainThread
+    private void handleAmbientLuxChange(float rawLux) {
+        if (rawLux < 0) {
+            Slog.w(TAG, "Light sensor doesn't have valid value");
+            return;
+        }
+        updateSmoothedLux(rawLux);
+
+        if (mHysteresisState != HysteresisState.IMMEDIATE
+                && mSmoothedLux == mSmoothedLuxAtLastAdjustment) {
+            mHysteresisState = HysteresisState.STABLE;
+            return;
+        }
+
+        int newStepIndex = Math.max(0, mCurrentBrightnessStepIndex);
+        int numSteps = mBrightnessSteps.length;
+
+        if (mSmoothedLux > mSmoothedLuxAtLastAdjustment) {
+            if (mHysteresisState != HysteresisState.IMMEDIATE
+                    && mHysteresisState != HysteresisState.INCREASING) {
+                if (DEBUG) {
+                    Slog.d(TAG, "ALS transitioned to brightness increasing state");
+                }
+                mHysteresisState = HysteresisState.INCREASING;
+                mHysteresisCount = 0;
+            }
+            for (; newStepIndex < numSteps; newStepIndex++) {
+                if (mSmoothedLux < mBrightnessSteps[newStepIndex].mIncreaseLuxThreshold) {
+                    break;
+                }
+            }
+        } else if (mSmoothedLux < mSmoothedLuxAtLastAdjustment) {
+            if (mHysteresisState != HysteresisState.IMMEDIATE
+                    && mHysteresisState != HysteresisState.DECREASING) {
+                if (DEBUG) {
+                    Slog.d(TAG, "ALS transitioned to brightness decreasing state");
+                }
+                mHysteresisState = HysteresisState.DECREASING;
+                mHysteresisCount = 0;
+            }
+            for (; newStepIndex >= 0; newStepIndex--) {
+                if (mSmoothedLux > mBrightnessSteps[newStepIndex].mDecreaseLuxThreshold) {
+                    break;
+                }
+            }
+        }
+
+        if (mHysteresisState == HysteresisState.IMMEDIATE) {
+            mCurrentBrightnessStepIndex = newStepIndex;
+            mSmoothedLuxAtLastAdjustment = mSmoothedLux;
+            mHysteresisState = HysteresisState.STABLE;
+            mHysteresisCount = 0;
+            sendBrightnessAdjustment(mBrightnessSteps[newStepIndex].mBrightnessValue);
+            return;
+        }
+
+        if (newStepIndex == mCurrentBrightnessStepIndex) {
+            return;
+        }
+
+        mHysteresisCount++;
+        if (DEBUG) {
+            Slog.d(TAG, "Incremented hysteresis count to " + mHysteresisCount + " (lux went from "
+                    + mSmoothedLuxAtLastAdjustment + " to " + mSmoothedLux + ")");
+        }
+        if (mHysteresisCount >= HYSTERESIS_THRESHOLD) {
+            mCurrentBrightnessStepIndex = newStepIndex;
+            mSmoothedLuxAtLastAdjustment = mSmoothedLux;
+            mHysteresisCount = 1;
+            sendBrightnessAdjustment(mBrightnessSteps[newStepIndex].mBrightnessValue);
+        }
+    }
+
+    @MainThread
+    private void handleDisplayChange() {
+        DisplayManagerInternal displayManagerInternal = LocalServices.getService(
+                DisplayManagerInternal.class);
+        DisplayInfo displayInfo = displayManagerInternal.getDisplayInfo(Display.DEFAULT_DISPLAY);
+        synchronized (sAmbientControllerLock) {
+            if (Objects.equals(mCurrentDefaultDisplayUniqueId, displayInfo.uniqueId)) {
+                return;
+            }
+            if (DEBUG) {
+                Slog.d(TAG, "Default display changed: resetting the light sensor");
+            }
+            // Keep track of current default display
+            mCurrentDefaultDisplayUniqueId = displayInfo.uniqueId;
+            // Clear all existing sensor listeners
+            if (!mAmbientKeyboardBacklightListeners.isEmpty()) {
+                removeSensorListener(mLightSensor);
+            }
+            mLightSensor = getAmbientLightSensor(
+                    displayManagerInternal.getAmbientLightSensorData(Display.DEFAULT_DISPLAY));
+            // Re-add sensor listeners if required;
+            if (!mAmbientKeyboardBacklightListeners.isEmpty()) {
+                addSensorListener(mLightSensor);
+            }
+        }
+    }
+
+    private Sensor getAmbientLightSensor(
+            DisplayManagerInternal.AmbientLightSensorData ambientSensor) {
+        SensorManager sensorManager = Objects.requireNonNull(
+                mContext.getSystemService(SensorManager.class));
+        if (DEBUG) {
+            Slog.d(TAG, "Ambient Light sensor data: " + ambientSensor);
+        }
+        return SensorUtils.findSensor(sensorManager, ambientSensor.sensorType,
+                ambientSensor.sensorName, Sensor.TYPE_LIGHT);
+    }
+
+    private void updateSmoothedLux(float rawLux) {
+        // For the first sensor reading, use raw lux value directly without smoothing.
+        if (mHysteresisState == HysteresisState.IMMEDIATE) {
+            mSmoothedLux = (int) rawLux;
+        } else {
+            mSmoothedLux =
+                    (int) (mSmoothingConstant * rawLux + (1 - mSmoothingConstant) * mSmoothedLux);
+        }
+        if (DEBUG) {
+            Slog.d(TAG, "Current smoothed lux from ALS = " + mSmoothedLux);
+        }
+    }
+
+    @VisibleForTesting
+    public void addSensorListener(@Nullable Sensor sensor) {
+        SensorManager sensorManager = mContext.getSystemService(SensorManager.class);
+        if (sensorManager == null || sensor == null) {
+            return;
+        }
+        // Reset values before registering listener
+        reset();
+        sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL, mHandler);
+        if (DEBUG) {
+            Slog.d(TAG, "Registering ALS listener");
+        }
+    }
+
+    private void removeSensorListener(@Nullable Sensor sensor) {
+        SensorManager sensorManager = mContext.getSystemService(SensorManager.class);
+        if (sensorManager == null || sensor == null) {
+            return;
+        }
+        sensorManager.unregisterListener(this, sensor);
+        if (DEBUG) {
+            Slog.d(TAG, "Unregistering ALS listener");
+        }
+    }
+
+    private void initConfiguration() {
+        Resources res = mContext.getResources();
+        int[] brightnessValueArray = res.getIntArray(
+                com.android.internal.R.array.config_autoKeyboardBacklightBrightnessValues);
+        int[] decreaseThresholdArray = res.getIntArray(
+                com.android.internal.R.array.config_autoKeyboardBacklightDecreaseLuxThreshold);
+        int[] increaseThresholdArray = res.getIntArray(
+                com.android.internal.R.array.config_autoKeyboardBacklightIncreaseLuxThreshold);
+        if (brightnessValueArray.length != decreaseThresholdArray.length
+                || decreaseThresholdArray.length != increaseThresholdArray.length) {
+            throw new IllegalArgumentException(
+                    "The config files for auto keyboard backlight brightness must contain arrays "
+                            + "of equal lengths");
+        }
+        final int size = brightnessValueArray.length;
+        mBrightnessSteps = new BrightnessStep[size];
+        for (int i = 0; i < size; i++) {
+            int increaseThreshold =
+                    increaseThresholdArray[i] < 0 ? Integer.MAX_VALUE : increaseThresholdArray[i];
+            int decreaseThreshold =
+                    decreaseThresholdArray[i] < 0 ? Integer.MIN_VALUE : decreaseThresholdArray[i];
+            mBrightnessSteps[i] = new BrightnessStep(brightnessValueArray[i], increaseThreshold,
+                    decreaseThreshold);
+        }
+
+        int numSteps = mBrightnessSteps.length;
+        if (numSteps == 0 || mBrightnessSteps[0].mDecreaseLuxThreshold != Integer.MIN_VALUE
+                || mBrightnessSteps[numSteps - 1].mIncreaseLuxThreshold != Integer.MAX_VALUE) {
+            throw new IllegalArgumentException(
+                    "The config files for auto keyboard backlight brightness must contain arrays "
+                            + "of length > 0 and have -1 or Integer.MIN_VALUE as lower bound for "
+                            + "decrease thresholds and -1 or Integer.MAX_VALUE as upper bound for "
+                            + "increase thresholds");
+        }
+
+        final TypedValue smoothingConstantValue = new TypedValue();
+        res.getValue(
+                com.android.internal.R.dimen.config_autoKeyboardBrightnessSmoothingConstant,
+                smoothingConstantValue,
+                true /*resolveRefs*/);
+        mSmoothingConstant = smoothingConstantValue.getFloat();
+        if (mSmoothingConstant <= 0.0 || mSmoothingConstant > 1.0) {
+            throw new IllegalArgumentException(
+                    "The config files for auto keyboard backlight brightness must contain "
+                            + "smoothing constant in range (0.0, 1.0].");
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "Brightness steps: " + Arrays.toString(mBrightnessSteps)
+                    + " Smoothing constant = " + mSmoothingConstant);
+        }
+    }
+
+    private void reset() {
+        mHysteresisState = HysteresisState.IMMEDIATE;
+        mSmoothedLux = 0;
+        mSmoothedLuxAtLastAdjustment = 0;
+        mCurrentBrightnessStepIndex = -1;
+    }
+
+    private boolean handleMessage(Message msg) {
+        switch (msg.what) {
+            case MSG_BRIGHTNESS_CALLBACK:
+                handleBrightnessCallback((int) msg.obj);
+                return true;
+            case MSG_SETUP_DISPLAY_AND_SENSOR:
+                handleDisplayChange();
+                return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onSensorChanged(SensorEvent event) {
+        handleAmbientLuxChange(event.values[0]);
+    }
+
+    @Override
+    public void onAccuracyChanged(Sensor sensor, int accuracy) {
+    }
+
+    @Override
+    public void onDisplayAdded(int displayId) {
+        handleDisplayChange();
+    }
+
+    @Override
+    public void onDisplayRemoved(int displayId) {
+        handleDisplayChange();
+    }
+
+    @Override
+    public void onDisplayChanged(int displayId) {
+        handleDisplayChange();
+    }
+
+    public interface AmbientKeyboardBacklightListener {
+        /**
+         * @param value between [0, 255] to which keyboard backlight needs to be set according
+         *              to Ambient light sensor.
+         */
+        void onKeyboardBacklightValueChanged(int value);
+    }
+
+    private static class BrightnessStep {
+        private final int mBrightnessValue;
+        private final int mIncreaseLuxThreshold;
+        private final int mDecreaseLuxThreshold;
+
+        private BrightnessStep(int brightnessValue, int increaseLuxThreshold,
+                int decreaseLuxThreshold) {
+            mBrightnessValue = brightnessValue;
+            mIncreaseLuxThreshold = increaseLuxThreshold;
+            mDecreaseLuxThreshold = decreaseLuxThreshold;
+        }
+
+        @Override
+        public String toString() {
+            return "BrightnessStep{" + "mBrightnessValue=" + mBrightnessValue
+                    + ", mIncreaseThreshold=" + mIncreaseLuxThreshold + ", mDecreaseThreshold="
+                    + mDecreaseLuxThreshold + '}';
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java
index ff9ce6f..38a0d37 100644
--- a/services/core/java/com/android/server/input/BatteryController.java
+++ b/services/core/java/com/android/server/input/BatteryController.java
@@ -46,7 +46,6 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.input.BatteryController.UEventManager.UEventBatteryListener;
 
 import java.io.PrintWriter;
 import java.util.Arrays;
@@ -102,8 +101,9 @@
     @GuardedBy("mLock")
     private BluetoothBatteryManager.BluetoothBatteryListener mBluetoothBatteryListener;
 
-    BatteryController(Context context, NativeInputManagerService nativeService, Looper looper) {
-        this(context, nativeService, looper, new UEventManager() {},
+    BatteryController(Context context, NativeInputManagerService nativeService, Looper looper,
+            UEventManager uEventManager) {
+        this(context, nativeService, looper, uEventManager,
                 new LocalBluetoothBatteryManager(context, looper));
     }
 
@@ -567,7 +567,7 @@
         private BluetoothAdapter.OnMetadataChangedListener mBluetoothMetadataListener;
 
         @Nullable
-        private UEventBatteryListener mUEventBatteryListener;
+        private BatteryController.UEventBatteryListener mUEventBatteryListener;
 
         DeviceMonitor(int deviceId) {
             mState = new State(deviceId);
@@ -630,7 +630,7 @@
                 return;
             }
             final int deviceId = mState.deviceId;
-            mUEventBatteryListener = new UEventBatteryListener() {
+            mUEventBatteryListener = new BatteryController.UEventBatteryListener() {
                 @Override
                 public void onBatteryUEvent(long eventTime) {
                     handleUEventNotification(deviceId, eventTime);
@@ -898,40 +898,25 @@
         }
     }
 
-    // An interface used to change the API of UEventObserver to a more test-friendly format.
     @VisibleForTesting
-    interface UEventManager {
-
-        @VisibleForTesting
-        abstract class UEventBatteryListener {
-            private final UEventObserver mObserver = new UEventObserver() {
-                @Override
-                public void onUEvent(UEvent event) {
-                    final long eventTime = SystemClock.uptimeMillis();
-                    if (DEBUG) {
-                        Slog.d(TAG,
-                                "UEventListener: Received UEvent: "
-                                        + event + " eventTime: " + eventTime);
-                    }
-                    if (!"CHANGE".equalsIgnoreCase(event.get("ACTION"))
-                            || !"POWER_SUPPLY".equalsIgnoreCase(event.get("SUBSYSTEM"))) {
-                        // Disregard any UEvents that do not correspond to battery changes.
-                        return;
-                    }
-                    UEventBatteryListener.this.onBatteryUEvent(eventTime);
-                }
-            };
-
-            public abstract void onBatteryUEvent(long eventTime);
+    abstract static class UEventBatteryListener extends UEventManager.UEventListener {
+        @Override
+        public void onUEvent(UEventObserver.UEvent event) {
+            final long eventTime = SystemClock.uptimeMillis();
+            if (DEBUG) {
+                Slog.d(TAG,
+                        "UEventListener: Received UEvent: "
+                                + event + " eventTime: " + eventTime);
+            }
+            if (!"CHANGE".equalsIgnoreCase(event.get("ACTION"))
+                    || !"POWER_SUPPLY".equalsIgnoreCase(event.get("SUBSYSTEM"))) {
+                // Disregard any UEvents that do not correspond to battery changes.
+                return;
+            }
+            UEventBatteryListener.this.onBatteryUEvent(eventTime);
         }
 
-        default void addListener(UEventBatteryListener listener, String match) {
-            listener.mObserver.startObserving(match);
-        }
-
-        default void removeListener(UEventBatteryListener listener) {
-            listener.mObserver.stopObserving();
-        }
+        public abstract void onBatteryUEvent(long eventTime);
     }
 
     // An interface used to change the API of adding a bluetooth battery listener to a more
diff --git a/services/core/java/com/android/server/input/InputFeatureFlagProvider.java b/services/core/java/com/android/server/input/InputFeatureFlagProvider.java
new file mode 100644
index 0000000..a646d1e
--- /dev/null
+++ b/services/core/java/com/android/server/input/InputFeatureFlagProvider.java
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+
+package com.android.server.input;
+
+import android.sysprop.InputProperties;
+
+import java.util.Optional;
+
+/**
+ * A component of {@link InputManagerService} responsible for managing the input sysprop flags
+ *
+ * @hide
+ */
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public final class InputFeatureFlagProvider {
+
+    // To disable Keyboard backlight control via Framework, run:
+    // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart)
+    private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED =
+            InputProperties.enable_keyboard_backlight_control().orElse(true);
+
+    // To disable Framework controlled keyboard backlight animation run:
+    // adb shell setprop persist.input.keyboard.backlight_animation.enabled false (requires restart)
+    private static final boolean KEYBOARD_BACKLIGHT_ANIMATION_ENABLED =
+            InputProperties.enable_keyboard_backlight_animation().orElse(false);
+
+    // To disable Custom keyboard backlight levels support via IDC files run:
+    // adb shell setprop persist.input.keyboard.backlight_custom_levels.enabled false (requires
+    // restart)
+    private static final boolean KEYBOARD_BACKLIGHT_CUSTOM_LEVELS_ENABLED =
+            InputProperties.enable_keyboard_backlight_custom_levels().orElse(true);
+
+    // To disable als based ambient keyboard backlight control run:
+    // adb shell setprop persist.input.keyboard.ambient_backlight_control.enabled false (requires
+    // restart)
+    private static final boolean AMBIENT_KEYBOARD_BACKLIGHT_CONTROL_ENABLED =
+            InputProperties.enable_ambient_keyboard_backlight_control().orElse(true);
+
+    private static Optional<Boolean> sKeyboardBacklightControlOverride = Optional.empty();
+    private static Optional<Boolean> sKeyboardBacklightAnimationOverride = Optional.empty();
+    private static Optional<Boolean> sKeyboardBacklightCustomLevelsOverride = Optional.empty();
+    private static Optional<Boolean> sAmbientKeyboardBacklightControlOverride = Optional.empty();
+
+    public static boolean isKeyboardBacklightControlEnabled() {
+        return sKeyboardBacklightControlOverride.orElse(KEYBOARD_BACKLIGHT_CONTROL_ENABLED);
+    }
+
+    public static boolean isKeyboardBacklightAnimationEnabled() {
+        return sKeyboardBacklightAnimationOverride.orElse(KEYBOARD_BACKLIGHT_ANIMATION_ENABLED);
+    }
+
+    public static boolean isKeyboardBacklightCustomLevelsEnabled() {
+        return sKeyboardBacklightCustomLevelsOverride.orElse(
+                KEYBOARD_BACKLIGHT_CUSTOM_LEVELS_ENABLED);
+    }
+
+    public static boolean isAmbientKeyboardBacklightControlEnabled() {
+        return sAmbientKeyboardBacklightControlOverride.orElse(
+                AMBIENT_KEYBOARD_BACKLIGHT_CONTROL_ENABLED);
+    }
+
+    public static void setKeyboardBacklightControlEnabled(boolean enabled) {
+        sKeyboardBacklightControlOverride = Optional.of(enabled);
+    }
+
+    public static void setKeyboardBacklightAnimationEnabled(boolean enabled) {
+        sKeyboardBacklightAnimationOverride = Optional.of(enabled);
+    }
+
+    public static void setKeyboardBacklightCustomLevelsEnabled(boolean enabled) {
+        sKeyboardBacklightCustomLevelsOverride = Optional.of(enabled);
+    }
+
+    public static void setAmbientKeyboardBacklightControlEnabled(boolean enabled) {
+        sAmbientKeyboardBacklightControlOverride = Optional.of(enabled);
+    }
+
+    /**
+     * Clears all input feature flag overrides.
+     */
+    public static void clearOverrides() {
+        sKeyboardBacklightControlOverride = Optional.empty();
+        sKeyboardBacklightAnimationOverride = Optional.empty();
+        sKeyboardBacklightCustomLevelsOverride = Optional.empty();
+        sAmbientKeyboardBacklightControlOverride = Optional.empty();
+    }
+}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index a809c75..8f9f204 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -74,7 +74,6 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
-import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.VibrationEffect;
 import android.os.vibrator.StepSegment;
@@ -160,11 +159,6 @@
     private static final AdditionalDisplayInputProperties
             DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES = new AdditionalDisplayInputProperties();
 
-    // To disable Keyboard backlight control via Framework, run:
-    // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart)
-    private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = SystemProperties.getBoolean(
-            "persist.input.keyboard.backlight_control.enabled", true);
-
     private final NativeInputManagerService mNative;
 
     private final Context mContext;
@@ -399,10 +393,12 @@
     static class Injector {
         private final Context mContext;
         private final Looper mLooper;
+        private final UEventManager mUEventManager;
 
-        Injector(Context context, Looper looper) {
+        Injector(Context context, Looper looper, UEventManager uEventManager) {
             mContext = context;
             mLooper = looper;
+            mUEventManager = uEventManager;
         }
 
         Context getContext() {
@@ -413,6 +409,10 @@
             return mLooper;
         }
 
+        UEventManager getUEventManager() {
+            return mUEventManager;
+        }
+
         NativeInputManagerService getNativeService(InputManagerService service) {
             return new NativeInputManagerService.NativeImpl(service, mLooper.getQueue());
         }
@@ -423,7 +423,7 @@
     }
 
     public InputManagerService(Context context) {
-        this(new Injector(context, DisplayThread.get().getLooper()));
+        this(new Injector(context, DisplayThread.get().getLooper(), new UEventManager() {}));
     }
 
     @VisibleForTesting
@@ -438,11 +438,12 @@
         mSettingsObserver = new InputSettingsObserver(mContext, mHandler, this, mNative);
         mKeyboardLayoutManager = new KeyboardLayoutManager(mContext, mNative, mDataStore,
                 injector.getLooper());
-        mBatteryController = new BatteryController(mContext, mNative, injector.getLooper());
-        mKeyboardBacklightController =
-                KEYBOARD_BACKLIGHT_CONTROL_ENABLED ? new KeyboardBacklightController(mContext,
-                        mNative, mDataStore, injector.getLooper())
-                        : new KeyboardBacklightControllerInterface() {};
+        mBatteryController = new BatteryController(mContext, mNative, injector.getLooper(),
+                injector.getUEventManager());
+        mKeyboardBacklightController = InputFeatureFlagProvider.isKeyboardBacklightControlEnabled()
+                ? new KeyboardBacklightController(mContext, mNative, mDataStore,
+                        injector.getLooper(), injector.getUEventManager())
+                : new KeyboardBacklightControllerInterface() {};
         mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper());
 
         mUseDevInputEventForAudioJack =
@@ -2804,6 +2805,11 @@
         void notifyConfigurationChanged();
 
         /**
+         * This callback is invoked when the pointer location changes.
+         */
+        void notifyPointerLocationChanged(boolean pointerLocationEnabled);
+
+        /**
          * This callback is invoked when the camera lens cover switch changes state.
          * @param whenNanos the time when the change occurred
          * @param lensCovered true is the lens is covered
@@ -3385,6 +3391,10 @@
         }
     }
 
+    void updatePointerLocationEnabled(boolean enabled) {
+        mWindowManagerCallbacks.notifyPointerLocationChanged(enabled);
+    }
+
     void updateFocusEventDebugViewEnabled(boolean enabled) {
         FocusEventDebugView view;
         synchronized (mFocusEventDebugViewLock) {
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index 651063e..a608b4f 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -67,6 +67,8 @@
                         (reason) -> updateTouchpadRightClickZoneEnabled()),
                 Map.entry(Settings.System.getUriFor(Settings.System.SHOW_TOUCHES),
                         (reason) -> updateShowTouches()),
+                Map.entry(Settings.System.getUriFor(Settings.System.POINTER_LOCATION),
+                        (reason) -> updatePointerLocation()),
                 Map.entry(
                         Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON),
                         (reason) -> updateAccessibilityLargePointer()),
@@ -115,41 +117,45 @@
         return setting != 0;
     }
 
-    private int getPointerSpeedValue(String settingName) {
-        int speed = Settings.System.getIntForUser(mContext.getContentResolver(),
-                settingName, InputSettings.DEFAULT_POINTER_SPEED, UserHandle.USER_CURRENT);
+    private int constrainPointerSpeedValue(int speed) {
         return Math.min(Math.max(speed, InputSettings.MIN_POINTER_SPEED),
                 InputSettings.MAX_POINTER_SPEED);
     }
 
     private void updateMousePointerSpeed() {
-        mNative.setPointerSpeed(getPointerSpeedValue(Settings.System.POINTER_SPEED));
+        int speed = Settings.System.getIntForUser(mContext.getContentResolver(),
+                Settings.System.POINTER_SPEED, InputSettings.DEFAULT_POINTER_SPEED,
+                UserHandle.USER_CURRENT);
+        mNative.setPointerSpeed(constrainPointerSpeedValue(speed));
     }
 
     private void updateTouchpadPointerSpeed() {
         mNative.setTouchpadPointerSpeed(
-                getPointerSpeedValue(Settings.System.TOUCHPAD_POINTER_SPEED));
+                constrainPointerSpeedValue(InputSettings.getTouchpadPointerSpeed(mContext)));
     }
 
     private void updateTouchpadNaturalScrollingEnabled() {
         mNative.setTouchpadNaturalScrollingEnabled(
-                getBoolean(Settings.System.TOUCHPAD_NATURAL_SCROLLING, true));
+                InputSettings.useTouchpadNaturalScrolling(mContext));
     }
 
     private void updateTouchpadTapToClickEnabled() {
-        mNative.setTouchpadTapToClickEnabled(
-                getBoolean(Settings.System.TOUCHPAD_TAP_TO_CLICK, true));
+        mNative.setTouchpadTapToClickEnabled(InputSettings.useTouchpadTapToClick(mContext));
     }
 
     private void updateTouchpadRightClickZoneEnabled() {
-        mNative.setTouchpadRightClickZoneEnabled(
-                getBoolean(Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE, false));
+        mNative.setTouchpadRightClickZoneEnabled(InputSettings.useTouchpadRightClickZone(mContext));
     }
 
     private void updateShowTouches() {
         mNative.setShowTouches(getBoolean(Settings.System.SHOW_TOUCHES, false));
     }
 
+    private void updatePointerLocation() {
+        mService.updatePointerLocationEnabled(
+                getBoolean(Settings.System.POINTER_LOCATION, false));
+    }
+
     private void updateShowKeyPresses() {
         mService.updateFocusEventDebugViewEnabled(
                 getBoolean(Settings.System.SHOW_KEY_PRESSES, false));
diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java
index 48c346a..c3205af 100644
--- a/services/core/java/com/android/server/input/KeyboardBacklightController.java
+++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java
@@ -16,7 +16,9 @@
 
 package com.android.server.input;
 
+import android.animation.ValueAnimator;
 import android.annotation.BinderThread;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Color;
 import android.hardware.input.IKeyboardBacklightListener;
@@ -45,6 +47,7 @@
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.OptionalInt;
+import java.util.TreeSet;
 
 /**
  * A thread-safe component of {@link InputManagerService} responsible for managing the keyboard
@@ -69,7 +72,11 @@
     private static final int MSG_NOTIFY_USER_INACTIVITY = 5;
     private static final int MSG_INTERACTIVE_STATE_CHANGED = 6;
     private static final int MAX_BRIGHTNESS = 255;
-    private static final int NUM_BRIGHTNESS_CHANGE_STEPS = 10;
+    private static final int DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS = 10;
+    @VisibleForTesting
+    static final int MAX_BRIGHTNESS_CHANGE_STEPS = 10;
+    private static final long TRANSITION_ANIMATION_DURATION_MILLIS =
+            Duration.ofSeconds(1).toMillis();
 
     private static final String UEVENT_KEYBOARD_BACKLIGHT_TAG = "kbd_backlight";
 
@@ -77,7 +84,8 @@
     static final long USER_INACTIVITY_THRESHOLD_MILLIS = Duration.ofSeconds(30).toMillis();
 
     @VisibleForTesting
-    static final int[] BRIGHTNESS_VALUE_FOR_LEVEL = new int[NUM_BRIGHTNESS_CHANGE_STEPS + 1];
+    static final int[] DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL =
+            new int[DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS + 1];
 
     private final Context mContext;
     private final NativeInputManagerService mNative;
@@ -85,6 +93,8 @@
     @GuardedBy("mDataStore")
     private final PersistentDataStore mDataStore;
     private final Handler mHandler;
+    private final AnimatorFactory mAnimatorFactory;
+    private final UEventManager mUEventManager;
     // Always access on handler thread or need to lock this for synchronization.
     private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1);
     // Maintains state if all backlights should be on or turned off
@@ -97,22 +107,38 @@
     private final SparseArray<KeyboardBacklightListenerRecord> mKeyboardBacklightListenerRecords =
             new SparseArray<>();
 
+    private final AmbientKeyboardBacklightController mAmbientController;
+    @Nullable
+    private AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener mAmbientListener;
+
+    private int mAmbientBacklightValue = 0;
+
     static {
         // Fixed brightness levels to avoid issues when converting back and forth from the
         // device brightness range to [0-255]
-        // Levels are: 0, 25, 51, ..., 255
-        for (int i = 0; i <= NUM_BRIGHTNESS_CHANGE_STEPS; i++) {
-            BRIGHTNESS_VALUE_FOR_LEVEL[i] = (int) Math.floor(
-                    ((float) i * MAX_BRIGHTNESS) / NUM_BRIGHTNESS_CHANGE_STEPS);
+        // Levels are: 0, 51, ..., 255
+        for (int i = 0; i <= DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS; i++) {
+            DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[i] = (int) Math.floor(
+                    ((float) i * MAX_BRIGHTNESS) / DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS);
         }
     }
 
     KeyboardBacklightController(Context context, NativeInputManagerService nativeService,
-            PersistentDataStore dataStore, Looper looper) {
+            PersistentDataStore dataStore, Looper looper, UEventManager uEventManager) {
+        this(context, nativeService, dataStore, looper, ValueAnimator::ofInt, uEventManager);
+    }
+
+    @VisibleForTesting
+    KeyboardBacklightController(Context context, NativeInputManagerService nativeService,
+            PersistentDataStore dataStore, Looper looper, AnimatorFactory animatorFactory,
+            UEventManager uEventManager) {
         mContext = context;
         mNative = nativeService;
         mDataStore = dataStore;
         mHandler = new Handler(looper, this::handleMessage);
+        mAnimatorFactory = animatorFactory;
+        mAmbientController = new AmbientKeyboardBacklightController(context, looper);
+        mUEventManager = uEventManager;
     }
 
     @Override
@@ -129,13 +155,17 @@
         // We want to observe creation of such LED nodes since they might be created after device
         // FD created and InputDevice creation logic doesn't initialize LED nodes which leads to
         // backlight not working.
-        UEventObserver observer = new UEventObserver() {
+        mUEventManager.addListener(new UEventManager.UEventListener() {
             @Override
-            public void onUEvent(UEvent event) {
+            public void onUEvent(UEventObserver.UEvent event) {
                 onKeyboardBacklightUEvent(event);
             }
-        };
-        observer.startObserving(UEVENT_KEYBOARD_BACKLIGHT_TAG);
+        }, UEVENT_KEYBOARD_BACKLIGHT_TAG);
+
+        if (InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) {
+            // Start ambient backlight controller
+            mAmbientController.systemRunning();
+        }
     }
 
     @Override
@@ -168,43 +198,89 @@
         if (inputDevice == null || state == null) {
             return;
         }
-        Light keyboardBacklight = state.mLight;
         // Follow preset levels of brightness defined in BRIGHTNESS_LEVELS
-        final int currBrightnessLevel = state.mBrightnessLevel;
+        final int currBrightnessLevel;
+        if (state.mUseAmbientController) {
+            int index = Arrays.binarySearch(state.mBrightnessValueForLevel, mAmbientBacklightValue);
+            // Set current level to the lower bound of the ambient value in the brightness array.
+            if (index < 0) {
+                int lowerBound = Math.max(0, -(index + 1) - 1);
+                currBrightnessLevel =
+                        direction == Direction.DIRECTION_UP ? lowerBound : lowerBound + 1;
+            } else {
+                currBrightnessLevel = index;
+            }
+        } else {
+            currBrightnessLevel = state.mBrightnessLevel;
+        }
         final int newBrightnessLevel;
         if (direction == Direction.DIRECTION_UP) {
-            newBrightnessLevel = Math.min(currBrightnessLevel + 1, NUM_BRIGHTNESS_CHANGE_STEPS);
+            newBrightnessLevel = Math.min(currBrightnessLevel + 1,
+                    state.getNumBrightnessChangeSteps());
         } else {
             newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0);
         }
-        updateBacklightState(deviceId, keyboardBacklight, newBrightnessLevel,
-                true /* isTriggeredByKeyPress */);
 
+        state.setBrightnessLevel(newBrightnessLevel);
+
+        // Might need to stop listening to ALS since user has manually selected backlight
+        // level through keyboard up/down button
+        updateAmbientLightListener();
+
+        maybeBackupBacklightBrightness(inputDevice, state.mLight,
+                state.mBrightnessValueForLevel[newBrightnessLevel]);
+
+        if (DEBUG) {
+            Slog.d(TAG,
+                    "Changing state from " + state.mBrightnessLevel + " to " + newBrightnessLevel);
+        }
+
+        synchronized (mKeyboardBacklightListenerRecords) {
+            for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) {
+                IKeyboardBacklightState callbackState = new IKeyboardBacklightState();
+                callbackState.brightnessLevel = newBrightnessLevel;
+                callbackState.maxBrightnessLevel = state.getNumBrightnessChangeSteps();
+                mKeyboardBacklightListenerRecords.valueAt(i).notifyKeyboardBacklightChanged(
+                        deviceId, callbackState, true);
+            }
+        }
+    }
+
+    private void maybeBackupBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight,
+            int brightnessValue) {
+        // Don't back up or restore when ALS based keyboard backlight is enabled
+        if (InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) {
+            return;
+        }
         synchronized (mDataStore) {
             try {
                 mDataStore.setKeyboardBacklightBrightness(inputDevice.getDescriptor(),
                         keyboardBacklight.getId(),
-                        BRIGHTNESS_VALUE_FOR_LEVEL[newBrightnessLevel]);
+                        brightnessValue);
             } finally {
                 mDataStore.saveIfNeeded();
             }
         }
     }
 
-    private void restoreBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight) {
+    private void maybeRestoreBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight) {
+        // Don't back up or restore when ALS based keyboard backlight is enabled
+        if (InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) {
+            return;
+        }
+        KeyboardBacklightState state = mKeyboardBacklights.get(inputDevice.getId());
         OptionalInt brightness;
         synchronized (mDataStore) {
             brightness = mDataStore.getKeyboardBacklightBrightness(
                     inputDevice.getDescriptor(), keyboardBacklight.getId());
         }
-        if (brightness.isPresent()) {
+        if (state != null && brightness.isPresent()) {
             int brightnessValue = Math.max(0, Math.min(MAX_BRIGHTNESS, brightness.getAsInt()));
-            int index = Arrays.binarySearch(BRIGHTNESS_VALUE_FOR_LEVEL, brightnessValue);
-            if (index < 0) {
-                index = Math.min(NUM_BRIGHTNESS_CHANGE_STEPS, -(index + 1));
+            int newLevel = Arrays.binarySearch(state.mBrightnessValueForLevel, brightnessValue);
+            if (newLevel < 0) {
+                newLevel = Math.min(state.getNumBrightnessChangeSteps(), -(newLevel + 1));
             }
-            updateBacklightState(inputDevice.getId(), keyboardBacklight, index,
-                    false /* isTriggeredByKeyPress */);
+            state.setBrightnessLevel(newLevel);
             if (DEBUG) {
                 Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt());
             }
@@ -217,14 +293,10 @@
         if (!mIsInteractive) {
             return;
         }
-        if (!mIsBacklightOn) {
-            mIsBacklightOn = true;
-            for (int i = 0; i < mKeyboardBacklights.size(); i++) {
-                int deviceId = mKeyboardBacklights.keyAt(i);
-                KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
-                updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel,
-                        false /* isTriggeredByKeyPress */);
-            }
+        mIsBacklightOn = true;
+        for (int i = 0; i < mKeyboardBacklights.size(); i++) {
+            KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
+            state.onBacklightStateChanged();
         }
         mHandler.removeMessages(MSG_NOTIFY_USER_INACTIVITY);
         mHandler.sendEmptyMessageAtTime(MSG_NOTIFY_USER_INACTIVITY,
@@ -232,14 +304,10 @@
     }
 
     private void handleUserInactivity() {
-        if (mIsBacklightOn) {
-            mIsBacklightOn = false;
-            for (int i = 0; i < mKeyboardBacklights.size(); i++) {
-                int deviceId = mKeyboardBacklights.keyAt(i);
-                KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
-                updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel,
-                        false /* isTriggeredByKeyPress */);
-            }
+        mIsBacklightOn = false;
+        for (int i = 0; i < mKeyboardBacklights.size(); i++) {
+            KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
+            state.onBacklightStateChanged();
         }
     }
 
@@ -253,6 +321,16 @@
         } else {
             handleUserInactivity();
         }
+        updateAmbientLightListener();
+    }
+
+    @VisibleForTesting
+    public void handleAmbientLightValueChanged(int brightnessValue) {
+        mAmbientBacklightValue = brightnessValue;
+        for (int i = 0; i < mKeyboardBacklights.size(); i++) {
+            KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
+            state.onAmbientBacklightValueChanged();
+        }
     }
 
     private boolean handleMessage(Message msg) {
@@ -285,12 +363,14 @@
     @Override
     public void onInputDeviceAdded(int deviceId) {
         onInputDeviceChanged(deviceId);
+        updateAmbientLightListener();
     }
 
     @VisibleForTesting
     @Override
     public void onInputDeviceRemoved(int deviceId) {
         mKeyboardBacklights.remove(deviceId);
+        updateAmbientLightListener();
     }
 
     @VisibleForTesting
@@ -310,8 +390,8 @@
             return;
         }
         // The keyboard backlight was added or changed.
-        mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(keyboardBacklight));
-        restoreBacklightBrightness(inputDevice, keyboardBacklight);
+        mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(deviceId, keyboardBacklight));
+        maybeRestoreBacklightBrightness(inputDevice, keyboardBacklight);
     }
 
     private InputDevice getInputDevice(int deviceId) {
@@ -372,33 +452,6 @@
         }
     }
 
-    private void updateBacklightState(int deviceId, Light light, int brightnessLevel,
-            boolean isTriggeredByKeyPress) {
-        KeyboardBacklightState state = mKeyboardBacklights.get(deviceId);
-        if (state == null) {
-            return;
-        }
-
-        mNative.setLightColor(deviceId, light.getId(),
-                mIsBacklightOn ? Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel], 0, 0, 0)
-                        : 0);
-        if (DEBUG) {
-            Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel
-                    + "(isBacklightOn = " + mIsBacklightOn + ")");
-        }
-        state.mBrightnessLevel = brightnessLevel;
-
-        synchronized (mKeyboardBacklightListenerRecords) {
-            for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) {
-                IKeyboardBacklightState callbackState = new IKeyboardBacklightState();
-                callbackState.brightnessLevel = brightnessLevel;
-                callbackState.maxBrightnessLevel = NUM_BRIGHTNESS_CHANGE_STEPS;
-                mKeyboardBacklightListenerRecords.valueAt(i).notifyKeyboardBacklightChanged(
-                        deviceId, callbackState, isTriggeredByKeyPress);
-            }
-        }
-    }
-
     private void onKeyboardBacklightListenerDied(int pid) {
         synchronized (mKeyboardBacklightListenerRecords) {
             mKeyboardBacklightListenerRecords.remove(pid);
@@ -416,6 +469,25 @@
         }
     }
 
+    private void updateAmbientLightListener() {
+        if (!InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) {
+            return;
+        }
+        boolean needToListenAmbientLightSensor = false;
+        for (int i = 0; i < mKeyboardBacklights.size(); i++) {
+            needToListenAmbientLightSensor |= mKeyboardBacklights.valueAt(i).mUseAmbientController;
+        }
+        needToListenAmbientLightSensor &= mIsInteractive;
+        if (needToListenAmbientLightSensor && mAmbientListener == null) {
+            mAmbientListener = this::handleAmbientLightValueChanged;
+            mAmbientController.registerAmbientBacklightListener(mAmbientListener);
+        }
+        if (!needToListenAmbientLightSensor && mAmbientListener != null) {
+            mAmbientController.unregisterAmbientBacklightListener(mAmbientListener);
+            mAmbientListener = null;
+        }
+    }
+
     private static boolean isValidBacklightNodePath(String devPath) {
         if (TextUtils.isEmpty(devPath)) {
             return false;
@@ -436,10 +508,7 @@
     @Override
     public void dump(PrintWriter pw) {
         IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
-        ipw.println(
-                TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights, isBacklightOn = "
-                        + mIsBacklightOn);
-
+        ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights");
         ipw.increaseIndent();
         for (int i = 0; i < mKeyboardBacklights.size(); i++) {
             KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
@@ -478,12 +547,99 @@
         }
     }
 
-    private static class KeyboardBacklightState {
+    private class KeyboardBacklightState {
+        private final int mDeviceId;
         private final Light mLight;
         private int mBrightnessLevel;
+        private ValueAnimator mAnimator;
+        private final int[] mBrightnessValueForLevel;
+        private boolean mUseAmbientController =
+                InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled();
 
-        KeyboardBacklightState(Light light) {
+        KeyboardBacklightState(int deviceId, Light light) {
+            mDeviceId = deviceId;
             mLight = light;
+            mBrightnessValueForLevel = setupBrightnessLevels();
+        }
+
+        private int[] setupBrightnessLevels() {
+            if (!InputFeatureFlagProvider.isKeyboardBacklightCustomLevelsEnabled()) {
+                return DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL;
+            }
+            int[] customLevels = mLight.getPreferredBrightnessLevels();
+            if (customLevels == null || customLevels.length == 0) {
+                return DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL;
+            }
+            TreeSet<Integer> brightnessLevels = new TreeSet<>();
+            brightnessLevels.add(0);
+            for (int level : customLevels) {
+                if (level > 0 && level < MAX_BRIGHTNESS) {
+                    brightnessLevels.add(level);
+                }
+            }
+            brightnessLevels.add(MAX_BRIGHTNESS);
+            int brightnessChangeSteps = brightnessLevels.size() - 1;
+            if (brightnessChangeSteps > MAX_BRIGHTNESS_CHANGE_STEPS) {
+                return DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL;
+            }
+            int[] result = new int[brightnessLevels.size()];
+            int index = 0;
+            for (int val : brightnessLevels) {
+                result[index++] = val;
+            }
+            return result;
+        }
+
+        private int getNumBrightnessChangeSteps() {
+            return mBrightnessValueForLevel.length - 1;
+        }
+
+        private void onBacklightStateChanged() {
+            int toValue = mUseAmbientController ? mAmbientBacklightValue
+                    : mBrightnessValueForLevel[mBrightnessLevel];
+            setBacklightValue(mIsBacklightOn ? toValue : 0);
+        }
+        private void setBrightnessLevel(int brightnessLevel) {
+            // Once we manually set level, disregard ambient light controller
+            mUseAmbientController = false;
+            if (mIsBacklightOn) {
+                setBacklightValue(mBrightnessValueForLevel[brightnessLevel]);
+            }
+            mBrightnessLevel = brightnessLevel;
+        }
+
+        private void onAmbientBacklightValueChanged() {
+            if (mIsBacklightOn && mUseAmbientController) {
+                setBacklightValue(mAmbientBacklightValue);
+            }
+        }
+
+        private void cancelAnimation() {
+            if (mAnimator != null && mAnimator.isRunning()) {
+                mAnimator.cancel();
+            }
+        }
+
+        private void setBacklightValue(int toValue) {
+            int fromValue = Color.alpha(mNative.getLightColor(mDeviceId, mLight.getId()));
+            if (fromValue == toValue) {
+                return;
+            }
+            if (InputFeatureFlagProvider.isKeyboardBacklightAnimationEnabled()) {
+                startAnimation(fromValue, toValue);
+            } else {
+                mNative.setLightColor(mDeviceId, mLight.getId(), Color.argb(toValue, 0, 0, 0));
+            }
+        }
+
+        private void startAnimation(int fromValue, int toValue) {
+            // Cancel any ongoing animation before starting a new one
+            cancelAnimation();
+            mAnimator = mAnimatorFactory.makeIntAnimator(fromValue, toValue);
+            mAnimator.addUpdateListener(
+                    (animation) -> mNative.setLightColor(mDeviceId, mLight.getId(),
+                            Color.argb((int) animation.getAnimatedValue(), 0, 0, 0)));
+            mAnimator.setDuration(TRANSITION_ANIMATION_DURATION_MILLIS).start();
         }
 
         @Override
@@ -493,4 +649,9 @@
                     + "}";
         }
     }
+
+    @VisibleForTesting
+    interface AnimatorFactory {
+        ValueAnimator makeIntAnimator(int from, int to);
+    }
 }
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index 6ec4022..3ac1594 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -16,6 +16,10 @@
 
 package com.android.server.input;
 
+import static com.android.server.input.KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE;
+import static com.android.server.input.KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER;
+import static com.android.server.input.KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD;
+
 import android.annotation.AnyThread;
 import android.annotation.MainThread;
 import android.annotation.NonNull;
@@ -67,6 +71,8 @@
 import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.util.XmlUtils;
+import com.android.server.input.KeyboardMetricsCollector.KeyboardConfigurationEvent;
+import com.android.server.input.KeyboardMetricsCollector.LayoutSelectionCriteria;
 import com.android.server.inputmethod.InputMethodManagerInternal;
 
 import libcore.io.Streams;
@@ -118,7 +124,7 @@
     // This cache stores "best-matched" layouts so that we don't need to run the matching
     // algorithm repeatedly.
     @GuardedBy("mKeyboardLayoutCache")
-    private final Map<String, String> mKeyboardLayoutCache = new ArrayMap<>();
+    private final Map<String, KeyboardLayoutInfo> mKeyboardLayoutCache = new ArrayMap<>();
     private final Object mImeInfoLock = new Object();
     @Nullable
     @GuardedBy("mImeInfoLock")
@@ -194,7 +200,8 @@
                         setCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier(), layout);
                     }
                 }
-                config.setCurrentLayout(layout);
+                config.setCurrentLayout(
+                        new KeyboardLayoutInfo(layout, LAYOUT_SELECTION_CRITERIA_USER));
                 if (layout == null) {
                     // In old settings show notification always until user manually selects a
                     // layout in the settings.
@@ -205,18 +212,19 @@
             final InputDeviceIdentifier identifier = inputDevice.getIdentifier();
             final String key = getLayoutDescriptor(identifier);
             Set<String> selectedLayouts = new HashSet<>();
-            for (ImeInfo imeInfo : getImeInfoListForLayoutMapping()) {
+            List<ImeInfo> imeInfoList = getImeInfoListForLayoutMapping();
+            List<KeyboardLayoutInfo> layoutInfoList = new ArrayList<>();
+            boolean hasMissingLayout = false;
+            for (ImeInfo imeInfo : imeInfoList) {
                 // Check if the layout has been previously configured
-                String layout = getKeyboardLayoutForInputDeviceInternal(identifier,
-                        new ImeInfo(imeInfo.mUserId, imeInfo.mImeSubtypeHandle,
-                                imeInfo.mImeSubtype));
-                if (layout == null) {
-                    // If even one layout not configured properly, we need to ask user to configure
-                    // the keyboard properly from the Settings.
-                    selectedLayouts.clear();
-                    break;
+                KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal(identifier,
+                        imeInfo);
+                boolean noLayoutFound = layoutInfo == null || layoutInfo.mDescriptor == null;
+                if (!noLayoutFound) {
+                    selectedLayouts.add(layoutInfo.mDescriptor);
                 }
-                selectedLayouts.add(layout);
+                layoutInfoList.add(layoutInfo);
+                hasMissingLayout |= noLayoutFound;
             }
 
             if (DEBUG) {
@@ -225,24 +233,38 @@
                                 + selectedLayouts);
             }
 
+            // If even one layout not configured properly, we need to ask user to configure
+            // the keyboard properly from the Settings.
+            if (hasMissingLayout) {
+                selectedLayouts.clear();
+            }
+
             config.setConfiguredLayouts(selectedLayouts);
 
             // Update current layout: If there is a change then need to reload.
             synchronized (mImeInfoLock) {
-                String layout = getKeyboardLayoutForInputDeviceInternal(
+                KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal(
                         inputDevice.getIdentifier(), mCurrentImeInfo);
-                if (!Objects.equals(layout, config.getCurrentLayout())) {
-                    config.setCurrentLayout(layout);
+                if (!Objects.equals(layoutInfo, config.getCurrentLayout())) {
+                    config.setCurrentLayout(layoutInfo);
                     mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
                 }
             }
 
             synchronized (mDataStore) {
                 try {
+                    boolean isFirstConfiguration = !mDataStore.hasInputDeviceEntry(key);
                     if (mDataStore.setSelectedKeyboardLayouts(key, selectedLayouts)) {
                         // Need to show the notification only if layout selection changed
                         // from the previous configuration
                         needToShowNotification = true;
+
+                        // Logging keyboard configuration data to statsd only if the
+                        // configuration changed from the previous configuration. Currently
+                        // only logging for New Settings UI where we are using IME to decide
+                        // the layout information.
+                        logKeyboardConfigurationEvent(inputDevice, imeInfoList, layoutInfoList,
+                                isFirstConfiguration);
                     }
                 } finally {
                     mDataStore.saveIfNeeded();
@@ -401,7 +423,7 @@
 
     @AnyThread
     @Nullable
-    public KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor) {
+    public KeyboardLayout getKeyboardLayout(@NonNull String keyboardLayoutDescriptor) {
         Objects.requireNonNull(keyboardLayoutDescriptor,
                 "keyboardLayoutDescriptor must not be null");
 
@@ -749,8 +771,9 @@
         String keyboardLayoutDescriptor;
         if (useNewSettingsUi()) {
             synchronized (mImeInfoLock) {
-                keyboardLayoutDescriptor = getKeyboardLayoutForInputDeviceInternal(identifier,
+                KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal(identifier,
                         mCurrentImeInfo);
+                keyboardLayoutDescriptor = layoutInfo == null ? null : layoutInfo.mDescriptor;
             }
         } else {
             keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier);
@@ -787,13 +810,13 @@
             return null;
         }
         InputMethodSubtypeHandle subtypeHandle = InputMethodSubtypeHandle.of(imeInfo, imeSubtype);
-        String layout = getKeyboardLayoutForInputDeviceInternal(identifier,
+        KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal(identifier,
                 new ImeInfo(userId, subtypeHandle, imeSubtype));
         if (DEBUG) {
             Slog.d(TAG, "getKeyboardLayoutForInputDevice() " + identifier.toString() + ", userId : "
-                    + userId + ", subtypeHandle = " + subtypeHandle + " -> " + layout);
+                    + userId + ", subtypeHandle = " + subtypeHandle + " -> " + layoutInfo);
         }
-        return layout;
+        return layoutInfo != null ? layoutInfo.mDescriptor : null;
     }
 
     @AnyThread
@@ -924,11 +947,11 @@
             for (int i = 0; i < mConfiguredKeyboards.size(); i++) {
                 InputDevice inputDevice = Objects.requireNonNull(
                         getInputDevice(mConfiguredKeyboards.keyAt(i)));
-                String layout = getKeyboardLayoutForInputDeviceInternal(inputDevice.getIdentifier(),
-                        mCurrentImeInfo);
+                KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal(
+                        inputDevice.getIdentifier(), mCurrentImeInfo);
                 KeyboardConfiguration config = mConfiguredKeyboards.valueAt(i);
-                if (!Objects.equals(layout, config.getCurrentLayout())) {
-                    config.setCurrentLayout(layout);
+                if (!Objects.equals(layoutInfo, config.getCurrentLayout())) {
+                    config.setCurrentLayout(layoutInfo);
                     mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
                     return;
                 }
@@ -937,39 +960,40 @@
     }
 
     @Nullable
-    private String getKeyboardLayoutForInputDeviceInternal(InputDeviceIdentifier identifier,
-            @Nullable ImeInfo imeInfo) {
+    private KeyboardLayoutInfo getKeyboardLayoutForInputDeviceInternal(
+            InputDeviceIdentifier identifier, @Nullable ImeInfo imeInfo) {
         InputDevice inputDevice = getInputDevice(identifier);
         if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
             return null;
         }
         String key = createLayoutKey(identifier, imeInfo);
-        String layout;
         synchronized (mDataStore) {
-            layout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key);
-        }
-        if (layout == null) {
-            synchronized (mKeyboardLayoutCache) {
-                // Check Auto-selected layout cache to see if layout had been previously selected
-                if (mKeyboardLayoutCache.containsKey(key)) {
-                    layout = mKeyboardLayoutCache.get(key);
-                } else {
-                    // NOTE: This list is already filtered based on IME Script code
-                    KeyboardLayout[] layoutList = getKeyboardLayoutListForInputDeviceInternal(
-                            identifier, imeInfo);
-                    // Call auto-matching algorithm to find the best matching layout
-                    layout = getDefaultKeyboardLayoutBasedOnImeInfo(inputDevice, imeInfo,
-                            layoutList);
-                    mKeyboardLayoutCache.put(key, layout);
-                }
+            String layout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key);
+            if (layout != null) {
+                return new KeyboardLayoutInfo(layout, LAYOUT_SELECTION_CRITERIA_USER);
             }
         }
-        return layout;
+
+        synchronized (mKeyboardLayoutCache) {
+            // Check Auto-selected layout cache to see if layout had been previously selected
+            if (mKeyboardLayoutCache.containsKey(key)) {
+                return mKeyboardLayoutCache.get(key);
+            } else {
+                // NOTE: This list is already filtered based on IME Script code
+                KeyboardLayout[] layoutList = getKeyboardLayoutListForInputDeviceInternal(
+                        identifier, imeInfo);
+                // Call auto-matching algorithm to find the best matching layout
+                KeyboardLayoutInfo layoutInfo =
+                        getDefaultKeyboardLayoutBasedOnImeInfo(inputDevice, imeInfo, layoutList);
+                mKeyboardLayoutCache.put(key, layoutInfo);
+                return layoutInfo;
+            }
+        }
     }
 
     @Nullable
-    private static String getDefaultKeyboardLayoutBasedOnImeInfo(InputDevice inputDevice,
-            @Nullable ImeInfo imeInfo, KeyboardLayout[] layoutList) {
+    private static KeyboardLayoutInfo getDefaultKeyboardLayoutBasedOnImeInfo(
+            InputDevice inputDevice, @Nullable ImeInfo imeInfo, KeyboardLayout[] layoutList) {
         Arrays.sort(layoutList);
 
         // Check <VendorID, ProductID> matching for explicitly declared custom KCM files.
@@ -982,7 +1006,8 @@
                                     + "vendor and product Ids. " + inputDevice.getIdentifier()
                                     + " : " + layout.getDescriptor());
                 }
-                return layout.getDescriptor();
+                return new KeyboardLayoutInfo(layout.getDescriptor(),
+                        LAYOUT_SELECTION_CRITERIA_DEVICE);
             }
         }
 
@@ -999,7 +1024,7 @@
                                     + "HW information (Language tag and Layout type). "
                                     + inputDevice.getIdentifier() + " : " + layoutDesc);
                 }
-                return layoutDesc;
+                return new KeyboardLayoutInfo(layoutDesc, LAYOUT_SELECTION_CRITERIA_DEVICE);
             }
         }
 
@@ -1021,7 +1046,10 @@
                             + "IME locale matching. " + inputDevice.getIdentifier() + " : "
                             + layoutDesc);
         }
-        return layoutDesc;
+        if (layoutDesc != null) {
+            return new KeyboardLayoutInfo(layoutDesc, LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD);
+        }
+        return null;
     }
 
     @Nullable
@@ -1227,6 +1255,26 @@
         }
     }
 
+    private void logKeyboardConfigurationEvent(@NonNull InputDevice inputDevice,
+            @NonNull List<ImeInfo> imeInfoList, @NonNull List<KeyboardLayoutInfo> layoutInfoList,
+            boolean isFirstConfiguration) {
+        if (imeInfoList.isEmpty() || layoutInfoList.isEmpty()) {
+            return;
+        }
+        KeyboardConfigurationEvent.Builder configurationEventBuilder =
+                new KeyboardConfigurationEvent.Builder(inputDevice).setIsFirstTimeConfiguration(
+                        isFirstConfiguration);
+        for (int i = 0; i < imeInfoList.size(); i++) {
+            KeyboardLayoutInfo layoutInfo = layoutInfoList.get(i);
+            boolean noLayoutFound = layoutInfo == null || layoutInfo.mDescriptor == null;
+            configurationEventBuilder.addLayoutSelection(imeInfoList.get(i).mImeSubtype,
+                    noLayoutFound ? null : getKeyboardLayout(layoutInfo.mDescriptor),
+                    noLayoutFound ? LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD
+                            : layoutInfo.mSelectionCriteria);
+        }
+        KeyboardMetricsCollector.logKeyboardConfiguredAtom(configurationEventBuilder.build());
+    }
+
     private boolean handleMessage(Message msg) {
         switch (msg.what) {
             case MSG_UPDATE_EXISTING_DEVICES:
@@ -1409,7 +1457,7 @@
 
         // If null, it means no layout is selected for the device.
         @Nullable
-        private String mCurrentLayout;
+        private KeyboardLayoutInfo mCurrentLayout;
 
         private boolean hasConfiguredLayouts() {
             return mConfiguredLayouts != null && !mConfiguredLayouts.isEmpty();
@@ -1425,15 +1473,42 @@
         }
 
         @Nullable
-        private String getCurrentLayout() {
+        private KeyboardLayoutInfo getCurrentLayout() {
             return mCurrentLayout;
         }
 
-        private void setCurrentLayout(String currentLayout) {
+        private void setCurrentLayout(KeyboardLayoutInfo currentLayout) {
             mCurrentLayout = currentLayout;
         }
     }
 
+    private static class KeyboardLayoutInfo {
+        @Nullable
+        private final String mDescriptor;
+        @LayoutSelectionCriteria
+        private final int mSelectionCriteria;
+
+        private KeyboardLayoutInfo(@Nullable String descriptor,
+                @LayoutSelectionCriteria int selectionCriteria) {
+            mDescriptor = descriptor;
+            mSelectionCriteria = selectionCriteria;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof KeyboardLayoutInfo) {
+                return Objects.equals(mDescriptor, ((KeyboardLayoutInfo) obj).mDescriptor)
+                        && mSelectionCriteria == ((KeyboardLayoutInfo) obj).mSelectionCriteria;
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return 31 * mSelectionCriteria + mDescriptor.hashCode();
+        }
+    }
+
     private interface KeyboardLayoutVisitor {
         void visitKeyboardLayout(Resources resources,
                 int keyboardLayoutResId, KeyboardLayout layout);
diff --git a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java
new file mode 100644
index 0000000..19fa7a8
--- /dev/null
+++ b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java
@@ -0,0 +1,308 @@
+/*
+ * 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.
+ */
+
+package com.android.server.input;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.input.KeyboardLayout;
+import android.icu.util.ULocale;
+import android.util.Log;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+import android.view.InputDevice;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.KeyboardConfiguredProto.KeyboardLayoutConfig;
+import com.android.internal.os.KeyboardConfiguredProto.RepeatedKeyboardLayoutConfig;
+import com.android.internal.util.FrameworkStatsLog;
+
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Collect Keyboard metrics
+ */
+public final class KeyboardMetricsCollector {
+    private static final String TAG = "KeyboardMetricCollector";
+
+    // To enable these logs, run: 'adb shell setprop log.tag.KeyboardMetricCollector DEBUG'
+    // (requires restart)
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    @Retention(SOURCE)
+    @IntDef(prefix = { "LAYOUT_SELECTION_CRITERIA_" }, value = {
+            LAYOUT_SELECTION_CRITERIA_USER,
+            LAYOUT_SELECTION_CRITERIA_DEVICE,
+            LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD
+    })
+    public @interface LayoutSelectionCriteria {}
+
+    /** Manual selection by user */
+    public static final int LAYOUT_SELECTION_CRITERIA_USER = 0;
+
+    /** Auto-detection based on device provided language tag and layout type */
+    public static final int LAYOUT_SELECTION_CRITERIA_DEVICE = 1;
+
+    /** Auto-detection based on IME provided language tag and layout type */
+    public static final int LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD = 2;
+
+    @VisibleForTesting
+    static final String DEFAULT_LAYOUT = "Default";
+
+    /**
+     * Log keyboard system shortcuts for the proto
+     * {@link com.android.os.input.KeyboardSystemsEventReported}
+     * defined in "stats/atoms/input/input_extension_atoms.proto"
+     */
+    public static void logKeyboardSystemsEventReportedAtom(InputDevice inputDevice,
+            int keyboardSystemEvent, int[] keyCode, int modifierState) {
+        int vendorId = inputDevice.getVendorId();
+        int productId = inputDevice.getProductId();
+        FrameworkStatsLog.write(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED,
+                vendorId, productId, keyboardSystemEvent, keyCode, modifierState);
+    }
+
+    /**
+     * Function to log the KeyboardConfigured
+     * {@link com.android.os.input.KeyboardConfigured} atom
+     *
+     * @param event {@link KeyboardConfigurationEvent} contains information about keyboard
+     *               configuration. Use {@link KeyboardConfigurationEvent.Builder} to create the
+     *               configuration event to log.
+     */
+    public static void logKeyboardConfiguredAtom(KeyboardConfigurationEvent event) {
+        // Creating proto to log nested field KeyboardLayoutConfig in atom
+        ProtoOutputStream proto = new ProtoOutputStream();
+
+        for (LayoutConfiguration layoutConfiguration : event.getLayoutConfigurations()) {
+            addKeyboardLayoutConfigurationToProto(proto, layoutConfiguration);
+        }
+        // Push the atom to Statsd
+        FrameworkStatsLog.write(FrameworkStatsLog.KEYBOARD_CONFIGURED,
+                event.isFirstConfiguration(), event.getVendorId(), event.getProductId(),
+                proto.getBytes());
+
+        if (DEBUG) {
+            Slog.d(TAG, "Logging Keyboard configuration event: " + event);
+        }
+    }
+
+    /**
+     * Populate the KeyboardLayoutConfig proto which is a repeated proto
+     * in the RepeatedKeyboardLayoutConfig proto with values from the
+     * {@link LayoutConfiguration} class
+     * The proto definitions can be found at:
+     * "frameworks/proto_logging/stats/atoms/input/input_extension_atoms.proto"
+     *
+     * @param proto Representing the nested proto RepeatedKeyboardLayoutConfig
+     * @param layoutConfiguration Class containing the fields for populating the
+     * KeyboardLayoutConfig proto
+     */
+    private static void addKeyboardLayoutConfigurationToProto(ProtoOutputStream proto,
+            LayoutConfiguration layoutConfiguration) {
+        // Start a new KeyboardLayoutConfig proto.
+        long keyboardLayoutConfigToken = proto.start(
+                RepeatedKeyboardLayoutConfig.KEYBOARD_LAYOUT_CONFIG);
+        proto.write(KeyboardLayoutConfig.KEYBOARD_LANGUAGE_TAG,
+                layoutConfiguration.keyboardLanguageTag);
+        proto.write(KeyboardLayoutConfig.KEYBOARD_LAYOUT_TYPE,
+                layoutConfiguration.keyboardLayoutType);
+        proto.write(KeyboardLayoutConfig.KEYBOARD_LAYOUT_NAME,
+                layoutConfiguration.keyboardLayoutName);
+        proto.write(KeyboardLayoutConfig.LAYOUT_SELECTION_CRITERIA,
+                layoutConfiguration.layoutSelectionCriteria);
+        proto.end(keyboardLayoutConfigToken);
+    }
+
+    /**
+     * Class representing the proto KeyboardLayoutConfig defined in
+     * "frameworks/proto_logging/stats/atoms/input/input_extension_atoms.proto
+     *
+     * @see com.android.os.input.KeyboardConfigured
+     */
+    public static class KeyboardConfigurationEvent {
+
+        private final InputDevice mInputDevice;
+        private final boolean mIsFirstConfiguration;
+        private final List<LayoutConfiguration> mLayoutConfigurations;
+
+        private KeyboardConfigurationEvent(InputDevice inputDevice, boolean isFirstConfiguration,
+                List<LayoutConfiguration> layoutConfigurations) {
+            mInputDevice = inputDevice;
+            mIsFirstConfiguration = isFirstConfiguration;
+            mLayoutConfigurations = layoutConfigurations;
+        }
+
+        public int getVendorId() {
+            return mInputDevice.getVendorId();
+        }
+
+        public int getProductId() {
+            return mInputDevice.getProductId();
+        }
+
+        public boolean isFirstConfiguration() {
+            return mIsFirstConfiguration;
+        }
+
+        public List<LayoutConfiguration> getLayoutConfigurations() {
+            return mLayoutConfigurations;
+        }
+
+        @Override
+        public String toString() {
+            return "InputDevice = {VendorId = " + Integer.toHexString(getVendorId())
+                    + ", ProductId = " + Integer.toHexString(getProductId())
+                    + "}, isFirstConfiguration = " + mIsFirstConfiguration
+                    + ", LayoutConfigurations = " + mLayoutConfigurations;
+        }
+
+        /**
+         * Builder class to help create {@link KeyboardConfigurationEvent}.
+         */
+        public static class Builder {
+            @NonNull
+            private final InputDevice mInputDevice;
+            private boolean mIsFirstConfiguration;
+            private final List<InputMethodSubtype> mImeSubtypeList = new ArrayList<>();
+            private final List<KeyboardLayout> mSelectedLayoutList = new ArrayList<>();
+            private final List<Integer> mLayoutSelectionCriteriaList = new ArrayList<>();
+
+            public Builder(@NonNull InputDevice inputDevice) {
+                Objects.requireNonNull(inputDevice, "InputDevice provided should not be null");
+                mInputDevice = inputDevice;
+            }
+
+            /**
+             * Set whether this is the first time this keyboard is configured.
+             */
+            public Builder setIsFirstTimeConfiguration(boolean isFirstTimeConfiguration) {
+                mIsFirstConfiguration = isFirstTimeConfiguration;
+                return this;
+            }
+
+            /**
+             * Adds keyboard layout configuration info for a particular IME subtype language
+             */
+            public Builder addLayoutSelection(@NonNull InputMethodSubtype imeSubtype,
+                    @Nullable KeyboardLayout selectedLayout,
+                    @LayoutSelectionCriteria int layoutSelectionCriteria) {
+                Objects.requireNonNull(imeSubtype, "IME subtype provided should not be null");
+                if (!isValidSelectionCriteria(layoutSelectionCriteria)) {
+                    throw new IllegalStateException("Invalid layout selection criteria");
+                }
+                mImeSubtypeList.add(imeSubtype);
+                mSelectedLayoutList.add(selectedLayout);
+                mLayoutSelectionCriteriaList.add(layoutSelectionCriteria);
+                return this;
+            }
+
+            /**
+             * Creates {@link KeyboardConfigurationEvent} from the provided information
+             */
+            public KeyboardConfigurationEvent build() {
+                int size = mImeSubtypeList.size();
+                if (size == 0) {
+                    throw new IllegalStateException("Should have at least one configuration");
+                }
+                List<LayoutConfiguration> configurationList = new ArrayList<>();
+                for (int i = 0; i < size; i++) {
+                    KeyboardLayout selectedLayout = mSelectedLayoutList.get(i);
+                    @LayoutSelectionCriteria int layoutSelectionCriteria =
+                            mLayoutSelectionCriteriaList.get(i);
+                    InputMethodSubtype imeSubtype =  mImeSubtypeList.get(i);
+                    String keyboardLanguageTag;
+                    String keyboardLayoutStringType;
+                    if (layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_DEVICE) {
+                        keyboardLanguageTag = mInputDevice.getKeyboardLanguageTag();
+                        keyboardLayoutStringType = mInputDevice.getKeyboardLayoutType();
+                    } else {
+                        ULocale pkLocale = imeSubtype.getPhysicalKeyboardHintLanguageTag();
+                        keyboardLanguageTag = pkLocale != null ? pkLocale.toLanguageTag()
+                                : imeSubtype.getCanonicalizedLanguageTag();
+                        keyboardLayoutStringType = imeSubtype.getPhysicalKeyboardHintLayoutType();
+                    }
+                    // Sanitize null values
+                    String keyboardLayoutName =
+                            selectedLayout == null ? DEFAULT_LAYOUT : selectedLayout.getLabel();
+                    keyboardLanguageTag = keyboardLanguageTag == null ? "" : keyboardLanguageTag;
+                    int keyboardLayoutType = KeyboardLayout.LayoutType.getLayoutTypeEnumValue(
+                            keyboardLayoutStringType);
+
+                    configurationList.add(
+                            new LayoutConfiguration(keyboardLayoutType, keyboardLanguageTag,
+                                    keyboardLayoutName, layoutSelectionCriteria));
+                }
+                return new KeyboardConfigurationEvent(mInputDevice, mIsFirstConfiguration,
+                        configurationList);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    static class LayoutConfiguration {
+        // This should match enum values defined in "frameworks/base/core/res/res/values/attrs.xml"
+        public final int keyboardLayoutType;
+        public final String keyboardLanguageTag;
+        public final String keyboardLayoutName;
+        @LayoutSelectionCriteria
+        public final int layoutSelectionCriteria;
+
+        private LayoutConfiguration(int keyboardLayoutType, String keyboardLanguageTag,
+                String keyboardLayoutName, @LayoutSelectionCriteria int layoutSelectionCriteria) {
+            this.keyboardLayoutType = keyboardLayoutType;
+            this.keyboardLanguageTag = keyboardLanguageTag;
+            this.keyboardLayoutName = keyboardLayoutName;
+            this.layoutSelectionCriteria = layoutSelectionCriteria;
+        }
+
+        @Override
+        public String toString() {
+            return "{keyboardLanguageTag = " + keyboardLanguageTag + " keyboardLayoutType = "
+                    + KeyboardLayout.LayoutType.getLayoutNameFromValue(keyboardLayoutType)
+                    + " keyboardLayoutName = " + keyboardLayoutName + " layoutSelectionCriteria = "
+                    + getStringForSelectionCriteria(layoutSelectionCriteria) + "}";
+        }
+    }
+
+    private static String getStringForSelectionCriteria(
+            @LayoutSelectionCriteria int layoutSelectionCriteria) {
+        switch (layoutSelectionCriteria) {
+            case LAYOUT_SELECTION_CRITERIA_USER:
+                return "LAYOUT_SELECTION_CRITERIA_USER";
+            case LAYOUT_SELECTION_CRITERIA_DEVICE:
+                return "LAYOUT_SELECTION_CRITERIA_DEVICE";
+            case LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD:
+                return "LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD";
+            default:
+                return "INVALID_CRITERIA";
+        }
+    }
+
+    private static boolean isValidSelectionCriteria(int layoutSelectionCriteria) {
+        return layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_USER
+                || layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_DEVICE
+                || layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD;
+    }
+}
+
diff --git a/services/core/java/com/android/server/input/PersistentDataStore.java b/services/core/java/com/android/server/input/PersistentDataStore.java
index bce210d..31083fd 100644
--- a/services/core/java/com/android/server/input/PersistentDataStore.java
+++ b/services/core/java/com/android/server/input/PersistentDataStore.java
@@ -101,6 +101,10 @@
         }
     }
 
+    public boolean hasInputDeviceEntry(String inputDeviceDescriptor) {
+        return getInputDeviceState(inputDeviceDescriptor) != null;
+    }
+
     public TouchCalibration getTouchCalibration(String inputDeviceDescriptor, int surfaceRotation) {
         InputDeviceState state = getInputDeviceState(inputDeviceDescriptor);
         if (state == null) {
diff --git a/services/core/java/com/android/server/input/UEventManager.java b/services/core/java/com/android/server/input/UEventManager.java
new file mode 100644
index 0000000..17d87e4
--- /dev/null
+++ b/services/core/java/com/android/server/input/UEventManager.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+package com.android.server.input;
+
+import android.os.UEventObserver;
+
+/** An interface used to change the API of UEventObserver to a more test-friendly format. */
+interface UEventManager {
+
+    abstract class UEventListener {
+        private final UEventObserver mObserver = new UEventObserver() {
+            @Override
+            public void onUEvent(UEvent event) {
+                UEventListener.this.onUEvent(event);
+            }
+        };
+
+        public abstract void onUEvent(UEventObserver.UEvent event);
+    }
+
+    default void addListener(UEventListener listener, String match) {
+        listener.mObserver.startObserving(match);
+    }
+
+    default void removeListener(UEventListener listener) {
+        listener.mObserver.stopObserving();
+    }
+}
diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
index 6a0550b..aa99dab 100644
--- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
+++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
@@ -291,6 +291,10 @@
         reset(false /* reinitializing */);
     }
 
+    void setInkWindowInitializer(Runnable inkWindowInitializer) {
+        mInkWindowInitRunnable = inkWindowInitializer;
+    }
+
     private void reset(boolean reinitializing) {
         if (mHandwritingEventReceiver != null) {
             mHandwritingEventReceiver.dispose();
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 57f8d14..02ee96a 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -48,6 +48,7 @@
 
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME;
 import static com.android.server.inputmethod.InputMethodBindingController.TIME_TO_RECONNECT;
 import static com.android.server.inputmethod.InputMethodUtils.isSoftInputModeStateVisibleAllowed;
 
@@ -2367,6 +2368,28 @@
         }
     }
 
+    /**
+     * Called when {@link #resetCurrentMethodAndClientLocked(int)} invoked for clean-up states
+     * before unbinding the current method.
+     */
+    @GuardedBy("ImfLock.class")
+    void onUnbindCurrentMethodByReset() {
+        final ImeTargetWindowState winState = mVisibilityStateComputer.getWindowStateOrNull(
+                mCurFocusedWindow);
+        if (winState != null && !winState.isRequestedImeVisible()
+                && !mVisibilityStateComputer.isInputShown()) {
+            // Normally, the focus window will apply the IME visibility state to
+            // WindowManager when the IME has applied it. But it would be too late when
+            // switching IMEs in between different users. (Since the focused IME will
+            // first unbind the service to switch to bind the next user of the IME
+            // service, that wouldn't make the attached IME token validity check in time)
+            // As a result, we have to notify WM to apply IME visibility before clearing the
+            // binding states in the first place.
+            mVisibilityApplier.applyImeVisibility(mCurFocusedWindow, mCurStatsToken,
+                    STATE_HIDE_IME);
+        }
+    }
+
     /** {@code true} when a {@link ClientState} has attached from starting the input connection. */
     @GuardedBy("ImfLock.class")
     boolean hasAttachedClient() {
@@ -2449,6 +2472,9 @@
                 curInputMethodInfo != null && curInputMethodInfo.suppressesSpellChecker();
         final SparseArray<IAccessibilityInputMethodSession> accessibilityInputMethodSessions =
                 createAccessibilityInputMethodSessions(mCurClient.mAccessibilitySessions);
+        if (mBindingController.supportsStylusHandwriting() && hasSupportedStylusLocked()) {
+            mHwController.setInkWindowInitializer(new InkWindowInitializer());
+        }
         return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
                 session.mSession, accessibilityInputMethodSessions,
                 (session.mChannel != null ? session.mChannel.dup() : null),
@@ -2838,6 +2864,8 @@
     @GuardedBy("ImfLock.class")
     void resetCurrentMethodAndClientLocked(@UnbindReason int unbindClientReason) {
         setSelectedMethodIdLocked(null);
+        // Callback before clean-up binding states.
+        onUnbindCurrentMethodByReset();
         mBindingController.unbindCurrentMethod();
         unbindCurrentClientLocked(unbindClientReason);
     }
@@ -3050,7 +3078,12 @@
     @GuardedBy("ImfLock.class")
     private boolean shouldShowImeSwitcherLocked(int visibility) {
         if (!mShowOngoingImeSwitcherForPhones) return false;
+        // When the IME switcher dialog is shown, the IME switcher button should be hidden.
         if (mMenuController.getSwitchingDialogLocked() != null) return false;
+        // When we are switching IMEs, the IME switcher button should be hidden.
+        if (!Objects.equals(getCurIdLocked(), getSelectedMethodIdLocked())) {
+            return false;
+        }
         if (mWindowManagerInternal.isKeyguardShowingAndNotOccluded()
                 && mWindowManagerInternal.isKeyguardSecure(mSettings.getCurrentUserId())) {
             return false;
@@ -3205,7 +3238,12 @@
             } else {
                 vis &= ~InputMethodService.IME_VISIBLE_IMPERCEPTIBLE;
             }
-            // mImeWindowVis should be updated before calling shouldShowImeSwitcherLocked().
+            if (mMenuController.getSwitchingDialogLocked() != null
+                    || !Objects.equals(getCurIdLocked(), getSelectedMethodIdLocked())) {
+                // When the IME switcher dialog is shown, or we are switching IMEs,
+                // the back button should be in the default state (as if the IME is not shown).
+                backDisposition = InputMethodService.BACK_DISPOSITION_ADJUST_NOTHING;
+            }
             final boolean needsToShowImeSwitcher = shouldShowImeSwitcherLocked(vis);
             if (mStatusBarManagerInternal != null) {
                 mStatusBarManagerInternal.setImeWindowStatus(mCurTokenDisplayId,
@@ -5769,7 +5807,7 @@
                 // input target changed, in case seeing the dialog dismiss flickering during
                 // the next focused window starting the input connection.
                 if (mLastImeTargetWindow != mCurFocusedWindow) {
-                    mMenuController.hideInputMethodMenu();
+                    mMenuController.hideInputMethodMenuLocked();
                 }
             }
         }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
index c212e8e..c2ef83d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
@@ -175,13 +175,13 @@
                     int subtypeId = mSubtypeIds[which];
                     adapter.mCheckedItem = which;
                     adapter.notifyDataSetChanged();
-                    hideInputMethodMenu();
                     if (im != null) {
                         if (subtypeId < 0 || subtypeId >= im.getSubtypeCount()) {
                             subtypeId = NOT_A_SUBTYPE_ID;
                         }
                         mService.setInputMethodLocked(im.getId(), subtypeId);
                     }
+                    hideInputMethodMenuLocked();
                 }
             };
             mDialogBuilder.setSingleChoiceItems(adapter, checkedItem, choiceListener);
@@ -220,12 +220,18 @@
         }
     }
 
+    /**
+     * Hides the input method switcher menu.
+     */
     void hideInputMethodMenu() {
         synchronized (ImfLock.class) {
             hideInputMethodMenuLocked();
         }
     }
 
+    /**
+     * Hides the input method switcher menu, synchronised version of {@link #hideInputMethodMenu}.
+     */
     @GuardedBy("ImfLock.class")
     void hideInputMethodMenuLocked() {
         if (DEBUG) Slog.v(TAG, "Hide switching menu");
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 5f78374..93c66a1 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -52,6 +52,7 @@
 import android.os.ShellCallback;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.provider.Settings;
 import android.util.Log;
 import android.util.Pair;
@@ -169,6 +170,8 @@
 
     private SensorPrivacyManagerInternal mSensorPrivacyManagerInternal;
 
+    private UserManager mUserManager = null;
+
     private final Map<Integer, AtomicLong> mLastRestartTimestampMap = new HashMap<>();
 
     /**
@@ -491,6 +494,14 @@
             return;
         }
 
+        if (mUserManager == null) {
+            mUserManager = mContext.getSystemService(UserManager.class);
+            if (mUserManager == null) {
+                Log.e(TAG, "Unable to get the UserManager service");
+                return;
+            }
+        }
+
         sendMicrophoneDisableSettingUpdateForCurrentUser();
         if (mSensorPrivacyManagerInternal == null) {
             Log.e(TAG, "Unable to add a sensor privacy listener for all users");
@@ -499,8 +510,9 @@
 
         mSensorPrivacyManagerInternal.addSensorPrivacyListenerForAllUsers(
                 SensorPrivacyManager.Sensors.MICROPHONE, (userId, enabled) -> {
-                    if (userId == getCurrentUserId()) {
-                        Log.d(TAG, "User: " + userId + "mic privacy: " + enabled);
+                    // If we are in HSUM mode, any user can change the microphone setting
+                    if (mUserManager.isHeadlessSystemUserMode() || userId == getCurrentUserId()) {
+                        Log.d(TAG, "User: " + userId + " mic privacy: " + enabled);
                         sendMicrophoneDisableSettingUpdate(enabled);
                     }
                 });
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 2e62ef4..a96e4ad 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -31,7 +31,6 @@
 import static android.os.UserHandle.USER_SYSTEM;
 
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
-import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN;
 import static com.android.internal.widget.LockPatternUtils.CURRENT_LSKF_BASED_PROTECTOR_ID_KEY;
@@ -40,8 +39,12 @@
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE;
 import static com.android.internal.widget.LockPatternUtils.USER_FRP;
+import static com.android.internal.widget.LockPatternUtils.USER_REPAIR_MODE;
 import static com.android.internal.widget.LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE;
+import static com.android.internal.widget.LockPatternUtils.VERIFY_FLAG_WRITE_REPAIR_MODE_PW;
 import static com.android.internal.widget.LockPatternUtils.frpCredentialEnabled;
+import static com.android.internal.widget.LockPatternUtils.isSpecialUserId;
+import static com.android.internal.widget.LockPatternUtils.pinOrPasswordQualityToCredentialType;
 import static com.android.internal.widget.LockPatternUtils.userOwnsFrpCredential;
 import static com.android.server.locksettings.SyntheticPasswordManager.TOKEN_TYPE_STRONG;
 import static com.android.server.locksettings.SyntheticPasswordManager.TOKEN_TYPE_WEAK;
@@ -278,8 +281,6 @@
     protected IGateKeeperService mGateKeeperService;
     protected IAuthSecret mAuthSecretService;
 
-    private static final String GSI_RUNNING_PROP = "ro.gsid.image_running";
-
     /**
      * The UIDs that are used for system credential storage in keystore.
      */
@@ -311,6 +312,7 @@
             if (phase == PHASE_ACTIVITY_MANAGER_READY) {
                 mLockSettingsService.migrateOldDataAfterSystemReady();
                 mLockSettingsService.loadEscrowData();
+                mLockSettingsService.deleteRepairModePersistentDataIfNeeded();
             }
         }
 
@@ -541,7 +543,7 @@
         }
 
         public boolean isGsiRunning() {
-            return SystemProperties.getInt(GSI_RUNNING_PROP, 0) > 0;
+            return LockPatternUtils.isGsiRunning();
         }
 
         public FingerprintManager getFingerprintManager() {
@@ -949,6 +951,16 @@
         return success;
     }
 
+    @VisibleForTesting
+    void deleteRepairModePersistentDataIfNeeded() {
+        if (!LockPatternUtils.isRepairModeSupported(mContext)
+                || LockPatternUtils.isRepairModeActive(mContext)
+                || mInjector.isGsiRunning()) {
+            return;
+        }
+        mStorage.deleteRepairModePersistentData();
+    }
+
     // This is called when Weaver is guaranteed to be available (if the device supports Weaver).
     // It does any synthetic password related work that was delayed from earlier in the boot.
     private void onThirdPartyAppsStarted() {
@@ -1279,8 +1291,8 @@
      * {@link #CREDENTIAL_TYPE_PASSWORD}
      */
     private int getCredentialTypeInternal(int userId) {
-        if (userId == USER_FRP) {
-            return getFrpCredentialType();
+        if (isSpecialUserId(userId)) {
+            return mSpManager.getSpecialUserCredentialType(userId);
         }
         synchronized (mSpManager) {
             final long protectorId = getCurrentLskfBasedProtectorId(userId);
@@ -1296,29 +1308,6 @@
         }
     }
 
-    private int getFrpCredentialType() {
-        PersistentData data = mStorage.readPersistentDataBlock();
-        if (data.type != PersistentData.TYPE_SP_GATEKEEPER &&
-                data.type != PersistentData.TYPE_SP_WEAVER) {
-            return CREDENTIAL_TYPE_NONE;
-        }
-        int credentialType = SyntheticPasswordManager.getFrpCredentialType(data.payload);
-        if (credentialType != CREDENTIAL_TYPE_PASSWORD_OR_PIN) {
-            return credentialType;
-        }
-        return pinOrPasswordQualityToCredentialType(data.qualityForUi);
-    }
-
-    private static int pinOrPasswordQualityToCredentialType(int quality) {
-        if (LockPatternUtils.isQualityAlphabeticPassword(quality)) {
-            return CREDENTIAL_TYPE_PASSWORD;
-        }
-        if (LockPatternUtils.isQualityNumericPin(quality)) {
-            return CREDENTIAL_TYPE_PIN;
-        }
-        throw new IllegalArgumentException("Quality is neither Pin nor password: " + quality);
-    }
-
     private boolean isUserSecure(int userId) {
         return getCredentialTypeInternal(userId) != CREDENTIAL_TYPE_NONE;
     }
@@ -1560,8 +1549,8 @@
      * unlock operation.
      */
     private void sendCredentialsOnUnlockIfRequired(LockscreenCredential credential, int userId) {
-        // Don't send credentials during the factory reset protection flow.
-        if (userId == USER_FRP) {
+        // Don't send credentials during the special user flow.
+        if (isSpecialUserId(userId)) {
             return;
         }
 
@@ -2185,15 +2174,19 @@
             Slog.e(TAG, "FRP credential can only be verified prior to provisioning.");
             return VerifyCredentialResponse.ERROR;
         }
+        if (userId == USER_REPAIR_MODE && !LockPatternUtils.isRepairModeActive(mContext)) {
+            Slog.e(TAG, "Repair mode is not active on the device.");
+            return VerifyCredentialResponse.ERROR;
+        }
         Slogf.i(TAG, "Verifying lockscreen credential for user %d", userId);
 
         final AuthenticationResult authResult;
         VerifyCredentialResponse response;
 
         synchronized (mSpManager) {
-            if (userId == USER_FRP) {
-                return mSpManager.verifyFrpCredential(getGateKeeperService(), credential,
-                        progressCallback);
+            if (isSpecialUserId(userId)) {
+                return mSpManager.verifySpecialUserCredential(userId, getGateKeeperService(),
+                        credential, progressCallback);
             }
 
             long protectorId = getCurrentLskfBasedProtectorId(userId);
@@ -2202,6 +2195,12 @@
             response = authResult.gkResponse;
 
             if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+                if ((flags & VERIFY_FLAG_WRITE_REPAIR_MODE_PW) != 0) {
+                    if (!mSpManager.writeRepairModeCredentialLocked(protectorId, userId)) {
+                        Slog.e(TAG, "Failed to write repair mode credential");
+                        return VerifyCredentialResponse.ERROR;
+                    }
+                }
                 // credential has matched
                 mBiometricDeferredQueue.addPendingLockoutResetForUser(userId,
                         authResult.syntheticPassword.deriveGkPassword());
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
index 2fa637e..1c5ecb7 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
@@ -19,7 +19,7 @@
 import static android.content.Context.USER_SERVICE;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
-import static com.android.internal.widget.LockPatternUtils.USER_FRP;
+import static com.android.internal.widget.LockPatternUtils.isSpecialUserId;
 
 import android.annotation.Nullable;
 import android.app.admin.DevicePolicyManager;
@@ -90,6 +90,9 @@
 
     private static final String SYNTHETIC_PASSWORD_DIRECTORY = "spblob/";
 
+    private static final String REPAIR_MODE_DIRECTORY = "repair-mode/";
+    private static final String REPAIR_MODE_PERSISTENT_FILE = "pst";
+
     private static final Object DEFAULT = new Object();
 
     private static final String[] SETTINGS_TO_BACKUP = new String[] {
@@ -390,6 +393,29 @@
         }
     }
 
+    @VisibleForTesting
+    File getRepairModePersistentDataFile() {
+        final File directory = new File(Environment.getMetadataDirectory(), REPAIR_MODE_DIRECTORY);
+        return new File(directory, REPAIR_MODE_PERSISTENT_FILE);
+    }
+
+    public PersistentData readRepairModePersistentData() {
+        final byte[] data = readFile(getRepairModePersistentDataFile());
+        if (data == null) {
+            return PersistentData.NONE;
+        }
+        return PersistentData.fromBytes(data);
+    }
+
+    public void writeRepairModePersistentData(int persistentType, int userId, byte[] payload) {
+        writeFile(getRepairModePersistentDataFile(),
+                PersistentData.toBytes(persistentType, userId, /* qualityForUi= */0, payload));
+    }
+
+    public void deleteRepairModePersistentData() {
+        deleteFile(getRepairModePersistentDataFile());
+    }
+
     /**
      * Writes the synthetic password state file for the given user ID, protector ID, and state name.
      * If the file already exists, then it is atomically replaced.
@@ -510,7 +536,8 @@
     }
 
     public void setString(String key, String value, int userId) {
-        Preconditions.checkArgument(userId != USER_FRP, "cannot store lock settings for FRP user");
+        Preconditions.checkArgument(!isSpecialUserId(userId),
+                "cannot store lock settings for special user: %d", userId);
 
         writeKeyValue(key, value, userId);
         if (ArrayUtils.contains(SETTINGS_TO_BACKUP, key)) {
@@ -535,7 +562,7 @@
     }
 
     public String getString(String key, String defaultValue, int userId) {
-        if (userId == USER_FRP) {
+        if (isSpecialUserId(userId)) {
             return null;
         }
         return readKeyValue(key, defaultValue, userId);
@@ -583,6 +610,17 @@
         }
     }
 
+    /**
+     * Provides a concrete data structure to represent the minimal information from
+     * a user's LSKF-based SP protector that is needed to verify the user's LSKF,
+     * in combination with the corresponding Gatekeeper enrollment or Weaver slot.
+     * It can be stored in {@link com.android.server.PersistentDataBlockService} for
+     * FRP to live across factory resets not initiated via the Settings UI.
+     * Written to {@link #REPAIR_MODE_PERSISTENT_FILE} to support verification for
+     * exiting repair mode, since the device runs with an empty data partition in
+     * repair mode and the same credential be provided to exit repair mode is
+     * required.
+     */
     public static class PersistentData {
         static final byte VERSION_1 = 1;
         static final int VERSION_1_HEADER_SIZE = 1 + 1 + 4 + 4;
@@ -685,6 +723,19 @@
             }
             pw.decreaseIndent();
         }
+        // Dump repair mode file states
+        final File repairModeFile = getRepairModePersistentDataFile();
+        if (repairModeFile.exists()) {
+            pw.println(TextUtils.formatSimple("Repair Mode [%s]:", repairModeFile.getParent()));
+            pw.increaseIndent();
+            pw.println(TextUtils.formatSimple("%6d %s %s", repairModeFile.length(),
+                    LockSettingsService.timestampToString(repairModeFile.lastModified()),
+                    repairModeFile.getName()));
+            final PersistentData data = readRepairModePersistentData();
+            pw.println(TextUtils.formatSimple("type: %d, user id: %d, payload size: %d",
+                    data.type, data.userId, data.payload != null ? data.payload.length : 0));
+            pw.decreaseIndent();
+        }
     }
 
     static class DatabaseHelper extends SQLiteOpenHelper {
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index 66f862a..7349088 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -16,9 +16,14 @@
 
 package com.android.server.locksettings;
 
+import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
+import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN;
 import static com.android.internal.widget.LockPatternUtils.EscrowTokenStateChangeCallback;
 import static com.android.internal.widget.LockPatternUtils.PIN_LENGTH_UNAVAILABLE;
+import static com.android.internal.widget.LockPatternUtils.USER_FRP;
+import static com.android.internal.widget.LockPatternUtils.USER_REPAIR_MODE;
+import static com.android.internal.widget.LockPatternUtils.pinOrPasswordQualityToCredentialType;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -718,11 +723,30 @@
         return PasswordData.fromBytes(passwordData).credentialType;
     }
 
-    static int getFrpCredentialType(byte[] payload) {
-        if (payload == null) {
+    int getSpecialUserCredentialType(int userId) {
+        final PersistentData data = getSpecialUserPersistentData(userId);
+        if (data.type != PersistentData.TYPE_SP_GATEKEEPER
+                && data.type != PersistentData.TYPE_SP_WEAVER) {
+            return CREDENTIAL_TYPE_NONE;
+        }
+        if (data.payload == null) {
             return LockPatternUtils.CREDENTIAL_TYPE_NONE;
         }
-        return PasswordData.fromBytes(payload).credentialType;
+        final int credentialType = PasswordData.fromBytes(data.payload).credentialType;
+        if (credentialType != CREDENTIAL_TYPE_PASSWORD_OR_PIN) {
+            return credentialType;
+        }
+        return pinOrPasswordQualityToCredentialType(data.qualityForUi);
+    }
+
+    private PersistentData getSpecialUserPersistentData(int userId) {
+        if (userId == USER_FRP) {
+            return mStorage.readPersistentDataBlock();
+        }
+        if (userId == USER_REPAIR_MODE) {
+            return mStorage.readRepairModePersistentData();
+        }
+        throw new IllegalArgumentException("Unknown special user id " + userId);
     }
 
     /**
@@ -1005,10 +1029,10 @@
         return sizeOfCredential;
     }
 
-    public VerifyCredentialResponse verifyFrpCredential(IGateKeeperService gatekeeper,
-            LockscreenCredential userCredential,
+    public VerifyCredentialResponse verifySpecialUserCredential(int sourceUserId,
+            IGateKeeperService gatekeeper, LockscreenCredential userCredential,
             ICheckCredentialProgressCallback progressCallback) {
-        PersistentData persistentData = mStorage.readPersistentDataBlock();
+        final PersistentData persistentData = getSpecialUserPersistentData(sourceUserId);
         if (persistentData.type == PersistentData.TYPE_SP_GATEKEEPER) {
             PasswordData pwd = PasswordData.fromBytes(persistentData.payload);
             byte[] stretchedLskf = stretchLskf(userCredential, pwd);
@@ -1019,13 +1043,13 @@
                         0 /* challenge */, pwd.passwordHandle,
                         stretchedLskfToGkPassword(stretchedLskf));
             } catch (RemoteException e) {
-                Slog.e(TAG, "FRP verifyChallenge failed", e);
+                Slog.e(TAG, "Persistent data credential verifyChallenge failed", e);
                 return VerifyCredentialResponse.ERROR;
             }
             return VerifyCredentialResponse.fromGateKeeperResponse(response);
         } else if (persistentData.type == PersistentData.TYPE_SP_WEAVER) {
             if (!isWeaverAvailable()) {
-                Slog.e(TAG, "No weaver service to verify SP-based FRP credential");
+                Slog.e(TAG, "No weaver service to verify SP-based persistent data credential");
                 return VerifyCredentialResponse.ERROR;
             }
             PasswordData pwd = PasswordData.fromBytes(persistentData.payload);
@@ -1114,6 +1138,57 @@
         }
     }
 
+    /**
+     * Writes the user's synthetic password data to the repair mode file.
+     *
+     * @param protectorId current LSKF based protectorId
+     * @param userId user id of the user
+     */
+    public boolean writeRepairModeCredentialLocked(long protectorId, int userId) {
+        if (!shouldWriteRepairModeCredential(userId)) {
+            return false;
+        }
+        final byte[] data = loadState(PASSWORD_DATA_NAME, protectorId, userId);
+        if (data == null) {
+            Slogf.w(TAG, "Password data not found for user %d", userId);
+            return false;
+        }
+        final PasswordData pwd = PasswordData.fromBytes(data);
+        if (isNoneCredential(pwd)) {
+            Slogf.w(TAG, "User %d has NONE credential", userId);
+            return false;
+        }
+        Slogf.d(TAG, "Writing repair mode credential tied to user %d", userId);
+        final int weaverSlot = loadWeaverSlot(protectorId, userId);
+        if (weaverSlot != INVALID_WEAVER_SLOT) {
+            // write weaver password
+            mStorage.writeRepairModePersistentData(
+                    PersistentData.TYPE_SP_WEAVER, weaverSlot, pwd.toBytes());
+        } else {
+            // write gatekeeper password
+            mStorage.writeRepairModePersistentData(
+                    PersistentData.TYPE_SP_GATEKEEPER, userId, pwd.toBytes());
+        }
+        return true;
+    }
+
+    private boolean shouldWriteRepairModeCredential(int userId) {
+        final UserInfo userInfo = mUserManager.getUserInfo(userId);
+        if (!LockPatternUtils.canUserEnterRepairMode(mContext, userInfo)) {
+            Slogf.w(TAG, "User %d can't enter repair mode", userId);
+            return false;
+        }
+        if (LockPatternUtils.isRepairModeActive(mContext)) {
+            Slog.w(TAG, "Can't write repair mode credential while repair mode is already active");
+            return false;
+        }
+        if (LockPatternUtils.isGsiRunning()) {
+            Slog.w(TAG, "Can't write repair mode credential while GSI is running");
+            return false;
+        }
+        return true;
+    }
+
     private ArrayMap<Integer, ArrayMap<Long, TokenData>> tokenMap = new ArrayMap<>();
 
     /**
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 65e34e6..398e470 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -147,7 +147,7 @@
         mInjector = injector;
         mClock = injector.createClock();
         mDeathEaters = new ArrayMap<IBinder, IBinder.DeathRecipient>();
-        mCallbackDelegate = new CallbackDelegate();
+        mCallbackDelegate = new CallbackDelegate(injector.createCallbackLooper());
         mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
         mPackageManager = mContext.getPackageManager();
@@ -182,6 +182,11 @@
         Clock createClock() {
             return SystemClock::uptimeMillis;
         }
+
+        /** Creates the {@link Looper} to be used when notifying callbacks. */
+        Looper createCallbackLooper() {
+            return Looper.getMainLooper();
+        }
     }
 
     @Override
@@ -268,7 +273,8 @@
         dispatchStop(projection);
     }
 
-    private void addCallback(final IMediaProjectionWatcherCallback callback) {
+    @VisibleForTesting
+    void addCallback(final IMediaProjectionWatcherCallback callback) {
         IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
             @Override
             public void binderDied() {
@@ -315,6 +321,12 @@
         mCallbackDelegate.dispatchStop(projection);
     }
 
+    private void dispatchSessionSet(
+            @NonNull MediaProjectionInfo projectionInfo,
+            @Nullable ContentRecordingSession session) {
+        mCallbackDelegate.dispatchSession(projectionInfo, session);
+    }
+
     /**
      * Returns {@code true} when updating the current mirroring session on WM succeeded, and
      * {@code false} otherwise.
@@ -335,6 +347,7 @@
             if (mProjectionGrant != null) {
                 // Cache the session details.
                 mProjectionGrant.mSession = incomingSession;
+                dispatchSessionSet(mProjectionGrant.getProjectionInfo(), incomingSession);
             }
             return true;
         }
@@ -1155,8 +1168,8 @@
         private Handler mHandler;
         private final Object mLock = new Object();
 
-        public CallbackDelegate() {
-            mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/);
+        CallbackDelegate(Looper callbackLooper) {
+            mHandler = new Handler(callbackLooper, null, true /*async*/);
             mClientCallbacks = new ArrayMap<IBinder, IMediaProjectionCallback>();
             mWatcherCallbacks = new ArrayMap<IBinder, IMediaProjectionWatcherCallback>();
         }
@@ -1219,6 +1232,16 @@
             }
         }
 
+        public void dispatchSession(
+                @NonNull MediaProjectionInfo projectionInfo,
+                @Nullable ContentRecordingSession session) {
+            synchronized (mLock) {
+                for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) {
+                    mHandler.post(new WatcherSessionCallback(callback, projectionInfo, session));
+                }
+            }
+        }
+
         public void dispatchResize(MediaProjection projection, int width, int height) {
             if (projection == null) {
                 Slog.e(TAG,
@@ -1335,6 +1358,29 @@
         }
     }
 
+    private static final class WatcherSessionCallback implements Runnable {
+        private final IMediaProjectionWatcherCallback mCallback;
+        private final MediaProjectionInfo mProjectionInfo;
+        private final ContentRecordingSession mSession;
+
+        WatcherSessionCallback(
+                @NonNull IMediaProjectionWatcherCallback callback,
+                @NonNull MediaProjectionInfo projectionInfo,
+                @Nullable ContentRecordingSession session) {
+            mCallback = callback;
+            mProjectionInfo = projectionInfo;
+            mSession = session;
+        }
+
+        @Override
+        public void run() {
+            try {
+                mCallback.onRecordingSessionSet(mProjectionInfo, mSession);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Failed to notify content recording session changed", e);
+            }
+        }
+    }
 
     private static String typeToString(int type) {
         switch (type) {
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 4bd66f2..e0e6410 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -2344,6 +2344,7 @@
                 mRankingHandler,
                 mZenModeHelper,
                 mPermissionHelper,
+                mPermissionManager,
                 mNotificationChannelLogger,
                 mAppOps,
                 new SysUiStatsEvent.BuilderFactory(),
@@ -6777,6 +6778,8 @@
             return false;
         }
 
+        mUsageStats.registerEnqueuedByAppAndAccepted(pkg);
+
         if (info != null) {
             // Cache the shortcut synchronously after the associated notification is posted in case
             // the app unpublishes this shortcut immediately after posting the notification. If the
@@ -7215,11 +7218,12 @@
                             + " cannot create notifications");
                 }
 
-                // rate limit updates that aren't completed progress notifications
-                if (mNotificationsByKey.get(r.getSbn().getKey()) != null
-                        && !r.getNotification().hasCompletedProgress()
-                        && !isAutogroup) {
-
+                // Rate limit updates that aren't completed progress notifications
+                // Search for the original one in the posted and not-yet-posted (enqueued) lists.
+                boolean isUpdate = mNotificationsByKey.get(r.getSbn().getKey()) != null
+                        || findNotificationByListLocked(mEnqueuedNotifications, r.getSbn().getKey())
+                        != null;
+                if (isUpdate && !r.getNotification().hasCompletedProgress() && !isAutogroup) {
                     final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg);
                     if (appEnqueueRate > mMaxPackageEnqueueRate) {
                         mUsageStats.registerOverRateQuota(pkg);
@@ -7844,15 +7848,8 @@
             boolean posted = false;
             synchronized (mNotificationLock) {
                 try {
-                    NotificationRecord r = null;
-                    int N = mEnqueuedNotifications.size();
-                    for (int i = 0; i < N; i++) {
-                        final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
-                        if (Objects.equals(key, enqueued.getKey())) {
-                            r = enqueued;
-                            break;
-                        }
-                    }
+                    NotificationRecord r = findNotificationByListLocked(mEnqueuedNotifications,
+                            key);
                     if (r == null) {
                         Slog.i(TAG, "Cannot find enqueued record for key: " + key);
                         return false;
@@ -9511,14 +9508,19 @@
      * Determine whether the userId applies to the notification in question, either because
      * they match exactly, or one of them is USER_ALL (which is treated as a wildcard).
      */
-    private boolean notificationMatchesUserId(NotificationRecord r, int userId) {
-        return
+    private static boolean notificationMatchesUserId(NotificationRecord r, int userId,
+            boolean isAutogroupSummary) {
+        if (isAutogroupSummary) {
+            return r.getUserId() == userId;
+        } else {
+            return
                 // looking for USER_ALL notifications? match everything
-                   userId == UserHandle.USER_ALL
-                // a notification sent to USER_ALL matches any query
-                || r.getUserId() == UserHandle.USER_ALL
-                // an exact user match
-                || r.getUserId() == userId;
+                userId == UserHandle.USER_ALL
+                        // a notification sent to USER_ALL matches any query
+                        || r.getUserId() == UserHandle.USER_ALL
+                        // an exact user match
+                        || r.getUserId() == userId;
+        }
     }
 
     /**
@@ -9527,7 +9529,7 @@
      * because it matches one of the users profiles.
      */
     private boolean notificationMatchesCurrentProfiles(NotificationRecord r, int userId) {
-        return notificationMatchesUserId(r, userId)
+        return notificationMatchesUserId(r, userId, false)
                 || mUserProfiles.isCurrentProfile(r.getUserId());
     }
 
@@ -9596,7 +9598,7 @@
                 if (!notificationMatchesCurrentProfiles(r, userId)) {
                     continue;
                 }
-            } else if (!notificationMatchesUserId(r, userId)) {
+            } else if (!notificationMatchesUserId(r, userId, false)) {
                 continue;
             }
             // Don't remove notifications to all, if there's no package name specified
@@ -9834,7 +9836,7 @@
         final int len = list.size();
         for (int i = 0; i < len; i++) {
             NotificationRecord r = list.get(i);
-            if (notificationMatchesUserId(r, userId) && r.getGroupKey().equals(groupKey)
+            if (notificationMatchesUserId(r, userId, false) && r.getGroupKey().equals(groupKey)
                     && r.getSbn().getPackageName().equals(pkg)) {
                 records.add(r);
             }
@@ -9870,14 +9872,14 @@
         return null;
     }
 
-    @GuardedBy("mNotificationLock")
-    private NotificationRecord findNotificationByListLocked(ArrayList<NotificationRecord> list,
-            String pkg, String tag, int id, int userId) {
+    @Nullable
+    private static NotificationRecord findNotificationByListLocked(
+            ArrayList<NotificationRecord> list, String pkg, String tag, int id, int userId) {
         final int len = list.size();
         for (int i = 0; i < len; i++) {
             NotificationRecord r = list.get(i);
-            if (notificationMatchesUserId(r, userId) && r.getSbn().getId() == id &&
-                    TextUtils.equals(r.getSbn().getTag(), tag)
+            if (notificationMatchesUserId(r, userId, (r.getFlags() & GroupHelper.BASE_FLAGS) != 0)
+                    && r.getSbn().getId() == id && TextUtils.equals(r.getSbn().getTag(), tag)
                     && r.getSbn().getPackageName().equals(pkg)) {
                 return r;
             }
@@ -9885,15 +9887,14 @@
         return null;
     }
 
-    @GuardedBy("mNotificationLock")
-    private List<NotificationRecord> findNotificationsByListLocked(
+    private static List<NotificationRecord> findNotificationsByListLocked(
             ArrayList<NotificationRecord> list, String pkg, String tag, int id, int userId) {
         List<NotificationRecord> matching = new ArrayList<>();
         final int len = list.size();
         for (int i = 0; i < len; i++) {
             NotificationRecord r = list.get(i);
-            if (notificationMatchesUserId(r, userId) && r.getSbn().getId() == id &&
-                    TextUtils.equals(r.getSbn().getTag(), tag)
+            if (notificationMatchesUserId(r, userId, false) && r.getSbn().getId() == id
+                    && TextUtils.equals(r.getSbn().getTag(), tag)
                     && r.getSbn().getPackageName().equals(pkg)) {
                 matching.add(r);
             }
@@ -9901,9 +9902,9 @@
         return matching;
     }
 
-    @GuardedBy("mNotificationLock")
-    private NotificationRecord findNotificationByListLocked(ArrayList<NotificationRecord> list,
-            String key) {
+    @Nullable
+    private static NotificationRecord findNotificationByListLocked(
+            ArrayList<NotificationRecord> list, String key) {
         final int N = list.size();
         for (int i = 0; i < N; i++) {
             if (key.equals(list.get(i).getKey())) {
@@ -9913,29 +9914,6 @@
         return null;
     }
 
-    /**
-     * There may be multiple records that match your criteria. For instance if there have been
-     * multiple notifications posted which are enqueued for the same pkg, tag, id, userId. This
-     * method will find all of them in the given list
-     * @return
-     */
-    @GuardedBy("mNotificationLock")
-    private List<NotificationRecord> findEnqueuedNotificationsForCriteria(
-            String pkg, String tag, int id, int userId) {
-        final ArrayList<NotificationRecord> records = new ArrayList<>();
-        final int n = mEnqueuedNotifications.size();
-        for (int i = 0; i < n; i++) {
-            NotificationRecord r = mEnqueuedNotifications.get(i);
-            if (notificationMatchesUserId(r, userId)
-                    && r.getSbn().getId() == id
-                    && TextUtils.equals(r.getSbn().getTag(), tag)
-                    && r.getSbn().getPackageName().equals(pkg)) {
-                records.add(r);
-            }
-        }
-        return records;
-    }
-
     @GuardedBy("mNotificationLock")
     int indexOfNotificationLocked(String key) {
         final int N = mNotificationList.size();
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index c9a6c63..b2d3fca 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -27,6 +27,7 @@
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.IActivityManager;
+import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.Person;
@@ -46,6 +47,7 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.PowerManager;
 import android.os.UserHandle;
 import android.os.VibrationEffect;
 import android.provider.Settings;
@@ -101,7 +103,8 @@
     final int mTargetSdkVersion;
     final int mOriginalFlags;
     private final Context mContext;
-
+    private KeyguardManager mKeyguardManager;
+    private final PowerManager mPowerManager;
     NotificationUsageStats.SingleNotificationStats stats;
     boolean isCanceled;
     IBinder permissionOwner;
@@ -228,6 +231,8 @@
         mUpdateTimeMs = mCreationTimeMs;
         mInterruptionTimeMs = mCreationTimeMs;
         mContext = context;
+        mKeyguardManager = mContext.getSystemService(KeyguardManager.class);
+        mPowerManager = mContext.getSystemService(PowerManager.class);
         stats = new NotificationUsageStats.SingleNotificationStats();
         mChannel = channel;
         mPreChannelsNotification = isPreChannelsNotification();
@@ -1619,6 +1624,22 @@
         return mPhoneNumbers;
     }
 
+    boolean isLocked() {
+        return getKeyguardManager().isKeyguardLocked()
+                || !mPowerManager.isInteractive();  // Unlocked AOD
+    }
+
+    /**
+     * For some early {@link NotificationRecord}, {@link KeyguardManager} can be {@code null} in
+     * the constructor. Retrieve it again if it is null.
+     */
+    private KeyguardManager getKeyguardManager() {
+        if (mKeyguardManager == null) {
+            mKeyguardManager = mContext.getSystemService(KeyguardManager.class);
+        }
+        return mKeyguardManager;
+    }
+
     @VisibleForTesting
     static final class Light {
         public final int color;
diff --git a/services/core/java/com/android/server/notification/NotificationRecordLogger.java b/services/core/java/com/android/server/notification/NotificationRecordLogger.java
index 0cc4fc4..5ca882c 100644
--- a/services/core/java/com/android/server/notification/NotificationRecordLogger.java
+++ b/services/core/java/com/android/server/notification/NotificationRecordLogger.java
@@ -31,9 +31,12 @@
 import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationStats;
 
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags;
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags;
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
+import com.android.internal.util.FrameworkStatsLog;
 
 import java.util.ArrayList;
 import java.util.Objects;
@@ -500,6 +503,8 @@
         final boolean is_foreground_service;
         final long timeout_millis;
         final boolean is_non_dismissible;
+        final int fsi_state;
+        final boolean is_locked;
         @DurationMillisLong long post_duration_millis; // Not final; calculated at the end.
 
         NotificationReported(NotificationRecordPair p,
@@ -530,6 +535,20 @@
             this.is_foreground_service = NotificationRecordLogger.isForegroundService(p.r);
             this.timeout_millis = p.r.getSbn().getNotification().getTimeoutAfter();
             this.is_non_dismissible = NotificationRecordLogger.isNonDismissible(p.r);
+
+            final boolean isStickyHunFlagEnabled = SystemUiSystemPropertiesFlags.getResolver()
+                    .isEnabled(NotificationFlags.SHOW_STICKY_HUN_FOR_DENIED_FSI);
+
+            final boolean hasFullScreenIntent =
+                    p.r.getSbn().getNotification().fullScreenIntent != null;
+
+            final boolean hasFsiRequestedButDeniedFlag =  (p.r.getSbn().getNotification().flags
+                    & Notification.FLAG_FSI_REQUESTED_BUT_DENIED) != 0;
+
+            this.fsi_state = NotificationRecordLogger.getFsiState(isStickyHunFlagEnabled,
+                    hasFullScreenIntent, hasFsiRequestedButDeniedFlag, eventType);
+
+            this.is_locked = p.r.isLocked();
         }
     }
 
@@ -558,7 +577,6 @@
     }
 
     /**
-     * @param r NotificationRecord
      * @return Whether the notification is a non-dismissible notification.
      */
     static boolean isNonDismissible(@NonNull NotificationRecord r) {
@@ -567,4 +585,28 @@
         }
         return (r.getNotification().flags & Notification.FLAG_NO_DISMISS) != 0;
     }
+
+    /**
+     * @return FrameworkStatsLog enum of the state of the full screen intent posted with this
+     * notification.
+     */
+    static int getFsiState(boolean isStickyHunFlagEnabled,
+                           boolean hasFullScreenIntent,
+                           boolean hasFsiRequestedButDeniedFlag,
+                           NotificationReportedEvent eventType) {
+
+        if (!isStickyHunFlagEnabled
+                || eventType == NotificationReportedEvent.NOTIFICATION_UPDATED) {
+            // Zeroes in protos take zero bandwidth, but non-zero numbers take bandwidth,
+            // so we should log 0 when possible.
+            return 0;
+        }
+        if (hasFullScreenIntent) {
+            return FrameworkStatsLog.NOTIFICATION_REPORTED__FSI_STATE__FSI_ALLOWED;
+        }
+        if (hasFsiRequestedButDeniedFlag) {
+            return FrameworkStatsLog.NOTIFICATION_REPORTED__FSI_STATE__FSI_DENIED;
+        }
+        return FrameworkStatsLog.NOTIFICATION_REPORTED__FSI_STATE__NO_FSI;
+    }
 }
diff --git a/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java b/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java
index feb75ef..9da0e98 100644
--- a/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java
+++ b/services/core/java/com/android/server/notification/NotificationRecordLoggerImpl.java
@@ -75,7 +75,9 @@
                 notificationReported.is_foreground_service,
                 notificationReported.timeout_millis,
                 notificationReported.is_non_dismissible,
-                notificationReported.post_duration_millis);
+                notificationReported.post_duration_millis,
+                notificationReported.fsi_state,
+                notificationReported.is_locked);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/notification/NotificationUsageStats.java b/services/core/java/com/android/server/notification/NotificationUsageStats.java
index ffe33a8..e960f4b 100644
--- a/services/core/java/com/android/server/notification/NotificationUsageStats.java
+++ b/services/core/java/com/android/server/notification/NotificationUsageStats.java
@@ -16,23 +16,16 @@
 
 package com.android.server.notification;
 
-import static android.app.NotificationManager.IMPORTANCE_HIGH;
-
 import android.app.Notification;
-import android.content.ContentValues;
 import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteFullException;
-import android.database.sqlite.SQLiteOpenHelper;
 import android.os.Handler;
-import android.os.HandlerThread;
 import android.os.Message;
 import android.os.SystemClock;
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.logging.MetricsLogger;
 import com.android.server.notification.NotificationManagerService.DumpFilter;
 
@@ -42,8 +35,6 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayDeque;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
@@ -63,7 +54,6 @@
     private static final String TAG = "NotificationUsageStats";
 
     private static final boolean ENABLE_AGGREGATED_IN_MEMORY_STATS = true;
-    private static final boolean ENABLE_SQLITE_LOG = true;
     private static final AggregatedStats[] EMPTY_AGGREGATED_STATS = new AggregatedStats[0];
     private static final String DEVICE_GLOBAL_STATS = "__global"; // packages start with letters
     private static final int MSG_EMIT = 1;
@@ -73,12 +63,15 @@
     public static final int FOUR_HOURS = 1000 * 60 * 60 * 4;
     private static final long EMIT_PERIOD = DEBUG ? TEN_SECONDS : FOUR_HOURS;
 
-    // Guarded by synchronized(this).
+    @GuardedBy("this")
     private final Map<String, AggregatedStats> mStats = new HashMap<>();
+    @GuardedBy("this")
     private final ArrayDeque<AggregatedStats[]> mStatsArrays = new ArrayDeque<>();
+    @GuardedBy("this")
     private ArraySet<String> mStatExpiredkeys = new ArraySet<>();
     private final Context mContext;
     private final Handler mHandler;
+    @GuardedBy("this")
     private long mLastEmitTime;
 
     public NotificationUsageStats(Context context) {
@@ -105,11 +98,7 @@
      */
     public synchronized float getAppEnqueueRate(String packageName) {
         AggregatedStats stats = getOrCreateAggregatedStatsLocked(packageName);
-        if (stats != null) {
-            return stats.getEnqueueRate(SystemClock.elapsedRealtime());
-        } else {
-            return 0f;
-        }
+        return stats.getEnqueueRate(SystemClock.elapsedRealtime());
     }
 
     /**
@@ -117,11 +106,7 @@
      */
     public synchronized boolean isAlertRateLimited(String packageName) {
         AggregatedStats stats = getOrCreateAggregatedStatsLocked(packageName);
-        if (stats != null) {
-            return stats.isAlertRateLimited();
-        } else {
-            return false;
-        }
+        return stats.isAlertRateLimited();
     }
 
     /**
@@ -136,16 +121,32 @@
     }
 
     /**
+     * Called when a notification that was enqueued by an app is effectively enqueued to be
+     * posted. This is after rate checking, to update the rate.
+     *
+     * <p>Note that if we updated the arrival estimate <em>before</em> checking it, then an app
+     * enqueueing at slightly above the acceptable rate would never get their notifications
+     * accepted; updating afterwards allows the rate to dip below the threshold and thus lets
+     * through some of them.
+     */
+    public synchronized void registerEnqueuedByAppAndAccepted(String packageName) {
+        final long now = SystemClock.elapsedRealtime();
+        AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
+        for (AggregatedStats stats : aggregatedStatsArray) {
+            stats.updateInterarrivalEstimate(now);
+        }
+        releaseAggregatedStatsLocked(aggregatedStatsArray);
+    }
+
+    /**
      * Called when a notification has been posted.
      */
     public synchronized void registerPostedByApp(NotificationRecord notification) {
-        final long now = SystemClock.elapsedRealtime();
-        notification.stats.posttimeElapsedMs = now;
+        notification.stats.posttimeElapsedMs = SystemClock.elapsedRealtime();
 
         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
         for (AggregatedStats stats : aggregatedStatsArray) {
             stats.numPostedByApp++;
-            stats.updateInterarrivalEstimate(now);
             stats.countApiUse(notification);
             stats.numUndecoratedRemoteViews += (notification.hasUndecoratedRemoteView() ? 1 : 0);
         }
@@ -161,7 +162,6 @@
         AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
         for (AggregatedStats stats : aggregatedStatsArray) {
             stats.numUpdatedByApp++;
-            stats.updateInterarrivalEstimate(SystemClock.elapsedRealtime());
             stats.countApiUse(notification);
         }
         releaseAggregatedStatsLocked(aggregatedStatsArray);
@@ -257,12 +257,12 @@
         }
     }
 
-    // Locked by this.
+    @GuardedBy("this")
     private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
         return getAggregatedStatsLocked(record.getSbn().getPackageName());
     }
 
-    // Locked by this.
+    @GuardedBy("this")
     private AggregatedStats[] getAggregatedStatsLocked(String packageName) {
         if (!ENABLE_AGGREGATED_IN_MEMORY_STATS) {
             return EMPTY_AGGREGATED_STATS;
@@ -277,7 +277,7 @@
         return array;
     }
 
-    // Locked by this.
+    @GuardedBy("this")
     private void releaseAggregatedStatsLocked(AggregatedStats[] array) {
         for(int i = 0; i < array.length; i++) {
             array[i] = null;
@@ -285,7 +285,7 @@
         mStatsArrays.offer(array);
     }
 
-    // Locked by this.
+    @GuardedBy("this")
     private AggregatedStats getOrCreateAggregatedStatsLocked(String key) {
         AggregatedStats result = mStats.get(key);
         if (result == null) {
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 4399a3c..838a0fb 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -30,6 +30,9 @@
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES;
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES;
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -41,6 +44,7 @@
 import android.app.NotificationChannel;
 import android.app.NotificationChannelGroup;
 import android.app.NotificationManager;
+import android.content.AttributionSource;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
@@ -53,6 +57,7 @@
 import android.os.Build;
 import android.os.Process;
 import android.os.UserHandle;
+import android.permission.PermissionManager;
 import android.provider.Settings;
 import android.service.notification.ConversationChannelWrapper;
 import android.service.notification.NotificationListenerService;
@@ -72,6 +77,8 @@
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags;
+import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
@@ -183,6 +190,7 @@
     private final RankingHandler mRankingHandler;
     private final ZenModeHelper mZenModeHelper;
     private final PermissionHelper mPermissionHelper;
+    private final PermissionManager mPermissionManager;
     private final NotificationChannelLogger mNotificationChannelLogger;
     private final AppOpsManager mAppOps;
 
@@ -198,7 +206,7 @@
     private boolean mAllowInvalidShortcuts = false;
 
     public PreferencesHelper(Context context, PackageManager pm, RankingHandler rankingHandler,
-            ZenModeHelper zenHelper, PermissionHelper permHelper,
+            ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager,
             NotificationChannelLogger notificationChannelLogger,
             AppOpsManager appOpsManager,
             SysUiStatsEvent.BuilderFactory statsEventBuilderFactory,
@@ -207,6 +215,7 @@
         mZenModeHelper = zenHelper;
         mRankingHandler = rankingHandler;
         mPermissionHelper = permHelper;
+        mPermissionManager = permManager;
         mPm = pm;
         mNotificationChannelLogger = notificationChannelLogger;
         mAppOps = appOpsManager;
@@ -2027,6 +2036,43 @@
     }
 
     /**
+     * @return State of the full screen intent permission for this package.
+     */
+    @VisibleForTesting
+    int getFsiState(String pkg, int uid, boolean requestedFSIPermission, boolean isFlagEnabled) {
+        if (!isFlagEnabled) {
+            return 0;
+        }
+        if (!requestedFSIPermission) {
+            return PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
+        }
+        final AttributionSource attributionSource =
+                new AttributionSource.Builder(uid).setPackageName(pkg).build();
+
+        final int result = mPermissionManager.checkPermissionForPreflight(
+                android.Manifest.permission.USE_FULL_SCREEN_INTENT, attributionSource);
+
+        if (result == PermissionManager.PERMISSION_GRANTED) {
+            return PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
+        }
+        return PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED;
+    }
+
+    /**
+     * @return True if the current full screen intent permission state for this package was set by
+     * the user.
+     */
+    @VisibleForTesting
+    boolean isFsiPermissionUserSet(String pkg, int uid, int fsiState, int currentPermissionFlags,
+                                   boolean isStickyHunFlagEnabled) {
+        if (!isStickyHunFlagEnabled
+                || fsiState == PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED) {
+            return false;
+        }
+        return (currentPermissionFlags & PackageManager.FLAG_PERMISSION_USER_SET) != 0;
+    }
+
+    /**
      * Fills out {@link PackageNotificationPreferences} proto and wraps it in a {@link StatsEvent}.
      */
     public void pullPackagePreferencesStats(List<StatsEvent> events,
@@ -2070,7 +2116,33 @@
 
                 event.writeInt(r.visibility);
                 event.writeInt(r.lockedAppFields);
-                event.writeBoolean(importanceIsUserSet);  // optional bool user_set_importance = 5;
+
+                // optional bool user_set_importance = 5;
+                event.writeBoolean(importanceIsUserSet);
+
+                // optional FsiState fsi_state = 6;
+                final boolean isStickyHunFlagEnabled = SystemUiSystemPropertiesFlags.getResolver()
+                        .isEnabled(NotificationFlags.SHOW_STICKY_HUN_FOR_DENIED_FSI);
+
+                final boolean requestedFSIPermission = mPermissionHelper.hasRequestedPermission(
+                        android.Manifest.permission.USE_FULL_SCREEN_INTENT, r.pkg, r.uid);
+
+                final int fsiState = getFsiState(r.pkg, r.uid, requestedFSIPermission,
+                        isStickyHunFlagEnabled);
+
+                event.writeInt(fsiState);
+
+                // optional bool is_fsi_permission_user_set = 7;
+                final int currentPermissionFlags = mPm.getPermissionFlags(
+                        android.Manifest.permission.USE_FULL_SCREEN_INTENT, r.pkg,
+                        UserHandle.getUserHandleForUid(r.uid));
+
+                final boolean isUserSet =
+                        isFsiPermissionUserSet(r.pkg, r.uid, fsiState, currentPermissionFlags,
+                                isStickyHunFlagEnabled);
+
+                event.writeBoolean(isUserSet);
+
                 events.add(event.build());
             }
         }
diff --git a/services/core/java/com/android/server/notification/RateEstimator.java b/services/core/java/com/android/server/notification/RateEstimator.java
index a2f93dc..eda96ac 100644
--- a/services/core/java/com/android/server/notification/RateEstimator.java
+++ b/services/core/java/com/android/server/notification/RateEstimator.java
@@ -22,9 +22,10 @@
  *
  * {@hide}
  */
-public class RateEstimator {
-    private static final double RATE_ALPHA = 0.8;
+class RateEstimator {
+    private static final double RATE_ALPHA = 0.7;
     private static final double MINIMUM_DT = 0.0005;
+
     private Long mLastEventTime;
     private double mInterarrivalTime;
 
@@ -34,18 +35,12 @@
     }
 
     /** Update the estimate to account for an event that just happened. */
-    public float update(long now) {
-        float rate;
-        if (mLastEventTime == null) {
-            // No last event time, rate is zero.
-            rate = 0f;
-        } else {
+    public void update(long now) {
+        if (mLastEventTime != null) {
             // Calculate the new inter-arrival time based on last event time.
             mInterarrivalTime = getInterarrivalEstimate(now);
-            rate = (float) (1.0 / mInterarrivalTime);
         }
         mLastEventTime = now;
-        return rate;
     }
 
     /** @return the estimated rate if there were a new event right now. */
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 5f52c16..c1d5af5 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -610,7 +610,8 @@
                     PackageManager.UNINSTALL_REASON_UNKNOWN,
                     null /*harmfulAppWarning*/,
                     null /*splashScreenTheme*/,
-                    0 /*firstInstallTime*/);
+                    0 /*firstInstallTime*/,
+                    PackageManager.USER_MIN_ASPECT_RATIO_UNSET);
         }
         mPm.mSettings.writeKernelMappingLPr(ps);
     }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index db47306..2fc22bf 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -30,6 +30,7 @@
 import static android.content.pm.PackageManager.MATCH_FACTORY_ONLY;
 import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
 import static android.os.Process.INVALID_UID;
 import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
 import static android.os.storage.StorageManager.FLAG_STORAGE_CE;
@@ -5239,6 +5240,20 @@
         }
 
         @Override
+        @PackageManager.UserMinAspectRatio
+        public int getUserMinAspectRatio(@NonNull String packageName, int userId) {
+            final Computer snapshot = snapshotComputer();
+            final int callingUid = Binder.getCallingUid();
+            snapshot.enforceCrossUserPermission(
+                    callingUid, userId, false /* requireFullPermission */,
+                    false /* checkShell */, "getUserMinAspectRatio");
+            final PackageStateInternal packageState = snapshot
+                    .getPackageStateForInstalledAndFiltered(packageName, callingUid, userId);
+            return packageState == null ? USER_MIN_ASPECT_RATIO_UNSET
+                    : packageState.getUserStateOrDefault(userId).getMinAspectRatio();
+        }
+
+        @Override
         public Bundle getSuspendedPackageAppExtras(String packageName, int userId) {
             final int callingUid = Binder.getCallingUid();
             final Computer snapshot = snapshot();
@@ -6201,6 +6216,32 @@
             return true;
         }
 
+        @android.annotation.EnforcePermission(android.Manifest.permission.INSTALL_PACKAGES)
+        @Override
+        public void setUserMinAspectRatio(@NonNull String packageName, int userId,
+                @PackageManager.UserMinAspectRatio int aspectRatio) {
+            setUserMinAspectRatio_enforcePermission();
+            final int callingUid = Binder.getCallingUid();
+            final Computer snapshot = snapshotComputer();
+            snapshot.enforceCrossUserPermission(callingUid, userId,
+                    false /* requireFullPermission */, false /* checkShell */,
+                    "setUserMinAspectRatio");
+            enforceOwnerRights(snapshot, packageName, callingUid);
+
+            final PackageStateInternal packageState = snapshot
+                    .getPackageStateForInstalledAndFiltered(packageName, callingUid, userId);
+            if (packageState == null) {
+                return;
+            }
+
+            if (packageState.getUserStateOrDefault(userId).getMinAspectRatio() == aspectRatio) {
+                return;
+            }
+
+            commitPackageStateMutation(null, packageName, state ->
+                    state.userState(userId).setMinAspectRatio(aspectRatio));
+        }
+
         @Override
         @SuppressWarnings("GuardedBy")
         public void setRuntimePermissionsVersion(int version, @UserIdInt int userId) {
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 7fda092..3e9ccac 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -337,7 +337,7 @@
             final long previousFirstInstallTime =
                     replacedPkgSetting.getUserStateOrDefault(userId).getFirstInstallTimeMillis();
             if (previousFirstInstallTime != 0) {
-                modifyUserState(userId).setFirstInstallTime(previousFirstInstallTime);
+                modifyUserState(userId).setFirstInstallTimeMillis(previousFirstInstallTime);
             }
         }
         onChanged();
@@ -352,10 +352,10 @@
         if (userId == UserHandle.USER_ALL) {
             int userStateCount = mUserStates.size();
             for (int i = 0; i < userStateCount; i++) {
-                mUserStates.valueAt(i).setFirstInstallTime(firstInstallTime);
+                mUserStates.valueAt(i).setFirstInstallTimeMillis(firstInstallTime);
             }
         } else {
-            modifyUserState(userId).setFirstInstallTime(firstInstallTime);
+            modifyUserState(userId).setFirstInstallTimeMillis(firstInstallTime);
         }
         onChanged();
         return this;
@@ -875,7 +875,7 @@
             ArraySet<String> enabledComponents, ArraySet<String> disabledComponents,
             int installReason, int uninstallReason,
             String harmfulAppWarning, String splashScreenTheme,
-            long firstInstallTime) {
+            long firstInstallTime, int aspectRatio) {
         modifyUserState(userId)
                 .setSuspendParams(suspendParams)
                 .setCeDataInode(ceDataInode)
@@ -894,7 +894,8 @@
                 .setVirtualPreload(virtualPreload)
                 .setHarmfulAppWarning(harmfulAppWarning)
                 .setSplashScreenTheme(splashScreenTheme)
-                .setFirstInstallTime(firstInstallTime);
+                .setFirstInstallTimeMillis(firstInstallTime)
+                .setMinAspectRatio(aspectRatio);
         onChanged();
     }
 
@@ -912,7 +913,7 @@
                         ? null : otherState.getDisabledComponentsNoCopy().untrackedStorage(),
                 otherState.getInstallReason(), otherState.getUninstallReason(),
                 otherState.getHarmfulAppWarning(), otherState.getSplashScreenTheme(),
-                otherState.getFirstInstallTimeMillis());
+                otherState.getFirstInstallTimeMillis(), otherState.getMinAspectRatio());
     }
 
     WatchedArraySet<String> getEnabledComponents(int userId) {
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index aaf13eb..532ae71 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -351,6 +351,7 @@
     private static final String ATTR_VIRTUAL_PRELOAD = "virtual-preload";
     private static final String ATTR_HARMFUL_APP_WARNING = "harmful-app-warning";
     private static final String ATTR_SPLASH_SCREEN_THEME = "splash-screen-theme";
+    private static final String ATTR_MIN_ASPECT_RATIO = "min-aspect-ratio";
 
     private static final String ATTR_PACKAGE_NAME = "packageName";
     private static final String ATTR_BUILD_FINGERPRINT = "buildFingerprint";
@@ -1122,7 +1123,8 @@
                                 PackageManager.UNINSTALL_REASON_UNKNOWN,
                                 null /*harmfulAppWarning*/,
                                 null /*splashscreenTheme*/,
-                                0 /*firstInstallTime*/
+                                0 /*firstInstallTime*/,
+                                PackageManager.USER_MIN_ASPECT_RATIO_UNSET
                         );
                     }
                 }
@@ -1776,7 +1778,8 @@
                                     PackageManager.UNINSTALL_REASON_UNKNOWN,
                                     null /*harmfulAppWarning*/,
                                     null /* splashScreenTheme*/,
-                                    0 /*firstInstallTime*/
+                                    0 /*firstInstallTime*/,
+                                    PackageManager.USER_MIN_ASPECT_RATIO_UNSET
                             );
                         }
                         return;
@@ -1871,6 +1874,9 @@
                                 ATTR_SPLASH_SCREEN_THEME);
                         final long firstInstallTime = parser.getAttributeLongHex(null,
                                 ATTR_FIRST_INSTALL_TIME, 0);
+                        final int minAspectRatio = parser.getAttributeInt(null,
+                                ATTR_MIN_ASPECT_RATIO,
+                                PackageManager.USER_MIN_ASPECT_RATIO_UNSET);
 
                         ArraySet<String> enabledComponents = null;
                         ArraySet<String> disabledComponents = null;
@@ -1947,7 +1953,8 @@
                                 enabledCaller, enabledComponents, disabledComponents, installReason,
                                 uninstallReason, harmfulAppWarning, splashScreenTheme,
                                 firstInstallTime != 0 ? firstInstallTime :
-                                        origFirstInstallTimes.getOrDefault(name, 0L));
+                                        origFirstInstallTimes.getOrDefault(name, 0L),
+                                minAspectRatio);
 
                         mDomainVerificationManager.setLegacyUserState(name, userId, verifState);
                     } else if (tagName.equals("preferred-activities")) {
@@ -2242,6 +2249,11 @@
                             serializer.attribute(null, ATTR_SPLASH_SCREEN_THEME,
                                     ustate.getSplashScreenTheme());
                         }
+                        if (ustate.getMinAspectRatio()
+                                != PackageManager.USER_MIN_ASPECT_RATIO_UNSET) {
+                            serializer.attributeInt(null, ATTR_MIN_ASPECT_RATIO,
+                                    ustate.getMinAspectRatio());
+                        }
                         if (ustate.isSuspended()) {
                             for (int i = 0; i < ustate.getSuspendParams().size(); i++) {
                                 final String suspendingPackage = ustate.getSuspendParams().keyAt(i);
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 5b3514c..710e0b7 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.pm;
 
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
 import static android.provider.DeviceConfig.NAMESPACE_SYSTEMUI;
 
 import android.Manifest.permission;
@@ -24,6 +25,7 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
+import android.app.ActivityOptions;
 import android.app.AppGlobals;
 import android.app.IUidObserver;
 import android.app.IUriGrantsManager;
@@ -4407,8 +4409,11 @@
             return;
         }
         try {
+            ActivityOptions options = ActivityOptions.makeBasic()
+                    .setPendingIntentBackgroundActivityStartMode(
+                            MODE_BACKGROUND_ACTIVITY_START_DENIED);
             intentSender.sendIntent(mContext, /* code= */ 0, extras,
-                    /* onFinished=*/ null, /* handler= */ null);
+                    /* onFinished=*/ null, /* handler= */ null, null, options.toBundle());
         } catch (SendIntentException e) {
             Slog.w(TAG, "sendIntent failed().", e);
         }
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 7e88e13..f8bd328 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1037,7 +1037,7 @@
                 final UserData userData = mUsers.valueAt(i);
                 final int userId = userData.info.id;
                 if (userId != currentUser && userData.info.isFull() && !userData.info.partial
-                        && !mRemovingUserIds.get(userId)) {
+                        && userData.info.isEnabled() && !mRemovingUserIds.get(userId)) {
                     final long userEnteredTime = userData.mLastEnteredForegroundTimeMillis;
                     if (userEnteredTime > latestEnteredTime) {
                         latestEnteredTime = userEnteredTime;
@@ -2927,14 +2927,14 @@
             Preconditions.checkState(mCachedEffectiveUserRestrictions.getRestrictions(userId)
                     != newBaseRestrictions);
 
-            if (mBaseUserRestrictions.updateRestrictions(userId, newBaseRestrictions)) {
+            if (mBaseUserRestrictions.updateRestrictions(userId, new Bundle(newBaseRestrictions))) {
                 scheduleWriteUser(userId);
             }
         }
 
         final Bundle effective = computeEffectiveUserRestrictionsLR(userId);
 
-        mCachedEffectiveUserRestrictions.updateRestrictions(userId, effective);
+        mCachedEffectiveUserRestrictions.updateRestrictions(userId, new Bundle(effective));
 
         // Apply the new restrictions.
         if (DBG) {
@@ -5589,8 +5589,14 @@
         }
     }
 
-    @GuardedBy("mUsersLock")
     @VisibleForTesting
+    void addRemovingUserId(@UserIdInt int userId) {
+        synchronized (mUsersLock) {
+            addRemovingUserIdLocked(userId);
+        }
+    }
+
+    @GuardedBy("mUsersLock")
     void addRemovingUserIdLocked(@UserIdInt int userId) {
         // We remember deleted user IDs to prevent them from being
         // reused during the current boot; they can still be reused
diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java b/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
index 91a25db3..3d056e8 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
@@ -377,6 +377,8 @@
         private final int mUninstallReason;
         @Nullable
         private final String mSplashScreenTheme;
+        @PackageManager.UserMinAspectRatio
+        private final int mMinAspectRatio;
         private final long mFirstInstallTimeMillis;
 
         private UserStateImpl(@NonNull PackageUserState userState) {
@@ -392,6 +394,7 @@
             mSharedLibraryOverlayPaths = userState.getSharedLibraryOverlayPaths();
             mUninstallReason = userState.getUninstallReason();
             mSplashScreenTheme = userState.getSplashScreenTheme();
+            mMinAspectRatio = userState.getMinAspectRatio();
             setBoolean(Booleans.HIDDEN, userState.isHidden());
             setBoolean(Booleans.INSTALLED, userState.isInstalled());
             setBoolean(Booleans.INSTANT_APP, userState.isInstantApp());
@@ -543,6 +546,11 @@
         }
 
         @DataClass.Generated.Member
+        public @PackageManager.UserMinAspectRatio int getMinAspectRatio() {
+            return mMinAspectRatio;
+        }
+
+        @DataClass.Generated.Member
         public long getFirstInstallTimeMillis() {
             return mFirstInstallTimeMillis;
         }
@@ -554,10 +562,10 @@
         }
 
         @DataClass.Generated(
-                time = 1671671043891L,
+                time = 1687938966108L,
                 codegenVersion = "1.0.23",
                 sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java",
-                inputSignatures = "private  int mBooleans\nprivate final  long mCeDataInode\nprivate final @android.annotation.NonNull android.util.ArraySet<java.lang.String> mDisabledComponents\nprivate final @android.content.pm.PackageManager.DistractionRestriction int mDistractionFlags\nprivate final @android.annotation.NonNull android.util.ArraySet<java.lang.String> mEnabledComponents\nprivate final  int mEnabledState\nprivate final @android.annotation.Nullable java.lang.String mHarmfulAppWarning\nprivate final @android.content.pm.PackageManager.InstallReason int mInstallReason\nprivate final @android.annotation.Nullable java.lang.String mLastDisableAppCaller\nprivate final @android.annotation.NonNull android.content.pm.overlay.OverlayPaths mOverlayPaths\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,android.content.pm.overlay.OverlayPaths> mSharedLibraryOverlayPaths\nprivate final @android.content.pm.PackageManager.UninstallReason int mUninstallReason\nprivate final @android.annotation.Nullable java.lang.String mSplashScreenTheme\nprivate final  long mFirstInstallTimeMillis\npublic static  com.android.server.pm.pkg.PackageUserState copy(com.android.server.pm.pkg.PackageUserState)\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\npublic @java.lang.Override boolean isHidden()\npublic @java.lang.Override boolean isInstalled()\npublic @java.lang.Override boolean isInstantApp()\npublic @java.lang.Override boolean isNotLaunched()\npublic @java.lang.Override boolean isStopped()\npublic @java.lang.Override boolean isSuspended()\npublic @java.lang.Override boolean isVirtualPreload()\npublic @java.lang.Override boolean isComponentEnabled(java.lang.String)\npublic @java.lang.Override boolean isComponentDisabled(java.lang.String)\npublic @java.lang.Override android.content.pm.overlay.OverlayPaths getAllOverlayPaths()\nclass UserStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageUserState]\nprivate static final  int HIDDEN\nprivate static final  int INSTALLED\nprivate static final  int INSTANT_APP\nprivate static final  int NOT_LAUNCHED\nprivate static final  int STOPPED\nprivate static final  int SUSPENDED\nprivate static final  int VIRTUAL_PRELOAD\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
+                inputSignatures = "private  int mBooleans\nprivate final  long mCeDataInode\nprivate final @android.annotation.NonNull android.util.ArraySet<java.lang.String> mDisabledComponents\nprivate final @android.content.pm.PackageManager.DistractionRestriction int mDistractionFlags\nprivate final @android.annotation.NonNull android.util.ArraySet<java.lang.String> mEnabledComponents\nprivate final  int mEnabledState\nprivate final @android.annotation.Nullable java.lang.String mHarmfulAppWarning\nprivate final @android.content.pm.PackageManager.InstallReason int mInstallReason\nprivate final @android.annotation.Nullable java.lang.String mLastDisableAppCaller\nprivate final @android.annotation.NonNull android.content.pm.overlay.OverlayPaths mOverlayPaths\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,android.content.pm.overlay.OverlayPaths> mSharedLibraryOverlayPaths\nprivate final @android.content.pm.PackageManager.UninstallReason int mUninstallReason\nprivate final @android.annotation.Nullable java.lang.String mSplashScreenTheme\nprivate final @android.content.pm.PackageManager.UserMinAspectRatio int mMinAspectRatio\nprivate final  long mFirstInstallTimeMillis\npublic static  com.android.server.pm.pkg.PackageUserState copy(com.android.server.pm.pkg.PackageUserState)\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\npublic @java.lang.Override boolean isHidden()\npublic @java.lang.Override boolean isInstalled()\npublic @java.lang.Override boolean isInstantApp()\npublic @java.lang.Override boolean isNotLaunched()\npublic @java.lang.Override boolean isStopped()\npublic @java.lang.Override boolean isSuspended()\npublic @java.lang.Override boolean isVirtualPreload()\npublic @java.lang.Override boolean isComponentEnabled(java.lang.String)\npublic @java.lang.Override boolean isComponentDisabled(java.lang.String)\npublic @java.lang.Override android.content.pm.overlay.OverlayPaths getAllOverlayPaths()\nclass UserStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageUserState]\nprivate static final  int HIDDEN\nprivate static final  int INSTALLED\nprivate static final  int INSTANT_APP\nprivate static final  int NOT_LAUNCHED\nprivate static final  int STOPPED\nprivate static final  int SUSPENDED\nprivate static final  int VIRTUAL_PRELOAD\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
         @Deprecated
         private void __metadata() {}
 
diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserState.java b/services/core/java/com/android/server/pm/pkg/PackageUserState.java
index 2048d65..f75d214 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageUserState.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageUserState.java
@@ -217,4 +217,12 @@
      */
     @Nullable
     String getSplashScreenTheme();
+
+    /**
+     * @return the min aspect ratio setting of the package which by default is unset
+     * unless it has been set by the user
+     * @hide
+     */
+    @PackageManager.UserMinAspectRatio
+    int getMinAspectRatio();
 }
diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserStateDefault.java b/services/core/java/com/android/server/pm/pkg/PackageUserStateDefault.java
index 73fb672..1fb12a8 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageUserStateDefault.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageUserStateDefault.java
@@ -136,6 +136,11 @@
     }
 
     @Override
+    public @PackageManager.UserMinAspectRatio int getMinAspectRatio() {
+        return PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
+    }
+
+    @Override
     public long getFirstInstallTimeMillis() {
         return 0;
     }
diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java b/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
index ed4aab9..d911ac1 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
@@ -16,6 +16,7 @@
 
 package com.android.server.pm.pkg;
 
+import android.annotation.CurrentTimeMillisLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ComponentName;
@@ -61,6 +62,7 @@
     private int mDistractionFlags;
     private boolean mInstantApp;
     private boolean mVirtualPreload;
+    @PackageManager.EnabledState
     private int mEnabledState = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
     @PackageManager.InstallReason
     private int mInstallReason = PackageManager.INSTALL_REASON_UNKNOWN;
@@ -81,6 +83,9 @@
     @Nullable
     private String mSplashScreenTheme;
 
+    @PackageManager.UserMinAspectRatio
+    private int mMinAspectRatio = PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
+
     /**
      * Suspending package to suspend params
      */
@@ -90,7 +95,7 @@
     @Nullable
     private WatchedArrayMap<ComponentName, Pair<String, Integer>> mComponentLabelIconOverrideMap;
 
-    private long mFirstInstallTime;
+    private @CurrentTimeMillisLong long mFirstInstallTimeMillis;
 
     // TODO(b/239050028): Remove, enforce notifying parent through PMS commit method
     @Nullable
@@ -144,10 +149,11 @@
         mHarmfulAppWarning = other.mHarmfulAppWarning;
         mLastDisableAppCaller = other.mLastDisableAppCaller;
         mSplashScreenTheme = other.mSplashScreenTheme;
+        mMinAspectRatio = other.mMinAspectRatio;
         mSuspendParams = other.mSuspendParams == null ? null : other.mSuspendParams.snapshot();
         mComponentLabelIconOverrideMap = other.mComponentLabelIconOverrideMap == null
                 ? null : other.mComponentLabelIconOverrideMap.snapshot();
-        mFirstInstallTime = other.mFirstInstallTime;
+        mFirstInstallTimeMillis = other.mFirstInstallTimeMillis;
         mSnapshot = new SnapshotCache.Sealed<>();
     }
 
@@ -506,6 +512,19 @@
     }
 
     /**
+     * Sets user min aspect ratio override value
+     * @see PackageManager.UserMinAspectRatio
+     */
+    public @NonNull PackageUserStateImpl setMinAspectRatio(
+            @PackageManager.UserMinAspectRatio int value) {
+        mMinAspectRatio = value;
+        com.android.internal.util.AnnotationValidations.validate(
+                PackageManager.UserMinAspectRatio.class, null, mMinAspectRatio);
+        onChanged();
+        return this;
+    }
+
+    /**
      * Suspending package to suspend params
      */
     public @NonNull PackageUserStateImpl setSuspendParams(
@@ -538,8 +557,8 @@
         return this;
     }
 
-    public @NonNull PackageUserStateImpl setFirstInstallTime(long value) {
-        mFirstInstallTime = value;
+    public @NonNull PackageUserStateImpl setFirstInstallTimeMillis(long value) {
+        mFirstInstallTimeMillis = value;
         onChanged();
         return this;
     }
@@ -643,7 +662,7 @@
     }
 
     @DataClass.Generated.Member
-    public int getEnabledState() {
+    public @PackageManager.EnabledState int getEnabledState() {
         return mEnabledState;
     }
 
@@ -677,6 +696,11 @@
         return mSplashScreenTheme;
     }
 
+    @DataClass.Generated.Member
+    public @PackageManager.UserMinAspectRatio int getMinAspectRatio() {
+        return mMinAspectRatio;
+    }
+
     /**
      * Suspending package to suspend params
      */
@@ -691,8 +715,8 @@
     }
 
     @DataClass.Generated.Member
-    public long getFirstInstallTimeMillis() {
-        return mFirstInstallTime;
+    public @CurrentTimeMillisLong long getFirstInstallTimeMillis() {
+        return mFirstInstallTimeMillis;
     }
 
     @DataClass.Generated.Member
@@ -764,9 +788,10 @@
                 && Objects.equals(mOverlayPaths, that.mOverlayPaths)
                 && Objects.equals(mSharedLibraryOverlayPaths, that.mSharedLibraryOverlayPaths)
                 && Objects.equals(mSplashScreenTheme, that.mSplashScreenTheme)
+                && mMinAspectRatio == that.mMinAspectRatio
                 && Objects.equals(mSuspendParams, that.mSuspendParams)
                 && Objects.equals(mComponentLabelIconOverrideMap, that.mComponentLabelIconOverrideMap)
-                && mFirstInstallTime == that.mFirstInstallTime
+                && mFirstInstallTimeMillis == that.mFirstInstallTimeMillis
                 && watchableEquals(that.mWatchable)
                 && snapshotEquals(that.mSnapshot);
     }
@@ -796,19 +821,20 @@
         _hash = 31 * _hash + Objects.hashCode(mOverlayPaths);
         _hash = 31 * _hash + Objects.hashCode(mSharedLibraryOverlayPaths);
         _hash = 31 * _hash + Objects.hashCode(mSplashScreenTheme);
+        _hash = 31 * _hash + mMinAspectRatio;
         _hash = 31 * _hash + Objects.hashCode(mSuspendParams);
         _hash = 31 * _hash + Objects.hashCode(mComponentLabelIconOverrideMap);
-        _hash = 31 * _hash + Long.hashCode(mFirstInstallTime);
+        _hash = 31 * _hash + Long.hashCode(mFirstInstallTimeMillis);
         _hash = 31 * _hash + watchableHashCode();
         _hash = 31 * _hash + snapshotHashCode();
         return _hash;
     }
 
     @DataClass.Generated(
-            time = 1668033772891L,
+            time = 1687938397579L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java",
-            inputSignatures = "protected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mDisabledComponentsWatched\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mEnabledComponentsWatched\nprivate  long mCeDataInode\nprivate  boolean mInstalled\nprivate  boolean mStopped\nprivate  boolean mNotLaunched\nprivate  boolean mHidden\nprivate  int mDistractionFlags\nprivate  boolean mInstantApp\nprivate  boolean mVirtualPreload\nprivate  int mEnabledState\nprivate @android.content.pm.PackageManager.InstallReason int mInstallReason\nprivate @android.content.pm.PackageManager.UninstallReason int mUninstallReason\nprivate @android.annotation.Nullable java.lang.String mHarmfulAppWarning\nprivate @android.annotation.Nullable java.lang.String mLastDisableAppCaller\nprivate @android.annotation.Nullable android.content.pm.overlay.OverlayPaths mOverlayPaths\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths> mSharedLibraryOverlayPaths\nprivate @android.annotation.Nullable java.lang.String mSplashScreenTheme\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams> mSuspendParams\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>> mComponentLabelIconOverrideMap\nprivate  long mFirstInstallTime\nprivate @android.annotation.Nullable com.android.server.utils.Watchable mWatchable\nfinal @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> mSnapshot\nprivate  com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> makeCache()\nprivate  void onChanged()\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserStateImpl snapshot()\npublic @android.annotation.Nullable boolean setOverlayPaths(android.content.pm.overlay.OverlayPaths)\npublic  boolean setSharedLibraryOverlayPaths(java.lang.String,android.content.pm.overlay.OverlayPaths)\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponentsNoCopy()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponentsNoCopy()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getDisabledComponents()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getEnabledComponents()\npublic @java.lang.Override boolean isComponentEnabled(java.lang.String)\npublic @java.lang.Override boolean isComponentDisabled(java.lang.String)\npublic @java.lang.Override android.content.pm.overlay.OverlayPaths getAllOverlayPaths()\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer)\npublic  void resetOverrideComponentLabelIcon()\npublic @android.annotation.Nullable android.util.Pair<java.lang.String,java.lang.Integer> getOverrideLabelIconForComponent(android.content.ComponentName)\npublic @java.lang.Override boolean isSuspended()\npublic  com.android.server.pm.pkg.PackageUserStateImpl putSuspendParams(java.lang.String,com.android.server.pm.pkg.SuspendParams)\npublic  com.android.server.pm.pkg.PackageUserStateImpl removeSuspension(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setCeDataInode(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstalled(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setStopped(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setNotLaunched(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHidden(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDistractionFlags(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstantApp(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setVirtualPreload(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setUninstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHarmfulAppWarning(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setLastDisableAppCaller(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSharedLibraryOverlayPaths(android.util.ArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSplashScreenTheme(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSuspendParams(android.util.ArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setComponentLabelIconOverrideMap(android.util.ArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setFirstInstallTime(long)\npublic @android.annotation.NonNull @java.lang.Override java.util.Map<java.lang.String,android.content.pm.overlay.OverlayPaths> getSharedLibraryOverlayPaths()\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setWatchable(com.android.server.utils.Watchable)\nprivate  boolean watchableEquals(com.android.server.utils.Watchable)\nprivate  int watchableHashCode()\nprivate  boolean snapshotEquals(com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl>)\nprivate  int snapshotHashCode()\nclass PackageUserStateImpl extends com.android.server.utils.WatchableImpl implements [com.android.server.pm.pkg.PackageUserStateInternal, com.android.server.utils.Snappable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=false, genEqualsHashCode=true)")
+            inputSignatures = "protected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mDisabledComponentsWatched\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mEnabledComponentsWatched\nprivate  long mCeDataInode\nprivate  boolean mInstalled\nprivate  boolean mStopped\nprivate  boolean mNotLaunched\nprivate  boolean mHidden\nprivate  int mDistractionFlags\nprivate  boolean mInstantApp\nprivate  boolean mVirtualPreload\nprivate @android.content.pm.PackageManager.EnabledState int mEnabledState\nprivate @android.content.pm.PackageManager.InstallReason int mInstallReason\nprivate @android.content.pm.PackageManager.UninstallReason int mUninstallReason\nprivate @android.annotation.Nullable java.lang.String mHarmfulAppWarning\nprivate @android.annotation.Nullable java.lang.String mLastDisableAppCaller\nprivate @android.annotation.Nullable android.content.pm.overlay.OverlayPaths mOverlayPaths\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths> mSharedLibraryOverlayPaths\nprivate @android.annotation.Nullable java.lang.String mSplashScreenTheme\nprivate @android.content.pm.PackageManager.UserMinAspectRatio int mMinAspectRatio\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams> mSuspendParams\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>> mComponentLabelIconOverrideMap\nprivate @android.annotation.CurrentTimeMillisLong long mFirstInstallTimeMillis\nprivate @android.annotation.Nullable com.android.server.utils.Watchable mWatchable\nfinal @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> mSnapshot\nprivate  com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> makeCache()\nprivate  void onChanged()\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserStateImpl snapshot()\npublic @android.annotation.Nullable boolean setOverlayPaths(android.content.pm.overlay.OverlayPaths)\npublic  boolean setSharedLibraryOverlayPaths(java.lang.String,android.content.pm.overlay.OverlayPaths)\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponentsNoCopy()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponentsNoCopy()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getDisabledComponents()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getEnabledComponents()\npublic @java.lang.Override boolean isComponentEnabled(java.lang.String)\npublic @java.lang.Override boolean isComponentDisabled(java.lang.String)\npublic @java.lang.Override android.content.pm.overlay.OverlayPaths getAllOverlayPaths()\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer)\npublic  void resetOverrideComponentLabelIcon()\npublic @android.annotation.Nullable android.util.Pair<java.lang.String,java.lang.Integer> getOverrideLabelIconForComponent(android.content.ComponentName)\npublic @java.lang.Override boolean isSuspended()\npublic  com.android.server.pm.pkg.PackageUserStateImpl putSuspendParams(java.lang.String,com.android.server.pm.pkg.SuspendParams)\npublic  com.android.server.pm.pkg.PackageUserStateImpl removeSuspension(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setCeDataInode(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstalled(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setStopped(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setNotLaunched(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHidden(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDistractionFlags(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstantApp(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setVirtualPreload(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setUninstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHarmfulAppWarning(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setLastDisableAppCaller(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSharedLibraryOverlayPaths(android.util.ArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSplashScreenTheme(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setMinAspectRatio(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSuspendParams(android.util.ArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setComponentLabelIconOverrideMap(android.util.ArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setFirstInstallTimeMillis(long)\npublic @android.annotation.NonNull @java.lang.Override java.util.Map<java.lang.String,android.content.pm.overlay.OverlayPaths> getSharedLibraryOverlayPaths()\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setWatchable(com.android.server.utils.Watchable)\nprivate  boolean watchableEquals(com.android.server.utils.Watchable)\nprivate  int watchableHashCode()\nprivate  boolean snapshotEquals(com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl>)\nprivate  int snapshotHashCode()\nclass PackageUserStateImpl extends com.android.server.utils.WatchableImpl implements [com.android.server.pm.pkg.PackageUserStateInternal, com.android.server.utils.Snappable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=false, genEqualsHashCode=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java b/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java
index 8125b0f..8430cf7 100644
--- a/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java
+++ b/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java
@@ -439,6 +439,16 @@
                 }
                 return null;
             }
+
+            @NonNull
+            @Override
+            public PackageUserStateWrite setMinAspectRatio(
+                    @PackageManager.UserMinAspectRatio int aspectRatio) {
+                if (mUserState != null) {
+                    mUserState.setMinAspectRatio(aspectRatio);
+                }
+                return this;
+            }
         }
     }
 }
diff --git a/services/core/java/com/android/server/pm/pkg/mutate/PackageUserStateWrite.java b/services/core/java/com/android/server/pm/pkg/mutate/PackageUserStateWrite.java
index 11d6d97..0c6c672 100644
--- a/services/core/java/com/android/server/pm/pkg/mutate/PackageUserStateWrite.java
+++ b/services/core/java/com/android/server/pm/pkg/mutate/PackageUserStateWrite.java
@@ -22,6 +22,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.overlay.OverlayPaths;
 
+import com.android.server.pm.pkg.PackageUserStateImpl;
 import com.android.server.pm.pkg.SuspendParams;
 
 public interface PackageUserStateWrite {
@@ -68,4 +69,8 @@
     @NonNull
     PackageUserStateWrite setComponentLabelIcon(@NonNull ComponentName componentName,
             @Nullable String nonLocalizedLabel, @Nullable Integer icon);
+
+    /** @see PackageUserStateImpl#setMinAspectRatio(int) */
+    @NonNull
+    PackageUserStateWrite setMinAspectRatio(@PackageManager.UserMinAspectRatio int aspectRatio);
 }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index a88e2b0..279a480 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -91,6 +91,7 @@
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.app.ActivityManager;
 import android.app.ActivityManager.RecentTaskInfo;
 import android.app.ActivityManagerInternal;
@@ -124,6 +125,7 @@
 import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.HdmiPlaybackClient;
 import android.hardware.hdmi.HdmiPlaybackClient.OneTouchPlayCallback;
+import android.hardware.input.InputManager;
 import android.media.AudioManager;
 import android.media.AudioManagerInternal;
 import android.media.AudioSystem;
@@ -205,6 +207,7 @@
 import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.AccessibilityManagerInternal;
 import com.android.server.ExtconStateObserver;
@@ -215,6 +218,7 @@
 import com.android.server.UiThread;
 import com.android.server.display.BrightnessUtils;
 import com.android.server.input.InputManagerInternal;
+import com.android.server.input.KeyboardMetricsCollector;
 import com.android.server.inputmethod.InputMethodManagerInternal;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.policy.KeyCombinationManager.TwoKeysCombinationRule;
@@ -536,10 +540,6 @@
     int mShortPressOnSleepBehavior;
     int mShortPressOnWindowBehavior;
     int mPowerVolUpBehavior;
-    int mShortPressOnStemPrimaryBehavior;
-    int mDoublePressOnStemPrimaryBehavior;
-    int mTriplePressOnStemPrimaryBehavior;
-    int mLongPressOnStemPrimaryBehavior;
     boolean mStylusButtonsEnabled = true;
     boolean mHasSoftInput = false;
     boolean mHapticTextHandleEnabled;
@@ -553,6 +553,12 @@
     int mSearchKeyBehavior;
     ComponentName mSearchKeyTargetActivity;
 
+    // Key Behavior - Stem Primary
+    private int mShortPressOnStemPrimaryBehavior;
+    private int mDoublePressOnStemPrimaryBehavior;
+    private int mTriplePressOnStemPrimaryBehavior;
+    private int mLongPressOnStemPrimaryBehavior;
+
     private boolean mHandleVolumeKeysInWM;
 
     private boolean mPendingKeyguardOccluded;
@@ -595,8 +601,8 @@
     // 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.
-    boolean mLockAfterAppTransitionFinished;
+    // Whether to lock the device after the next dreaming transition has finished.
+    private boolean mLockAfterDreamingTransitionFinished;
 
     // Allowed theater mode wake actions
     private boolean mAllowTheaterModeWakeFromKey;
@@ -627,6 +633,8 @@
 
     SettingsObserver mSettingsObserver;
     ModifierShortcutManager mModifierShortcutManager;
+    /** Currently fully consumed key codes per device */
+    private final SparseArray<Set<Integer>> mConsumedKeysForDevice = new SparseArray<>();
     PowerManager.WakeLock mBroadcastWakeLock;
     PowerManager.WakeLock mPowerKeyWakeLock;
     boolean mHavePendingMediaKeyRepeatWithWakeLock;
@@ -676,6 +684,7 @@
     private static final int MSG_LAUNCH_ASSIST = 23;
     private static final int MSG_RINGER_TOGGLE_CHORD = 24;
     private static final int MSG_SWITCH_KEYBOARD_LAYOUT = 25;
+    private static final int MSG_LOG_KEYBOARD_SYSTEM_EVENT = 26;
 
     private class PolicyHandler extends Handler {
         @Override
@@ -752,6 +761,9 @@
                 case MSG_SWITCH_KEYBOARD_LAYOUT:
                     handleSwitchKeyboardLayout(msg.arg1, msg.arg2);
                     break;
+                case MSG_LOG_KEYBOARD_SYSTEM_EVENT:
+                    handleKeyboardSystemEvent(msg.arg2, (KeyEvent) msg.obj);
+                    break;
             }
         }
     }
@@ -818,7 +830,6 @@
 
         @Override public void onChange(boolean selfChange) {
             updateSettings();
-            updateRotation(false);
         }
     }
 
@@ -1098,7 +1109,7 @@
         synchronized (mLock) {
             // If the setting to lock instantly on power button press is true, then set the flag to
             // lock after the dream transition has finished.
-            mLockAfterAppTransitionFinished =
+            mLockAfterDreamingTransitionFinished =
                     mLockPatternUtils.getPowerButtonInstantlyLocks(mCurrentUserId);
         }
 
@@ -1809,7 +1820,7 @@
             mDisplayId = displayId;
         }
 
-        int handleHomeButton(IBinder focusedToken, KeyEvent event) {
+        boolean handleHomeButton(IBinder focusedToken, KeyEvent event) {
             final boolean keyguardOn = keyguardOn();
             final int repeatCount = event.getRepeatCount();
             final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
@@ -1830,12 +1841,12 @@
                 mHomePressed = false;
                 if (mHomeConsumed) {
                     mHomeConsumed = false;
-                    return -1;
+                    return true;
                 }
 
                 if (canceled) {
                     Log.i(TAG, "Ignoring HOME; event canceled.");
-                    return -1;
+                    return true;
                 }
 
                 // Delay handling home if a double-tap is possible.
@@ -1847,13 +1858,13 @@
                         mHomeDoubleTapPending = true;
                         mHandler.postDelayed(mHomeDoubleTapTimeoutRunnable,
                                 ViewConfiguration.getDoubleTapTimeout());
-                        return -1;
+                        return true;
                     }
                 }
 
                 // Post to main thread to avoid blocking input pipeline.
                 mHandler.post(() -> handleShortPressOnHome(mDisplayId));
-                return -1;
+                return true;
             }
 
             final KeyInterceptionInfo info =
@@ -1865,12 +1876,12 @@
                         || (info.layoutParamsType == TYPE_NOTIFICATION_SHADE
                         && isKeyguardShowing())) {
                     // the "app" is keyguard, so give it the key
-                    return 0;
+                    return false;
                 }
                 for (int t : WINDOW_TYPES_WHERE_HOME_DOESNT_WORK) {
                     if (info.layoutParamsType == t) {
                         // don't do anything, but also don't pass it to the app
-                        return -1;
+                        return true;
                     }
                 }
             }
@@ -1895,7 +1906,7 @@
                             event.getEventTime()));
                 }
             }
-            return -1;
+            return true;
         }
 
         private void handleDoubleTapOnHome() {
@@ -1994,6 +2005,21 @@
         Supplier<GlobalActions> getGlobalActionsFactory() {
             return () -> new GlobalActions(mContext, mWindowManagerFuncs);
         }
+
+        KeyguardServiceDelegate getKeyguardServiceDelegate() {
+            return new KeyguardServiceDelegate(mContext,
+                    new StateCallback() {
+                        @Override
+                        public void onTrustedChanged() {
+                            mWindowManagerFuncs.notifyKeyguardTrustedChanged();
+                        }
+
+                        @Override
+                        public void onShowingChanged() {
+                            mWindowManagerFuncs.onKeyguardShowingAndNotOccludedChanged();
+                        }
+                    });
+        }
     }
 
     /** {@inheritDoc} */
@@ -2232,37 +2258,28 @@
                         true /* notifyOccluded */);
 
                 synchronized (mLock) {
-                    mLockAfterAppTransitionFinished = false;
+                    mLockAfterDreamingTransitionFinished = false;
                 }
             }
 
             @Override
             public void onAppTransitionFinishedLocked(IBinder token) {
                 synchronized (mLock) {
-                    if (!mLockAfterAppTransitionFinished) {
-                        return;
+                    final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal();
+                    // check both isDreaming and mLockAfterDreamingTransitionFinished before lockNow
+                    // so it won't relock after dreaming has stopped
+                    if (dreamManagerInternal != null && dreamManagerInternal.isDreaming()
+                            && mLockAfterDreamingTransitionFinished) {
+                        lockNow(null);
                     }
-                    mLockAfterAppTransitionFinished = false;
+                    mLockAfterDreamingTransitionFinished = false;
                 }
-
-                lockNow(null);
             }
         });
 
         mKeyguardDrawnTimeout = mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_keyguardDrawnTimeout);
-        mKeyguardDelegate = new KeyguardServiceDelegate(mContext,
-                new StateCallback() {
-                    @Override
-                    public void onTrustedChanged() {
-                        mWindowManagerFuncs.notifyKeyguardTrustedChanged();
-                    }
-
-                    @Override
-                    public void onShowingChanged() {
-                        mWindowManagerFuncs.onKeyguardShowingAndNotOccludedChanged();
-                    }
-                });
+        mKeyguardDelegate = injector.getKeyguardServiceDelegate();
         initKeyCombinationRules();
         initSingleKeyGestureRules();
         mSideFpsEventHandler = new SideFpsEventHandler(mContext, mHandler, mPowerManager);
@@ -2287,6 +2304,7 @@
                             cancelPendingScreenshotChordAction();
                         }
                     });
+
             if (mHasFeatureWatch) {
                 mKeyCombinationManager.addRule(
                         new TwoKeysCombinationRule(KEYCODE_POWER, KEYCODE_STEM_PRIMARY) {
@@ -2906,29 +2924,49 @@
             WindowManager.LayoutParams.TYPE_SYSTEM_ERROR,
         };
 
+    /**
+     * Log the keyboard shortcuts without blocking the current thread.
+     *
+     * We won't log keyboard events when the input device is null
+     * or when it is virtual.
+     */
+    private void handleKeyboardSystemEvent(int keyboardSystemEvent, KeyEvent event) {
+        final InputManager inputManager = mContext.getSystemService(InputManager.class);
+        final InputDevice inputDevice = inputManager != null
+                ? inputManager.getInputDevice(event.getDeviceId()) : null;
+        if (inputDevice != null && !inputDevice.isVirtual()) {
+            KeyboardMetricsCollector.logKeyboardSystemsEventReportedAtom(
+                    inputDevice, keyboardSystemEvent,
+                    new int[]{event.getKeyCode()}, event.getMetaState());
+        }
+    }
+
+    private void logKeyboardSystemsEvent(KeyEvent event, int keyboardSystemEvent) {
+        mHandler.obtainMessage(MSG_LOG_KEYBOARD_SYSTEM_EVENT, 0, keyboardSystemEvent, event)
+                .sendToTarget();
+    }
+
     // TODO(b/117479243): handle it in InputPolicy
+    // TODO (b/283241997): Add the remaining keyboard shortcut logging after refactoring
     /** {@inheritDoc} */
     @Override
     public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event,
             int policyFlags) {
-        final boolean keyguardOn = keyguardOn();
         final int keyCode = event.getKeyCode();
-        final int repeatCount = event.getRepeatCount();
-        final int metaState = event.getMetaState();
         final int flags = event.getFlags();
-        final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
-        final boolean canceled = event.isCanceled();
-        final int displayId = event.getDisplayId();
-        final long key_consumed = -1;
-        final long key_not_consumed = 0;
+        final long keyConsumed = -1;
+        final long keyNotConsumed = 0;
+        final int deviceId = event.getDeviceId();
 
         if (DEBUG_INPUT) {
-            Log.d(TAG, "interceptKeyTi keyCode=" + keyCode + " down=" + down + " repeatCount="
-                    + repeatCount + " keyguardOn=" + keyguardOn + " canceled=" + canceled);
+            Log.d(TAG,
+                    "interceptKeyTi keyCode=" + keyCode + " action=" + event.getAction()
+                            + " repeatCount=" + event.getRepeatCount() + " keyguardOn="
+                            + keyguardOn() + " canceled=" + event.isCanceled());
         }
 
         if (mKeyCombinationManager.isKeyConsumed(event)) {
-            return key_consumed;
+            return keyConsumed;
         }
 
         if ((flags & KeyEvent.FLAG_FALLBACK) == 0) {
@@ -2939,8 +2977,54 @@
             }
         }
 
-        // Cancel any pending meta actions if we see any other keys being pressed between the down
-        // of the meta key and its corresponding up.
+        Set<Integer> consumedKeys = mConsumedKeysForDevice.get(deviceId);
+        if (consumedKeys == null) {
+            consumedKeys = new HashSet<>();
+            mConsumedKeysForDevice.put(deviceId, consumedKeys);
+        }
+
+        if (interceptSystemKeysAndShortcuts(focusedToken, event)
+                && event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+            consumedKeys.add(keyCode);
+            return keyConsumed;
+        }
+
+        boolean needToConsumeKey = consumedKeys.contains(keyCode);
+        if (event.getAction() == KeyEvent.ACTION_UP || event.isCanceled()) {
+            consumedKeys.remove(keyCode);
+            if (consumedKeys.isEmpty()) {
+                mConsumedKeysForDevice.remove(deviceId);
+            }
+        }
+
+        return needToConsumeKey ? keyConsumed : keyNotConsumed;
+    }
+
+    // You can only start consuming the key gesture if ACTION_DOWN and repeat count
+    // is 0. If you start intercepting the key halfway, then key will not be consumed
+    // and will be sent to apps for processing too.
+    // e.g. If a certain combination is only handled on ACTION_UP (i.e.
+    // interceptShortcut() returns true only for ACTION_UP), then since we already
+    // sent the ACTION_DOWN events to the application, we MUST also send the
+    // ACTION_UP to the application.
+    // So, to ensure that your intercept logic works properly, and we don't send any
+    // conflicting events to application, make sure to consume the event on
+    // ACTION_DOWN even if you want to do something on ACTION_UP. This is essential
+    // to maintain event parity and to not have incomplete key gestures.
+    @SuppressLint("MissingPermission")
+    private boolean interceptSystemKeysAndShortcuts(IBinder focusedToken, KeyEvent event) {
+        final boolean keyguardOn = keyguardOn();
+        final int keyCode = event.getKeyCode();
+        final int repeatCount = event.getRepeatCount();
+        final int metaState = event.getMetaState();
+        final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
+        final boolean canceled = event.isCanceled();
+        final int displayId = event.getDisplayId();
+        final int deviceId = event.getDeviceId();
+        final boolean firstDown = down && repeatCount == 0;
+
+        // Cancel any pending meta actions if we see any other keys being pressed between the
+        // down of the meta key and its corresponding up.
         if (mPendingMetaAction && !KeyEvent.isMetaKey(keyCode)) {
             mPendingMetaAction = false;
         }
@@ -2954,46 +3038,49 @@
                 dismissKeyboardShortcutsMenu();
                 mPendingMetaAction = false;
                 mPendingCapsLockToggle = false;
-                return key_consumed;
+                return true;
             }
         }
 
-        switch(keyCode) {
+        switch (keyCode) {
             case KeyEvent.KEYCODE_HOME:
+                logKeyboardSystemsEvent(event,
+                        FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__HOME);
                 return handleHomeShortcuts(displayId, focusedToken, event);
             case KeyEvent.KEYCODE_MENU:
                 // Hijack modified menu keys for debugging features
                 final int chordBug = KeyEvent.META_SHIFT_ON;
 
-                if (down && repeatCount == 0) {
-                    if (mEnableShiftMenuBugReports && (metaState & chordBug) == chordBug) {
-                        Intent intent = new Intent(Intent.ACTION_BUG_REPORT);
-                        mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT,
-                                null, null, null, 0, null, null);
-                        return key_consumed;
-                    }
+                if (mEnableShiftMenuBugReports && firstDown
+                        && (metaState & chordBug) == chordBug) {
+                    Intent intent = new Intent(Intent.ACTION_BUG_REPORT);
+                    mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT,
+                            null, null, null, 0, null, null);
+                    return true;
                 }
                 break;
             case KeyEvent.KEYCODE_RECENT_APPS:
-                if (down && repeatCount == 0) {
+                if (firstDown) {
                     showRecentApps(false /* triggeredFromAltTab */);
+                    logKeyboardSystemsEvent(event,
+                            FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__RECENT_APPS);
                 }
-                return key_consumed;
+                return true;
             case KeyEvent.KEYCODE_APP_SWITCH:
                 if (!keyguardOn) {
-                    if (down && repeatCount == 0) {
+                    if (firstDown) {
                         preloadRecentApps();
                     } else if (!down) {
                         toggleRecentApps();
                     }
                 }
-                return key_consumed;
+                return true;
             case KeyEvent.KEYCODE_A:
-                if (down && event.isMetaPressed()) {
+                if (firstDown && event.isMetaPressed()) {
                     launchAssistAction(Intent.EXTRA_ASSIST_INPUT_HINT_KEYBOARD,
-                            event.getDeviceId(),
-                            event.getEventTime(), AssistUtils.INVOCATION_TYPE_UNKNOWN);
-                    return key_consumed;
+                            deviceId, event.getEventTime(),
+                            AssistUtils.INVOCATION_TYPE_UNKNOWN);
+                    return true;
                 }
                 break;
             case KeyEvent.KEYCODE_H:
@@ -3003,67 +3090,73 @@
                 }
                 break;
             case KeyEvent.KEYCODE_I:
-                if (down && event.isMetaPressed()) {
+                if (firstDown && event.isMetaPressed()) {
                     showSystemSettings();
-                    return key_consumed;
+                    return true;
+                }
+                break;
+            case KeyEvent.KEYCODE_L:
+                if (firstDown && event.isMetaPressed()) {
+                    lockNow(null /* options */);
+                    return true;
                 }
                 break;
             case KeyEvent.KEYCODE_N:
-                if (down && event.isMetaPressed()) {
+                if (firstDown && event.isMetaPressed()) {
                     if (event.isCtrlPressed()) {
                         sendSystemKeyToStatusBarAsync(event);
                     } else {
                         toggleNotificationPanel();
                     }
-                    return key_consumed;
+                    return true;
                 }
                 break;
             case KeyEvent.KEYCODE_S:
-                if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) {
+                if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) {
                     interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
-                    return key_consumed;
+                    return true;
                 }
                 break;
             case KeyEvent.KEYCODE_T:
-                if (down && event.isMetaPressed()) {
+                if (firstDown && event.isMetaPressed()) {
                     toggleTaskbar();
-                    return key_consumed;
+                    return true;
                 }
                 break;
             case KeyEvent.KEYCODE_DPAD_UP:
-                if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) {
+                if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) {
                     StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
                     if (statusbar != null) {
                         statusbar.goToFullscreenFromSplit();
+                        return true;
                     }
-                    return key_consumed;
                 }
                 break;
             case KeyEvent.KEYCODE_DPAD_LEFT:
-                if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) {
+                if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) {
                     enterStageSplitFromRunningApp(true /* leftOrTop */);
-                    return key_consumed;
+                    return true;
                 }
                 break;
             case KeyEvent.KEYCODE_DPAD_RIGHT:
-                if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) {
+                if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) {
                     enterStageSplitFromRunningApp(false /* leftOrTop */);
-                    return key_consumed;
+                    return true;
                 }
                 break;
             case KeyEvent.KEYCODE_SLASH:
-                if (down && repeatCount == 0 && event.isMetaPressed() && !keyguardOn) {
+                if (firstDown && event.isMetaPressed() && !keyguardOn) {
                     toggleKeyboardShortcutsMenu(event.getDeviceId());
-                    return key_consumed;
+                    return true;
                 }
                 break;
             case KeyEvent.KEYCODE_ASSIST:
                 Slog.wtf(TAG, "KEYCODE_ASSIST should be handled in interceptKeyBeforeQueueing");
-                return key_consumed;
+                return true;
             case KeyEvent.KEYCODE_VOICE_ASSIST:
                 Slog.wtf(TAG, "KEYCODE_VOICE_ASSIST should be handled in"
                         + " interceptKeyBeforeQueueing");
-                return key_consumed;
+                return true;
             case KeyEvent.KEYCODE_VIDEO_APP_1:
             case KeyEvent.KEYCODE_VIDEO_APP_2:
             case KeyEvent.KEYCODE_VIDEO_APP_3:
@@ -3081,7 +3174,7 @@
             case KeyEvent.KEYCODE_DEMO_APP_3:
             case KeyEvent.KEYCODE_DEMO_APP_4:
                 Slog.wtf(TAG, "KEYCODE_APP_X should be handled in interceptKeyBeforeQueueing");
-                return key_consumed;
+                return true;
             case KeyEvent.KEYCODE_BRIGHTNESS_UP:
             case KeyEvent.KEYCODE_BRIGHTNESS_DOWN:
                 if (down) {
@@ -3121,20 +3214,20 @@
                     startActivityAsUser(new Intent(Intent.ACTION_SHOW_BRIGHTNESS_DIALOG),
                             UserHandle.CURRENT_OR_SELF);
                 }
-                return key_consumed;
+                return true;
             case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN:
                 if (down) {
                     mInputManagerInternal.decrementKeyboardBacklight(event.getDeviceId());
                 }
-                return key_consumed;
+                return true;
             case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP:
                 if (down) {
                     mInputManagerInternal.incrementKeyboardBacklight(event.getDeviceId());
                 }
-                return key_consumed;
+                return true;
             case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE:
                 // TODO: Add logic
-                return key_consumed;
+                return true;
             case KeyEvent.KEYCODE_VOLUME_UP:
             case KeyEvent.KEYCODE_VOLUME_DOWN:
             case KeyEvent.KEYCODE_VOLUME_MUTE:
@@ -3142,7 +3235,7 @@
                     // On TVs or when the configuration is enabled, volume keys never
                     // go to the foreground app.
                     dispatchDirectAudioEvent(event);
-                    return key_consumed;
+                    return true;
                 }
 
                 // If the device is in VR mode and keys are "internal" (e.g. on the side of the
@@ -3151,26 +3244,23 @@
                 if (mDefaultDisplayPolicy.isPersistentVrModeEnabled()) {
                     final InputDevice d = event.getDevice();
                     if (d != null && !d.isExternal()) {
-                        return key_consumed;
+                        return true;
                     }
                 }
                 break;
             case KeyEvent.KEYCODE_TAB:
-                if (down && event.isMetaPressed()) {
-                    if (!keyguardOn && isUserSetupComplete()) {
+                if (firstDown && !keyguardOn && isUserSetupComplete()) {
+                    if (event.isMetaPressed()) {
                         showRecentApps(false);
-                        return key_consumed;
-                    }
-                } else if (down && repeatCount == 0) {
-                    // Display task switcher for ALT-TAB.
-                    if (mRecentAppsHeldModifiers == 0 && !keyguardOn && isUserSetupComplete()) {
+                        return true;
+                    } else if (mRecentAppsHeldModifiers == 0) {
                         final int shiftlessModifiers =
                                 event.getModifiers() & ~KeyEvent.META_SHIFT_MASK;
                         if (KeyEvent.metaStateHasModifiers(
                                 shiftlessModifiers, KeyEvent.META_ALT_ON)) {
                             mRecentAppsHeldModifiers = shiftlessModifiers;
                             showRecentApps(true);
-                            return key_consumed;
+                            return true;
                         }
                     }
                 }
@@ -3182,18 +3272,18 @@
                     msg.setAsynchronous(true);
                     msg.sendToTarget();
                 }
-                return key_consumed;
+                return true;
             case KeyEvent.KEYCODE_NOTIFICATION:
                 if (!down) {
                     toggleNotificationPanel();
                 }
-                return key_consumed;
+                return true;
             case KeyEvent.KEYCODE_SEARCH:
-                if (down && repeatCount == 0 && !keyguardOn()) {
-                    switch(mSearchKeyBehavior) {
+                if (firstDown && !keyguardOn) {
+                    switch (mSearchKeyBehavior) {
                         case SEARCH_BEHAVIOR_TARGET_ACTIVITY: {
                             launchTargetSearchActivity();
-                            return key_consumed;
+                            return true;
                         }
                         case SEARCH_BEHAVIOR_DEFAULT_SEARCH:
                         default:
@@ -3202,21 +3292,18 @@
                 }
                 break;
             case KeyEvent.KEYCODE_LANGUAGE_SWITCH:
-                if (down && repeatCount == 0) {
+                if (firstDown) {
                     int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1;
                     sendSwitchKeyboardLayout(event, direction);
-                    return key_consumed;
+                    return true;
                 }
                 break;
             case KeyEvent.KEYCODE_SPACE:
                 // Handle keyboard layout switching. (META + SPACE)
-                if ((metaState & KeyEvent.META_META_MASK) == 0) {
-                    return key_not_consumed;
-                }
-                if (down && repeatCount == 0) {
+                if (firstDown && event.isMetaPressed()) {
                     int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1;
                     sendSwitchKeyboardLayout(event, direction);
-                    return key_consumed;
+                    return true;
                 }
                 break;
             case KeyEvent.KEYCODE_META_LEFT:
@@ -3241,7 +3328,7 @@
                         mPendingMetaAction = false;
                     }
                 }
-                return key_consumed;
+                return true;
             case KeyEvent.KEYCODE_ALT_LEFT:
             case KeyEvent.KEYCODE_ALT_RIGHT:
                 if (down) {
@@ -3257,14 +3344,14 @@
                             && (metaState & mRecentAppsHeldModifiers) == 0) {
                         mRecentAppsHeldModifiers = 0;
                         hideRecentApps(true, false);
-                        return key_consumed;
+                        return true;
                     }
 
                     // Toggle Caps Lock on META-ALT.
                     if (mPendingCapsLockToggle) {
                         mInputManagerInternal.toggleCapsLock(event.getDeviceId());
                         mPendingCapsLockToggle = false;
-                        return key_consumed;
+                        return true;
                     }
                 }
                 break;
@@ -3274,24 +3361,18 @@
             case KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL:
                 Slog.wtf(TAG, "KEYCODE_STYLUS_BUTTON_* should be handled in"
                         + " interceptKeyBeforeQueueing");
-                return key_consumed;
+                return true;
         }
-
         if (isValidGlobalKey(keyCode)
                 && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) {
-            return key_consumed;
+            return true;
         }
 
         // Reserve all the META modifier combos for system behavior
-        if ((metaState & KeyEvent.META_META_ON) != 0) {
-            return key_consumed;
-        }
-
-        // Let the application handle the key.
-        return key_not_consumed;
+        return (metaState & KeyEvent.META_META_ON) != 0;
     }
 
-    private int handleHomeShortcuts(int displayId, IBinder focusedToken, KeyEvent event) {
+    private boolean handleHomeShortcuts(int displayId, IBinder focusedToken, KeyEvent event) {
         // First we always handle the home key here, so applications
         // can never break it, although if keyguard is on, we do let
         // it handle it, because that gives us the correct 5 second
diff --git a/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java b/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java
index 9c3b38a..b999bbb3 100644
--- a/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java
+++ b/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java
@@ -314,7 +314,9 @@
             if (eventTime < mLastDownTime + mActiveRule.getVeryLongPressTimeoutMs()) {
                 mHandler.removeMessages(MSG_KEY_VERY_LONG_PRESS);
             } else {
-                mHandledByLongPress = mActiveRule.supportVeryLongPress();
+                // If long press or very long press (~3.5s) had been handled, we should skip the
+                // short press behavior.
+                mHandledByLongPress |= mActiveRule.supportVeryLongPress();
             }
         }
 
diff --git a/services/core/java/com/android/server/power/TEST_MAPPING b/services/core/java/com/android/server/power/TEST_MAPPING
index cf1bfc3..fbfe291 100644
--- a/services/core/java/com/android/server/power/TEST_MAPPING
+++ b/services/core/java/com/android/server/power/TEST_MAPPING
@@ -20,6 +20,7 @@
       "name": "FrameworksServicesTests",
       "options": [
         {"include-filter": "com.android.server.power"},
+        {"exclude-filter": "com.android.server.power.BatteryStatsTests"},
         {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
         {"exclude-annotation": "androidx.test.filters.FlakyTest"}
       ]
@@ -38,7 +39,8 @@
     {
       "name": "FrameworksServicesTests",
       "options": [
-        {"include-filter": "com.android.server.power"}
+        {"include-filter": "com.android.server.power"},
+        {"exclude-filter": "com.android.server.power.BatteryStatsTests"}
       ]
     }
   ]
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 044d30b..cc849b6 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -346,9 +346,10 @@
 
         @Override
         public void showScreenPinningRequest(int taskId) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.showScreenPinningRequest(taskId);
+                    bar.showScreenPinningRequest(taskId);
                 } catch (RemoteException e) {
                 }
             }
@@ -356,9 +357,10 @@
 
         @Override
         public void showAssistDisclosure() {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.showAssistDisclosure();
+                    bar.showAssistDisclosure();
                 } catch (RemoteException e) {
                 }
             }
@@ -366,9 +368,10 @@
 
         @Override
         public void startAssist(Bundle args) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.startAssist(args);
+                    bar.startAssist(args);
                 } catch (RemoteException e) {
                 }
             }
@@ -376,9 +379,10 @@
 
         @Override
         public void onCameraLaunchGestureDetected(int source) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.onCameraLaunchGestureDetected(source);
+                    bar.onCameraLaunchGestureDetected(source);
                 } catch (RemoteException e) {
                 }
             }
@@ -392,9 +396,10 @@
         @Override
         public void onEmergencyActionLaunchGestureDetected() {
             if (SPEW) Slog.d(TAG, "Launching emergency action");
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.onEmergencyActionLaunchGestureDetected();
+                    bar.onEmergencyActionLaunchGestureDetected();
                 } catch (RemoteException e) {
                     if (SPEW) Slog.d(TAG, "Failed to launch emergency action");
                 }
@@ -409,9 +414,10 @@
         @Override
         public void toggleSplitScreen() {
             enforceStatusBarService();
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.toggleSplitScreen();
+                    bar.toggleSplitScreen();
                 } catch (RemoteException ex) {}
             }
         }
@@ -419,27 +425,30 @@
         @Override
         public void appTransitionFinished(int displayId) {
             enforceStatusBarService();
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.appTransitionFinished(displayId);
+                    bar.appTransitionFinished(displayId);
                 } catch (RemoteException ex) {}
             }
         }
 
         @Override
         public void toggleTaskbar() {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.toggleTaskbar();
+                    bar.toggleTaskbar();
                 } catch (RemoteException ex) {}
             }
         }
 
         @Override
         public void toggleRecentApps() {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.toggleRecentApps();
+                    bar.toggleRecentApps();
                 } catch (RemoteException ex) {}
             }
         }
@@ -453,45 +462,50 @@
 
         @Override
         public void preloadRecentApps() {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.preloadRecentApps();
+                    bar.preloadRecentApps();
                 } catch (RemoteException ex) {}
             }
         }
 
         @Override
         public void cancelPreloadRecentApps() {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.cancelPreloadRecentApps();
+                    bar.cancelPreloadRecentApps();
                 } catch (RemoteException ex) {}
             }
         }
 
         @Override
         public void showRecentApps(boolean triggeredFromAltTab) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.showRecentApps(triggeredFromAltTab);
+                    bar.showRecentApps(triggeredFromAltTab);
                 } catch (RemoteException ex) {}
             }
         }
 
         @Override
         public void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.hideRecentApps(triggeredFromAltTab, triggeredFromHomeKey);
+                    bar.hideRecentApps(triggeredFromAltTab, triggeredFromHomeKey);
                 } catch (RemoteException ex) {}
             }
         }
 
         @Override
         public void collapsePanels() {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.animateCollapsePanels();
+                    bar.animateCollapsePanels();
                 } catch (RemoteException ex) {
                 }
             }
@@ -499,18 +513,20 @@
 
         @Override
         public void dismissKeyboardShortcutsMenu() {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.dismissKeyboardShortcutsMenu();
+                    bar.dismissKeyboardShortcutsMenu();
                 } catch (RemoteException ex) {}
             }
         }
 
         @Override
         public void toggleKeyboardShortcutsMenu(int deviceId) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.toggleKeyboardShortcutsMenu(deviceId);
+                    bar.toggleKeyboardShortcutsMenu(deviceId);
                 } catch (RemoteException ex) {}
             }
         }
@@ -536,9 +552,10 @@
 
         @Override
         public void showChargingAnimation(int batteryLevel) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.showWirelessChargingAnimation(batteryLevel);
+                    bar.showWirelessChargingAnimation(batteryLevel);
                 } catch (RemoteException ex){
                 }
             }
@@ -546,7 +563,8 @@
 
         @Override
         public void showPictureInPictureMenu() {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
                     mBar.showPictureInPictureMenu();
                 } catch (RemoteException ex) {}
@@ -555,27 +573,30 @@
 
         @Override
         public void setWindowState(int displayId, int window, int state) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.setWindowState(displayId, window, state);
+                    bar.setWindowState(displayId, window, state);
                 } catch (RemoteException ex) {}
             }
         }
 
         @Override
         public void appTransitionPending(int displayId) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.appTransitionPending(displayId);
+                    bar.appTransitionPending(displayId);
                 } catch (RemoteException ex) {}
             }
         }
 
         @Override
         public void appTransitionCancelled(int displayId) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.appTransitionCancelled(displayId);
+                    bar.appTransitionCancelled(displayId);
                 } catch (RemoteException ex) {}
             }
         }
@@ -583,9 +604,10 @@
         @Override
         public void appTransitionStarting(int displayId, long statusBarAnimationsStartTime,
                 long statusBarAnimationsDuration) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.appTransitionStarting(
+                    bar.appTransitionStarting(
                             displayId, statusBarAnimationsStartTime, statusBarAnimationsDuration);
                 } catch (RemoteException ex) {}
             }
@@ -593,9 +615,10 @@
 
         @Override
         public void setTopAppHidesStatusBar(boolean hidesStatusBar) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.setTopAppHidesStatusBar(hidesStatusBar);
+                    bar.setTopAppHidesStatusBar(hidesStatusBar);
                 } catch (RemoteException ex) {}
             }
         }
@@ -605,9 +628,10 @@
             if (!mContext.getResources().getBoolean(R.bool.config_showSysuiShutdown)) {
                 return false;
             }
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.showShutdownUi(isReboot, reason);
+                    bar.showShutdownUi(isReboot, reason);
                     return true;
                 } catch (RemoteException ex) {}
             }
@@ -626,18 +650,20 @@
 
         @Override
         public void onDisplayReady(int displayId) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.onDisplayReady(displayId);
+                    bar.onDisplayReady(displayId);
                 } catch (RemoteException ex) {}
             }
         }
 
         @Override
         public void onRecentsAnimationStateChanged(boolean running) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.onRecentsAnimationStateChanged(running);
+                    bar.onRecentsAnimationStateChanged(running);
                 } catch (RemoteException ex) {}
             }
 
@@ -651,9 +677,10 @@
             getUiState(displayId).setBarAttributes(appearance, appearanceRegions,
                     navbarColorManagedByIme, behavior, requestedVisibleTypes, packageName,
                     letterboxDetails);
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.onSystemBarAttributesChanged(displayId, appearance, appearanceRegions,
+                    bar.onSystemBarAttributesChanged(displayId, appearance, appearanceRegions,
                             navbarColorManagedByIme, behavior, requestedVisibleTypes, packageName,
                             letterboxDetails);
                 } catch (RemoteException ex) { }
@@ -664,9 +691,10 @@
         public void showTransient(int displayId, @InsetsType int types,
                 boolean isGestureOnSystemBar) {
             getUiState(displayId).showTransient(types);
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.showTransient(displayId, types, isGestureOnSystemBar);
+                    bar.showTransient(displayId, types, isGestureOnSystemBar);
                 } catch (RemoteException ex) { }
             }
         }
@@ -674,9 +702,10 @@
         @Override
         public void abortTransient(int displayId, @InsetsType int types) {
             getUiState(displayId).clearTransient(types);
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.abortTransient(displayId, types);
+                    bar.abortTransient(displayId, types);
                 } catch (RemoteException ex) { }
             }
         }
@@ -685,9 +714,10 @@
         public void showToast(int uid, String packageName, IBinder token, CharSequence text,
                 IBinder windowToken, int duration,
                 @Nullable ITransientNotificationCallback callback, int displayId) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.showToast(uid, packageName, token, text, windowToken, duration, callback,
+                    bar.showToast(uid, packageName, token, text, windowToken, duration, callback,
                             displayId);
                 } catch (RemoteException ex) { }
             }
@@ -695,18 +725,20 @@
 
         @Override
         public void hideToast(String packageName, IBinder token) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.hideToast(packageName, token);
+                    bar.hideToast(packageName, token);
                 } catch (RemoteException ex) { }
             }
         }
 
         @Override
         public boolean requestWindowMagnificationConnection(boolean request) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.requestWindowMagnificationConnection(request);
+                    bar.requestWindowMagnificationConnection(request);
                     return true;
                 } catch (RemoteException ex) { }
             }
@@ -715,9 +747,10 @@
 
         @Override
         public void setNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.setNavigationBarLumaSamplingEnabled(displayId, enable);
+                    bar.setNavigationBarLumaSamplingEnabled(displayId, enable);
                 } catch (RemoteException ex) { }
             }
         }
@@ -727,45 +760,50 @@
             synchronized (mLock) {
                 mUdfpsRefreshRateRequestCallback = callback;
             }
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.setUdfpsRefreshRateCallback(callback);
+                    bar.setUdfpsRefreshRateCallback(callback);
                 } catch (RemoteException ex) { }
             }
         }
 
         @Override
         public void showRearDisplayDialog(int currentBaseState) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.showRearDisplayDialog(currentBaseState);
+                    bar.showRearDisplayDialog(currentBaseState);
                 } catch (RemoteException ex) { }
             }
         }
 
         @Override
         public void goToFullscreenFromSplit() {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.goToFullscreenFromSplit();
+                    bar.goToFullscreenFromSplit();
                 } catch (RemoteException ex) { }
             }
         }
 
         @Override
         public void enterStageSplitFromRunningApp(boolean leftOrTop) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.enterStageSplitFromRunningApp(leftOrTop);
+                    bar.enterStageSplitFromRunningApp(leftOrTop);
                 } catch (RemoteException ex) { }
             }
         }
 
         @Override
         public void showMediaOutputSwitcher(String packageName) {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.showMediaOutputSwitcher(packageName);
+                    bar.showMediaOutputSwitcher(packageName);
                 } catch (RemoteException ex) {
                 }
             }
@@ -788,9 +826,10 @@
 
         @Override
         public void showGlobalActions() {
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.showGlobalActionsMenu();
+                    bar.showGlobalActionsMenu();
                 } catch (RemoteException ex) {}
             }
         }
@@ -1113,9 +1152,10 @@
         if (!state.disableEquals(net1, net2)) {
             state.setDisabled(net1, net2);
             mHandler.post(() -> mNotificationDelegate.onSetDisabled(net1));
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.disable(displayId, net1, net2);
+                    bar.disable(displayId, net1, net2);
                 } catch (RemoteException ex) {
                 }
             }
@@ -1173,9 +1213,10 @@
             //Slog.d(TAG, "setIcon slot=" + slot + " index=" + index + " icon=" + icon);
             mIcons.put(slot, icon);
 
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.setIcon(slot, icon);
+                    bar.setIcon(slot, icon);
                 } catch (RemoteException ex) {
                 }
             }
@@ -1194,9 +1235,10 @@
             if (icon.visible != visibility) {
                 icon.visible = visibility;
 
-                if (mBar != null) {
+                IStatusBar bar = mBar;
+                if (bar != null) {
                     try {
-                        mBar.setIcon(slot, icon);
+                        bar.setIcon(slot, icon);
                     } catch (RemoteException ex) {
                     }
                 }
@@ -1211,9 +1253,10 @@
         synchronized (mIcons) {
             mIcons.remove(slot);
 
-            if (mBar != null) {
+            IStatusBar bar = mBar;
+            if (bar != null) {
                 try {
-                    mBar.removeIcon(slot);
+                    bar.removeIcon(slot);
                 } catch (RemoteException ex) {
                 }
             }
@@ -1815,9 +1858,10 @@
     @Override
     public void showInattentiveSleepWarning() {
         enforceStatusBarService();
-        if (mBar != null) {
+        IStatusBar bar = mBar;
+        if (bar != null) {
             try {
-                mBar.showInattentiveSleepWarning();
+                bar.showInattentiveSleepWarning();
             } catch (RemoteException ex) {
             }
         }
@@ -1826,9 +1870,10 @@
     @Override
     public void dismissInattentiveSleepWarning(boolean animated) {
         enforceStatusBarService();
-        if (mBar != null) {
+        IStatusBar bar = mBar;
+        if (bar != null) {
             try {
-                mBar.dismissInattentiveSleepWarning(animated);
+                bar.dismissInattentiveSleepWarning(animated);
             } catch (RemoteException ex) {
             }
         }
@@ -1837,9 +1882,10 @@
     @Override
     public void suppressAmbientDisplay(boolean suppress) {
         enforceStatusBarService();
-        if (mBar != null) {
+        IStatusBar bar = mBar;
+        if (bar != null) {
             try {
-                mBar.suppressAmbientDisplay(suppress);
+                bar.suppressAmbientDisplay(suppress);
             } catch (RemoteException ex) {
             }
         }
@@ -1913,9 +1959,10 @@
                 }
             }
         }
-        if (mBar != null) {
+        IStatusBar bar = mBar;
+        if (bar != null) {
             try {
-                mBar.requestTileServiceListeningState(componentName);
+                bar.requestTileServiceListeningState(componentName);
             } catch (RemoteException e) {
                 Slog.e(TAG, "requestTileServiceListeningState", e);
             }
@@ -2028,9 +2075,10 @@
 
         CharSequence appName = r.serviceInfo.applicationInfo
                 .loadLabel(mContext.getPackageManager());
-        if (mBar != null) {
+        IStatusBar bar = mBar;
+        if (bar != null) {
             try {
-                mBar.requestAddTile(componentName, appName, label, icon, proxyCallback);
+                bar.requestAddTile(componentName, appName, label, icon, proxyCallback);
                 return;
             } catch (RemoteException e) {
                 Slog.e(TAG, "requestAddTile", e);
@@ -2052,9 +2100,10 @@
 
     private void cancelRequestAddTileInternal(String packageName) {
         clearTileAddRequest(packageName);
-        if (mBar != null) {
+        IStatusBar bar = mBar;
+        if (bar != null) {
             try {
-                mBar.cancelRequestAddTile(packageName);
+                bar.cancelRequestAddTile(packageName);
             } catch (RemoteException e) {
                 Slog.e(TAG, "requestAddTile", e);
             }
@@ -2183,9 +2232,10 @@
             @Nullable IUndoMediaTransferCallback undoCallback
     ) {
         enforceMediaContentControl();
-        if (mBar != null) {
+        IStatusBar bar = mBar;
+        if (bar != null) {
             try {
-                mBar.updateMediaTapToTransferSenderDisplay(displayState, routeInfo, undoCallback);
+                bar.updateMediaTapToTransferSenderDisplay(displayState, routeInfo, undoCallback);
             } catch (RemoteException e) {
                 Slog.e(TAG, "updateMediaTapToTransferSenderDisplay", e);
             }
@@ -2206,9 +2256,10 @@
             @Nullable Icon appIcon,
             @Nullable CharSequence appName) {
         enforceMediaContentControl();
-        if (mBar != null) {
+        IStatusBar bar = mBar;
+        if (bar != null) {
             try {
-                mBar.updateMediaTapToTransferReceiverDisplay(
+                bar.updateMediaTapToTransferReceiverDisplay(
                         displayState, routeInfo, appIcon, appName);
             } catch (RemoteException e) {
                 Slog.e(TAG, "updateMediaTapToTransferReceiverDisplay", e);
@@ -2232,9 +2283,10 @@
             @NonNull INearbyMediaDevicesProvider provider
     ) {
         enforceMediaContentControl();
-        if (mBar != null) {
+        IStatusBar bar = mBar;
+        if (bar != null) {
             try {
-                mBar.registerNearbyMediaDevicesProvider(provider);
+                bar.registerNearbyMediaDevicesProvider(provider);
             } catch (RemoteException e) {
                 Slog.e(TAG, "registerNearbyMediaDevicesProvider", e);
             }
@@ -2257,9 +2309,10 @@
             @NonNull INearbyMediaDevicesProvider provider
     ) {
         enforceMediaContentControl();
-        if (mBar != null) {
+        IStatusBar bar = mBar;
+        if (bar != null) {
             try {
-                mBar.unregisterNearbyMediaDevicesProvider(provider);
+                bar.unregisterNearbyMediaDevicesProvider(provider);
             } catch (RemoteException e) {
                 Slog.e(TAG, "unregisterNearbyMediaDevicesProvider", e);
             }
@@ -2270,9 +2323,10 @@
     @Override
     public void showRearDisplayDialog(int currentState) {
         enforceControlDeviceStatePermission();
-        if (mBar != null) {
+        IStatusBar bar = mBar;
+        if (bar != null) {
             try {
-                mBar.showRearDisplayDialog(currentState);
+                bar.showRearDisplayDialog(currentState);
             } catch (RemoteException e) {
                 Slog.e(TAG, "showRearDisplayDialog", e);
             }
diff --git a/services/core/java/com/android/server/utils/AlarmQueue.java b/services/core/java/com/android/server/utils/AlarmQueue.java
index 09ba195..83605ae 100644
--- a/services/core/java/com/android/server/utils/AlarmQueue.java
+++ b/services/core/java/com/android/server/utils/AlarmQueue.java
@@ -151,6 +151,10 @@
     @GuardedBy("mLock")
     @ElapsedRealtimeLong
     private long mTriggerTimeElapsed = NOT_SCHEDULED;
+    /** The last time an alarm went off (ie. the last time {@link #onAlarm()} was called}). */
+    @GuardedBy("mLock")
+    @ElapsedRealtimeLong
+    private long mLastFireTimeElapsed;
 
     /**
      * @param alarmTag               The tag to use when scheduling the alarm with AlarmManager.
@@ -284,7 +288,7 @@
     /** Sets an alarm with {@link AlarmManager} for the earliest alarm in the queue after now. */
     @GuardedBy("mLock")
     private void setNextAlarmLocked() {
-        setNextAlarmLocked(mInjector.getElapsedRealtime());
+        setNextAlarmLocked(mLastFireTimeElapsed + mMinTimeBetweenAlarmsMs);
     }
 
     /**
@@ -334,6 +338,7 @@
         final ArraySet<K> expired = new ArraySet<>();
         synchronized (mLock) {
             final long nowElapsed = mInjector.getElapsedRealtime();
+            mLastFireTimeElapsed = nowElapsed;
             while (mAlarmPriorityQueue.size() > 0) {
                 final Pair<K, Long> alarm = mAlarmPriorityQueue.peek();
                 if (alarm.second <= nowElapsed) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index 49b125c..b773ade 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -81,7 +81,7 @@
         if (DEBUG) {
             Slog.v(TAG, "Generating crop for new wallpaper(s): 0x"
                     + Integer.toHexString(wallpaper.mWhich)
-                    + " to " + wallpaper.cropFile.getName()
+                    + " to " + wallpaper.getCropFile().getName()
                     + " crop=(" + cropHint.width() + 'x' + cropHint.height()
                     + ") dim=(" + wpData.mWidth + 'x' + wpData.mHeight + ')');
         }
@@ -89,7 +89,7 @@
         // Analyse the source; needed in multiple cases
         BitmapFactory.Options options = new BitmapFactory.Options();
         options.inJustDecodeBounds = true;
-        BitmapFactory.decodeFile(wallpaper.wallpaperFile.getAbsolutePath(), options);
+        BitmapFactory.decodeFile(wallpaper.getWallpaperFile().getAbsolutePath(), options);
         if (options.outWidth <= 0 || options.outHeight <= 0) {
             Slog.w(TAG, "Invalid wallpaper data");
             success = false;
@@ -154,11 +154,10 @@
                 //  may be we can try to remove this optimized way in the future,
                 //  that means, we will always go into the 'else' block.
 
-                success = FileUtils.copyFile(wallpaper.wallpaperFile, wallpaper.cropFile);
+                success = FileUtils.copyFile(wallpaper.getWallpaperFile(), wallpaper.getCropFile());
 
                 if (!success) {
-                    wallpaper.cropFile.delete();
-                    // TODO: fall back to default wallpaper in this case
+                    wallpaper.getCropFile().delete();
                 }
 
                 if (DEBUG) {
@@ -226,7 +225,7 @@
 
                     //Create a record file and will delete if ImageDecoder work well.
                     final String recordName =
-                            (wallpaper.wallpaperFile.getName().equals(WALLPAPER)
+                            (wallpaper.getWallpaperFile().getName().equals(WALLPAPER)
                                     ? RECORD_FILE : RECORD_LOCK_FILE);
                     final File record = new File(getWallpaperDir(wallpaper.userId), recordName);
                     record.createNewFile();
@@ -234,7 +233,7 @@
                             + ", record name =" + record.getName());
 
                     final ImageDecoder.Source srcData =
-                            ImageDecoder.createSource(wallpaper.wallpaperFile);
+                            ImageDecoder.createSource(wallpaper.getWallpaperFile());
                     final int sampleSize = scale;
                     Bitmap cropped = ImageDecoder.decodeBitmap(srcData, (decoder, info, src) -> {
                         decoder.setTargetSampleSize(sampleSize);
@@ -257,7 +256,7 @@
                                     + " h=" + finalCrop.getHeight());
                         }
 
-                        f = new FileOutputStream(wallpaper.cropFile);
+                        f = new FileOutputStream(wallpaper.getCropFile());
                         bos = new BufferedOutputStream(f, 32 * 1024);
                         finalCrop.compress(Bitmap.CompressFormat.PNG, 100, bos);
                         // don't rely on the implicit flush-at-close when noting success
@@ -277,11 +276,11 @@
 
         if (!success) {
             Slog.e(TAG, "Unable to apply new wallpaper");
-            wallpaper.cropFile.delete();
+            wallpaper.getCropFile().delete();
         }
 
-        if (wallpaper.cropFile.exists()) {
-            boolean didRestorecon = SELinux.restorecon(wallpaper.cropFile.getAbsoluteFile());
+        if (wallpaper.getCropFile().exists()) {
+            boolean didRestorecon = SELinux.restorecon(wallpaper.getCropFile().getAbsoluteFile());
             if (DEBUG) {
                 Slog.v(TAG, "restorecon() of crop file returned " + didRestorecon);
             }
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java
index d87fca4..b0b66cf 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperData.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java
@@ -40,10 +40,7 @@
  */
 class WallpaperData {
 
-    int userId;
-
-    final File wallpaperFile;   // source image
-    final File cropFile;        // eventual destination
+    final int userId;
 
     /**
      * True while the client is writing a new wallpaper
@@ -133,14 +130,13 @@
      */
     final Rect cropHint = new Rect(0, 0, 0, 0);
 
+    // map of which -> File
+    private final SparseArray<File> mWallpaperFiles = new SparseArray<>();
+    private final SparseArray<File> mCropFiles = new SparseArray<>();
+
     WallpaperData(int userId, @SetWallpaperFlags int wallpaperType) {
         this.userId = userId;
         this.mWhich = wallpaperType;
-        File wallpaperDir = getWallpaperDir(userId);
-        String wallpaperFileName = (wallpaperType == FLAG_LOCK) ? WALLPAPER_LOCK_ORIG : WALLPAPER;
-        String cropFileName = (wallpaperType == FLAG_LOCK) ? WALLPAPER_LOCK_CROP : WALLPAPER_CROP;
-        this.wallpaperFile = new File(wallpaperDir, wallpaperFileName);
-        this.cropFile = new File(wallpaperDir, cropFileName);
     }
 
     /**
@@ -154,8 +150,6 @@
      */
     WallpaperData(WallpaperData source) {
         this.userId = source.userId;
-        this.wallpaperFile = source.wallpaperFile;
-        this.cropFile = source.cropFile;
         this.wallpaperComponent = source.wallpaperComponent;
         this.mWhich = source.mWhich;
         this.wallpaperId = source.wallpaperId;
@@ -169,6 +163,25 @@
         }
     }
 
+    File getWallpaperFile() {
+        String fileName = mWhich == FLAG_LOCK ? WALLPAPER_LOCK_ORIG : WALLPAPER;
+        return getFile(mWallpaperFiles, fileName);
+    }
+
+    File getCropFile() {
+        String fileName = mWhich == FLAG_LOCK ? WALLPAPER_LOCK_CROP : WALLPAPER_CROP;
+        return getFile(mCropFiles, fileName);
+    }
+
+    private File getFile(SparseArray<File> map, String fileName) {
+        File result = map.get(mWhich);
+        if (result == null) {
+            result = new File(getWallpaperDir(userId), fileName);
+            map.put(userId, result);
+        }
+        return result;
+    }
+
     @Override
     public String toString() {
         StringBuilder out = new StringBuilder(defaultString(this));
@@ -177,7 +190,7 @@
         out.append(", which: ");
         out.append(mWhich);
         out.append(", file mod: ");
-        out.append(wallpaperFile != null ? wallpaperFile.lastModified() : "null");
+        out.append(getWallpaperFile() != null ? getWallpaperFile().lastModified() : "null");
         if (connection == null) {
             out.append(", no connection");
         } else {
@@ -202,10 +215,10 @@
 
     // Called during initialization of a given user's wallpaper bookkeeping
     boolean cropExists() {
-        return cropFile.exists();
+        return getCropFile().exists();
     }
 
     boolean sourceExists() {
-        return wallpaperFile.exists();
+        return getWallpaperFile().exists();
     }
 }
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
index 3e498d7..c54e3bd 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
@@ -87,7 +87,7 @@
         mImageWallpaper = ComponentName.unflattenFromString(
                 context.getResources().getString(R.string.image_wallpaper_component));
         mIsLockscreenLiveWallpaperEnabled =
-                SystemProperties.getBoolean("persist.wm.debug.lockscreen_live_wallpaper", false);
+                SystemProperties.getBoolean("persist.wm.debug.lockscreen_live_wallpaper", true);
     }
 
     private JournaledFile makeJournaledFile(int userId) {
@@ -542,12 +542,12 @@
                     }
 
                     res = r.openRawResource(resId);
-                    if (wallpaper.wallpaperFile.exists()) {
-                        wallpaper.wallpaperFile.delete();
-                        wallpaper.cropFile.delete();
+                    if (wallpaper.getWallpaperFile().exists()) {
+                        wallpaper.getWallpaperFile().delete();
+                        wallpaper.getCropFile().delete();
                     }
-                    fos = new FileOutputStream(wallpaper.wallpaperFile);
-                    cos = new FileOutputStream(wallpaper.cropFile);
+                    fos = new FileOutputStream(wallpaper.getWallpaperFile());
+                    cos = new FileOutputStream(wallpaper.getCropFile());
 
                     byte[] buffer = new byte[32768];
                     int amt;
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 7a940ff..206b80c 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -371,6 +371,7 @@
                     if (lockedWallpaper != null) {
                         detachWallpaperLocked(lockedWallpaper);
                     }
+                    clearWallpaperBitmaps(mWallpaper.userId, FLAG_LOCK);
                     mLockWallpaperMap.remove(wallpaper.userId);
                     notifyColorsWhich |= FLAG_LOCK;
                 }
@@ -674,8 +675,8 @@
             // Not having a wallpaperComponent means it's a lock screen wallpaper.
             final boolean imageWallpaper = mImageWallpaper.equals(wallpaper.wallpaperComponent)
                     || wallpaper.wallpaperComponent == null;
-            if (imageWallpaper && wallpaper.cropFile != null && wallpaper.cropFile.exists()) {
-                cropFile = wallpaper.cropFile.getAbsolutePath();
+            if (imageWallpaper && wallpaper.getCropFile().exists()) {
+                cropFile = wallpaper.getCropFile().getAbsolutePath();
             } else if (imageWallpaper && !wallpaper.cropExists() && !wallpaper.sourceExists()) {
                 defaultImageWallpaper = true;
             }
@@ -1462,6 +1463,10 @@
                 }
             }
 
+            synchronized (mLock) {
+                saveSettingsLocked(mNewWallpaper.userId);
+            }
+
             if (DEBUG) {
                 Slog.v(TAG, "--- wallpaper changed --");
                 Slog.v(TAG, "new sysWp: " + mWallpaperMap.get(mCurrentUserId));
@@ -1633,7 +1638,7 @@
         mShuttingDown = false;
         mImageWallpaper = ComponentName.unflattenFromString(
                 context.getResources().getString(R.string.image_wallpaper_component));
-        mDefaultWallpaperComponent = WallpaperManager.getDefaultWallpaperComponent(context);
+        mDefaultWallpaperComponent = WallpaperManager.getCmfDefaultWallpaperComponent(context);
         mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
         mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
         mIPackageManager = AppGlobals.getPackageManager();
@@ -1649,7 +1654,7 @@
                 mWallpaperCropper);
 
         mIsLockscreenLiveWallpaperEnabled =
-                SystemProperties.getBoolean("persist.wm.debug.lockscreen_live_wallpaper", false);
+                SystemProperties.getBoolean("persist.wm.debug.lockscreen_live_wallpaper", true);
         mIsMultiCropEnabled =
                 SystemProperties.getBoolean("persist.wm.debug.wallpaper_multi_crop", false);
         LocalServices.addService(WallpaperManagerInternal.class, new LocalService());
@@ -1820,21 +1825,23 @@
             if (record.exists()) {
                 Slog.w(TAG, "User:" + userID + ", wallpaper tyep = " + type
                         + ", wallpaper fail detect!! reset to default wallpaper");
-                clearWallpaperData(userID, type);
+                clearWallpaperBitmaps(userID, type);
                 record.delete();
             }
         });
     }
 
-    private void clearWallpaperData(int userID, int wallpaperType) {
+    private void clearWallpaperBitmaps(int userID, int wallpaperType) {
         final WallpaperData wallpaper = new WallpaperData(userID, wallpaperType);
-        if (wallpaper.sourceExists()) {
-            wallpaper.wallpaperFile.delete();
-        }
-        if (wallpaper.cropExists()) {
-            wallpaper.cropFile.delete();
-        }
+        clearWallpaperBitmaps(wallpaper);
+    }
 
+    private boolean clearWallpaperBitmaps(WallpaperData wallpaper) {
+        boolean sourceExists = wallpaper.sourceExists();
+        boolean cropExists = wallpaper.cropExists();
+        if (sourceExists) wallpaper.getWallpaperFile().delete();
+        if (cropExists) wallpaper.getCropFile().delete();
+        return sourceExists || cropExists;
     }
 
     @Override
@@ -2011,10 +2018,7 @@
 
         // files from the previous static wallpaper may still be stored in memory.
         // delete them in order to show the default wallpaper.
-        if (wallpaper.wallpaperFile.exists()) {
-            wallpaper.wallpaperFile.delete();
-            wallpaper.cropFile.delete();
-        }
+        clearWallpaperBitmaps(wallpaper);
 
         bindWallpaperComponentLocked(mImageWallpaper, true, false, fallback, reply);
         if ((wallpaper.mWhich & FLAG_SYSTEM) != 0) mHomeWallpaperWaitingForUnlock = true;
@@ -2033,7 +2037,11 @@
 
         WallpaperData data = null;
         synchronized (mLock) {
-            clearWallpaperLocked(false, which, userId, null);
+            if (mIsLockscreenLiveWallpaperEnabled) {
+                clearWallpaperLocked(callingPackage, false, which, userId);
+            } else {
+                clearWallpaperLocked(false, which, userId, null);
+            }
 
             if (which == FLAG_LOCK) {
                 data = mLockWallpaperMap.get(userId);
@@ -2050,7 +2058,64 @@
         }
     }
 
-    void clearWallpaperLocked(boolean defaultFailed, int which, int userId, IRemoteCallback reply) {
+    private void clearWallpaperLocked(String callingPackage, boolean defaultFailed,
+            int which, int userId) {
+
+        // Might need to bring it in the first time to establish our rewrite
+        if (!mWallpaperMap.contains(userId)) {
+            loadSettingsLocked(userId, false, FLAG_LOCK | FLAG_SYSTEM);
+        }
+        final WallpaperData wallpaper = mWallpaperMap.get(userId);
+        final WallpaperData lockWallpaper = mLockWallpaperMap.get(userId);
+        if (which == FLAG_LOCK && lockWallpaper == null) {
+            // It's already gone; we're done.
+            if (DEBUG) {
+                Slog.i(TAG, "Lock wallpaper already cleared");
+            }
+            return;
+        }
+
+        RuntimeException e = null;
+        try {
+            if (userId != mCurrentUserId && !hasCrossUserPermission()) return;
+
+            final ComponentName component;
+            final int finalWhich;
+
+            if ((which & FLAG_LOCK) > 0 && lockWallpaper != null) {
+                clearWallpaperBitmaps(lockWallpaper);
+            }
+            if ((which & FLAG_SYSTEM) > 0) {
+                clearWallpaperBitmaps(wallpaper);
+            }
+
+            // lock only case: set the system wallpaper component to both screens
+            if (which == FLAG_LOCK) {
+                component = wallpaper.wallpaperComponent;
+                finalWhich = FLAG_LOCK | FLAG_SYSTEM;
+            } else {
+                component = defaultFailed ? mImageWallpaper : null;
+                finalWhich = which;
+            }
+
+            boolean success = withCleanCallingIdentity(() -> setWallpaperComponent(
+                    component, callingPackage, finalWhich, userId));
+            if (success) return;
+        } catch (IllegalArgumentException e1) {
+            e = e1;
+        }
+
+        // This can happen if the default wallpaper component doesn't
+        // exist. This should be a system configuration problem, but
+        // let's not let it crash the system and just live with no
+        // wallpaper.
+        Slog.e(TAG, "Default wallpaper component not found!", e);
+        withCleanCallingIdentity(() -> clearWallpaperComponentLocked(wallpaper));
+    }
+
+    // TODO(b/266818039) remove this version of the method
+    private void clearWallpaperLocked(boolean defaultFailed, int which, int userId,
+            IRemoteCallback reply) {
         if (which != FLAG_SYSTEM && which != FLAG_LOCK) {
             throw new IllegalArgumentException("Must specify exactly one kind of wallpaper to clear");
         }
@@ -2079,9 +2144,7 @@
 
         final long ident = Binder.clearCallingIdentity();
         try {
-            if (wallpaper.wallpaperFile.exists()) {
-                wallpaper.wallpaperFile.delete();
-                wallpaper.cropFile.delete();
+            if (clearWallpaperBitmaps(wallpaper)) {
                 if (which == FLAG_LOCK) {
                     mLockWallpaperMap.remove(userId);
                     final IWallpaperManagerCallback cb = mKeyguardListener;
@@ -2376,13 +2439,13 @@
                     wallpaper.callbacks.register(cb);
                 }
 
-                File fileToReturn = getCropped ? wallpaper.cropFile : wallpaper.wallpaperFile;
+                File result = getCropped ? wallpaper.getCropFile() : wallpaper.getWallpaperFile();
 
-                if (!fileToReturn.exists()) {
+                if (!result.exists()) {
                     return null;
                 }
 
-                return ParcelFileDescriptor.open(fileToReturn, MODE_READ_ONLY);
+                return ParcelFileDescriptor.open(result, MODE_READ_ONLY);
             } catch (FileNotFoundException e) {
                 /* Shouldn't happen as we check to see if the file exists */
                 Slog.w(TAG, "Error getting wallpaper", e);
@@ -3146,19 +3209,24 @@
 
         // Migrate the bitmap files outright; no need to copy
         try {
-            Os.rename(sysWP.wallpaperFile.getAbsolutePath(), lockWP.wallpaperFile.getAbsolutePath());
-            Os.rename(sysWP.cropFile.getAbsolutePath(), lockWP.cropFile.getAbsolutePath());
+            if (!mIsLockscreenLiveWallpaperEnabled || sysWP.getWallpaperFile().exists()) {
+                Os.rename(sysWP.getWallpaperFile().getAbsolutePath(),
+                        lockWP.getWallpaperFile().getAbsolutePath());
+            }
+            if (!mIsLockscreenLiveWallpaperEnabled || sysWP.getCropFile().exists()) {
+                Os.rename(sysWP.getCropFile().getAbsolutePath(),
+                        lockWP.getCropFile().getAbsolutePath());
+            }
             mLockWallpaperMap.put(userId, lockWP);
             if (mIsLockscreenLiveWallpaperEnabled) {
-                SELinux.restorecon(lockWP.wallpaperFile);
+                SELinux.restorecon(lockWP.getWallpaperFile());
                 mLastLockWallpaper = lockWP;
             }
             return true;
         } catch (ErrnoException e) {
             // can happen when migrating default wallpaper (which is not stored in wallpaperFile)
             Slog.w(TAG, "Couldn't migrate system wallpaper: " + e.getMessage());
-            lockWP.wallpaperFile.delete();
-            lockWP.cropFile.delete();
+            clearWallpaperBitmaps(lockWP);
             return false;
         }
     }
@@ -3175,11 +3243,11 @@
                         FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
                         -1, -1);
             }
-            ParcelFileDescriptor fd = ParcelFileDescriptor.open(wallpaper.wallpaperFile,
+            ParcelFileDescriptor fd = ParcelFileDescriptor.open(wallpaper.getWallpaperFile(),
                     MODE_CREATE|MODE_READ_WRITE|MODE_TRUNCATE);
-            if (!SELinux.restorecon(wallpaper.wallpaperFile)) {
+            if (!SELinux.restorecon(wallpaper.getWallpaperFile())) {
                 Slog.w(TAG, "restorecon failed for wallpaper file: " +
-                        wallpaper.wallpaperFile.getPath());
+                        wallpaper.getWallpaperFile().getPath());
                 return null;
             }
             wallpaper.name = name;
@@ -3190,7 +3258,7 @@
             // Nullify field to require new computation
             wallpaper.primaryColors = null;
             Slog.v(TAG, "updateWallpaperBitmapLocked() : id=" + wallpaper.wallpaperId
-                    + " name=" + name + " file=" + wallpaper.wallpaperFile.getName());
+                    + " name=" + name + " file=" + wallpaper.getWallpaperFile().getName());
             return fd;
         } catch (FileNotFoundException e) {
             Slog.w(TAG, "Error setting wallpaper", e);
@@ -3214,16 +3282,17 @@
     }
 
     @VisibleForTesting
-    void setWallpaperComponent(ComponentName name, String callingPackage,
+    boolean setWallpaperComponent(ComponentName name, String callingPackage,
             @SetWallpaperFlags int which, int userId) {
         if (mIsLockscreenLiveWallpaperEnabled) {
-            setWallpaperComponentInternal(name, callingPackage, which, userId);
+            return setWallpaperComponentInternal(name, callingPackage, which, userId);
         } else {
             setWallpaperComponentInternalLegacy(name, callingPackage, which, userId);
+            return true;
         }
     }
 
-    private void setWallpaperComponentInternal(ComponentName name, String callingPackage,
+    private boolean setWallpaperComponentInternal(ComponentName name, String callingPackage,
             @SetWallpaperFlags int which, int userIdIn) {
         if (DEBUG) {
             Slog.v(TAG, "Setting new live wallpaper: which=" + which + ", component: " + name);
@@ -3234,6 +3303,7 @@
         checkPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT);
 
         boolean shouldNotifyColors = false;
+        boolean bindSuccess;
         final WallpaperData newWallpaper;
 
         synchronized (mLock) {
@@ -3282,7 +3352,7 @@
                  */
                 boolean forceRebind = same && systemIsBoth && which == FLAG_SYSTEM;
 
-                boolean bindSuccess = bindWallpaperComponentLocked(name, /* force */
+                bindSuccess = bindWallpaperComponentLocked(name, /* force */
                         forceRebind, /* fromUser */ true, newWallpaper, callback);
                 if (bindSuccess) {
                     if (!same) {
@@ -3301,6 +3371,11 @@
                             });
                         }
                     }
+                    boolean lockBitmapCleared = false;
+                    if (!mImageWallpaper.equals(newWallpaper.wallpaperComponent)) {
+                        clearWallpaperBitmaps(newWallpaper);
+                        lockBitmapCleared = newWallpaper.mWhich == FLAG_LOCK;
+                    }
                     newWallpaper.wallpaperId = makeWallpaperIdLocked();
                     notifyCallbacksLocked(newWallpaper);
                     shouldNotifyColors = true;
@@ -3317,6 +3392,9 @@
                                 updateEngineFlags(newWallpaper);
                             }
                         }
+                        if (!lockBitmapCleared) {
+                            clearWallpaperBitmaps(newWallpaper.userId, FLAG_LOCK);
+                        }
                         mLockWallpaperMap.remove(newWallpaper.userId);
                     }
                 }
@@ -3329,6 +3407,7 @@
             notifyWallpaperColorsChanged(newWallpaper, which);
             notifyWallpaperColorsChanged(mFallbackWallpaper, FLAG_SYSTEM);
         }
+        return bindSuccess;
     }
 
     // TODO(b/266818039) Remove this method
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index f300113..b3ae2ee 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -1280,45 +1280,53 @@
                 }
 
                 void drawIfNeeded(SurfaceControl.Transaction t) {
+                    // Drawing variables (alpha, dirty rect, and bounds) access is synchronized
+                    // using WindowManagerGlobalLock. Grab copies of these values before
+                    // drawing on the canvas so that drawing can be performed outside of the lock.
+                    int alpha;
+                    Rect drawingRect = null;
+                    Region drawingBounds = null;
                     synchronized (mService.mGlobalLock) {
                         if (!mInvalidated) {
                             return;
                         }
                         mInvalidated = false;
-                        if (mAlpha > 0) {
-                            Canvas canvas = null;
-                            try {
-                                // Empty dirty rectangle means unspecified.
-                                if (mDirtyRect.isEmpty()) {
-                                    mBounds.getBounds(mDirtyRect);
-                                }
-                                mDirtyRect.inset(-mHalfBorderWidth, -mHalfBorderWidth);
-                                canvas = mSurface.lockCanvas(mDirtyRect);
-                                if (DEBUG_VIEWPORT_WINDOW) {
-                                    Slog.i(LOG_TAG, "Dirty rect: " + mDirtyRect);
-                                }
-                            } catch (IllegalArgumentException iae) {
-                                /* ignore */
-                            } catch (Surface.OutOfResourcesException oore) {
-                                /* ignore */
-                            }
-                            if (canvas == null) {
-                                return;
-                            }
-                            if (DEBUG_VIEWPORT_WINDOW) {
-                                Slog.i(LOG_TAG, "Bounds: " + mBounds);
-                            }
-                            canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
-                            mPaint.setAlpha(mAlpha);
-                            Path path = mBounds.getBoundaryPath();
-                            canvas.drawPath(path, mPaint);
 
-                            mSurface.unlockCanvasAndPost(canvas);
-                            t.show(mSurfaceControl);
-                        } else {
-                            t.hide(mSurfaceControl);
+                        alpha = mAlpha;
+                        if (alpha > 0) {
+                            drawingBounds = new Region(mBounds);
+                            // Empty dirty rectangle means unspecified.
+                            if (mDirtyRect.isEmpty()) {
+                                mBounds.getBounds(mDirtyRect);
+                            }
+                            mDirtyRect.inset(-mHalfBorderWidth, -mHalfBorderWidth);
+                            drawingRect = new Rect(mDirtyRect);
+                            if (DEBUG_VIEWPORT_WINDOW) {
+                                Slog.i(LOG_TAG, "ViewportWindow bounds: " + mBounds);
+                                Slog.i(LOG_TAG, "ViewportWindow dirty rect: " + mDirtyRect);
+                            }
                         }
                     }
+
+                    // Draw without holding WindowManagerGlobalLock.
+                    if (alpha > 0) {
+                        Canvas canvas = null;
+                        try {
+                            canvas = mSurface.lockCanvas(drawingRect);
+                        } catch (IllegalArgumentException | OutOfResourcesException e) {
+                            /* ignore */
+                        }
+                        if (canvas == null) {
+                            return;
+                        }
+                        canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
+                        mPaint.setAlpha(alpha);
+                        canvas.drawPath(drawingBounds.getBoundaryPath(), mPaint);
+                        mSurface.unlockCanvasAndPost(canvas);
+                        t.show(mSurfaceControl);
+                    } else {
+                        t.hide(mSurfaceControl);
+                    }
                 }
 
                 void releaseSurface() {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 788bfbc..0994fa4 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -4560,6 +4560,26 @@
         }
         task.forAllActivities(fromActivity -> {
             if (fromActivity == this) return true;
+            // The snapshot starting window could remove itself when receive resized request without
+            // redraw, so transfer it to a different size activity could only cause flicker.
+            // By schedule remove snapshot starting window, the remove process will happen when
+            // transition ready, transition ready means the app window is drawn.
+            final StartingData tmpStartingData = fromActivity.mStartingData;
+            if (tmpStartingData != null && tmpStartingData.mAssociatedTask == null
+                    && mTransitionController.isCollecting(fromActivity)
+                    && tmpStartingData instanceof SnapshotStartingData) {
+                final Rect fromBounds = fromActivity.getBounds();
+                final Rect myBounds = getBounds();
+                if (!fromBounds.equals(myBounds)) {
+                    // Mark as no animation, so these changes won't merge into playing transition.
+                    if (mTransitionController.inPlayingTransition(fromActivity)) {
+                        mTransitionController.setNoAnimation(this);
+                        mTransitionController.setNoAnimation(fromActivity);
+                    }
+                    fromActivity.removeStartingWindow();
+                    return true;
+                }
+            }
             return !fromActivity.isVisibleRequested() && transferStartingWindow(fromActivity);
         });
     }
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index a0a45e6..4262e94e 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -898,6 +898,11 @@
         }
         mLastStartReason = request.reason;
         mLastStartActivityTimeMs = System.currentTimeMillis();
+        // Reset the ActivityRecord#mCurrentLaunchCanTurnScreenOn state of last start activity in
+        // case the state is not yet consumed during rapid activity launch.
+        if (mLastStartActivityRecord != null) {
+            mLastStartActivityRecord.setCurrentLaunchCanTurnScreenOn(false);
+        }
         mLastStartActivityRecord = null;
 
         final IApplicationThread caller = request.caller;
@@ -1583,21 +1588,14 @@
                 newTransition = null;
             }
         }
-        if (isTransientLaunch) {
-            if (forceTransientTransition) {
-                transitionController.collect(mLastStartActivityRecord);
-                transitionController.collect(mPriorAboveTask);
-            }
-            // `started` isn't guaranteed to be the actual relevant activity, so we must wait
-            // until after we launched to identify the relevant activity.
-            transitionController.setTransientLaunch(mLastStartActivityRecord, mPriorAboveTask);
-            if (forceTransientTransition) {
-                final DisplayContent dc = mLastStartActivityRecord.getDisplayContent();
-                // update wallpaper target to TransientHide
-                dc.mWallpaperController.adjustWallpaperWindows();
-                // execute transition because there is no change
-                transitionController.setReady(dc, true /* ready */);
-            }
+        if (forceTransientTransition) {
+            transitionController.collect(mLastStartActivityRecord);
+            transitionController.collect(mPriorAboveTask);
+            final DisplayContent dc = mLastStartActivityRecord.getDisplayContent();
+            // update wallpaper target to TransientHide
+            dc.mWallpaperController.adjustWallpaperWindows();
+            // execute transition because there is no change
+            transitionController.setReady(dc, true /* ready */);
         }
         if (!userLeaving) {
             // no-user-leaving implies not entering PiP.
@@ -1686,7 +1684,9 @@
             }
             // When running transient transition, the transient launch target should keep on top.
             // So disallow the transient hide activity to move itself to front, e.g. trampoline.
-            if (!mAvoidMoveToFront && r.mTransitionController.isTransientHide(targetTask)) {
+            if (!mAvoidMoveToFront && (mService.mHomeProcess == null
+                    || mService.mHomeProcess.mUid != realCallingUid)
+                    && r.mTransitionController.isTransientHide(targetTask)) {
                 mAvoidMoveToFront = true;
             }
             mPriorAboveTask = TaskDisplayArea.getRootTaskAbove(targetTask.getRootTask());
@@ -1705,6 +1705,7 @@
                     activity.destroyIfPossible("Removes redundant singleInstance");
                 }
             }
+            recordTransientLaunchIfNeeded(targetTaskTop);
             // Recycle the target task for this launch.
             startResult = recycleTask(targetTask, targetTaskTop, reusedTask, intentGrants);
             if (startResult != START_SUCCESS) {
@@ -1736,6 +1737,9 @@
             addOrReparentStartingActivity(targetTask, "adding to task");
         }
 
+        // After activity is attached to task, but before actual start
+        recordTransientLaunchIfNeeded(mLastStartActivityRecord);
+
         if (!mAvoidMoveToFront && mDoResume) {
             mTargetRootTask.getRootTask().moveToFront("reuseOrNewTask", targetTask);
             if (!mTargetRootTask.isTopRootTaskInDisplayArea() && mService.isDreaming()
@@ -1832,6 +1836,14 @@
         return START_SUCCESS;
     }
 
+    private void recordTransientLaunchIfNeeded(ActivityRecord r) {
+        if (r == null || !mTransientLaunch) return;
+        final TransitionController controller = r.mTransitionController;
+        if (controller.isCollecting() && !controller.isTransientCollect(r)) {
+            controller.setTransientLaunch(r, mPriorAboveTask);
+        }
+    }
+
     /** Returns the leaf task where the target activity may be placed. */
     private Task computeTargetTask() {
         if (mStartActivity.resultTo == null && mInTask == null && !mAddingToTask
@@ -2928,15 +2940,6 @@
             }
         }
 
-        // If the matching task is already in the adjacent task of the launch target. Adjust to use
-        // the adjacent task as its launch target. So the existing task will be launched into the
-        // closer one and won't be reparent redundantly.
-        final Task adjacentTargetTask = mTargetRootTask.getAdjacentTask();
-        if (adjacentTargetTask != null && intentActivity.isDescendantOf(adjacentTargetTask)
-                && intentTask.isOnTop()) {
-            mTargetRootTask = adjacentTargetTask;
-        }
-
         // If the target task is not in the front, then we need to bring it to the front...
         // except...  well, with SINGLE_TASK_LAUNCH it's not entirely clear. We'd like to have
         // the same behavior as if a new instance was being started, which means not bringing it
@@ -2977,7 +2980,9 @@
                     // should be START_DELIVERED_TO_TOP instead of START_TASK_TO_FRONT.
                     final boolean wasTopOfVisibleRootTask = intentActivity.isVisibleRequested()
                             && intentActivity.inMultiWindowMode()
-                            && intentActivity == mTargetRootTask.topRunningActivity();
+                            && intentActivity == mTargetRootTask.topRunningActivity()
+                            && !intentActivity.mTransitionController.isTransientHide(
+                                    mTargetRootTask);
                     // We only want to move to the front, if we aren't going to launch on a
                     // different root task. If we launch on a different root task, we will put the
                     // task on top there.
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
index 32f1f42..a2547fd 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
@@ -23,6 +23,7 @@
 import android.app.AppProtoEnums;
 import android.app.BackgroundStartPrivileges;
 import android.app.IActivityManager;
+import android.app.IAppTask;
 import android.app.IApplicationThread;
 import android.app.ITaskStackListener;
 import android.app.ProfilerInfo;
@@ -308,6 +309,12 @@
     public abstract void notifyActiveDreamChanged(@Nullable ComponentName activeDreamComponent);
 
     /**
+     * Starts a dream activity in the DreamService's process.
+     */
+    public abstract IAppTask startDreamActivity(@NonNull Intent intent, int callingUid,
+            int callingPid);
+
+    /**
      * Set a uid that is allowed to bypass stopped app switches, launching an app
      * whenever it wants.
      *
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 47b51ac..78da5de 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -546,6 +546,7 @@
      */
     private volatile long mLastStopAppSwitchesTime;
 
+    @GuardedBy("itself")
     private final List<AnrController> mAnrController = new ArrayList<>();
     IActivityController mController = null;
     boolean mControllerIsAMonkey = false;
@@ -733,7 +734,7 @@
     private boolean mShowDialogs = true;
 
     /** Set if we are shutting down the system, similar to sleeping. */
-    boolean mShuttingDown = false;
+    volatile boolean mShuttingDown;
 
     /**
      * We want to hold a wake lock while running a voice interaction session, since
@@ -1411,29 +1412,39 @@
 
             final long origId = Binder.clearCallingIdentity();
             // TODO(b/64750076): Check if calling pid should really be -1.
-            final int res = getActivityStartController()
-                    .obtainStarter(intent, "startNextMatchingActivity")
-                    .setCaller(r.app.getThread())
-                    .setResolvedType(r.resolvedType)
-                    .setActivityInfo(aInfo)
-                    .setResultTo(resultTo != null ? resultTo.token : null)
-                    .setResultWho(resultWho)
-                    .setRequestCode(requestCode)
-                    .setCallingPid(-1)
-                    .setCallingUid(r.launchedFromUid)
-                    .setCallingPackage(r.launchedFromPackage)
-                    .setCallingFeatureId(r.launchedFromFeatureId)
-                    .setRealCallingPid(-1)
-                    .setRealCallingUid(r.launchedFromUid)
-                    .setActivityOptions(options)
-                    .execute();
-            Binder.restoreCallingIdentity(origId);
+            try {
+                if (options == null) {
+                    options = new SafeActivityOptions(ActivityOptions.makeBasic());
+                }
 
-            r.finishing = wasFinishing;
-            if (res != ActivityManager.START_SUCCESS) {
-                return false;
+                // Fixes b/230492947
+                // Prevents background activity launch through #startNextMatchingActivity
+                // An activity going into the background could still go back to the foreground
+                // if the intent used matches both:
+                // - the activity in the background
+                // - a second activity.
+                options.getOptions(r).setAvoidMoveToFront();
+                final int res = getActivityStartController()
+                        .obtainStarter(intent, "startNextMatchingActivity")
+                        .setCaller(r.app.getThread())
+                        .setResolvedType(r.resolvedType)
+                        .setActivityInfo(aInfo)
+                        .setResultTo(resultTo != null ? resultTo.token : null)
+                        .setResultWho(resultWho)
+                        .setRequestCode(requestCode)
+                        .setCallingPid(-1)
+                        .setCallingUid(r.launchedFromUid)
+                        .setCallingPackage(r.launchedFromPackage)
+                        .setCallingFeatureId(r.launchedFromFeatureId)
+                        .setRealCallingPid(-1)
+                        .setRealCallingUid(r.launchedFromUid)
+                        .setActivityOptions(options)
+                        .execute();
+                r.finishing = wasFinishing;
+                return res == ActivityManager.START_SUCCESS;
+            } finally {
+                Binder.restoreCallingIdentity(origId);
             }
-            return true;
         }
     }
 
@@ -1456,23 +1467,8 @@
         return false;
     }
 
-    private void enforceCallerIsDream(String callerPackageName) {
-        final long origId = Binder.clearCallingIdentity();
-        try {
-            if (!canLaunchDreamActivity(callerPackageName)) {
-                throw new SecurityException("The dream activity can be started only when the device"
-                        + " is dreaming and only by the active dream package.");
-            }
-        } finally {
-            Binder.restoreCallingIdentity(origId);
-        }
-    }
-
-    @Override
-    public boolean startDreamActivity(@NonNull Intent intent) {
-        assertPackageMatchesCallingUid(intent.getPackage());
-        enforceCallerIsDream(intent.getPackage());
-
+    private IAppTask startDreamActivityInternal(@NonNull Intent intent, int callingUid,
+            int callingPid) {
         final ActivityInfo a = new ActivityInfo();
         a.theme = com.android.internal.R.style.Theme_Dream;
         a.exported = true;
@@ -1490,7 +1486,7 @@
         options.setLaunchActivityType(ACTIVITY_TYPE_DREAM);
 
         synchronized (mGlobalLock) {
-            final WindowProcessController process = mProcessMap.getProcess(Binder.getCallingPid());
+            final WindowProcessController process = mProcessMap.getProcess(callingPid);
 
             a.packageName = process.mInfo.packageName;
             a.applicationInfo = process.mInfo;
@@ -1498,26 +1494,25 @@
             a.uiOptions = process.mInfo.uiOptions;
             a.taskAffinity = "android:" + a.packageName + "/dream";
 
-            final int callingUid = Binder.getCallingUid();
-            final int callingPid = Binder.getCallingPid();
 
-            final long origId = Binder.clearCallingIdentity();
-            try {
-                getActivityStartController().obtainStarter(intent, "dream")
-                        .setCallingUid(callingUid)
-                        .setCallingPid(callingPid)
-                        .setCallingPackage(intent.getPackage())
-                        .setActivityInfo(a)
-                        .setActivityOptions(createSafeActivityOptionsWithBalAllowed(options))
-                        // To start the dream from background, we need to start it from a persistent
-                        // system process. Here we set the real calling uid to the system server uid
-                        .setRealCallingUid(Binder.getCallingUid())
-                        .setBackgroundStartPrivileges(BackgroundStartPrivileges.ALLOW_BAL)
-                        .execute();
-                return true;
-            } finally {
-                Binder.restoreCallingIdentity(origId);
-            }
+            final ActivityRecord[] outActivity = new ActivityRecord[1];
+            getActivityStartController().obtainStarter(intent, "dream")
+                    .setCallingUid(callingUid)
+                    .setCallingPid(callingPid)
+                    .setCallingPackage(intent.getPackage())
+                    .setActivityInfo(a)
+                    .setActivityOptions(createSafeActivityOptionsWithBalAllowed(options))
+                    .setOutActivity(outActivity)
+                    // To start the dream from background, we need to start it from a persistent
+                    // system process. Here we set the real calling uid to the system server uid
+                    .setRealCallingUid(Binder.getCallingUid())
+                    .setBackgroundStartPrivileges(BackgroundStartPrivileges.ALLOW_BAL)
+                    .execute();
+
+            final ActivityRecord started = outActivity[0];
+            final IAppTask appTask = started == null ? null :
+                    new AppTaskImpl(this, started.getTask().mTaskId, callingUid);
+            return appTask;
         }
     }
 
@@ -2288,14 +2283,14 @@
 
     /** Register an {@link AnrController} to control the ANR dialog behavior */
     public void registerAnrController(AnrController controller) {
-        synchronized (mGlobalLock) {
+        synchronized (mAnrController) {
             mAnrController.add(controller);
         }
     }
 
     /** Unregister an {@link AnrController} */
     public void unregisterAnrController(AnrController controller) {
-        synchronized (mGlobalLock) {
+        synchronized (mAnrController) {
             mAnrController.remove(controller);
         }
     }
@@ -2311,7 +2306,7 @@
         }
 
         final ArrayList<AnrController> controllers;
-        synchronized (mGlobalLock) {
+        synchronized (mAnrController) {
             controllers = new ArrayList<>(mAnrController);
         }
 
@@ -5915,6 +5910,11 @@
         }
 
         @Override
+        public IAppTask startDreamActivity(@NonNull Intent intent, int callingUid, int callingPid) {
+            return startDreamActivityInternal(intent, callingUid, callingPid);
+        }
+
+        @Override
         public void setAllowAppSwitches(@NonNull String type, int uid, int userId) {
             if (!mAmInternal.isUserRunning(userId, ActivityManager.FLAG_OR_STOPPED)) {
                 return;
@@ -6024,15 +6024,13 @@
 
         @Override
         public boolean isShuttingDown() {
-            synchronized (mGlobalLock) {
-                return mShuttingDown;
-            }
+            return mShuttingDown;
         }
 
         @Override
         public boolean shuttingDown(boolean booted, int timeout) {
+            mShuttingDown = true;
             synchronized (mGlobalLock) {
-                mShuttingDown = true;
                 mRootWindowContainer.prepareForShutdown();
                 updateEventDispatchingLocked(booted);
                 notifyTaskPersisterLocked(null, true);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 0171c20..738797b 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -1891,7 +1891,7 @@
         // DestroyActivityItem may be called first.
         final ActivityRecord top = task.getTopMostActivity();
         if (top != null && top.finishing && !top.mAppStopped && top.lastVisibleTime > 0
-                && !task.mKillProcessesOnDestroyed) {
+                && !task.mKillProcessesOnDestroyed && top.hasProcess()) {
             task.mKillProcessesOnDestroyed = true;
             mHandler.sendMessageDelayed(
                     mHandler.obtainMessage(KILL_TASK_PROCESSES_TIMEOUT_MSG, task),
@@ -2367,6 +2367,10 @@
         final ActivityRecord prevTopActivity = mTopResumedActivity;
         final Task topRootTask = mRootWindowContainer.getTopDisplayFocusedRootTask();
         if (topRootTask == null || topRootTask.getTopResumedActivity() == prevTopActivity) {
+            if (topRootTask == null) {
+                // There's no focused task and there won't have any resumed activity either.
+                scheduleTopResumedActivityStateLossIfNeeded();
+            }
             if (mService.isSleepingLocked()) {
                 // There won't be a next resumed activity. The top process should still be updated
                 // according to the current top focused activity.
@@ -2376,16 +2380,7 @@
         }
 
         // Ask previous activity to release the top state.
-        final boolean prevActivityReceivedTopState =
-                prevTopActivity != null && !mTopResumedActivityWaitingForPrev;
-        // mTopResumedActivityWaitingForPrev == true at this point would mean that an activity
-        // before the prevTopActivity one hasn't reported back yet. So server never sent the top
-        // resumed state change message to prevTopActivity.
-        if (prevActivityReceivedTopState
-                && prevTopActivity.scheduleTopResumedActivityChanged(false /* onTop */)) {
-            scheduleTopResumedStateLossTimeout(prevTopActivity);
-            mTopResumedActivityWaitingForPrev = true;
-        }
+        scheduleTopResumedActivityStateLossIfNeeded();
 
         // Update the current top activity.
         mTopResumedActivity = topRootTask.getTopResumedActivity();
@@ -2410,6 +2405,23 @@
         mService.updateTopApp(mTopResumedActivity);
     }
 
+    /** Schedule current top resumed activity state loss */
+    private void scheduleTopResumedActivityStateLossIfNeeded() {
+        if (mTopResumedActivity == null) {
+            return;
+        }
+
+        // mTopResumedActivityWaitingForPrev == true at this point would mean that an activity
+        // before the prevTopActivity one hasn't reported back yet. So server never sent the top
+        // resumed state change message to prevTopActivity.
+        if (!mTopResumedActivityWaitingForPrev
+                && mTopResumedActivity.scheduleTopResumedActivityChanged(false /* onTop */)) {
+            scheduleTopResumedStateLossTimeout(mTopResumedActivity);
+            mTopResumedActivityWaitingForPrev = true;
+            mTopResumedActivity = null;
+        }
+    }
+
     /** Schedule top resumed state change if previous top activity already reported back. */
     private void scheduleTopResumedActivityStateIfNeeded() {
         if (mTopResumedActivity != null && !mTopResumedActivityWaitingForPrev) {
diff --git a/services/core/java/com/android/server/wm/AppWarnings.java b/services/core/java/com/android/server/wm/AppWarnings.java
index f7ccc0d..0273a30 100644
--- a/services/core/java/com/android/server/wm/AppWarnings.java
+++ b/services/core/java/com/android/server/wm/AppWarnings.java
@@ -16,7 +16,9 @@
 
 package com.android.server.wm;
 
+import android.annotation.NonNull;
 import android.annotation.UiThread;
+import android.annotation.WorkerThread;
 import android.app.AlertDialog;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -30,14 +32,18 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.SystemProperties;
+import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.DisplayMetrics;
 import android.util.Slog;
 import android.util.Xml;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.ArrayUtils;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.IoThread;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -45,9 +51,7 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Manages warning dialogs shown during application lifecycle.
@@ -61,11 +65,12 @@
     public static final int FLAG_HIDE_DEPRECATED_SDK = 0x04;
     public static final int FLAG_HIDE_DEPRECATED_ABI = 0x08;
 
-    private final HashMap<String, Integer> mPackageFlags = new HashMap<>();
+    @GuardedBy("mPackageFlags")
+    private final ArrayMap<String, Integer> mPackageFlags = new ArrayMap<>();
 
     private final ActivityTaskManagerService mAtm;
     private final Context mUiContext;
-    private final ConfigHandler mHandler;
+    private final WriteConfigTask mWriteConfigTask;
     private final UiHandler mUiHandler;
     private final AtomicFile mConfigFile;
 
@@ -75,30 +80,20 @@
     private DeprecatedAbiDialog mDeprecatedAbiDialog;
 
     /** @see android.app.ActivityManager#alwaysShowUnsupportedCompileSdkWarning */
-    private HashSet<ComponentName> mAlwaysShowUnsupportedCompileSdkWarningActivities =
-            new HashSet<>();
+    private final ArraySet<ComponentName> mAlwaysShowUnsupportedCompileSdkWarningActivities =
+            new ArraySet<>();
 
     /** @see android.app.ActivityManager#alwaysShowUnsupportedCompileSdkWarning */
     void alwaysShowUnsupportedCompileSdkWarning(ComponentName activity) {
         mAlwaysShowUnsupportedCompileSdkWarningActivities.add(activity);
     }
 
-    /**
-     * Creates a new warning dialog manager.
-     * <p>
-     * <strong>Note:</strong> Must be called from the ActivityManagerService thread.
-     *
-     * @param atm
-     * @param uiContext
-     * @param handler
-     * @param uiHandler
-     * @param systemDir
-     */
+    /** Creates a new warning dialog manager. */
     public AppWarnings(ActivityTaskManagerService atm, Context uiContext, Handler handler,
             Handler uiHandler, File systemDir) {
         mAtm = atm;
         mUiContext = uiContext;
-        mHandler = new ConfigHandler(handler.getLooper());
+        mWriteConfigTask = new WriteConfigTask();
         mUiHandler = new UiHandler(uiHandler.getLooper());
         mConfigFile = new AtomicFile(new File(systemDir, CONFIG_FILE_NAME), "warnings-config");
 
@@ -256,8 +251,9 @@
         mUiHandler.hideDialogsForPackage(name);
 
         synchronized (mPackageFlags) {
-            mPackageFlags.remove(name);
-            mHandler.scheduleWrite();
+            if (mPackageFlags.remove(name) != null) {
+                mWriteConfigTask.schedule();
+            }
         }
     }
 
@@ -425,7 +421,7 @@
                 } else {
                     mPackageFlags.remove(name);
                 }
-                mHandler.scheduleWrite();
+                mWriteConfigTask.schedule();
             }
         }
     }
@@ -556,46 +552,30 @@
         }
     }
 
-    /**
-     * Handles messages on the ActivityTaskManagerService thread.
-     */
-    private final class ConfigHandler extends Handler {
-        private static final int MSG_WRITE = 1;
-
-        private static final int DELAY_MSG_WRITE = 10000;
-
-        public ConfigHandler(Looper looper) {
-            super(looper, null, true);
-        }
+    private final class WriteConfigTask implements Runnable {
+        private static final long WRITE_CONFIG_DELAY_MS = 10000;
+        final AtomicReference<ArrayMap<String, Integer>> mPendingPackageFlags =
+                new AtomicReference<>();
 
         @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_WRITE:
-                    writeConfigToFileAmsThread();
-                    break;
+        public void run() {
+            final ArrayMap<String, Integer> packageFlags = mPendingPackageFlags.getAndSet(null);
+            if (packageFlags != null) {
+                writeConfigToFile(packageFlags);
             }
         }
 
-        public void scheduleWrite() {
-            removeMessages(MSG_WRITE);
-            sendEmptyMessageDelayed(MSG_WRITE, DELAY_MSG_WRITE);
+        @GuardedBy("mPackageFlags")
+        void schedule() {
+            if (mPendingPackageFlags.getAndSet(new ArrayMap<>(mPackageFlags)) == null) {
+                IoThread.getHandler().postDelayed(this, WRITE_CONFIG_DELAY_MS);
+            }
         }
     }
 
-    /**
-     * Writes the configuration file.
-     * <p>
-     * <strong>Note:</strong> Should be called from the ActivityManagerService thread unless you
-     * don't care where you're doing I/O operations. But you <i>do</i> care, don't you?
-     */
-    private void writeConfigToFileAmsThread() {
-        // Create a shallow copy so that we don't have to synchronize on config.
-        final HashMap<String, Integer> packageFlags;
-        synchronized (mPackageFlags) {
-            packageFlags = new HashMap<>(mPackageFlags);
-        }
-
+    /** Writes the configuration file. */
+    @WorkerThread
+    private void writeConfigToFile(@NonNull ArrayMap<String, Integer> packageFlags) {
         FileOutputStream fos = null;
         try {
             fos = mConfigFile.startWrite();
@@ -605,9 +585,9 @@
             out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
             out.startTag(null, "packages");
 
-            for (Map.Entry<String, Integer> entry : packageFlags.entrySet()) {
-                String pkg = entry.getKey();
-                int mode = entry.getValue();
+            for (int i = 0; i < packageFlags.size(); i++) {
+                final String pkg = packageFlags.keyAt(i);
+                final int mode = packageFlags.valueAt(i);
                 if (mode == 0) {
                     continue;
                 }
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index e91c9d4..2b6b62e 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -80,8 +80,8 @@
             appendLog("not in bounds phase, skipping");
             return RESULT_SKIP;
         }
-        if (!task.isActivityTypeStandard()) {
-            appendLog("not standard activity type, skipping");
+        if (!task.isActivityTypeStandardOrUndefined()) {
+            appendLog("not standard or undefined activity type, skipping");
             return RESULT_SKIP;
         }
         if (!currentParams.mBounds.isEmpty()) {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index ef19eef..bfd2a10 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1065,7 +1065,7 @@
         if (obscuredChanged && w.isVisible() && mWallpaperController.isWallpaperTarget(w)) {
             // This is the wallpaper target and its obscured state changed... make sure the
             // current wallpaper's visibility has been updated accordingly.
-            mWallpaperController.updateWallpaperVisibility();
+            mWallpaperController.updateWallpaperTokens(mDisplayContent.isKeyguardLocked());
         }
 
         w.handleWindowMovedIfNeeded();
@@ -1214,8 +1214,7 @@
         mDisplayRotationCompatPolicy =
                 // Not checking DeviceConfig value here to allow enabling via DeviceConfig
                 // without the need to restart the device.
-                mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
-                            /* checkDeviceConfig */ false)
+                mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabledAtBuildTime()
                         ? new DisplayRotationCompatPolicy(this) : null;
         mRotationReversionController = new DisplayRotationReversionController(this);
 
@@ -1714,9 +1713,7 @@
     }
 
     private int getMinimalTaskSizeDp() {
-        final Context displayConfigurationContext =
-                mAtmService.mContext.createConfigurationContext(getConfiguration());
-        final Resources res = displayConfigurationContext.getResources();
+        final Resources res = getDisplayUiContext().getResources();
         final TypedValue value = new TypedValue();
         res.getValue(R.dimen.default_minimal_size_resizable_task, value, true /* resolveRefs */);
         final int valueUnit = ((value.data >> COMPLEX_UNIT_SHIFT) & COMPLEX_UNIT_MASK);
@@ -2717,6 +2714,7 @@
         if (mDisplayPolicy != null) {
             mDisplayPolicy.onConfigurationChanged();
             mPinnedTaskController.onPostDisplayConfigurationChanged();
+            mMinSizeOfResizeableTaskDp = getMinimalTaskSizeDp();
         }
         // Update IME parent if needed.
         updateImeParent();
@@ -2858,7 +2856,6 @@
 
     void onDisplayInfoChanged() {
         updateDisplayFrames(false /* notifyInsetsChange */);
-        mMinSizeOfResizeableTaskDp = getMinimalTaskSizeDp();
         mInputMonitor.layoutInputConsumers(mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
         mDisplayPolicy.onDisplayInfoChanged(mDisplayInfo);
     }
@@ -3704,6 +3701,7 @@
         mInputMonitor.dump(pw, "  ");
         pw.println();
         mInsetsStateController.dump(prefix, pw);
+        mInsetsPolicy.dump(prefix, pw);
         mDwpcHelper.dump(prefix, pw);
         pw.println();
     }
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 3f1a077..2717a6a 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -176,7 +176,7 @@
 
     // TODO(b/266197298): Remove this by a more general protocol from the insets providers.
     private static final boolean USE_CACHED_INSETS_FOR_DISPLAY_SWITCH =
-            SystemProperties.getBoolean("persist.wm.debug.cached_insets_switch", false);
+            SystemProperties.getBoolean("persist.wm.debug.cached_insets_switch", true);
 
     private final WindowManagerService mService;
     private final Context mContext;
@@ -271,13 +271,13 @@
     private WindowState mSystemUiControllingWindow;
 
     // Candidate window to determine the color of navigation bar. The window needs to be top
-    // fullscreen-app windows or dim layers that are intersecting with the window frame of status
-    // bar.
+    // fullscreen-app windows or dim layers that are intersecting with the window frame of
+    // navigation bar.
     private WindowState mNavBarColorWindowCandidate;
 
-    // The window to determine opacity and background of translucent navigation bar. The window
-    // needs to be opaque.
-    private WindowState mNavBarBackgroundWindow;
+    // Candidate window to determine opacity and background of translucent navigation bar.
+    // The window frame must intersect the frame of navigation bar.
+    private WindowState mNavBarBackgroundWindowCandidate;
 
     /**
      * A collection of {@link AppearanceRegion} to indicate that which region of status bar applies
@@ -327,8 +327,6 @@
     private WindowState mTopFullscreenOpaqueWindowState;
     private boolean mTopIsFullscreen;
     private int mNavBarOpacityMode = NAV_BAR_OPAQUE_WHEN_FREEFORM_OR_DOCKED;
-    private boolean mForceConsumeSystemBars;
-    private boolean mForceShowSystemBars;
 
     /**
      * Windows that provides gesture insets. If multiple windows provide gesture insets at the same
@@ -961,12 +959,6 @@
         if (!win.mSession.mCanSetUnrestrictedGestureExclusion) {
             attrs.privateFlags &= ~PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION;
         }
-
-        final InsetsSourceProvider provider = win.getControllableInsetProvider();
-        if (provider != null && provider.getSource().insetsRoundedCornerFrame()
-                != attrs.insetsRoundedCornerFrame) {
-            provider.getSource().setInsetsRoundedCornerFrame(attrs.insetsRoundedCornerFrame);
-        }
     }
 
     /**
@@ -1104,9 +1096,11 @@
                 } else {
                     overrideProviders = null;
                 }
-                mDisplayContent.getInsetsStateController().getOrCreateSourceProvider(
-                        provider.getId(), provider.getType()).setWindowContainer(
-                                win, frameProvider, overrideProviders);
+                final InsetsSourceProvider sourceProvider = mDisplayContent
+                        .getInsetsStateController().getOrCreateSourceProvider(provider.getId(),
+                                provider.getType());
+                sourceProvider.getSource().setFlags(provider.getFlags());
+                sourceProvider.setWindowContainer(win, frameProvider, overrideProviders);
                 mInsetsSourceWindowsExceptIme.add(win);
             }
         }
@@ -1290,18 +1284,10 @@
         return ANIMATION_STYLEABLE;
     }
 
-    /**
-     * @return true if the system bars are forced to be consumed
-     */
+    // TODO (b/277891341): Remove this and related usages. This has been replaced by
+    //                     InsetsSource#FLAG_FORCE_CONSUMING.
     public boolean areSystemBarsForcedConsumedLw() {
-        return mForceConsumeSystemBars;
-    }
-
-    /**
-     * @return true if the system bars are forced to stay visible
-     */
-    public boolean areSystemBarsForcedShownLw() {
-        return mForceShowSystemBars;
+        return false;
     }
 
     /**
@@ -1387,7 +1373,7 @@
         mBottomGestureHost = null;
         mTopFullscreenOpaqueWindowState = null;
         mNavBarColorWindowCandidate = null;
-        mNavBarBackgroundWindow = null;
+        mNavBarBackgroundWindowCandidate = null;
         mStatusBarAppearanceRegionList.clear();
         mLetterboxDetails.clear();
         mStatusBarBackgroundWindows.clear();
@@ -1514,8 +1500,8 @@
                     mNavBarColorWindowCandidate = win;
                     addSystemBarColorApp(win);
                 }
-                if (mNavBarBackgroundWindow == null) {
-                    mNavBarBackgroundWindow = win;
+                if (mNavBarBackgroundWindowCandidate == null) {
+                    mNavBarBackgroundWindowCandidate = win;
                 }
             }
 
@@ -1531,20 +1517,32 @@
             }
         } else if (win.isDimming()) {
             if (mStatusBar != null) {
-                if (addStatusBarAppearanceRegionsForDimmingWindow(
-                        win.mAttrs.insetsFlags.appearance & APPEARANCE_LIGHT_STATUS_BARS,
-                        mStatusBar.getFrame(), win.getBounds(), win.getFrame())) {
+                // If the dim window is below status bar window, we should update the appearance
+                // region if needed. Otherwise, leave it as it is.
+                final int statusBarLayer = mStatusBar.mToken.getWindowLayerFromType();
+                final int targetWindowLayer = win.mToken.getWindowLayerFromType();
+                if (targetWindowLayer < statusBarLayer
+                        && addStatusBarAppearanceRegionsForDimmingWindow(
+                                win.mAttrs.insetsFlags.appearance & APPEARANCE_LIGHT_STATUS_BARS,
+                                mStatusBar.getFrame(), win.getBounds(), win.getFrame())) {
                     addSystemBarColorApp(win);
                 }
             }
             if (isOverlappingWithNavBar(win) && mNavBarColorWindowCandidate == null) {
                 mNavBarColorWindowCandidate = win;
+                addSystemBarColorApp(win);
             }
-        } else if (appWindow && attached == null && mNavBarColorWindowCandidate == null
+        } else if (appWindow && attached == null
+                && (mNavBarColorWindowCandidate == null || mNavBarBackgroundWindowCandidate == null)
                 && win.getFrame().contains(
                         getBarContentFrameForWindow(win, Type.navigationBars()))) {
-            mNavBarColorWindowCandidate = win;
-            addSystemBarColorApp(win);
+            if (mNavBarColorWindowCandidate == null) {
+                mNavBarColorWindowCandidate = win;
+                addSystemBarColorApp(win);
+            }
+            if (mNavBarBackgroundWindowCandidate == null) {
+                mNavBarBackgroundWindowCandidate = win;
+            }
         }
     }
 
@@ -1686,7 +1684,8 @@
      * @return Whether the top fullscreen app hides the given type of system bar.
      */
     boolean topAppHidesSystemBar(@InsetsType int type) {
-        if (mTopFullscreenOpaqueWindowState == null || mForceShowSystemBars) {
+        if (mTopFullscreenOpaqueWindowState == null
+                || getInsetsPolicy().areTypesForciblyShowing(type)) {
             return false;
         }
         return !mTopFullscreenOpaqueWindowState.isRequestedVisible(type);
@@ -2363,14 +2362,7 @@
         final boolean freeformRootTaskVisible =
                 defaultTaskDisplayArea.isRootTaskVisible(WINDOWING_MODE_FREEFORM);
 
-        // We need to force showing system bars when adjacent tasks or freeform roots visible.
-        mForceShowSystemBars = adjacentTasksVisible || freeformRootTaskVisible;
-        // We need to force the consumption of the system bars if they are force shown or if they
-        // are controlled by a remote insets controller.
-        mForceConsumeSystemBars = mForceShowSystemBars
-                || getInsetsPolicy().remoteInsetsControllerControlsSystemBars(win)
-                || getInsetsPolicy().forcesShowingNavigationBars(win);
-        mDisplayContent.getInsetsPolicy().updateBarControlTarget(win);
+        getInsetsPolicy().updateSystemBars(win, adjacentTasksVisible, freeformRootTaskVisible);
 
         final boolean topAppHidesStatusBar = topAppHidesSystemBar(Type.statusBars());
         if (getStatusBar() != null) {
@@ -2484,7 +2476,7 @@
         return win.isFullyTransparentBarAllowed(getBarContentFrameForWindow(win, type));
     }
 
-    private boolean drawsBarBackground(WindowState win) {
+    private static boolean drawsBarBackground(WindowState win) {
         if (win == null) {
             return true;
         }
@@ -2524,7 +2516,14 @@
      */
     private int configureNavBarOpacity(int appearance, boolean multiWindowTaskVisible,
             boolean freeformRootTaskVisible) {
-        final boolean drawBackground = drawsBarBackground(mNavBarBackgroundWindow);
+        final WindowState navBackgroundWin = chooseNavigationBackgroundWindow(
+                mNavBarBackgroundWindowCandidate,
+                mDisplayContent.mInputMethodWindow,
+                mNavigationBarPosition);
+        final boolean drawBackground = navBackgroundWin != null
+                // There is no app window showing underneath nav bar. (e.g., The screen is locked.)
+                // Let system windows (ex: notification shade) draw nav bar background.
+                || mNavBarBackgroundWindowCandidate == null;
 
         if (mNavBarOpacityMode == NAV_BAR_FORCE_TRANSPARENT) {
             if (drawBackground) {
@@ -2544,7 +2543,7 @@
             }
         }
 
-        if (!isFullyTransparentAllowed(mNavBarBackgroundWindow, Type.navigationBars())) {
+        if (!isFullyTransparentAllowed(navBackgroundWin, Type.navigationBars())) {
             appearance |= APPEARANCE_SEMI_TRANSPARENT_NAVIGATION_BARS;
         }
 
@@ -2555,11 +2554,26 @@
         return appearance & ~APPEARANCE_OPAQUE_NAVIGATION_BARS;
     }
 
+    @VisibleForTesting
+    @Nullable
+    static WindowState chooseNavigationBackgroundWindow(WindowState candidate,
+            WindowState imeWindow, @NavigationBarPosition int navBarPosition) {
+        if (imeWindow != null && imeWindow.isVisible() && navBarPosition == NAV_BAR_BOTTOM
+                && drawsBarBackground(imeWindow)) {
+            return imeWindow;
+        }
+        if (drawsBarBackground(candidate)) {
+            return candidate;
+        }
+        return null;
+    }
+
     private boolean isImmersiveMode(WindowState win) {
         if (win == null) {
             return false;
         }
-        if (win == getNotificationShade() || win.isActivityTypeDream()) {
+        if (win.mPolicy.getWindowLayerLw(win) > win.mPolicy.getWindowLayerFromTypeLw(
+                WindowManager.LayoutParams.TYPE_STATUS_BAR) || win.isActivityTypeDream()) {
             return false;
         }
         return getInsetsPolicy().hasHiddenSources(Type.navigationBars());
@@ -2727,9 +2741,9 @@
             pw.print(prefix); pw.print("mNavBarColorWindowCandidate=");
             pw.println(mNavBarColorWindowCandidate);
         }
-        if (mNavBarBackgroundWindow != null) {
-            pw.print(prefix); pw.print("mNavBarBackgroundWindow=");
-            pw.println(mNavBarBackgroundWindow);
+        if (mNavBarBackgroundWindowCandidate != null) {
+            pw.print(prefix); pw.print("mNavBarBackgroundWindowCandidate=");
+            pw.println(mNavBarBackgroundWindowCandidate);
         }
         if (mLastStatusBarAppearanceRegions != null) {
             pw.print(prefix); pw.println("mLastStatusBarAppearanceRegions=");
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index 2b34bb2..f96f99d 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -336,8 +336,7 @@
      * </ul>
      */
     private boolean isTreatmentEnabledForDisplay() {
-        return mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
-                    /* checkDeviceConfig */ true)
+        return mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled()
                 && mDisplayContent.getIgnoreOrientationRequest()
                 // TODO(b/225928882): Support camera compat rotation for external displays
                 && mDisplayContent.getDisplay().getType() == TYPE_INTERNAL;
diff --git a/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java
index 74494dd..de70c4d 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java
@@ -44,7 +44,7 @@
             @NonNull final DisplayRotation displayRotation,
             @NonNull final DisplayContent displayContent) {
         if (!letterboxConfiguration
-                .isDisplayRotationImmersiveAppCompatPolicyEnabled(/* checkDeviceConfig */ false)) {
+                .isDisplayRotationImmersiveAppCompatPolicyEnabledAtBuildTime()) {
             return null;
         }
 
@@ -87,8 +87,7 @@
      * @return {@code true}, if there is a need to lock screen rotation, {@code false} otherwise.
      */
     boolean isRotationLockEnforced(@Surface.Rotation final int proposedRotation) {
-        if (!mLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled(
-                /* checkDeviceConfig */ true)) {
+        if (!mLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled()) {
             return false;
         }
         synchronized (mDisplayContent.mWmService.mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 2583514..4a3d0c1 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -181,7 +181,11 @@
                     }
                 } finally {
                     if (surface != null) {
-                        surface.release();
+                        try (final SurfaceControl.Transaction transaction =
+                                mService.mTransactionFactory.get()) {
+                            transaction.remove(surface);
+                            transaction.apply();
+                        }
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
index 44d6768..98027bb 100644
--- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java
+++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
@@ -23,6 +23,7 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 import static com.android.server.wm.WindowStateProto.IDENTIFIER;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -217,10 +218,10 @@
                     mHostWindowState.mInputWindowHandle.getInputApplicationHandle());
         }
 
-        InputChannel openInputChannel() {
+        void openInputChannel(@NonNull InputChannel outInputChannel) {
             final String name = toString();
             mInputChannel = mWmService.mInputManager.createInputChannel(name);
-            return mInputChannel;
+            mInputChannel.copyTo(outInputChannel);
         }
 
         void onRemoved() {
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 0a47fe0..20595ea 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -128,6 +128,21 @@
         }
     }
 
+    /** Notifies that the pointer location configuration has changed. */
+    @Override
+    public void notifyPointerLocationChanged(boolean pointerLocationEnabled) {
+        if (mService.mPointerLocationEnabled == pointerLocationEnabled) {
+            return;
+        }
+
+        synchronized (mService.mGlobalLock) {
+            mService.mPointerLocationEnabled = pointerLocationEnabled;
+            mService.mRoot.forAllDisplayPolicies(
+                    p -> p.setPointerLocationEnabled(mService.mPointerLocationEnabled)
+            );
+        }
+    }
+
     /** Notifies that the lid switch changed state. */
     @Override
     public void notifyLidSwitchChanged(long whenNanos, boolean lidOpen) {
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index daa823c..835c92d 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -21,12 +21,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.view.InsetsController.ANIMATION_TYPE_HIDE;
-import static android.view.InsetsController.ANIMATION_TYPE_SHOW;
-import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_HIDDEN;
-import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_SHOWN;
 import static android.view.InsetsSource.ID_IME;
-import static android.view.SyncRtSurfaceTransactionApplier.applyParams;
 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
@@ -39,16 +34,13 @@
 import android.app.WindowConfiguration;
 import android.content.ComponentName;
 import android.content.res.Resources;
+import android.os.Handler;
+import android.os.IBinder;
 import android.util.SparseArray;
-import android.view.InsetsAnimationControlCallbacks;
-import android.view.InsetsAnimationControlImpl;
-import android.view.InsetsAnimationControlRunner;
 import android.view.InsetsController;
 import android.view.InsetsFrameProvider;
 import android.view.InsetsSource;
-import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.InternalInsetsAnimationController;
 import android.view.SurfaceControl;
 import android.view.SyncRtSurfaceTransactionApplier;
 import android.view.WindowInsets;
@@ -56,14 +48,16 @@
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsAnimation;
 import android.view.WindowInsetsAnimation.Bounds;
-import android.view.WindowInsetsAnimationControlListener;
 import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.DisplayThread;
 import com.android.server.statusbar.StatusBarManagerInternal;
 
+import java.io.PrintWriter;
+import java.util.List;
+
 /**
  * Policy that implements who gets control over the windows generating insets.
  */
@@ -77,47 +71,19 @@
     private final DisplayContent mDisplayContent;
     private final DisplayPolicy mPolicy;
 
-    /** For resetting visibilities of insets sources. */
-    private final InsetsControlTarget mDummyControlTarget = new InsetsControlTarget() {
+    /** Used to show system bars transiently. This won't affect the layout. */
+    private final InsetsControlTarget mTransientControlTarget;
 
-        @Override
-        public void notifyInsetsControlChanged() {
-            boolean hasLeash = false;
-            final InsetsSourceControl[] controls =
-                    mStateController.getControlsForDispatch(this);
-            if (controls == null) {
-                return;
-            }
-            for (InsetsSourceControl control : controls) {
-                if (isTransient(control.getType())) {
-                    // The visibilities of transient bars will be handled with animations.
-                    continue;
-                }
-                final SurfaceControl leash = control.getLeash();
-                if (leash != null) {
-                    hasLeash = true;
-
-                    // We use alpha to control the visibility here which aligns the logic at
-                    // SurfaceAnimator.createAnimationLeash
-                    final boolean visible =
-                            (control.getType() & WindowInsets.Type.defaultVisible()) != 0;
-                    mDisplayContent.getPendingTransaction().setAlpha(leash, visible ? 1f : 0f);
-                }
-            }
-            if (hasLeash) {
-                mDisplayContent.scheduleAnimation();
-            }
-        }
-    };
+    /** Used to show system bars permanently. This will affect the layout. */
+    private final InsetsControlTarget mPermanentControlTarget;
 
     private WindowState mFocusedWin;
     private final BarWindow mStatusBar = new BarWindow(StatusBarManager.WINDOW_STATUS_BAR);
     private final BarWindow mNavBar = new BarWindow(StatusBarManager.WINDOW_NAVIGATION_BAR);
     private @InsetsType int mShowingTransientTypes;
-    private boolean mAnimatingShown;
+    private @InsetsType int mForcedShowingTypes;
 
     private final boolean mHideNavBarForKeyboard;
-    private final float[] mTmpFloat9 = new float[9];
 
     InsetsPolicy(InsetsStateController stateController, DisplayContent displayContent) {
         mStateController = stateController;
@@ -125,9 +91,12 @@
         mPolicy = displayContent.getDisplayPolicy();
         final Resources r = mPolicy.getContext().getResources();
         mHideNavBarForKeyboard = r.getBoolean(R.bool.config_hideNavBarForKeyboard);
+        mTransientControlTarget = new ControlTarget(
+                stateController, displayContent.mWmService.mH, "TransientControlTarget");
+        mPermanentControlTarget = new ControlTarget(
+                stateController, displayContent.mWmService.mH, "PermanentControlTarget");
     }
 
-
     /** Updates the target which can control system bars. */
     void updateBarControlTarget(@Nullable WindowState focusedWin) {
         if (mFocusedWin != focusedWin) {
@@ -142,13 +111,13 @@
         final WindowState topApp = mPolicy.getTopFullscreenOpaqueWindow();
         mStateController.onBarControlTargetChanged(
                 statusControlTarget,
-                statusControlTarget == mDummyControlTarget
+                statusControlTarget == mTransientControlTarget
                         ? getStatusControlTarget(focusedWin, true /* fake */)
                         : statusControlTarget == notificationShade
                                 ? getStatusControlTarget(topApp, true /* fake */)
                                 : null,
                 navControlTarget,
-                navControlTarget == mDummyControlTarget
+                navControlTarget == mTransientControlTarget
                         ? getNavControlTarget(focusedWin, true /* fake */)
                         : navControlTarget == notificationShade
                                 ? getNavControlTarget(topApp, true /* fake */)
@@ -198,19 +167,19 @@
                     mFocusedWin,
                     (showingTransientTypes & (Type.statusBars() | Type.navigationBars())) != 0,
                     isGestureOnSystemBar);
-
-            // The leashes can be created while updating bar control target. The surface transaction
-            // of the new leashes might not be applied yet. The callback posted here ensures we can
-            // get the valid leashes because the surface transaction will be applied in the next
-            // animation frame which will be triggered if a new leash is created.
-            mDisplayContent.mWmService.mAnimator.getChoreographer().postFrameCallback(time -> {
-                synchronized (mDisplayContent.mWmService.mGlobalLock) {
-                    startAnimation(true /* show */, null /* callback */);
-                }
-            });
         }
     }
 
+    @VisibleForTesting
+    InsetsControlTarget getTransientControlTarget() {
+        return  mTransientControlTarget;
+    }
+
+    @VisibleForTesting
+    InsetsControlTarget getPermanentControlTarget() {
+        return  mPermanentControlTarget;
+    }
+
     void hideTransient() {
         if (mShowingTransientTypes == 0) {
             return;
@@ -221,23 +190,8 @@
                 /* areVisible= */ false,
                 /* wereRevealedFromSwipeOnSystemBar= */ false);
 
-        startAnimation(false /* show */, () -> {
-            synchronized (mDisplayContent.mWmService.mGlobalLock) {
-                final SparseArray<InsetsSourceProvider> providers =
-                        mStateController.getSourceProviders();
-                for (int i = providers.size() - 1; i >= 0; i--) {
-                    final InsetsSourceProvider provider = providers.valueAt(i);
-                    if (!isTransient(provider.getSource().getType())) {
-                        continue;
-                    }
-                    // We are about to clear mShowingTransientTypes, we don't want the transient bar
-                    // can cause insets on the client. Restore the client visibility.
-                    provider.setClientVisible(false);
-                }
-                mShowingTransientTypes = 0;
-                updateBarControlTarget(mFocusedWin);
-            }
-        });
+        mShowingTransientTypes = 0;
+        updateBarControlTarget(mFocusedWin);
     }
 
     boolean isTransient(@InsetsType int type) {
@@ -440,8 +394,8 @@
         return originalState;
     }
 
-    void onInsetsModified(InsetsControlTarget caller) {
-        mStateController.onInsetsModified(caller);
+    void onRequestedVisibleTypesChanged(InsetsControlTarget caller) {
+        mStateController.onRequestedVisibleTypesChanged(caller);
         checkAbortTransient(caller);
         updateBarControlTarget(mFocusedWin);
     }
@@ -500,7 +454,7 @@
     private @Nullable InsetsControlTarget getStatusControlTarget(@Nullable WindowState focusedWin,
             boolean fake) {
         if (!fake && isTransient(Type.statusBars())) {
-            return mDummyControlTarget;
+            return mTransientControlTarget;
         }
         final WindowState notificationShade = mPolicy.getNotificationShade();
         if (focusedWin == notificationShade) {
@@ -514,16 +468,16 @@
                     component, focusedWin.getRequestedVisibleTypes());
             return mDisplayContent.mRemoteInsetsControlTarget;
         }
-        if (mPolicy.areSystemBarsForcedShownLw()) {
+        if (areTypesForciblyShowing(Type.statusBars())) {
             // Status bar is forcibly shown. We don't want the client to control the status bar, and
             // we will dispatch the real visibility of status bar to the client.
-            return null;
+            return mPermanentControlTarget;
         }
         if (forceShowsStatusBarTransiently() && !fake) {
             // Status bar is forcibly shown transiently, and its new visibility won't be
             // dispatched to the client so that we can keep the layout stable. We will dispatch the
             // fake control to the client, so that it can re-show the bar during this scenario.
-            return mDummyControlTarget;
+            return mTransientControlTarget;
         }
         if (!canBeTopFullscreenOpaqueWindow(focusedWin)
                 && mPolicy.topAppHidesSystemBar(Type.statusBars())
@@ -554,7 +508,7 @@
             return null;
         }
         if (!fake && isTransient(Type.navigationBars())) {
-            return mDummyControlTarget;
+            return mTransientControlTarget;
         }
         if (focusedWin == mPolicy.getNotificationShade()) {
             // Notification shade has control anyways, no reason to force anything.
@@ -567,13 +521,6 @@
                 return focusedWin;
             }
         }
-        if (forcesShowingNavigationBars(focusedWin)) {
-            // When "force show navigation bar" is enabled, it means both force visible is true, and
-            // we are in 3-button navigation. In this mode, the navigation bar is forcibly shown
-            // when activity type is ACTIVITY_TYPE_STANDARD which means Launcher or Recent could
-            // still control the navigation bar in this mode.
-            return null;
-        }
         if (remoteInsetsControllerControlsSystemBars(focusedWin)) {
             ComponentName component = focusedWin.mActivityRecord != null
                     ? focusedWin.mActivityRecord.mActivityComponent : null;
@@ -581,16 +528,16 @@
                     component, focusedWin.getRequestedVisibleTypes());
             return mDisplayContent.mRemoteInsetsControlTarget;
         }
-        if (mPolicy.areSystemBarsForcedShownLw()) {
+        if (areTypesForciblyShowing(Type.navigationBars())) {
             // Navigation bar is forcibly shown. We don't want the client to control the navigation
             // bar, and we will dispatch the real visibility of navigation bar to the client.
-            return null;
+            return mPermanentControlTarget;
         }
         if (forceShowsNavigationBarTransiently() && !fake) {
             // Navigation bar is forcibly shown transiently, and its new visibility won't be
             // dispatched to the client so that we can keep the layout stable. We will dispatch the
             // fake control to the client, so that it can re-show the bar during this scenario.
-            return mDummyControlTarget;
+            return mTransientControlTarget;
         }
         final WindowState notificationShade = mPolicy.getNotificationShade();
         if (!canBeTopFullscreenOpaqueWindow(focusedWin)
@@ -603,7 +550,32 @@
         return focusedWin;
     }
 
-    boolean forcesShowingNavigationBars(WindowState win) {
+    boolean areTypesForciblyShowing(@InsetsType int types) {
+        return (mForcedShowingTypes & types) == types;
+    }
+
+    void updateSystemBars(WindowState win, boolean inSplitScreenMode, boolean inFreeformMode) {
+        mForcedShowingTypes = (inSplitScreenMode || inFreeformMode)
+                ? (Type.statusBars() | Type.navigationBars())
+                : forceShowingNavigationBars(win)
+                        ? Type.navigationBars()
+                        : 0;
+
+        // The client app won't be able to control these types of system bars. Here makes the client
+        // forcibly consume these types to prevent the app content from getting obscured.
+        mStateController.setForcedConsumingTypes(
+                mForcedShowingTypes | (remoteInsetsControllerControlsSystemBars(win)
+                        ? (Type.statusBars() | Type.navigationBars())
+                        : 0));
+
+        updateBarControlTarget(win);
+    }
+
+    private boolean forceShowingNavigationBars(WindowState win) {
+        // When "force show navigation bar" is enabled, it means both force visible is true, and
+        // we are in 3-button navigation. In this mode, the navigation bar is forcibly shown
+        // when activity type is ACTIVITY_TYPE_STANDARD which means Launcher or Recent could
+        // still control the navigation bar in this mode.
         return mPolicy.isForceShowNavigationBarEnabled() && win != null
                 && win.getActivityType() == ACTIVITY_TYPE_STANDARD;
     }
@@ -642,34 +614,6 @@
                 && (win.mAttrs.privateFlags & PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION) != 0;
     }
 
-    @VisibleForTesting
-    void startAnimation(boolean show, Runnable callback) {
-        @InsetsType int typesReady = 0;
-        final SparseArray<InsetsSourceControl> controlsReady = new SparseArray<>();
-        final InsetsSourceControl[] controls =
-                mStateController.getControlsForDispatch(mDummyControlTarget);
-        if (controls == null) {
-            if (callback != null) {
-                DisplayThread.getHandler().post(callback);
-            }
-            return;
-        }
-        for (InsetsSourceControl control : controls) {
-            if (isTransient(control.getType()) && control.getLeash() != null) {
-                typesReady |= control.getType();
-                controlsReady.put(control.getId(), new InsetsSourceControl(control));
-            }
-        }
-        controlAnimationUnchecked(typesReady, controlsReady, show, callback);
-    }
-
-    private void controlAnimationUnchecked(int typesReady,
-            SparseArray<InsetsSourceControl> controls, boolean show, Runnable callback) {
-        InsetsPolicyAnimationControlListener listener =
-                new InsetsPolicyAnimationControlListener(show, callback, typesReady);
-        listener.mControlCallbacks.controlAnimationUnchecked(typesReady, controls, show);
-    }
-
     private void dispatchTransientSystemBarsVisibilityChanged(
             @Nullable WindowState focusedWindow,
             boolean areVisible,
@@ -696,6 +640,21 @@
                         wereRevealedFromSwipeOnSystemBar);
     }
 
+    void dump(String prefix, PrintWriter pw) {
+        pw.println(prefix + "InsetsPolicy");
+        prefix = prefix + "  ";
+        pw.println(prefix + "status: " + StatusBarManager.windowStateToString(mStatusBar.mState));
+        pw.println(prefix + "nav: " + StatusBarManager.windowStateToString(mNavBar.mState));
+        if (mShowingTransientTypes != 0) {
+            pw.println(prefix + "mShowingTransientTypes="
+                    + WindowInsets.Type.toString(mShowingTransientTypes));
+        }
+        if (mForcedShowingTypes != 0) {
+            pw.println(prefix + "mForcedShowingTypes="
+                    + WindowInsets.Type.toString(mForcedShowingTypes));
+        }
+    }
+
     private class BarWindow {
 
         private final int mId;
@@ -725,105 +684,138 @@
         }
     }
 
-    private class InsetsPolicyAnimationControlListener extends
-            InsetsController.InternalAnimationControlListener {
-        Runnable mFinishCallback;
-        InsetsPolicyAnimationControlCallbacks mControlCallbacks;
+    private static class ControlTarget implements InsetsControlTarget {
 
-        InsetsPolicyAnimationControlListener(boolean show, Runnable finishCallback, int types) {
-            super(show, false /* hasCallbacks */, types, BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
-                    false /* disable */, 0 /* floatingImeBottomInsets */,
-                    null /* loggingListener */, null /* jankContext */);
-            mFinishCallback = finishCallback;
-            mControlCallbacks = new InsetsPolicyAnimationControlCallbacks(this);
+        private final InsetsState mState = new InsetsState();
+        private final InsetsController mInsetsController;
+        private final InsetsStateController mStateController;
+        private final String mName;
+
+        ControlTarget(InsetsStateController stateController, Handler handler, String name) {
+            mStateController = stateController;
+            mInsetsController = new InsetsController(new Host(handler, name));
+            mName = name;
         }
 
         @Override
-        protected void onAnimationFinish() {
-            super.onAnimationFinish();
-            if (mFinishCallback != null) {
-                DisplayThread.getHandler().post(mFinishCallback);
-            }
+        public void notifyInsetsControlChanged() {
+            mState.set(mStateController.getRawInsetsState(), true /* copySources */);
+            mInsetsController.onStateChanged(mState);
+            mInsetsController.onControlsChanged(mStateController.getControlsForDispatch(this));
         }
 
-        private class InsetsPolicyAnimationControlCallbacks implements
-                InsetsAnimationControlCallbacks {
-            private InsetsAnimationControlImpl mAnimationControl = null;
-            private InsetsPolicyAnimationControlListener mListener;
+        @Override
+        public String toString() {
+            return mName;
+        }
+    }
 
-            InsetsPolicyAnimationControlCallbacks(InsetsPolicyAnimationControlListener listener) {
-                mListener = listener;
+    private static class Host implements InsetsController.Host {
+
+        private final float[] mTmpFloat9 = new float[9];
+        private final Handler mHandler;
+        private final String mName;
+
+        Host(Handler handler, String name) {
+            mHandler = handler;
+            mName = name;
+        }
+
+        @Override
+        public Handler getHandler() {
+            return mHandler;
+        }
+
+        @Override
+        public void notifyInsetsChanged() { }
+
+        @Override
+        public void dispatchWindowInsetsAnimationPrepare(
+                @NonNull WindowInsetsAnimation animation) { }
+
+        @Override
+        public Bounds dispatchWindowInsetsAnimationStart(
+                @NonNull WindowInsetsAnimation animation,
+                @NonNull Bounds bounds) {
+            return bounds;
+        }
+
+        @Override
+        public WindowInsets dispatchWindowInsetsAnimationProgress(
+                @NonNull WindowInsets insets,
+                @NonNull List<WindowInsetsAnimation> runningAnimations) {
+            return insets;
+        }
+
+        @Override
+        public void dispatchWindowInsetsAnimationEnd(
+                @NonNull WindowInsetsAnimation animation) { }
+
+        @Override
+        public void applySurfaceParams(SyncRtSurfaceTransactionApplier.SurfaceParams... p) {
+            final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+            for (int i = p.length - 1; i >= 0; i--) {
+                SyncRtSurfaceTransactionApplier.applyParams(t, p[i], mTmpFloat9);
             }
+            t.apply();
+            t.close();
+        }
 
-            private void controlAnimationUnchecked(int typesReady,
-                    SparseArray<InsetsSourceControl> controls, boolean show) {
-                if (typesReady == 0) {
-                    // nothing to animate.
-                    return;
-                }
-                mAnimatingShown = show;
+        @Override
+        public void updateRequestedVisibleTypes(int types) { }
 
-                final InsetsState state = mFocusedWin.getInsetsState();
+        @Override
+        public boolean hasAnimationCallbacks() {
+            return false;
+        }
 
-                // We are about to playing the default animation. Passing a null frame indicates
-                // the controlled types should be animated regardless of the frame.
-                mAnimationControl = new InsetsAnimationControlImpl(controls,
-                        null /* frame */, state, mListener, typesReady, this,
-                        mListener.getDurationMs(), getInsetsInterpolator(),
-                        show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE, show
-                                ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN
-                                : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN,
-                        null /* translator */, null /* statsToken */);
-                SurfaceAnimationThread.getHandler().post(
-                        () -> mListener.onReady(mAnimationControl, typesReady));
-            }
+        @Override
+        public void setSystemBarsAppearance(int appearance, int mask) { }
 
-            /** Called on SurfaceAnimationThread without global WM lock held. */
-            @Override
-            public void scheduleApplyChangeInsets(InsetsAnimationControlRunner runner) {
-                if (mAnimationControl.applyChangeInsets(null /* outState */)) {
-                    mAnimationControl.finish(mAnimatingShown);
-                }
-            }
+        @Override
+        public int getSystemBarsAppearance() {
+            return 0;
+        }
 
-            @Override
-            public void notifyFinished(InsetsAnimationControlRunner runner, boolean shown) {
-                // Nothing's needed here. Finish steps is handled in the listener
-                // onAnimationFinished callback.
-            }
+        @Override
+        public void setSystemBarsBehavior(int behavior) { }
 
-            /** Called on SurfaceAnimationThread without global WM lock held. */
-            @Override
-            public void applySurfaceParams(
-                    final SyncRtSurfaceTransactionApplier.SurfaceParams... params) {
-                SurfaceControl.Transaction t = new SurfaceControl.Transaction();
-                for (int i = params.length - 1; i >= 0; i--) {
-                    SyncRtSurfaceTransactionApplier.SurfaceParams surfaceParams = params[i];
-                    applyParams(t, surfaceParams, mTmpFloat9);
-                }
-                t.apply();
-                t.close();
-            }
+        @Override
+        public int getSystemBarsBehavior() {
+            return BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
+        }
 
-            // Since we don't push applySurfaceParams to a Handler-queue we don't need
-            // to push release in this case.
-            @Override
-            public void releaseSurfaceControlFromRt(SurfaceControl sc) {
-                sc.release();
-            }
+        @Override
+        public void releaseSurfaceControlFromRt(SurfaceControl surfaceControl) {
+            surfaceControl.release();
+        }
 
-            @Override
-            public <T extends InsetsAnimationControlRunner & InternalInsetsAnimationController>
-            void startAnimation(T runner, WindowInsetsAnimationControlListener listener, int types,
-                    WindowInsetsAnimation animation,
-                    Bounds bounds) {
-            }
+        @Override
+        public void addOnPreDrawRunnable(Runnable r) { }
 
-            @Override
-            public void reportPerceptible(int types, boolean perceptible) {
-                // No-op for now - only client windows report perceptibility for now, with policy
-                // controllers assumed to always be perceptible.
-            }
+        @Override
+        public void postInsetsAnimationCallback(Runnable r) { }
+
+        @Override
+        public InputMethodManager getInputMethodManager() {
+            return null;
+        }
+
+        @Nullable
+        @Override
+        public String getRootViewTitle() {
+            return mName;
+        }
+
+        @Override
+        public int dipToPx(int dips) {
+            return 0;
+        }
+
+        @Nullable
+        @Override
+        public IBinder getWindowToken() {
+            return null;
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index 5e2618b..2b8312c 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -40,6 +40,7 @@
 import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
 import android.view.InsetsSource;
+import android.view.InsetsSource.Flags;
 import android.view.InsetsSourceControl;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
@@ -81,6 +82,8 @@
     private final Rect mSourceFrame = new Rect();
     private final Rect mLastSourceFrame = new Rect();
     private @NonNull Insets mInsetsHint = Insets.NONE;
+    private @Flags int mFlagsFromFrameProvider;
+    private @Flags int mFlagsFromServer;
 
     private final Consumer<Transaction> mSetLeashPositionConsumer = t -> {
         if (mControl != null) {
@@ -175,7 +178,7 @@
         if (windowContainer == null) {
             setServerVisible(false);
             mSource.setVisibleFrame(null);
-            mSource.setInsetsRoundedCornerFrame(false);
+            mSource.setFlags(0, 0xffffffff);
             mSourceFrame.setEmpty();
         } else {
             mWindowContainer.getInsetsSourceProviders().put(mSource.getId(), this);
@@ -189,6 +192,16 @@
         }
     }
 
+    boolean setFlags(@Flags int flags, @Flags int mask) {
+        mFlagsFromServer = (mFlagsFromServer & ~mask) | (flags & mask);
+        final @Flags int mergedFlags = mFlagsFromFrameProvider | mFlagsFromServer;
+        if (mSource.getFlags() != mergedFlags) {
+            mSource.setFlags(mergedFlags);
+            return true;
+        }
+        return false;
+    }
+
     /**
      * The source frame can affect the layout of other windows, so this should be called once the
      * window container gets laid out.
@@ -217,11 +230,11 @@
 
         mSourceFrame.set(frame);
         if (mFrameProvider != null) {
-            final int flags = mFrameProvider.apply(
+            mFlagsFromFrameProvider = mFrameProvider.apply(
                     mWindowContainer.getDisplayContent().mDisplayFrames,
                     mWindowContainer,
                     mSourceFrame);
-            mSource.setFlags(flags);
+            mSource.setFlags(mFlagsFromFrameProvider | mFlagsFromServer);
         }
         updateSourceFrameForServerVisibility();
 
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 249ead0..081ebe0 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -17,6 +17,7 @@
 package com.android.server.wm;
 
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
+import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
 import static android.view.InsetsSource.ID_IME;
 import static android.view.WindowInsets.Type.displayCutout;
 import static android.view.WindowInsets.Type.ime;
@@ -85,6 +86,8 @@
         }
     };
 
+    private @InsetsType int mForcedConsumingTypes;
+
     InsetsStateController(DisplayContent displayContent) {
         mDisplayContent = displayContent;
     }
@@ -122,6 +125,11 @@
         provider = id == ID_IME
                 ? new ImeInsetsSourceProvider(source, this, mDisplayContent)
                 : new InsetsSourceProvider(source, this, mDisplayContent);
+        provider.setFlags(
+                (mForcedConsumingTypes & type) != 0
+                        ? FLAG_FORCE_CONSUMING
+                        : 0,
+                FLAG_FORCE_CONSUMING);
         mProviders.put(id, provider);
         return provider;
     }
@@ -137,6 +145,24 @@
         }
     }
 
+    void setForcedConsumingTypes(@InsetsType int types) {
+        if (mForcedConsumingTypes != types) {
+            mForcedConsumingTypes = types;
+            boolean changed = false;
+            for (int i = mProviders.size() - 1; i >= 0; i--) {
+                final InsetsSourceProvider provider = mProviders.valueAt(i);
+                changed |= provider.setFlags(
+                        (types & provider.getSource().getType()) != 0
+                                ? FLAG_FORCE_CONSUMING
+                                : 0,
+                        FLAG_FORCE_CONSUMING);
+            }
+            if (changed) {
+                notifyInsetsChanged();
+            }
+        }
+    }
+
     /**
      * Called when a layout pass has occurred.
      */
@@ -190,7 +216,7 @@
         }
     }
 
-    void onInsetsModified(InsetsControlTarget caller) {
+    void onRequestedVisibleTypesChanged(InsetsControlTarget caller) {
         boolean changed = false;
         for (int i = mProviders.size() - 1; i >= 0; i--) {
             changed |= mProviders.valueAt(i).updateClientVisibility(caller);
@@ -352,7 +378,7 @@
             // to the clients, so that the clients can change the current visibilities to the
             // requested visibilities with animations.
             for (int i = newControlTargets.size() - 1; i >= 0; i--) {
-                onInsetsModified(newControlTargets.valueAt(i));
+                onRequestedVisibleTypesChanged(newControlTargets.valueAt(i));
             }
             newControlTargets.clear();
         });
@@ -391,6 +417,10 @@
         for (int i = mProviders.size() - 1; i >= 0; i--) {
             mProviders.valueAt(i).dump(pw, prefix + "  ");
         }
+        if (mForcedConsumingTypes != 0) {
+            pw.println(prefix + "mForcedConsumingTypes="
+                    + WindowInsets.Type.toString(mForcedConsumingTypes));
+        }
     }
 
     void dumpDebug(ProtoOutputStream proto, @WindowTraceLogLevel int logLevel) {
diff --git a/services/core/java/com/android/server/wm/LaunchParamsPersister.java b/services/core/java/com/android/server/wm/LaunchParamsPersister.java
index bf511adf0..2394da9 100644
--- a/services/core/java/com/android/server/wm/LaunchParamsPersister.java
+++ b/services/core/java/com/android/server/wm/LaunchParamsPersister.java
@@ -433,7 +433,7 @@
             final byte[] data = saveParamsToXml();
 
             final File launchParamFolder = getLaunchParamFolder(mUserId);
-            if (!launchParamFolder.isDirectory() && !launchParamFolder.mkdirs()) {
+            if (!launchParamFolder.isDirectory() && !launchParamFolder.mkdir()) {
                 Slog.w(TAG, "Failed to create folder for " + mUserId);
                 return;
             }
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 067a18c..fda22ca 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -18,11 +18,6 @@
 
 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;
@@ -44,6 +39,52 @@
 
     private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxConfiguration" : TAG_ATM;
 
+    // Whether camera compatibility treatment is enabled.
+    // See DisplayRotationCompatPolicy for context.
+    private static final String KEY_ENABLE_CAMERA_COMPAT_TREATMENT =
+            "enable_compat_camera_treatment";
+
+    private static final boolean DEFAULT_VALUE_ENABLE_CAMERA_COMPAT_TREATMENT = true;
+
+    // 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 when rotated. Additionally, some games rely
+    // on sensors for the gameplay so  users can trigger such rotations accidentally
+    // when auto rotation is on.
+    private 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;
+
+    // Whether ignore orientation request is allowed
+    private static final String KEY_ALLOW_IGNORE_ORIENTATION_REQUEST =
+            "allow_ignore_orientation_request";
+
+    private static final boolean DEFAULT_VALUE_ALLOW_IGNORE_ORIENTATION_REQUEST = true;
+
+    // Whether sending compat fake focus is enabled for unfocused apps in splitscreen.
+    // Some game engines wait to get focus before drawing the content of the app so
+    // this needs  to be used otherwise the apps get blacked out when they are resumed
+    // and do not have focus yet.
+    private static final String KEY_ENABLE_COMPAT_FAKE_FOCUS = "enable_compat_fake_focus";
+
+    private static final boolean DEFAULT_VALUE_ENABLE_COMPAT_FAKE_FOCUS = true;
+
+    // Whether translucent activities policy is enabled
+    private static final String KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY =
+            "enable_letterbox_translucent_activity";
+
+    private static final boolean DEFAULT_VALUE_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY = true;
+
+    // Whether per-app user aspect ratio override settings is enabled
+    private static final String KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS =
+            "enable_app_compat_user_aspect_ratio_settings";
+
+    // TODO(b/288142656): Enable user aspect ratio settings by default.
+    private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS = false;
+
     /**
      * Override of aspect ratio for fixed orientation letterboxing that is set via ADB with
      * set-fixed-orientation-letterbox-aspect-ratio or via {@link
@@ -205,26 +246,16 @@
     // unresizable apps
     private boolean mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox;
 
-    // Whether letterboxing strategy is enabled for translucent activities. If {@value false}
-    // all the feature is disabled
-    private boolean mTranslucentLetterboxingEnabled;
-
     // Allows to enable letterboxing strategy for translucent activities ignoring flags.
     private boolean mTranslucentLetterboxingOverrideEnabled;
 
-    // Whether sending compat fake focus is enabled for unfocused apps in splitscreen. Some game
-    // engines wait to get focus before drawing the content of the app so this needs to be used
-    // otherwise the apps get blacked out when they are resumed and do not have focus yet.
-    private boolean mIsCompatFakeFocusEnabled;
+    // Allows to enable user aspect ratio settings ignoring flags.
+    private boolean mUserAppAspectRatioSettingsOverrideEnabled;
 
     // Whether we should use split screen aspect ratio for the activity when camera compat treatment
     // is enabled and activity is connected to the camera in fullscreen.
     private final boolean mIsCameraCompatSplitScreenAspectRatioEnabled;
 
-    // Whether camera compatibility treatment is enabled.
-    // See DisplayRotationCompatPolicy for context.
-    private final boolean mIsCameraCompatTreatmentEnabled;
-
     // Whether activity "refresh" in camera compatibility treatment is enabled.
     // See RefreshCallbackItem for context.
     private boolean mIsCameraCompatTreatmentRefreshEnabled = true;
@@ -240,15 +271,8 @@
     // LetterboxUiController#shouldIgnoreRequestedOrientation for details.
     private final boolean mIsPolicyForIgnoringRequestedOrientationEnabled;
 
-    // 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
-    // when rotated. Additionally, some games rely on sensors for the gameplay so users can trigger
-    // such rotations accidentally when auto rotation is on.
-    private final boolean mIsDisplayRotationImmersiveAppCompatPolicyEnabled;
-
     // Flags dynamically updated with {@link android.provider.DeviceConfig}.
-    @NonNull private final LetterboxConfigurationDeviceConfig mDeviceConfig;
+    @NonNull private final SynchedDeviceConfig mDeviceConfig;
 
     LetterboxConfiguration(@NonNull final Context systemUiContext) {
         this(systemUiContext,
@@ -267,7 +291,6 @@
     LetterboxConfiguration(@NonNull final Context systemUiContext,
             @NonNull final LetterboxConfigurationPersister letterboxConfigurationPersister) {
         mContext = systemUiContext;
-        mDeviceConfig = new LetterboxConfigurationDeviceConfig(systemUiContext.getMainExecutor());
 
         mFixedOrientationLetterboxAspectRatio = mContext.getResources().getFloat(
                 R.dimen.config_fixedOrientationLetterboxAspectRatio);
@@ -305,36 +328,38 @@
         mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox = mContext.getResources()
                 .getBoolean(R.bool
                         .config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled);
-        mTranslucentLetterboxingEnabled = mContext.getResources().getBoolean(
-                R.bool.config_letterboxIsEnabledForTranslucentActivities);
-        mIsCameraCompatTreatmentEnabled = mContext.getResources().getBoolean(
-                R.bool.config_isWindowManagerCameraCompatTreatmentEnabled);
         mIsCameraCompatSplitScreenAspectRatioEnabled = mContext.getResources().getBoolean(
                 R.bool.config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled);
-        mIsCompatFakeFocusEnabled = mContext.getResources().getBoolean(
-                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();
+
+        mDeviceConfig = SynchedDeviceConfig.builder(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+                        systemUiContext.getMainExecutor())
+                .addDeviceConfigEntry(KEY_ENABLE_CAMERA_COMPAT_TREATMENT,
+                        DEFAULT_VALUE_ENABLE_CAMERA_COMPAT_TREATMENT,
+                        mContext.getResources().getBoolean(
+                                R.bool.config_isWindowManagerCameraCompatTreatmentEnabled))
+                .addDeviceConfigEntry(KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY,
+                        DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY,
+                        mContext.getResources().getBoolean(R.bool
+                                .config_letterboxIsDisplayRotationImmersiveAppCompatPolicyEnabled))
+                .addDeviceConfigEntry(KEY_ALLOW_IGNORE_ORIENTATION_REQUEST,
+                        DEFAULT_VALUE_ALLOW_IGNORE_ORIENTATION_REQUEST, /* enabled */ true)
+                .addDeviceConfigEntry(KEY_ENABLE_COMPAT_FAKE_FOCUS,
+                        DEFAULT_VALUE_ENABLE_COMPAT_FAKE_FOCUS,
+                        mContext.getResources().getBoolean(R.bool.config_isCompatFakeFocusEnabled))
+                .addDeviceConfigEntry(KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY,
+                        DEFAULT_VALUE_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY,
+                        mContext.getResources().getBoolean(
+                                R.bool.config_letterboxIsEnabledForTranslucentActivities))
+                .addDeviceConfigEntry(KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS,
+                        DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS,
+                        mContext.getResources().getBoolean(
+                                R.bool.config_appCompatUserAppAspectRatioSettingsIsEnabled))
+                .build();
     }
 
     /**
@@ -342,7 +367,7 @@
      * via {@link android.provider.DeviceConfig}.
      */
     boolean isIgnoreOrientationRequestAllowed() {
-        return mDeviceConfig.getFlag(KEY_ALLOW_IGNORE_ORIENTATION_REQUEST);
+        return mDeviceConfig.getFlagValue(KEY_ALLOW_IGNORE_ORIENTATION_REQUEST);
     }
 
     /**
@@ -1049,28 +1074,21 @@
     }
 
     boolean isTranslucentLetterboxingEnabled() {
-        return mTranslucentLetterboxingOverrideEnabled || (mTranslucentLetterboxingEnabled
-                && mDeviceConfig.getFlag(KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY));
-    }
-
-    void setTranslucentLetterboxingEnabled(boolean translucentLetterboxingEnabled) {
-        mTranslucentLetterboxingEnabled = translucentLetterboxingEnabled;
+        return mTranslucentLetterboxingOverrideEnabled
+                || mDeviceConfig.getFlagValue(KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY);
     }
 
     void setTranslucentLetterboxingOverrideEnabled(
             boolean translucentLetterboxingOverrideEnabled) {
         mTranslucentLetterboxingOverrideEnabled = translucentLetterboxingOverrideEnabled;
-        setTranslucentLetterboxingEnabled(translucentLetterboxingOverrideEnabled);
     }
 
     /**
      * Resets whether we use the constraints override strategy for letterboxing when dealing
-     * with translucent activities {@link R.bool.config_letterboxIsEnabledForTranslucentActivities}.
+     * with translucent activities
+     * {@link mDeviceConfig.getFlagValue(KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY)}.
      */
     void resetTranslucentLetterboxingEnabled() {
-        final boolean newValue = mContext.getResources().getBoolean(
-                R.bool.config_letterboxIsEnabledForTranslucentActivities);
-        setTranslucentLetterboxingEnabled(newValue);
         setTranslucentLetterboxingOverrideEnabled(false);
     }
 
@@ -1100,15 +1118,7 @@
 
     /** Whether fake sending focus is enabled for unfocused apps in splitscreen */
     boolean isCompatFakeFocusEnabled() {
-        return mIsCompatFakeFocusEnabled && mDeviceConfig.getFlag(KEY_ENABLE_COMPAT_FAKE_FOCUS);
-    }
-
-    /**
-     * Overrides whether fake sending focus is enabled for unfocused apps in splitscreen
-     */
-    @VisibleForTesting
-    void setIsCompatFakeFocusEnabled(boolean enabled) {
-        mIsCompatFakeFocusEnabled = enabled;
+        return mDeviceConfig.getFlagValue(KEY_ENABLE_COMPAT_FAKE_FOCUS);
     }
 
     /**
@@ -1128,10 +1138,20 @@
         return mIsCameraCompatSplitScreenAspectRatioEnabled;
     }
 
-    /** Whether camera compatibility treatment is enabled. */
-    boolean isCameraCompatTreatmentEnabled(boolean checkDeviceConfig) {
-        return mIsCameraCompatTreatmentEnabled && (!checkDeviceConfig
-                || mDeviceConfig.getFlag(KEY_ENABLE_CAMERA_COMPAT_TREATMENT));
+    /**
+     * @return Whether camera compatibility treatment is currently enabled.
+     */
+    boolean isCameraCompatTreatmentEnabled() {
+        return mDeviceConfig.getFlagValue(KEY_ENABLE_CAMERA_COMPAT_TREATMENT);
+    }
+
+    /**
+     * @return Whether camera compatibility treatment is enabled at build time. This is used when
+     * we need to safely initialize a component before the {@link DeviceConfig} flag value is
+     * available.
+     */
+    boolean isCameraCompatTreatmentEnabledAtBuildTime() {
+        return mDeviceConfig.isBuildTimeFlagEnabled(KEY_ENABLE_CAMERA_COMPAT_TREATMENT);
     }
 
     /** Whether camera compatibility refresh is enabled. */
@@ -1177,18 +1197,48 @@
 
     /**
      * Checks whether rotation compat policy for immersive apps that prevents auto rotation
-     * into non-optimal screen orientation while in fullscreen is enabled.
+     * into non-optimal screen orientation while in fullscreen is enabled at build time. This is
+     * used when we need to safely initialize a component before the {@link DeviceConfig} flag
+     * value is available.
      *
      * <p>This is needed because immersive apps, such as games, are often not optimized for all
      * orientations and can have a poor UX when rotated. Additionally, some games rely on sensors
      * for the gameplay so users can trigger such rotations accidentally when auto rotation is on.
-     *
-     * @param checkDeviceConfig whether should check both static config and a dynamic property
-     *        from {@link DeviceConfig} or only static value.
      */
-    boolean isDisplayRotationImmersiveAppCompatPolicyEnabled(final boolean checkDeviceConfig) {
-        return mIsDisplayRotationImmersiveAppCompatPolicyEnabled && (!checkDeviceConfig
-                || mDeviceConfig.getFlag(KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY));
+    boolean isDisplayRotationImmersiveAppCompatPolicyEnabledAtBuildTime() {
+        return mDeviceConfig.isBuildTimeFlagEnabled(
+                KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY);
     }
 
+    /**
+     * Checks whether rotation compat policy for immersive apps that prevents auto rotation
+     * into non-optimal screen orientation while in fullscreen is currently enabled.
+     *
+     * <p>This is needed because immersive apps, such as games, are often not optimized for all
+     * orientations and can have a poor UX when rotated. Additionally, some games rely on sensors
+     * for the gameplay so users can trigger such rotations accidentally when auto rotation is on.
+     */
+    boolean isDisplayRotationImmersiveAppCompatPolicyEnabled() {
+        return mDeviceConfig.getFlagValue(KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY);
+    }
+
+    /**
+     * Whether per-app user aspect ratio override settings is enabled
+     */
+    boolean isUserAppAspectRatioSettingsEnabled() {
+        return mUserAppAspectRatioSettingsOverrideEnabled
+                || mDeviceConfig.getFlagValue(KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS);
+    }
+
+    void setUserAppAspectRatioSettingsOverrideEnabled(boolean enabled) {
+        mUserAppAspectRatioSettingsOverrideEnabled = enabled;
+    }
+
+    /**
+     * Resets whether per-app user aspect ratio override settings is enabled
+     * {@code mDeviceConfig.getFlagValue(KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS)}.
+     */
+    void resetUserAppAspectRatioSettingsEnabled() {
+        setUserAppAspectRatioSettingsOverrideEnabled(false);
+    }
 }
diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java
deleted file mode 100644
index a739e42..0000000
--- a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java
+++ /dev/null
@@ -1,180 +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.server.wm;
-
-import android.annotation.NonNull;
-import android.provider.DeviceConfig;
-import android.util.ArraySet;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.util.Map;
-import java.util.concurrent.Executor;
-
-/**
- * Utility class that caches {@link DeviceConfig} flags for app compat features and listens
- * to updates by implementing {@link DeviceConfig.OnPropertiesChangedListener}.
- */
-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,
-            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
-    // when rotated. Additionally, some games rely on sensors for the gameplay so users can trigger
-    // such rotations accidentally when auto rotation is on.
-    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<>();
-
-    LetterboxConfigurationDeviceConfig(@NonNull final Executor executor) {
-        DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
-                executor, /* onPropertiesChangedListener */ this);
-    }
-
-    @Override
-    public void onPropertiesChanged(@NonNull final DeviceConfig.Properties properties) {
-        for (int i = mActiveDeviceConfigsSet.size() - 1; i >= 0; i--) {
-            String key = mActiveDeviceConfigsSet.valueAt(i);
-            // Reads the new configuration, if the device config properties contain the key.
-            if (properties.getKeyset().contains(key)) {
-                readAndSaveValueFromDeviceConfig(key);
-            }
-        }
-    }
-
-    /**
-     * Adds {@code key} to a set of flags that can be updated from the server if
-     * {@code isActive} is {@code true} and read it's current value from {@link DeviceConfig}.
-     */
-    void updateFlagActiveStatus(boolean isActive, String key) {
-        if (!isActive) {
-            return;
-        }
-        mActiveDeviceConfigsSet.add(key);
-        readAndSaveValueFromDeviceConfig(key);
-    }
-
-    /**
-     * Returns values of the {@code key} flag.
-     *
-     * @throws AssertionError {@code key} isn't recognised.
-     */
-    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);
-        }
-    }
-
-    private void readAndSaveValueFromDeviceConfig(String key) {
-        Boolean defaultValue = sKeyToDefaultValueMap.get(key);
-        if (defaultValue == null) {
-            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);
-        }
-    }
-
-    private boolean getDeviceConfig(String key, boolean defaultValue) {
-        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
-                key, defaultValue);
-    }
-}
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index d83c861..a816838 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -294,18 +294,15 @@
                         PROPERTY_COMPAT_ENABLE_FAKE_FOCUS);
         mBooleanPropertyCameraCompatAllowForceRotation =
                 readComponentProperty(packageManager, mActivityRecord.packageName,
-                        () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
-                                /* checkDeviceConfig */ true),
+                        () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(),
                         PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION);
         mBooleanPropertyCameraCompatAllowRefresh =
                 readComponentProperty(packageManager, mActivityRecord.packageName,
-                        () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
-                                /* checkDeviceConfig */ true),
+                        () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(),
                         PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH);
         mBooleanPropertyCameraCompatEnableRefreshViaPause =
                 readComponentProperty(packageManager, mActivityRecord.packageName,
-                        () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
-                                /* checkDeviceConfig */ true),
+                        () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(),
                         PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE);
 
         mBooleanPropertyAllowOrientationOverride =
@@ -697,7 +694,7 @@
     boolean shouldRefreshActivityForCameraCompat() {
         return shouldEnableWithOptOutOverrideAndProperty(
                 /* gatingCondition */ () -> mLetterboxConfiguration
-                        .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true),
+                        .isCameraCompatTreatmentEnabled(),
                 mIsOverrideCameraCompatDisableRefreshEnabled,
                 mBooleanPropertyCameraCompatAllowRefresh);
     }
@@ -719,7 +716,7 @@
     boolean shouldRefreshActivityViaPauseForCameraCompat() {
         return shouldEnableWithOverrideAndProperty(
                 /* gatingCondition */ () -> mLetterboxConfiguration
-                        .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true),
+                        .isCameraCompatTreatmentEnabled(),
                 mIsOverrideCameraCompatEnableRefreshViaPauseEnabled,
                 mBooleanPropertyCameraCompatEnableRefreshViaPause);
     }
@@ -738,7 +735,7 @@
     boolean shouldForceRotateForCameraCompat() {
         return shouldEnableWithOptOutOverrideAndProperty(
                 /* gatingCondition */ () -> mLetterboxConfiguration
-                        .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true),
+                        .isCameraCompatTreatmentEnabled(),
                 mIsOverrideCameraCompatDisableForceRotationEnabled,
                 mBooleanPropertyCameraCompatAllowForceRotation);
     }
@@ -1428,7 +1425,8 @@
         for (int i = state.sourceSize() - 1; i >= 0; i--) {
             final InsetsSource source = state.sourceAt(i);
             if (source.getType() == WindowInsets.Type.navigationBars()
-                    && source.insetsRoundedCornerFrame() && source.isVisible()) {
+                    && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)
+                    && source.isVisible()) {
                 return source;
             }
         }
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index e47787e..9ef5ed0 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -1104,6 +1104,10 @@
                         // front unless overridden by the provided activity options
                         mTasks.remove(taskIndex);
                         mTasks.add(0, task);
+                        if (taskIndex != 0) {
+                            // Only notify when position changes
+                            mTaskNotificationController.notifyTaskListUpdated();
+                        }
 
                         if (DEBUG_RECENTS) {
                             Slog.d(TAG_RECENTS, "addRecent: moving to top " + task
@@ -1552,7 +1556,7 @@
                         task.affinity != null && task.affinity.equals(t.affinity);
                 final boolean sameIntent = intent != null && intent.filterEquals(trIntent);
                 boolean multiTasksAllowed = false;
-                final int flags = intent.getFlags();
+                final int flags = intent != null ? intent.getFlags() : 0;
                 if ((flags & (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NEW_DOCUMENT)) != 0
                         && (flags & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
                     multiTasksAllowed = true;
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 7b10c63..b49c5fb 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -687,11 +687,11 @@
     @Override
     public void updateRequestedVisibleTypes(IWindow window, @InsetsType int requestedVisibleTypes) {
         synchronized (mService.mGlobalLock) {
-            final WindowState windowState = mService.windowForClientLocked(this, window,
+            final WindowState win = mService.windowForClientLocked(this, window,
                     false /* throwOnError */);
-            if (windowState != null) {
-                windowState.setRequestedVisibleTypes(requestedVisibleTypes);
-                windowState.getDisplayContent().getInsetsPolicy().onInsetsModified(windowState);
+            if (win != null) {
+                win.setRequestedVisibleTypes(requestedVisibleTypes);
+                win.getDisplayContent().getInsetsPolicy().onRequestedVisibleTypesChanged(win);
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/SynchedDeviceConfig.java b/services/core/java/com/android/server/wm/SynchedDeviceConfig.java
new file mode 100644
index 0000000..c2e819e
--- /dev/null
+++ b/services/core/java/com/android/server/wm/SynchedDeviceConfig.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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;
+
+import android.annotation.NonNull;
+import android.provider.DeviceConfig;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * Utility class that caches {@link DeviceConfig} flags and listens to updates by implementing
+ * {@link DeviceConfig.OnPropertiesChangedListener}.
+ */
+final class SynchedDeviceConfig implements DeviceConfig.OnPropertiesChangedListener {
+
+    private final String mNamespace;
+    private final Executor mExecutor;
+
+    private final Map<String, SynchedDeviceConfigEntry> mDeviceConfigEntries;
+
+    /**
+     * @param namespace The namespace for the {@link DeviceConfig}
+     * @param executor  The {@link Executor} implementation to use when receiving updates
+     * @return the Builder implementation for the SynchedDeviceConfig
+     */
+    @NonNull
+    static SynchedDeviceConfigBuilder builder(@NonNull String namespace,
+            @NonNull Executor executor) {
+        return new SynchedDeviceConfigBuilder(namespace, executor);
+    }
+
+    private SynchedDeviceConfig(@NonNull String namespace, @NonNull Executor executor,
+            @NonNull Map<String, SynchedDeviceConfigEntry> deviceConfigEntries) {
+        mNamespace = namespace;
+        mExecutor = executor;
+        mDeviceConfigEntries = deviceConfigEntries;
+    }
+
+    @Override
+    public void onPropertiesChanged(@NonNull final DeviceConfig.Properties properties) {
+        for (SynchedDeviceConfigEntry entry : mDeviceConfigEntries.values()) {
+            if (properties.getKeyset().contains(entry.mFlagKey)) {
+                entry.updateValue(properties.getBoolean(entry.mFlagKey, entry.mDefaultValue));
+            }
+        }
+    }
+
+    /**
+     * Builds the {@link SynchedDeviceConfig} and start listening to the {@link DeviceConfig}
+     * updates.
+     *
+     * @return The {@link SynchedDeviceConfig}
+     */
+    @NonNull
+    private SynchedDeviceConfig start() {
+        DeviceConfig.addOnPropertiesChangedListener(mNamespace,
+                mExecutor, /* onPropertiesChangedListener */ this);
+        return this;
+    }
+
+    /**
+     * Requests a {@link DeviceConfig} update for all the flags
+     */
+    @NonNull
+    private SynchedDeviceConfig updateFlags() {
+        mDeviceConfigEntries.forEach((key, entry) -> entry.updateValue(
+                isDeviceConfigFlagEnabled(key, entry.mDefaultValue)));
+        return this;
+    }
+
+    /**
+     * Returns values of the {@code key} flag with the following criteria:
+     *
+     * <ul>
+     *     <li>{@code false} if the build time flag is disabled.
+     *     <li>{@code defaultValue} if the build time flag is enabled and no {@link DeviceConfig}
+     *          updates happened
+     *     <li>Last value from {@link DeviceConfig} in case of updates.
+     * </ul>
+     *
+     * @throws IllegalArgumentException {@code key} isn't recognised.
+     */
+    boolean getFlagValue(@NonNull String key) {
+        return findEntry(key).map(SynchedDeviceConfigEntry::getValue)
+                .orElseThrow(() -> new IllegalArgumentException("Unexpected flag name: " + key));
+    }
+
+    /**
+     * @return {@code true} if the flag for the given {@code key} was enabled at build time.
+     */
+    boolean isBuildTimeFlagEnabled(@NonNull String key) {
+        return findEntry(key).map(SynchedDeviceConfigEntry::isBuildTimeFlagEnabled)
+                .orElseThrow(() -> new IllegalArgumentException("Unexpected flag name: " + key));
+    }
+
+    private boolean isDeviceConfigFlagEnabled(@NonNull String key, boolean defaultValue) {
+        return DeviceConfig.getBoolean(mNamespace, key, defaultValue);
+    }
+
+    @NonNull
+    private Optional<SynchedDeviceConfigEntry> findEntry(@NonNull String key) {
+        return Optional.ofNullable(mDeviceConfigEntries.get(key));
+    }
+
+    static class SynchedDeviceConfigBuilder {
+
+        private final String mNamespace;
+        private final Executor mExecutor;
+
+        private final Map<String, SynchedDeviceConfigEntry> mDeviceConfigEntries =
+                new ConcurrentHashMap<>();
+
+        private SynchedDeviceConfigBuilder(@NonNull String namespace, @NonNull Executor executor) {
+            mNamespace = namespace;
+            mExecutor = executor;
+        }
+
+        @NonNull
+        SynchedDeviceConfigBuilder addDeviceConfigEntry(@NonNull String key,
+                boolean defaultValue, boolean enabled) {
+            if (mDeviceConfigEntries.containsKey(key)) {
+                throw new AssertionError("Key already present: " + key);
+            }
+            mDeviceConfigEntries.put(key,
+                    new SynchedDeviceConfigEntry(key, defaultValue, enabled));
+            return this;
+        }
+
+        @NonNull
+        SynchedDeviceConfig build() {
+            return new SynchedDeviceConfig(mNamespace, mExecutor,
+                    mDeviceConfigEntries).updateFlags().start();
+        }
+    }
+
+    /**
+     * Contains all the information related to an entry to be managed by DeviceConfig
+     */
+    private static class SynchedDeviceConfigEntry {
+
+        // The key of the specific configuration flag
+        private final String mFlagKey;
+
+        // The value of the flag at build time.
+        private final boolean mBuildTimeFlagEnabled;
+
+        // The initial value of the flag when mBuildTimeFlagEnabled is true.
+        private final boolean mDefaultValue;
+
+        // The current value of the flag when mBuildTimeFlagEnabled is true.
+        private volatile boolean mOverrideValue;
+
+        private SynchedDeviceConfigEntry(@NonNull String flagKey, boolean defaultValue,
+                boolean enabled) {
+            mFlagKey = flagKey;
+            mOverrideValue = mDefaultValue = defaultValue;
+            mBuildTimeFlagEnabled = enabled;
+        }
+
+        @NonNull
+        private void updateValue(boolean newValue) {
+            mOverrideValue = newValue;
+        }
+
+        private boolean getValue() {
+            return mBuildTimeFlagEnabled && mOverrideValue;
+        }
+
+        private boolean isBuildTimeFlagEnabled() {
+            return mBuildTimeFlagEnabled;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java b/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java
index 878b33f..1007357 100644
--- a/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java
+++ b/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java
@@ -20,6 +20,8 @@
 import static android.view.DisplayCutout.BOUNDS_POSITION_LEFT;
 import static android.view.DisplayCutout.BOUNDS_POSITION_RIGHT;
 import static android.view.DisplayCutout.BOUNDS_POSITION_TOP;
+import static android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT;
+import static android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE;
 
 import android.annotation.NonNull;
 import android.content.Context;
@@ -59,6 +61,12 @@
     private static final int SWIPE_FROM_RIGHT = 3;
     private static final int SWIPE_FROM_LEFT = 4;
 
+    private static final int TRACKPAD_SWIPE_NONE = 0;
+    private static final int TRACKPAD_SWIPE_FROM_TOP = 1;
+    private static final int TRACKPAD_SWIPE_FROM_BOTTOM = 2;
+    private static final int TRACKPAD_SWIPE_FROM_RIGHT = 3;
+    private static final int TRACKPAD_SWIPE_FROM_LEFT = 4;
+
     private final Context mContext;
     private final Handler mHandler;
     private int mDisplayCutoutTouchableRegionSize;
@@ -207,6 +215,25 @@
                 break;
             case MotionEvent.ACTION_MOVE:
                 if (mSwipeFireable) {
+                    int trackpadSwipe = detectTrackpadThreeFingerSwipe(event);
+                    mSwipeFireable = trackpadSwipe == TRACKPAD_SWIPE_NONE;
+                    if (!mSwipeFireable) {
+                        if (trackpadSwipe == TRACKPAD_SWIPE_FROM_TOP) {
+                            if (DEBUG) Slog.d(TAG, "Firing onSwipeFromTop from trackpad");
+                            mCallbacks.onSwipeFromTop();
+                        } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_BOTTOM) {
+                            if (DEBUG) Slog.d(TAG, "Firing onSwipeFromBottom from trackpad");
+                            mCallbacks.onSwipeFromBottom();
+                        } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_RIGHT) {
+                            if (DEBUG) Slog.d(TAG, "Firing onSwipeFromRight from trackpad");
+                            mCallbacks.onSwipeFromRight();
+                        } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_LEFT) {
+                            if (DEBUG) Slog.d(TAG, "Firing onSwipeFromLeft from trackpad");
+                            mCallbacks.onSwipeFromLeft();
+                        }
+                        break;
+                    }
+
                     final int swipe = detectSwipe(event);
                     mSwipeFireable = swipe == SWIPE_NONE;
                     if (swipe == SWIPE_FROM_TOP) {
@@ -300,6 +327,31 @@
         return mDownPointers - 1;
     }
 
+    private int detectTrackpadThreeFingerSwipe(MotionEvent move) {
+        if (!isTrackpadThreeFingerSwipe(move)) {
+            return TRACKPAD_SWIPE_NONE;
+        }
+
+        float dx = move.getX() - mDownX[0];
+        float dy = move.getY() - mDownY[0];
+        if (Math.abs(dx) < Math.abs(dy)) {
+            if (Math.abs(dy) > mSwipeDistanceThreshold) {
+                return dy > 0 ? TRACKPAD_SWIPE_FROM_TOP : TRACKPAD_SWIPE_FROM_BOTTOM;
+            }
+        } else {
+            if (Math.abs(dx) > mSwipeDistanceThreshold) {
+                return dx > 0 ? TRACKPAD_SWIPE_FROM_LEFT : TRACKPAD_SWIPE_FROM_RIGHT;
+            }
+        }
+
+        return TRACKPAD_SWIPE_NONE;
+    }
+
+    private static boolean isTrackpadThreeFingerSwipe(MotionEvent event) {
+        return event.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE
+                && event.getAxisValue(AXIS_GESTURE_SWIPE_FINGER_COUNT) == 3;
+    }
+
     private int detectSwipe(MotionEvent move) {
         final int historySize = move.getHistorySize();
         final int pointerCount = move.getPointerCount();
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index ceb80d6..9c23beb 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -49,6 +49,7 @@
 import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
+import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
 import static android.view.SurfaceControl.METADATA_TASK_ID;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
@@ -1271,6 +1272,10 @@
         if (isPersistable) {
             mLastTimeMoved = System.currentTimeMillis();
         }
+        if (toTop && inRecents) {
+            // If task is in recents, ensure it is at the top
+            mTaskSupervisor.mRecentTasks.add(this);
+        }
     }
 
     // Close up recents linked list.
@@ -1842,6 +1847,9 @@
                 td.setEnsureStatusBarContrastWhenTransparent(
                         atd.getEnsureStatusBarContrastWhenTransparent());
             }
+            if (td.getStatusBarAppearance() == 0) {
+                td.setStatusBarAppearance(atd.getStatusBarAppearance());
+            }
             if (td.getNavigationBarColor() == 0) {
                 td.setNavigationBarColor(atd.getNavigationBarColor());
                 td.setEnsureNavigationBarContrastWhenTransparent(
@@ -2858,7 +2866,7 @@
                     getDisplayContent().getInsetsStateController().getRawInsetsState();
             for (int i = state.sourceSize() - 1; i >= 0; i--) {
                 final InsetsSource source = state.sourceAt(i);
-                if (source.insetsRoundedCornerFrame()) {
+                if (source.hasFlags(FLAG_INSETS_ROUNDED_CORNER)) {
                     animationBounds.inset(source.calculateVisibleInsets(animationBounds));
                 }
             }
@@ -3465,6 +3473,11 @@
                         top.mLetterboxUiController.getLetterboxPositionForVerticalReachability();
             }
         }
+        // User Aspect Ratio Settings is enabled if the app is not in SCM
+        info.topActivityEligibleForUserAspectRatioButton =
+                mWmService.mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled()
+                        && top != null && !info.topActivityInSizeCompat;
+        info.topActivityBoundsLetterboxed = top != null && top.areBoundsLetterboxed();
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/TaskPersister.java b/services/core/java/com/android/server/wm/TaskPersister.java
index 7e0e5a4..5ec0119 100644
--- a/services/core/java/com/android/server/wm/TaskPersister.java
+++ b/services/core/java/com/android/server/wm/TaskPersister.java
@@ -509,7 +509,7 @@
 
     private static boolean createParentDirectory(String filePath) {
         File parentDir = new File(filePath).getParentFile();
-        return parentDir.exists() || parentDir.mkdirs();
+        return parentDir.isDirectory() || parentDir.mkdir();
     }
 
     private static class TaskWriteQueueItem implements PersisterQueue.WriteQueueItem {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 2c582c1..5b3bbd5 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -78,6 +78,7 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.IRemoteCallback;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.Trace;
@@ -359,22 +360,30 @@
         mTransientLaunches.put(activity, restoreBelow);
         setTransientLaunchToChanges(activity);
 
-        if (restoreBelow != null) {
-            final Task transientRootTask = activity.getRootTask();
+        final Task transientRootTask = activity.getRootTask();
+        final WindowContainer<?> parent = restoreBelow != null ? restoreBelow.getParent()
+                : (transientRootTask != null ? transientRootTask.getParent() : null);
+        if (parent != null) {
             // Collect all visible tasks which can be occluded by the transient activity to
             // make sure they are in the participants so their visibilities can be updated when
             // finishing transition.
-            ((WindowContainer<?>) restoreBelow.getParent()).forAllTasks(t -> {
+            parent.forAllTasks(t -> {
+                // Skip transient-launch task
+                if (t == transientRootTask) return false;
                 if (t.isVisibleRequested() && !t.isAlwaysOnTop()
                         && !t.getWindowConfiguration().tasksAreFloating()) {
-                    if (t.isRootTask() && t != transientRootTask) {
+                    if (t.isRootTask()) {
                         mTransientHideTasks.add(t);
                     }
                     if (t.isLeafTask()) {
                         collect(t);
                     }
                 }
-                return t == restoreBelow;
+                return restoreBelow != null
+                        // Stop at the restoreBelow task
+                        ? t == restoreBelow
+                        // Or stop at the last visible task if no restore-below (new task)
+                        : (t.isRootTask() && t.fillsParent());
             });
             // Add FLAG_ABOVE_TRANSIENT_LAUNCH to the tree of transient-hide tasks,
             // so ChangeInfo#hasChanged() can return true to report the transition info.
@@ -606,12 +615,8 @@
             }
         }
         if (mParticipants.contains(wc)) return;
-        // Wallpaper is like in a static drawn state unless display may have changes, so exclude
-        // the case to reduce transition latency waiting for the unchanged wallpaper to redraw.
-        final boolean needSync = (!isWallpaper(wc) || mParticipants.contains(wc.mDisplayContent))
-                // Transient-hide may be hidden later, so no need to request redraw.
-                && !isInTransientHide(wc);
-        if (needSync) {
+        // Transient-hide may be hidden later, so no need to request redraw.
+        if (!isInTransientHide(wc)) {
             mSyncEngine.addToSyncSet(mSyncId, wc);
         }
         ChangeInfo info = mChanges.get(wc);
@@ -2689,6 +2694,26 @@
         });
     }
 
+    /**
+     * Returns {@code true} if the transition and the corresponding transaction should be applied
+     * on display thread. Currently, this only checks for display rotation change because the order
+     * of dispatching the new display info will be after requesting the windows to sync drawing.
+     * That avoids potential flickering of screen overlays (e.g. cutout, rounded corner). Also,
+     * because the display thread has a higher priority, it is faster to perform the configuration
+     * changes and window hierarchy traversal.
+     */
+    boolean shouldApplyOnDisplayThread() {
+        for (int i = mParticipants.size() - 1; i >= 0; --i) {
+            final DisplayContent dc = mParticipants.valueAt(i).asDisplayContent();
+            if (dc == null) continue;
+            final ChangeInfo changeInfo = mChanges.get(dc);
+            if (changeInfo != null && changeInfo.mRotation != dc.getRotation()) {
+                return Looper.myLooper() != mController.mAtm.mWindowManager.mH.getLooper();
+            }
+        }
+        return false;
+    }
+
     /** Applies the new configuration for the changed displays. */
     void applyDisplayChangeIfNeeded() {
         for (int i = mParticipants.size() - 1; i >= 0; --i) {
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index a539a48..79cb61b 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -588,15 +588,6 @@
     /** Sets the sync method for the display change. */
     private void setDisplaySyncMethod(@NonNull TransitionRequestInfo.DisplayChange displayChange,
             @NonNull Transition displayTransition, @NonNull DisplayContent displayContent) {
-        final int startRotation = displayChange.getStartRotation();
-        final int endRotation = displayChange.getEndRotation();
-        if (startRotation != endRotation && (startRotation + endRotation) % 2 == 0) {
-            // 180 degrees rotation change may not change screen size. So the clients may draw
-            // some frames before and after the display projection transaction is applied by the
-            // remote player. That may cause some buffers to show in different rotation. So use
-            // sync method to pause clients drawing until the projection transaction is applied.
-            mSyncEngine.setSyncMethod(displayTransition.getSyncId(), BLASTSyncEngine.METHOD_BLAST);
-        }
         final Rect startBounds = displayChange.getStartAbsBounds();
         final Rect endBounds = displayChange.getEndAbsBounds();
         if (startBounds == null || endBounds == null) return;
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index eb7b8c2..00bedcd 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -247,7 +247,7 @@
                 resources.getBoolean(
                         com.android.internal.R.bool.config_offsetWallpaperToCenterOfLargestDisplay);
         mIsLockscreenLiveWallpaperEnabled =
-                SystemProperties.getBoolean("persist.wm.debug.lockscreen_live_wallpaper", false);
+                SystemProperties.getBoolean("persist.wm.debug.lockscreen_live_wallpaper", true);
     }
 
     void resetLargestDisplay(Display display) {
@@ -308,29 +308,12 @@
         }
     }
 
-    private boolean shouldWallpaperBeVisible(WindowState wallpaperTarget) {
-        if (DEBUG_WALLPAPER) {
-            Slog.v(TAG, "Wallpaper vis: target " + wallpaperTarget + " prev="
-                    + mPrevWallpaperTarget);
-        }
-        return wallpaperTarget != null || mPrevWallpaperTarget != null;
-    }
-
     boolean isWallpaperTargetAnimating() {
         return mWallpaperTarget != null && mWallpaperTarget.isAnimating(TRANSITION | PARENTS)
                 && (mWallpaperTarget.mActivityRecord == null
                         || !mWallpaperTarget.mActivityRecord.isWaitingForTransitionStart());
     }
 
-    void updateWallpaperVisibility() {
-        final boolean visible = shouldWallpaperBeVisible(mWallpaperTarget);
-
-        for (int curTokenNdx = mWallpaperTokens.size() - 1; curTokenNdx >= 0; curTokenNdx--) {
-            final WallpaperWindowToken token = mWallpaperTokens.get(curTokenNdx);
-            token.setVisibility(visible);
-        }
-    }
-
     /**
      * Make one wallpaper visible, according to {@attr showHome}.
      * This is called during the keyguard unlocking transition
@@ -801,11 +784,20 @@
         result.setWallpaperTarget(wallpaperTarget);
     }
 
+    public void updateWallpaperTokens(boolean keyguardLocked) {
+        if (DEBUG_WALLPAPER) {
+            Slog.v(TAG, "Wallpaper vis: target " + mWallpaperTarget + " prev="
+                    + mPrevWallpaperTarget);
+        }
+        updateWallpaperTokens(mWallpaperTarget != null || mPrevWallpaperTarget != null,
+                keyguardLocked);
+    }
+
     /**
      * Change the visibility of the top wallpaper to {@param visibility} and hide all the others.
      */
-    private void updateWallpaperTokens(boolean visibility, boolean locked) {
-        WindowState topWallpaper = mFindResults.getTopWallpaper(locked);
+    private void updateWallpaperTokens(boolean visibility, boolean keyguardLocked) {
+        WindowState topWallpaper = mFindResults.getTopWallpaper(keyguardLocked);
         WallpaperWindowToken topWallpaperToken =
                 topWallpaper == null ? null : topWallpaper.mToken.asWallpaperToken();
         for (int curTokenNdx = mWallpaperTokens.size() - 1; curTokenNdx >= 0; curTokenNdx--) {
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 0152666..4bc4c26 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -4141,7 +4141,7 @@
                     getDisplayContent().getInsetsStateController().getSourceProviders();
             for (int i = providers.size(); i >= 0; i--) {
                 final InsetsSourceProvider insetProvider = providers.valueAt(i);
-                if (!insetProvider.getSource().insetsRoundedCornerFrame()) {
+                if (!insetProvider.getSource().hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)) {
                     return;
                 }
 
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index d9ff91f..0e19671 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -763,8 +763,6 @@
                 Settings.Secure.getUriFor(Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS);
         private final Uri mPolicyControlUri =
                 Settings.Global.getUriFor(Settings.Global.POLICY_CONTROL);
-        private final Uri mPointerLocationUri =
-                Settings.System.getUriFor(Settings.System.POINTER_LOCATION);
         private final Uri mForceDesktopModeOnExternalDisplaysUri = Settings.Global.getUriFor(
                         Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS);
         private final Uri mFreeformWindowUri = Settings.Global.getUriFor(
@@ -792,7 +790,6 @@
             resolver.registerContentObserver(mImmersiveModeConfirmationsUri, false, this,
                     UserHandle.USER_ALL);
             resolver.registerContentObserver(mPolicyControlUri, false, this, UserHandle.USER_ALL);
-            resolver.registerContentObserver(mPointerLocationUri, false, this, UserHandle.USER_ALL);
             resolver.registerContentObserver(mForceDesktopModeOnExternalDisplaysUri, false, this,
                     UserHandle.USER_ALL);
             resolver.registerContentObserver(mFreeformWindowUri, false, this, UserHandle.USER_ALL);
@@ -816,11 +813,6 @@
                 return;
             }
 
-            if (mPointerLocationUri.equals(uri)) {
-                updatePointerLocation();
-                return;
-            }
-
             if (mForceDesktopModeOnExternalDisplaysUri.equals(uri)) {
                 updateForceDesktopModeOnExternalDisplays();
                 return;
@@ -869,7 +861,6 @@
 
         void loadSettings() {
             updateSystemUiSettings(false /* handleChange */);
-            updatePointerLocation();
             updateMaximumObscuringOpacityForTouch();
         }
 
@@ -900,21 +891,6 @@
             }
         }
 
-        void updatePointerLocation() {
-            ContentResolver resolver = mContext.getContentResolver();
-            final boolean enablePointerLocation = Settings.System.getIntForUser(resolver,
-                    Settings.System.POINTER_LOCATION, 0, UserHandle.USER_CURRENT) != 0;
-
-            if (mPointerLocationEnabled == enablePointerLocation) {
-                return;
-            }
-            mPointerLocationEnabled = enablePointerLocation;
-            synchronized (mGlobalLock) {
-                mRoot.forAllDisplayPolicies(
-                        p -> p.setPointerLocationEnabled(mPointerLocationEnabled));
-            }
-        }
-
         void updateForceDesktopModeOnExternalDisplays() {
             ContentResolver resolver = mContext.getContentResolver();
             final boolean enableForceDesktopMode = Settings.Global.getInt(resolver,
@@ -4539,7 +4515,8 @@
                     return;
                 }
                 dc.mRemoteInsetsControlTarget.setRequestedVisibleTypes(requestedVisibleTypes);
-                dc.getInsetsStateController().onInsetsModified(dc.mRemoteInsetsControlTarget);
+                dc.getInsetsStateController().onRequestedVisibleTypesChanged(
+                        dc.mRemoteInsetsControlTarget);
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
@@ -7174,15 +7151,45 @@
 
     @Override
     public void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId) {
-        mContext.enforceCallingOrSelfPermission(REGISTER_WINDOW_MANAGER_LISTENERS,
-                "requestAppKeyboardShortcuts");
+        enforceRegisterWindowManagerListenersPermission("requestAppKeyboardShortcuts");
 
+        WindowState focusedWindow = getFocusedWindow();
+        if (focusedWindow == null || focusedWindow.mClient == null) {
+            notifyReceiverWithEmptyBundle(receiver);
+            return;
+        }
         try {
-            WindowState focusedWindow = getFocusedWindow();
-            if (focusedWindow != null && focusedWindow.mClient != null) {
-                getFocusedWindow().mClient.requestAppKeyboardShortcuts(receiver, deviceId);
-            }
+            focusedWindow.mClient.requestAppKeyboardShortcuts(receiver, deviceId);
         } catch (RemoteException e) {
+            notifyReceiverWithEmptyBundle(receiver);
+        }
+    }
+
+    @Override
+    public void requestImeKeyboardShortcuts(IResultReceiver receiver, int deviceId) {
+        enforceRegisterWindowManagerListenersPermission("requestImeKeyboardShortcuts");
+
+        WindowState imeWindow = mRoot.getCurrentInputMethodWindow();
+        if (imeWindow == null || imeWindow.mClient == null) {
+            notifyReceiverWithEmptyBundle(receiver);
+            return;
+        }
+        try {
+            imeWindow.mClient.requestAppKeyboardShortcuts(receiver, deviceId);
+        } catch (RemoteException e) {
+            notifyReceiverWithEmptyBundle(receiver);
+        }
+    }
+
+    private void enforceRegisterWindowManagerListenersPermission(String message) {
+        mContext.enforceCallingOrSelfPermission(REGISTER_WINDOW_MANAGER_LISTENERS, message);
+    }
+
+    private static void notifyReceiverWithEmptyBundle(IResultReceiver receiver) {
+        try {
+            receiver.send(0, Bundle.EMPTY);
+        } catch (RemoteException e) {
+            ProtoLog.e(WM_ERROR, "unable to call receiver for empty keyboard shortcuts");
         }
     }
 
@@ -8260,6 +8267,13 @@
         @Override
         public void addTrustedTaskOverlay(int taskId,
                 SurfaceControlViewHost.SurfacePackage overlay) {
+            if (overlay == null) {
+                throw new IllegalArgumentException("Invalid overlay passed in for task=" + taskId);
+            } else if (overlay.getSurfaceControl() == null
+                    || !overlay.getSurfaceControl().isValid()) {
+                throw new IllegalArgumentException(
+                        "Invalid overlay surfacecontrol passed in for task=" + taskId);
+            }
             synchronized (mGlobalLock) {
                 final Task task = mRoot.getRootTask(taskId);
                 if (task == null) {
@@ -8272,6 +8286,13 @@
         @Override
         public void removeTrustedTaskOverlay(int taskId,
                 SurfaceControlViewHost.SurfacePackage overlay) {
+            if (overlay == null) {
+                throw new IllegalArgumentException("Invalid overlay passed in for task=" + taskId);
+            } else if (overlay.getSurfaceControl() == null
+                    || !overlay.getSurfaceControl().isValid()) {
+                throw new IllegalArgumentException(
+                        "Invalid overlay surfacecontrol passed in for task=" + taskId);
+            }
             synchronized (mGlobalLock) {
                 final Task task = mRoot.getRootTask(taskId);
                 if (task == null) {
@@ -8737,24 +8758,22 @@
         final int sanitizedType = sanitizeWindowType(session, displayId, windowToken, type);
         final InputApplicationHandle applicationHandle;
         final String name;
-        final InputChannel clientChannel;
+        Objects.requireNonNull(outInputChannel);
         synchronized (mGlobalLock) {
             EmbeddedWindowController.EmbeddedWindow win =
                     new EmbeddedWindowController.EmbeddedWindow(session, this, window,
                             mInputToWindowMap.get(hostInputToken), callingUid, callingPid,
                             sanitizedType, displayId, focusGrantToken, inputHandleName,
                             (flags & FLAG_NOT_FOCUSABLE) == 0);
-            clientChannel = win.openInputChannel();
-            mEmbeddedWindowController.add(clientChannel.getToken(), win);
+            win.openInputChannel(outInputChannel);
+            mEmbeddedWindowController.add(outInputChannel.getToken(), win);
             applicationHandle = win.getApplicationHandle();
             name = win.toString();
         }
 
-        updateInputChannel(clientChannel.getToken(), callingUid, callingPid, displayId, surface,
+        updateInputChannel(outInputChannel.getToken(), callingUid, callingPid, displayId, surface,
                 name, applicationHandle, flags, privateFlags, inputFeatures, sanitizedType,
                 null /* region */, window);
-
-        clientChannel.copyTo(outInputChannel);
     }
 
     boolean transferEmbeddedTouchFocusToHost(IWindow embeddedWindow) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index a153708..05e858d 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -1006,13 +1006,16 @@
                     runSetBooleanFlag(pw, mLetterboxConfiguration
                             ::setTranslucentLetterboxingOverrideEnabled);
                     break;
+                case "--isUserAppAspectRatioSettingsEnabled":
+                    runSetBooleanFlag(pw, mLetterboxConfiguration
+                            ::setUserAppAspectRatioSettingsOverrideEnabled);
+                    break;
                 case "--isCameraCompatRefreshEnabled":
-                    runSetBooleanFlag(pw, enabled -> mLetterboxConfiguration
-                            .setCameraCompatRefreshEnabled(enabled));
+                    runSetBooleanFlag(pw, mLetterboxConfiguration::setCameraCompatRefreshEnabled);
                     break;
                 case "--isCameraCompatRefreshCycleThroughStopEnabled":
-                    runSetBooleanFlag(pw, enabled -> mLetterboxConfiguration
-                            .setCameraCompatRefreshCycleThroughStopEnabled(enabled));
+                    runSetBooleanFlag(pw,
+                            mLetterboxConfiguration::setCameraCompatRefreshCycleThroughStopEnabled);
                     break;
                 default:
                     getErrPrintWriter().println(
@@ -1084,6 +1087,9 @@
                     case "isTranslucentLetterboxingEnabled":
                         mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
                         break;
+                    case "isUserAppAspectRatioSettingsEnabled":
+                        mLetterboxConfiguration.resetUserAppAspectRatioSettingsEnabled();
+                        break;
                     case "isCameraCompatRefreshEnabled":
                         mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
                         break;
@@ -1194,6 +1200,7 @@
             mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
             mLetterboxConfiguration.resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
             mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
+            mLetterboxConfiguration.resetUserAppAspectRatioSettingsEnabled();
             mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
             mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
         }
@@ -1249,7 +1256,6 @@
                     + mLetterboxConfiguration.isCameraCompatRefreshEnabled());
             pw.println("    Refresh using \"stopped -> resumed\" cycle: "
                     + mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled());
-
             pw.println("Background type: "
                     + LetterboxConfiguration.letterboxBackgroundTypeToString(
                             mLetterboxConfiguration.getLetterboxBackgroundType()));
@@ -1259,12 +1265,10 @@
                     + mLetterboxConfiguration.getLetterboxBackgroundWallpaperBlurRadius());
             pw.println("    Wallpaper dark scrim alpha: "
                     + mLetterboxConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha());
-
-            if (mLetterboxConfiguration.isTranslucentLetterboxingEnabled()) {
-                pw.println("Letterboxing for translucent activities: enabled");
-            } else {
-                pw.println("Letterboxing for translucent activities: disabled");
-            }
+            pw.println("Is letterboxing for translucent activities enabled: "
+                    + mLetterboxConfiguration.isTranslucentLetterboxingEnabled());
+            pw.println("Is the user aspect ratio settings enabled: "
+                    + mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled());
         }
         return 0;
     }
@@ -1462,6 +1466,8 @@
         pw.println("        unresizable apps.");
         pw.println("      --isTranslucentLetterboxingEnabled [true|1|false|0]");
         pw.println("        Whether letterboxing for translucent activities is enabled.");
+        pw.println("      --isUserAppAspectRatioSettingsEnabled [true|1|false|0]");
+        pw.println("        Whether user aspect ratio settings are enabled.");
         pw.println("      --isCameraCompatRefreshEnabled [true|1|false|0]");
         pw.println("        Whether camera compatibility refresh is enabled.");
         pw.println("      --isCameraCompatRefreshCycleThroughStopEnabled [true|1|false|0]");
@@ -1473,7 +1479,7 @@
         pw.println("      |horizontalPositionMultiplier|verticalPositionMultiplier");
         pw.println("      |isHorizontalReachabilityEnabled|isVerticalReachabilityEnabled");
         pw.println("      |isEducationEnabled||defaultPositionMultiplierForHorizontalReachability");
-        pw.println("      |isTranslucentLetterboxingEnabled");
+        pw.println("      |isTranslucentLetterboxingEnabled|isUserAppAspectRatioSettingsEnabled");
         pw.println("      |defaultPositionMultiplierForVerticalReachability]");
         pw.println("    Resets overrides to default values for specified properties separated");
         pw.println("    by space, e.g. 'reset-letterbox-style aspectRatio cornerRadius'.");
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index a2f7ba4..31918f4 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -321,9 +321,18 @@
                     applyTransaction(wct, -1 /*syncId*/, null /*transition*/, caller);
                     return transition.getToken();
                 }
-                transition.start();
                 transition.mLogger.mStartWCT = wct;
-                applyTransaction(wct, -1 /*syncId*/, transition, caller);
+                if (transition.shouldApplyOnDisplayThread()) {
+                    mService.mH.post(() -> {
+                        synchronized (mService.mGlobalLock) {
+                            transition.start();
+                            applyTransaction(wct, -1 /* syncId */, transition, caller);
+                        }
+                    });
+                } else {
+                    transition.start();
+                    applyTransaction(wct, -1 /* syncId */, transition, caller);
+                }
                 // Since the transition is already provided, it means WMCore is determining the
                 // "readiness lifecycle" outside the provided transaction, so don't set ready here.
                 return transition.getToken();
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 5de5bab..0d4c2d6 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1445,6 +1445,8 @@
             Slog.v(TAG_WM, "Win " + this + " config changed: " + getConfiguration());
         }
 
+        final boolean dragResizingChanged = !mDragResizingChangeReported && isDragResizeChanged();
+
         final boolean attachedFrameChanged = LOCAL_LAYOUT
                 && mLayoutAttached && getParentWindow().frameChanged();
 
@@ -1458,6 +1460,7 @@
         if (didFrameInsetsChange
                 || configChanged
                 || insetsChanged
+                || dragResizingChanged
                 || shouldSendRedrawForSync()
                 || attachedFrameChanged) {
             ProtoLog.v(WM_DEBUG_RESIZE,
@@ -1478,7 +1481,8 @@
 
             // Reset the drawn state if the window need to redraw for the change, so the transition
             // can wait until it has finished drawing to start.
-            if ((configChanged || getOrientationChanging()) && isVisibleRequested()) {
+            if ((configChanged || getOrientationChanging() || dragResizingChanged)
+                    && isVisibleRequested()) {
                 winAnimator.mDrawState = DRAW_PENDING;
                 if (mActivityRecord != null) {
                     mActivityRecord.clearAllDrawn();
@@ -5661,7 +5665,7 @@
         }
         if (mIsWallpaper) {
             // TODO(b/233286785): Add sync support to wallpaper.
-            return false;
+            return true;
         }
         // In the WindowContainer implementation we immediately mark ready
         // since a generic WindowContainer only needs to wait for its
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index f0d718a..c065cb5 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -279,7 +279,7 @@
     base::Result<std::unique_ptr<InputChannel>> createInputChannel(const std::string& name);
     base::Result<std::unique_ptr<InputChannel>> createInputMonitor(int32_t displayId,
                                                                    const std::string& name,
-                                                                   int32_t pid);
+                                                                   gui::Pid pid);
     status_t removeInputChannel(const sp<IBinder>& connectionToken);
     status_t pilferPointers(const sp<IBinder>& token);
 
@@ -330,9 +330,9 @@
     void notifyConfigurationChanged(nsecs_t when) override;
     // ANR-related callbacks -- start
     void notifyNoFocusedWindowAnr(const std::shared_ptr<InputApplicationHandle>& handle) override;
-    void notifyWindowUnresponsive(const sp<IBinder>& token, std::optional<int32_t> pid,
+    void notifyWindowUnresponsive(const sp<IBinder>& token, std::optional<gui::Pid> pid,
                                   const std::string& reason) override;
-    void notifyWindowResponsive(const sp<IBinder>& token, std::optional<int32_t> pid) override;
+    void notifyWindowResponsive(const sp<IBinder>& token, std::optional<gui::Pid> pid) override;
     // ANR-related callbacks -- end
     void notifyInputChannelBroken(const sp<IBinder>& token) override;
     void notifyFocusChanged(const sp<IBinder>& oldToken, const sp<IBinder>& newToken) override;
@@ -355,6 +355,8 @@
     void onPointerDownOutsideFocus(const sp<IBinder>& touchedToken) override;
     void setPointerCapture(const PointerCaptureRequest& request) override;
     void notifyDropWindow(const sp<IBinder>& token, float x, float y) override;
+    void notifyDeviceInteraction(int32_t deviceId, nsecs_t timestamp,
+                                 const std::set<gui::Uid>& uids) override;
 
     /* --- PointerControllerPolicyInterface implementation --- */
 
@@ -539,7 +541,7 @@
 }
 
 base::Result<std::unique_ptr<InputChannel>> NativeInputManager::createInputMonitor(
-        int32_t displayId, const std::string& name, int32_t pid) {
+        int32_t displayId, const std::string& name, gui::Pid pid) {
     ATRACE_CALL();
     return mInputManager->getDispatcher().createInputMonitor(displayId, name, pid);
 }
@@ -883,7 +885,7 @@
 }
 
 void NativeInputManager::notifyWindowUnresponsive(const sp<IBinder>& token,
-                                                  std::optional<int32_t> pid,
+                                                  std::optional<gui::Pid> pid,
                                                   const std::string& reason) {
 #if DEBUG_INPUT_DISPATCHER_POLICY
     ALOGD("notifyWindowUnresponsive");
@@ -897,12 +899,12 @@
     ScopedLocalRef<jstring> reasonObj(env, env->NewStringUTF(reason.c_str()));
 
     env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyWindowUnresponsive, tokenObj,
-                        pid.value_or(0), pid.has_value(), reasonObj.get());
+                        pid.value_or(gui::Pid{0}).val(), pid.has_value(), reasonObj.get());
     checkAndClearExceptionFromCallback(env, "notifyWindowUnresponsive");
 }
 
 void NativeInputManager::notifyWindowResponsive(const sp<IBinder>& token,
-                                                std::optional<int32_t> pid) {
+                                                std::optional<gui::Pid> pid) {
 #if DEBUG_INPUT_DISPATCHER_POLICY
     ALOGD("notifyWindowResponsive");
 #endif
@@ -914,7 +916,7 @@
     jobject tokenObj = javaObjectForIBinder(env, token);
 
     env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyWindowResponsive, tokenObj,
-                        pid.value_or(0), pid.has_value());
+                        pid.value_or(gui::Pid{0}).val(), pid.has_value());
     checkAndClearExceptionFromCallback(env, "notifyWindowResponsive");
 }
 
@@ -966,6 +968,15 @@
     checkAndClearExceptionFromCallback(env, "notifyDropWindow");
 }
 
+void NativeInputManager::notifyDeviceInteraction(int32_t deviceId, nsecs_t timestamp,
+                                                 const std::set<gui::Uid>& uids) {
+    static const bool ENABLE_INPUT_DEVICE_USAGE_METRICS =
+            sysprop::InputProperties::enable_input_device_usage_metrics().value_or(true);
+    if (!ENABLE_INPUT_DEVICE_USAGE_METRICS) return;
+
+    mInputManager->getMetricsCollector().notifyDeviceInteraction(deviceId, timestamp, uids);
+}
+
 void NativeInputManager::notifySensorEvent(int32_t deviceId, InputDeviceSensorType sensorType,
                                            InputDeviceSensorAccuracy accuracy, nsecs_t timestamp,
                                            const std::vector<float>& values) {
@@ -1596,8 +1607,8 @@
 }
 
 PointerIconStyle NativeInputManager::getDefaultStylusIconId() {
-    // TODO: add resource for default stylus icon and change this
-    return PointerIconStyle::TYPE_CROSSHAIR;
+    // Use the empty icon as the default pointer icon for a hovering stylus.
+    return PointerIconStyle::TYPE_NULL;
 }
 
 PointerIconStyle NativeInputManager::getCustomPointerIconId() {
@@ -1808,7 +1819,7 @@
     std::string name = nameChars.c_str();
 
     base::Result<std::unique_ptr<InputChannel>> inputChannel =
-            im->createInputMonitor(displayId, name, pid);
+            im->createInputMonitor(displayId, name, gui::Pid{pid});
 
     if (!inputChannel.ok()) {
         std::string message = inputChannel.error().message();
@@ -1853,7 +1864,8 @@
                                      jint pid, jint uid, jboolean hasPermission, jint displayId) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
-    return im->getInputManager()->getDispatcher().setInTouchMode(inTouchMode, pid, uid,
+    return im->getInputManager()->getDispatcher().setInTouchMode(inTouchMode, gui::Pid{pid},
+                                                                 gui::Uid{static_cast<uid_t>(uid)},
                                                                  hasPermission, displayId);
 }
 
@@ -1869,7 +1881,7 @@
                                    jint timeoutMillis, jint policyFlags) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
-    const std::optional<int32_t> targetUid = injectIntoUid ? std::make_optional(uid) : std::nullopt;
+    const auto targetUid = injectIntoUid ? std::make_optional<gui::Uid>(uid) : std::nullopt;
     // static_cast is safe because the value was already checked at the Java layer
     InputEventInjectionSync mode = static_cast<InputEventInjectionSync>(syncMode);
 
@@ -2214,13 +2226,25 @@
             jCapability |= env->GetStaticIntField(gLightClassInfo.clazz,
                                                   gLightClassInfo.lightCapabilityColorRgb);
         }
+
+        ScopedLocalRef<jintArray> jPreferredBrightnessLevels{env};
+        if (!lightInfo.preferredBrightnessLevels.empty()) {
+            std::vector<int32_t> vec;
+            for (auto it : lightInfo.preferredBrightnessLevels) {
+              vec.push_back(ftl::to_underlying(it));
+            }
+            jPreferredBrightnessLevels.reset(env->NewIntArray(vec.size()));
+            env->SetIntArrayRegion(jPreferredBrightnessLevels.get(), 0, vec.size(), vec.data());
+        }
+
         ScopedLocalRef<jobject> lightObj(env,
                                          env->NewObject(gLightClassInfo.clazz,
                                                         gLightClassInfo.constructor,
                                                         static_cast<jint>(lightInfo.id),
                                                         env->NewStringUTF(lightInfo.name.c_str()),
                                                         static_cast<jint>(lightInfo.ordinal),
-                                                        jTypeId, jCapability));
+                                                        jTypeId, jCapability,
+                                                        jPreferredBrightnessLevels.get()));
         // Add light object to list
         env->CallBooleanMethod(jLights, gArrayListClassInfo.add, lightObj.get());
     }
@@ -2846,7 +2870,7 @@
     FIND_CLASS(gLightClassInfo.clazz, "android/hardware/lights/Light");
     gLightClassInfo.clazz = jclass(env->NewGlobalRef(gLightClassInfo.clazz));
     GET_METHOD_ID(gLightClassInfo.constructor, gLightClassInfo.clazz, "<init>",
-                  "(ILjava/lang/String;III)V");
+                  "(ILjava/lang/String;III[I)V");
 
     gLightClassInfo.clazz = jclass(env->NewGlobalRef(gLightClassInfo.clazz));
     gLightClassInfo.lightTypeInput =
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 29275ef..fe913b9 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -13598,6 +13598,9 @@
                     : SecurityLog.TAG_USER_RESTRICTION_REMOVED;
             SecurityLog.writeEvent(eventTag, caller.getPackageName(), caller.getUserId(), key);
         }
+
+        Slogf.i(LOG_TAG, "Changing user restriction %s to: %b caller: %s",
+                key, enabled, caller.toString());
     }
 
     private void saveUserRestrictionsLocked(int userId) {
@@ -16226,6 +16229,11 @@
         }
 
         @Override
+        public ComponentName getDeviceOwnerComponent(boolean callingUserOnly) {
+            return DevicePolicyManagerService.this.getDeviceOwnerComponent(callingUserOnly);
+        }
+
+        @Override
         public int getDeviceOwnerUserId() {
             return DevicePolicyManagerService.this.getDeviceOwnerUserId();
         }
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 1792964..f2a8fdd 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -3258,6 +3258,12 @@
                 Slog.d(TAG, "ContentCaptureService disabled because resource is not overlaid");
                 return;
             }
+            if (!deviceHasConfigString(context, R.string.config_defaultContentProtectionService)) {
+                Slog.d(
+                        TAG,
+                        "ContentProtectionService disabled because resource is not overlaid,"
+                            + " ContentCaptureService still enabled");
+            }
         }
 
         t.traceBegin("StartContentCaptureService");
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index a6ada4d..869497c 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -20,6 +20,7 @@
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
 
 import static com.android.internal.inputmethod.SoftInputShowHideReason.HIDE_SOFT_INPUT;
+import static com.android.internal.inputmethod.SoftInputShowHideReason.HIDE_SWITCH_USER;
 import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_SOFT_INPUT;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_EXPLICIT;
@@ -43,6 +44,7 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.internal.inputmethod.InputBindResult;
 import com.android.internal.inputmethod.StartInputFlags;
 import com.android.internal.inputmethod.StartInputReason;
@@ -165,6 +167,29 @@
         verify(mMockImeTargetVisibilityPolicy).removeImeScreenshot(eq(Display.DEFAULT_DISPLAY));
     }
 
+    @Test
+    public void testApplyImeVisibility_hideImeWhenUnbinding() {
+        mInputMethodManagerService.setAttachedClientForTesting(null);
+        startInputOrWindowGainedFocus(mWindowToken, SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+        ExtendedMockito.spyOn(mVisibilityApplier);
+
+        synchronized (ImfLock.class) {
+            // Simulate the system hides the IME when switching IME services in different users.
+            // (e.g. unbinding the IME from the current user to the profile user)
+            final int displayIdToShowIme = mInputMethodManagerService.getDisplayIdToShowImeLocked();
+            mInputMethodManagerService.hideCurrentInputLocked(mWindowToken, null, 0, null,
+                    HIDE_SWITCH_USER);
+            mInputMethodManagerService.onUnbindCurrentMethodByReset();
+
+            // Expects applyImeVisibility() -> hideIme() will be called to notify WM for syncing
+            // the IME hidden state.
+            verify(mVisibilityApplier).applyImeVisibility(eq(mWindowToken), any(),
+                    eq(STATE_HIDE_IME));
+            verify(mInputMethodManagerService.mWindowManagerInternal).hideIme(
+                    eq(mWindowToken), eq(displayIdToShowIme), eq(null));
+        }
+    }
+
     private InputBindResult startInputOrWindowGainedFocus(IBinder windowToken, int softInputMode) {
         return mInputMethodManagerService.startInputOrWindowGainedFocus(
                 StartInputReason.WINDOW_FOCUS_GAIN /* startInputReason */,
diff --git a/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/CrossUserPackageVisibilityTests.java b/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/CrossUserPackageVisibilityTests.java
index e33ca77..b7a0cf3 100644
--- a/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/CrossUserPackageVisibilityTests.java
+++ b/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/CrossUserPackageVisibilityTests.java
@@ -138,6 +138,14 @@
     }
 
     @Test
+    public void testGetUserMinAspectRatio_withCrossUserId() {
+        final int crossUserId = UserHandle.myUserId() + 1;
+        assertThrows(SecurityException.class,
+                () -> mIPackageManager.getUserMinAspectRatio(
+                        mInstrumentation.getContext().getPackageName(), crossUserId));
+    }
+
+    @Test
     public void testIsPackageSignedByKeySet_cannotDetectCrossUserPkg() throws Exception {
         final KeySet keySet = mIPackageManager.getSigningKeySet(mContext.getPackageName());
         assertThrows(IllegalArgumentException.class,
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java
index 435f0d7..a805e5c 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java
@@ -20,6 +20,7 @@
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 
 import static java.lang.reflect.Modifier.isFinal;
@@ -138,7 +139,7 @@
                 .setSecondaryCpuAbiString("secondaryCpuAbiString")
                 .setCpuAbiOverrideString("cpuAbiOverrideString")
                 .build();
-        pri.populateUsers(new int[] {
+        pri.populateUsers(new int[]{
                 1, 2, 3, 4, 5
         }, setting);
         Assert.assertNotNull(pri.mBroadcastUsers);
@@ -150,7 +151,7 @@
         pri.mBroadcastUsers = null;
         final int EXCLUDED_USER_ID = 4;
         setting.setInstantApp(true, EXCLUDED_USER_ID);
-        pri.populateUsers(new int[] {
+        pri.populateUsers(new int[]{
                 1, 2, 3, EXCLUDED_USER_ID, 5
         }, setting);
         Assert.assertNotNull(pri.mBroadcastUsers);
@@ -164,8 +165,8 @@
 
     @Test
     public void testPartitions() {
-        String[] partitions = { "system", "vendor", "odm", "oem", "product", "system_ext" };
-        String[] appdir = { "app", "priv-app" };
+        String[] partitions = {"system", "vendor", "odm", "oem", "product", "system_ext"};
+        String[] appdir = {"app", "priv-app"};
         for (int i = 0; i < partitions.length; i++) {
             final ScanPartition scanPartition =
                     PackageManagerService.SYSTEM_PARTITIONS.get(i);
@@ -425,10 +426,10 @@
     private String displayName(Method m) {
         String r = m.getName();
         String p = Arrays.toString(m.getGenericParameterTypes())
-                   .replaceAll("([a-zA-Z0-9]+\\.)+", "")
-                   .replace("class ", "")
-                   .replaceAll("^\\[", "(")
-                   .replaceAll("\\]$", ")");
+                .replaceAll("([a-zA-Z0-9]+\\.)+", "")
+                .replace("class ", "")
+                .replaceAll("^\\[", "(")
+                .replaceAll("\\]$", ")");
         return r + p;
     }
 
@@ -612,4 +613,22 @@
             runShellCommand("pm uninstall " + TEST_PKG_NAME);
         }
     }
+
+    @Test
+    public void testSetUserMinAspectRatio_samePackage_succeeds() throws Exception {
+        mIPackageManager.setUserMinAspectRatio(PACKAGE_NAME, UserHandle.myUserId(),
+                PackageManager.USER_MIN_ASPECT_RATIO_UNSET);
+        // Invoking setUserMinAspectRatio on the same package shouldn't get any exception.
+    }
+
+    @Test
+    public void testSetUserMinAspectRatio_differentPackage_fails() {
+        final File testApk = new File(TEST_DATA_PATH, TEST_APP_APK);
+        runShellCommand("pm install " + testApk);
+        assertThrows(SecurityException.class, () -> {
+            mIPackageManager.setUserMinAspectRatio(TEST_PKG_NAME, UserHandle.myUserId(),
+                    PackageManager.USER_MIN_ASPECT_RATIO_UNSET);
+        });
+        runShellCommand("pm uninstall " + TEST_PKG_NAME);
+    }
 }
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
index 836f858..16fb012 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@@ -971,7 +971,7 @@
         origPkgSetting01.setUserState(0, 100, 1, true, false, false, false, 0, null, false,
                 false, "lastDisabledCaller", new ArraySet<>(new String[]{"enabledComponent1"}),
                 new ArraySet<>(new String[]{"disabledComponent1"}), 0, 0, "harmfulAppWarning",
-                "splashScreenTheme", 1000L);
+                "splashScreenTheme", 1000L, PackageManager.USER_MIN_ASPECT_RATIO_UNSET);
         final PersistableBundle appExtras1 = createPersistableBundle(
                 PACKAGE_NAME_1, 1L, 0.01, true, "appString1");
         final PersistableBundle launcherExtras1 = createPersistableBundle(
@@ -1638,7 +1638,8 @@
                 : oldUserState.getSharedLibraryOverlayPaths() == null)
                 && userState.getSplashScreenTheme().equals(
                         oldUserState.getSplashScreenTheme())
-                && userState.getUninstallReason() == oldUserState.getUninstallReason();
+                && userState.getUninstallReason() == oldUserState.getUninstallReason()
+                && userState.getMinAspectRatio() == oldUserState.getMinAspectRatio();
     }
 
     private SharedUserSetting createSharedUserSetting(Settings settings, String userName,
diff --git a/services/tests/displayservicetests/Android.bp b/services/tests/displayservicetests/Android.bp
new file mode 100644
index 0000000..f1ff338
--- /dev/null
+++ b/services/tests/displayservicetests/Android.bp
@@ -0,0 +1,65 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+// Include all test java files.
+filegroup {
+    name: "displayservicetests-sources",
+    srcs: [
+        "src/**/*.java",
+    ],
+}
+
+android_test {
+    name: "DisplayServiceTests",
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    libs: [
+        "android.test.mock",
+    ],
+
+    static_libs: [
+        "androidx.test.ext.junit",
+        "display-core-libs",
+        "frameworks-base-testutils",
+        "junit",
+        "junit-params",
+        "platform-compat-test-rules",
+        "platform-test-annotations",
+        "services.core",
+        "servicestests-utils",
+    ],
+
+    defaults: [
+        "modules-utils-testable-device-config-defaults",
+    ],
+
+    platform_apis: true,
+    test_suites: [
+        "device-tests",
+        "automotive-tests",
+    ],
+
+    certificate: "platform",
+
+    dxflags: ["--multi-dex"],
+
+    optimize: {
+        enabled: false,
+    },
+}
+
+java_library {
+    name: "display-core-libs",
+    srcs: [
+        "src/com/android/server/display/TestUtils.java",
+    ],
+}
diff --git a/services/tests/displayservicetests/AndroidManifest.xml b/services/tests/displayservicetests/AndroidManifest.xml
new file mode 100644
index 0000000..d2bd10d
--- /dev/null
+++ b/services/tests/displayservicetests/AndroidManifest.xml
@@ -0,0 +1,45 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.frameworks.displayservicetests">
+
+    <!--
+    Insert permissions here. eg:
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    -->
+    <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS" />
+    <uses-permission android:name="android.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS" />
+    <uses-permission android:name="android.permission.DEVICE_POWER" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+    <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" />
+    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" />
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+    <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
+    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
+    <application android:debuggable="true"
+                 android:testOnly="true">
+        <uses-library android:name="android.test.mock" android:required="true" />
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:label="Display Service Tests"
+        android:targetPackage="com.android.frameworks.displayservicetests" />
+</manifest>
diff --git a/services/tests/displayservicetests/AndroidTest.xml b/services/tests/displayservicetests/AndroidTest.xml
new file mode 100644
index 0000000..2985f98
--- /dev/null
+++ b/services/tests/displayservicetests/AndroidTest.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.
+-->
+
+<configuration description="Runs Display Service Tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="install-arg" value="-t" />
+        <option name="test-file-name" value="DisplayServiceTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="DisplayServiceTests" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="com.android.frameworks.displayservicetests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false" />
+    </test>
+</configuration>
diff --git a/services/tests/displayservicetests/TEST_MAPPING b/services/tests/displayservicetests/TEST_MAPPING
new file mode 100644
index 0000000..d865519
--- /dev/null
+++ b/services/tests/displayservicetests/TEST_MAPPING
@@ -0,0 +1,13 @@
+{
+  "presubmit": [
+    {
+      "name": "DisplayServiceTests",
+      "options": [
+        {"include-filter": "com.android.server.display"},
+        {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+        {"exclude-annotation": "androidx.test.filters.FlakyTest"},
+        {"exclude-annotation": "org.junit.Ignore"}
+      ]
+    }
+  ]
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/AmbientBrightnessStatsTrackerTest.java b/services/tests/displayservicetests/src/com/android/server/display/AmbientBrightnessStatsTrackerTest.java
new file mode 100644
index 0000000..7333bc7
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/AmbientBrightnessStatsTrackerTest.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright 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.server.display;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.hardware.display.AmbientBrightnessDayStats;
+import android.os.SystemClock;
+import android.os.UserManager;
+
+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;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AmbientBrightnessStatsTrackerTest {
+
+    private TestInjector mTestInjector;
+
+    @Before
+    public void setUp() {
+        mTestInjector = new TestInjector();
+    }
+
+    @Test
+    public void testBrightnessStatsTrackerOverSingleDay() {
+        AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
+        ArrayList<AmbientBrightnessDayStats> userStats;
+        float[] expectedStats;
+        // Test case where no user data
+        userStats = statsTracker.getUserStats(0);
+        assertNull(userStats);
+        // Test after adding some user data
+        statsTracker.start();
+        statsTracker.add(0, 0);
+        mTestInjector.incrementTime(1000);
+        statsTracker.stop();
+        userStats = statsTracker.getUserStats(0);
+        assertEquals(1, userStats.size());
+        assertEquals(mTestInjector.getLocalDate(), userStats.get(0).getLocalDate());
+        expectedStats = getEmptyStatsArray();
+        expectedStats[0] = 1;
+        assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
+        // Test after adding some more user data
+        statsTracker.start();
+        statsTracker.add(0, 0.05f);
+        mTestInjector.incrementTime(1000);
+        statsTracker.add(0, 0.2f);
+        mTestInjector.incrementTime(1500);
+        statsTracker.add(0, 50000);
+        mTestInjector.incrementTime(2500);
+        statsTracker.stop();
+        userStats = statsTracker.getUserStats(0);
+        assertEquals(1, userStats.size());
+        assertEquals(mTestInjector.getLocalDate(), userStats.get(0).getLocalDate());
+        expectedStats = getEmptyStatsArray();
+        expectedStats[0] = 2;
+        expectedStats[1] = 1.5f;
+        expectedStats[11] = 2.5f;
+        assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
+    }
+
+    @Test
+    public void testBrightnessStatsTrackerOverMultipleDays() {
+        AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
+        ArrayList<AmbientBrightnessDayStats> userStats;
+        float[] expectedStats;
+        // Add data for day 1
+        statsTracker.start();
+        statsTracker.add(0, 0.05f);
+        mTestInjector.incrementTime(1000);
+        statsTracker.add(0, 0.2f);
+        mTestInjector.incrementTime(1500);
+        statsTracker.add(0, 1);
+        mTestInjector.incrementTime(2500);
+        statsTracker.stop();
+        // Add data for day 2
+        mTestInjector.incrementDate(1);
+        statsTracker.start();
+        statsTracker.add(0, 0);
+        mTestInjector.incrementTime(3500);
+        statsTracker.add(0, 5);
+        mTestInjector.incrementTime(5000);
+        statsTracker.stop();
+        // Test that the data is tracked as expected
+        userStats = statsTracker.getUserStats(0);
+        assertEquals(2, userStats.size());
+        assertEquals(mTestInjector.getLocalDate().minusDays(1), userStats.get(0).getLocalDate());
+        expectedStats = getEmptyStatsArray();
+        expectedStats[0] = 1;
+        expectedStats[1] = 1.5f;
+        expectedStats[3] = 2.5f;
+        assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
+        assertEquals(mTestInjector.getLocalDate(), userStats.get(1).getLocalDate());
+        expectedStats = getEmptyStatsArray();
+        expectedStats[0] = 3.5f;
+        expectedStats[4] = 5;
+        assertArrayEquals(expectedStats, userStats.get(1).getStats(), 0);
+    }
+
+    @Test
+    public void testBrightnessStatsTrackerOverMultipleUsers() {
+        AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
+        ArrayList<AmbientBrightnessDayStats> userStats;
+        float[] expectedStats;
+        // Add data for user 1
+        statsTracker.start();
+        statsTracker.add(0, 0.05f);
+        mTestInjector.incrementTime(1000);
+        statsTracker.add(0, 0.2f);
+        mTestInjector.incrementTime(1500);
+        statsTracker.add(0, 1);
+        mTestInjector.incrementTime(2500);
+        statsTracker.stop();
+        // Add data for user 2
+        mTestInjector.incrementDate(1);
+        statsTracker.start();
+        statsTracker.add(1, 0);
+        mTestInjector.incrementTime(3500);
+        statsTracker.add(1, 5);
+        mTestInjector.incrementTime(5000);
+        statsTracker.stop();
+        // Test that the data is tracked as expected
+        userStats = statsTracker.getUserStats(0);
+        assertEquals(1, userStats.size());
+        assertEquals(mTestInjector.getLocalDate().minusDays(1), userStats.get(0).getLocalDate());
+        expectedStats = getEmptyStatsArray();
+        expectedStats[0] = 1;
+        expectedStats[1] = 1.5f;
+        expectedStats[3] = 2.5f;
+        assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
+        userStats = statsTracker.getUserStats(1);
+        assertEquals(1, userStats.size());
+        assertEquals(mTestInjector.getLocalDate(), userStats.get(0).getLocalDate());
+        expectedStats = getEmptyStatsArray();
+        expectedStats[0] = 3.5f;
+        expectedStats[4] = 5;
+        assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
+    }
+
+    @Test
+    public void testBrightnessStatsTrackerOverMaxDays() {
+        AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
+        ArrayList<AmbientBrightnessDayStats> userStats;
+        // Add 10 extra days of data over the buffer limit
+        for (int i = 0; i < AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK + 10; i++) {
+            mTestInjector.incrementDate(1);
+            statsTracker.start();
+            statsTracker.add(0, 10);
+            mTestInjector.incrementTime(1000);
+            statsTracker.add(0, 20);
+            mTestInjector.incrementTime(1000);
+            statsTracker.stop();
+        }
+        // Assert that we are only tracking last "MAX_DAYS_TO_TRACK"
+        userStats = statsTracker.getUserStats(0);
+        assertEquals(AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK, userStats.size());
+        LocalDate runningDate = mTestInjector.getLocalDate();
+        for (int i = AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK - 1; i >= 0; i--) {
+            assertEquals(runningDate, userStats.get(i).getLocalDate());
+            runningDate = runningDate.minusDays(1);
+        }
+    }
+
+    @Test
+    public void testReadAmbientBrightnessStats() throws IOException {
+        AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
+        LocalDate date = mTestInjector.getLocalDate();
+        ArrayList<AmbientBrightnessDayStats> userStats;
+        String statsFile =
+                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\r\n"
+                        + "<ambient-brightness-stats>\r\n"
+                        // Old stats that shouldn't be read
+                        + "<ambient-brightness-day-stats user=\"10\" local-date=\""
+                        + date.minusDays(AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK)
+                        + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
+                        + "3000.0,10000.0\" bucket-stats=\"1.088,0.0,0.726,0.0,25.868,0.0,0.0,"
+                        + "0.0,0.0,0.0\" />\r\n"
+                        // Valid stats that should get read
+                        + "<ambient-brightness-day-stats user=\"10\" local-date=\""
+                        + date.minusDays(1)
+                        + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
+                        + "3000.0,10000.0\" bucket-stats=\"1.088,0.0,0.726,0.0,25.868,0.0,0.0,"
+                        + "0.0,0.0,0.0\" />\r\n"
+                        // Valid stats that should get read
+                        + "<ambient-brightness-day-stats user=\"10\" local-date=\"" + date
+                        + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
+                        + "3000.0,10000.0\" bucket-stats=\"0.0,0.0,0.0,0.0,4.482,0.0,0.0,0.0,0.0,"
+                        + "0.0\" />\r\n"
+                        + "</ambient-brightness-stats>";
+        statsTracker.readStats(getInputStream(statsFile));
+        userStats = statsTracker.getUserStats(0);
+        assertEquals(2, userStats.size());
+        assertEquals(new AmbientBrightnessDayStats(date.minusDays(1),
+                new float[]{0, 1, 3, 10, 30, 100, 300, 1000, 3000, 10000},
+                new float[]{1.088f, 0, 0.726f, 0, 25.868f, 0, 0, 0, 0, 0}), userStats.get(0));
+        assertEquals(new AmbientBrightnessDayStats(date,
+                new float[]{0, 1, 3, 10, 30, 100, 300, 1000, 3000, 10000},
+                new float[]{0, 0, 0, 0, 4.482f, 0, 0, 0, 0, 0}), userStats.get(1));
+    }
+
+    @Test
+    public void testFailedReadAmbientBrightnessStatsWithException() {
+        AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
+        LocalDate date = mTestInjector.getLocalDate();
+        String statsFile;
+        // Test with parse error
+        statsFile =
+                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\r\n"
+                        + "<ambient-brightness-stats>\r\n"
+                        // Incorrect since bucket boundaries not parsable
+                        + "<ambient-brightness-day-stats user=\"10\" local-date=\"" + date
+                        + "\" bucket-boundaries=\"asdf,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
+                        + "3000.0,10000.0\" bucket-stats=\"1.088,0.0,0.726,0.0,25.868,0.0,0.0,"
+                        + "0.0,0.0,0.0\" />\r\n"
+                        + "</ambient-brightness-stats>";
+        try {
+            statsTracker.readStats(getInputStream(statsFile));
+        } catch (IOException e) {
+            // Expected
+        }
+        assertNull(statsTracker.getUserStats(0));
+        // Test with incorrect data (bucket boundaries length not equal to stats length)
+        statsFile =
+                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\r\n"
+                        + "<ambient-brightness-stats>\r\n"
+                        // Correct data
+                        + "<ambient-brightness-day-stats user=\"10\" local-date=\""
+                        + date.minusDays(1)
+                        + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
+                        + "3000.0,10000.0\" bucket-stats=\"0.0,0.0,0.0,0.0,4.482,0.0,0.0,0.0,0.0,"
+                        + "0.0\" />\r\n"
+                        // Incorrect data
+                        + "<ambient-brightness-day-stats user=\"10\" local-date=\"" + date
+                        + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,1000.0,"
+                        + "3000.0,10000.0\" bucket-stats=\"1.088,0.0,0.726,0.0,25.868,0.0,0.0,"
+                        + "0.0,0.0,0.0\" />\r\n"
+                        + "</ambient-brightness-stats>";
+        try {
+            statsTracker.readStats(getInputStream(statsFile));
+        } catch (Exception e) {
+            // Expected
+        }
+        assertNull(statsTracker.getUserStats(0));
+        // Test with missing attribute
+        statsFile =
+                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\r\n"
+                        + "<ambient-brightness-stats>\r\n"
+                        + "<ambientBrightnessDayStats user=\"10\" local-date=\"" + date
+                        + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
+                        + "3000.0,10000.0\" />\r\n"
+                        + "</ambient-brightness-stats>";
+        try {
+            statsTracker.readStats(getInputStream(statsFile));
+        } catch (Exception e) {
+            // Expected
+        }
+        assertNull(statsTracker.getUserStats(0));
+    }
+
+    @Test
+    public void testWriteThenReadAmbientBrightnessStats() throws IOException {
+        AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
+        ArrayList<AmbientBrightnessDayStats> userStats;
+        float[] expectedStats;
+        // Generate some placeholder data
+        // Data: very old which should not be read
+        statsTracker.start();
+        statsTracker.add(0, 0.05f);
+        mTestInjector.incrementTime(1000);
+        statsTracker.add(0, 0.2f);
+        mTestInjector.incrementTime(1500);
+        statsTracker.add(0, 1);
+        mTestInjector.incrementTime(2500);
+        statsTracker.stop();
+        // Data: day 1 user 1
+        mTestInjector.incrementDate(AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK - 1);
+        statsTracker.start();
+        statsTracker.add(0, 0.05f);
+        mTestInjector.incrementTime(1000);
+        statsTracker.add(0, 0.2f);
+        mTestInjector.incrementTime(1500);
+        statsTracker.add(0, 1);
+        mTestInjector.incrementTime(2500);
+        statsTracker.stop();
+        // Data: day 1 user 2
+        statsTracker.start();
+        statsTracker.add(1, 0);
+        mTestInjector.incrementTime(3500);
+        statsTracker.add(1, 5);
+        mTestInjector.incrementTime(5000);
+        statsTracker.stop();
+        // Data: day 2 user 1
+        mTestInjector.incrementDate(1);
+        statsTracker.start();
+        statsTracker.add(0, 0);
+        mTestInjector.incrementTime(3500);
+        statsTracker.add(0, 50000);
+        mTestInjector.incrementTime(5000);
+        statsTracker.stop();
+        // Write them
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        statsTracker.writeStats(baos);
+        baos.flush();
+        // Read them back and assert that it's the same
+        ByteArrayInputStream input = new ByteArrayInputStream(baos.toByteArray());
+        AmbientBrightnessStatsTracker newStatsTracker = getTestStatsTracker();
+        newStatsTracker.readStats(input);
+        userStats = newStatsTracker.getUserStats(0);
+        assertEquals(2, userStats.size());
+        // Check day 1 user 1
+        assertEquals(mTestInjector.getLocalDate().minusDays(1), userStats.get(0).getLocalDate());
+        expectedStats = getEmptyStatsArray();
+        expectedStats[0] = 1;
+        expectedStats[1] = 1.5f;
+        expectedStats[3] = 2.5f;
+        assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
+        // Check day 2 user 1
+        assertEquals(mTestInjector.getLocalDate(), userStats.get(1).getLocalDate());
+        expectedStats = getEmptyStatsArray();
+        expectedStats[0] = 3.5f;
+        expectedStats[11] = 5;
+        assertArrayEquals(expectedStats, userStats.get(1).getStats(), 0);
+        userStats = newStatsTracker.getUserStats(1);
+        assertEquals(1, userStats.size());
+        // Check day 1 user 2
+        assertEquals(mTestInjector.getLocalDate().minusDays(1), userStats.get(0).getLocalDate());
+        expectedStats = getEmptyStatsArray();
+        expectedStats[0] = 3.5f;
+        expectedStats[4] = 5;
+        assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
+    }
+
+    @Test
+    public void testTimer() {
+        AmbientBrightnessStatsTracker.Timer timer = new AmbientBrightnessStatsTracker.Timer(
+                () -> mTestInjector.elapsedRealtimeMillis());
+        assertEquals(0, timer.totalDurationSec(), 0);
+        mTestInjector.incrementTime(1000);
+        assertEquals(0, timer.totalDurationSec(), 0);
+        assertFalse(timer.isRunning());
+        // Start timer
+        timer.start();
+        assertTrue(timer.isRunning());
+        assertEquals(0, timer.totalDurationSec(), 0);
+        mTestInjector.incrementTime(1000);
+        assertTrue(timer.isRunning());
+        assertEquals(1, timer.totalDurationSec(), 0);
+        // Reset timer
+        timer.reset();
+        assertEquals(0, timer.totalDurationSec(), 0);
+        assertFalse(timer.isRunning());
+        // Start again
+        timer.start();
+        assertTrue(timer.isRunning());
+        assertEquals(0, timer.totalDurationSec(), 0);
+        mTestInjector.incrementTime(2000);
+        assertTrue(timer.isRunning());
+        assertEquals(2, timer.totalDurationSec(), 0);
+        // Reset again
+        timer.reset();
+        assertEquals(0, timer.totalDurationSec(), 0);
+        assertFalse(timer.isRunning());
+    }
+
+    private class TestInjector extends AmbientBrightnessStatsTracker.Injector {
+
+        private long mElapsedRealtimeMillis = SystemClock.elapsedRealtime();
+        private LocalDate mLocalDate = LocalDate.now();
+
+        public void incrementTime(long timeMillis) {
+            mElapsedRealtimeMillis += timeMillis;
+        }
+
+        public void incrementDate(int numDays) {
+            mLocalDate = mLocalDate.plusDays(numDays);
+        }
+
+        @Override
+        public long elapsedRealtimeMillis() {
+            return mElapsedRealtimeMillis;
+        }
+
+        @Override
+        public int getUserSerialNumber(UserManager userManager, int userId) {
+            return userId + 10;
+        }
+
+        @Override
+        public int getUserId(UserManager userManager, int userSerialNumber) {
+            return userSerialNumber - 10;
+        }
+
+        @Override
+        public LocalDate getLocalDate() {
+            return mLocalDate;
+        }
+    }
+
+    private AmbientBrightnessStatsTracker getTestStatsTracker() {
+        return new AmbientBrightnessStatsTracker(
+                InstrumentationRegistry.getContext().getSystemService(UserManager.class),
+                mTestInjector);
+    }
+
+    private float[] getEmptyStatsArray() {
+        return new float[AmbientBrightnessStatsTracker.BUCKET_BOUNDARIES_FOR_NEW_STATS.length];
+    }
+
+    private InputStream getInputStream(String data) {
+        return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java
new file mode 100644
index 0000000..ee7826f
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java
@@ -0,0 +1,710 @@
+/*
+ * 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.
+ */
+
+package com.android.server.display;
+
+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.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.hardware.display.BrightnessConfiguration;
+import android.os.PowerManager;
+import android.util.MathUtils;
+import android.util.Spline;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.Arrays;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BrightnessMappingStrategyTest {
+
+    private static final float[] LUX_LEVELS = {
+        0,
+        5,
+        20,
+        40,
+        100,
+        325,
+        600,
+        1250,
+        2200,
+        4000,
+        5000
+    };
+
+    private static final int[] LUX_LEVELS_IDLE = {
+        0,
+        10,
+        40,
+        80,
+        200,
+        655,
+        1200,
+        2500,
+        4400,
+        8000,
+        10000
+    };
+
+    private static final float[] DISPLAY_LEVELS_NITS = {
+        13.25f,
+        54.0f,
+        78.85f,
+        105.02f,
+        132.7f,
+        170.12f,
+        212.1f,
+        265.2f,
+        335.8f,
+        415.2f,
+        478.5f,
+    };
+
+    private static final float[] DISPLAY_LEVELS_NITS_IDLE = {
+        23.25f,
+        64.0f,
+        88.85f,
+        115.02f,
+        142.7f,
+        180.12f,
+        222.1f,
+        275.2f,
+        345.8f,
+        425.2f,
+        468.5f,
+    };
+
+    private static final int[] DISPLAY_LEVELS_BACKLIGHT = {
+        9,
+        30,
+        45,
+        62,
+        78,
+        96,
+        119,
+        146,
+        178,
+        221,
+        255
+    };
+
+    private static final float[] DISPLAY_RANGE_NITS = { 2.685f, 478.5f };
+    private static final float[] BACKLIGHT_RANGE_ZERO_TO_ONE = { 0.0f, 1.0f };
+    private static final float[] DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT = { 0.03149606299f, 1.0f };
+
+    private static final float[] EMPTY_FLOAT_ARRAY = new float[0];
+    private static final int[] EMPTY_INT_ARRAY = new int[0];
+
+    private static final float MAXIMUM_GAMMA = 3.0f;
+
+    private static final float[] GAMMA_CORRECTION_LUX = {
+        0,
+        100,
+        1000,
+        2500,
+        4000,
+        4900,
+        5000
+    };
+    private static final float[] GAMMA_CORRECTION_NITS = {
+        1.0f,
+        10.55f,
+        96.5f,
+        239.75f,
+        383.0f,
+        468.95f,
+        478.5f,
+    };
+    private static final Spline GAMMA_CORRECTION_SPLINE = Spline.createSpline(
+            new float[] { 0.0f, 100.0f, 1000.0f, 2500.0f, 4000.0f, 4900.0f, 5000.0f },
+            new float[] { 0.0475f, 0.0475f, 0.2225f, 0.5140f, 0.8056f, 0.9805f, 1.0f });
+
+    private static final float TOLERANCE = 0.0001f;
+
+    @Mock
+    DisplayWhiteBalanceController mMockDwbc;
+
+    @Test
+    public void testSimpleStrategyMappingAtControlPoints() {
+        Resources res = createResources(DISPLAY_LEVELS_BACKLIGHT);
+        DisplayDeviceConfig ddc = createDdc();
+        BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        assertNotNull("BrightnessMappingStrategy should not be null", simple);
+        for (int i = 0; i < LUX_LEVELS.length; i++) {
+            final float expectedLevel = MathUtils.map(PowerManager.BRIGHTNESS_OFF + 1,
+                    PowerManager.BRIGHTNESS_ON, PowerManager.BRIGHTNESS_MIN,
+                    PowerManager.BRIGHTNESS_MAX, DISPLAY_LEVELS_BACKLIGHT[i]);
+            assertEquals(expectedLevel,
+                    simple.getBrightness(LUX_LEVELS[i]), 0.0001f /*tolerance*/);
+        }
+    }
+
+    @Test
+    public void testSimpleStrategyMappingBetweenControlPoints() {
+        Resources res = createResources(DISPLAY_LEVELS_BACKLIGHT);
+        DisplayDeviceConfig ddc = createDdc();
+        BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        assertNotNull("BrightnessMappingStrategy should not be null", simple);
+        for (int i = 1; i < LUX_LEVELS.length; i++) {
+            final float lux = (LUX_LEVELS[i - 1] + LUX_LEVELS[i]) / 2;
+            final float backlight = simple.getBrightness(lux) * PowerManager.BRIGHTNESS_ON;
+            assertTrue("Desired brightness should be between adjacent control points.",
+                    backlight > DISPLAY_LEVELS_BACKLIGHT[i - 1]
+                            && backlight < DISPLAY_LEVELS_BACKLIGHT[i]);
+        }
+    }
+
+    @Test
+    public void testSimpleStrategyIgnoresNewConfiguration() {
+        Resources res = createResources(DISPLAY_LEVELS_BACKLIGHT);
+        DisplayDeviceConfig ddc = createDdc();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+
+        final float[] lux = { 0f, 1f };
+        final float[] nits = { 0, PowerManager.BRIGHTNESS_ON };
+
+        BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
+                .build();
+        strategy.setBrightnessConfiguration(config);
+        assertNotEquals(1.0f, strategy.getBrightness(1f), 0.0001f /*tolerance*/);
+    }
+
+    @Test
+    public void testSimpleStrategyIgnoresNullConfiguration() {
+        Resources res = createResources(DISPLAY_LEVELS_BACKLIGHT);
+        DisplayDeviceConfig ddc = createDdc();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+
+        strategy.setBrightnessConfiguration(null);
+        final int n = DISPLAY_LEVELS_BACKLIGHT.length;
+        final float expectedBrightness =
+                (float) DISPLAY_LEVELS_BACKLIGHT[n - 1] / PowerManager.BRIGHTNESS_ON;
+        assertEquals(expectedBrightness,
+                strategy.getBrightness(LUX_LEVELS[n - 1]), 0.0001f /*tolerance*/);
+    }
+
+    @Test
+    public void testPhysicalStrategyMappingAtControlPoints() {
+        Resources res = createResources(EMPTY_INT_ARRAY);
+        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
+                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT,
+                LUX_LEVELS, DISPLAY_LEVELS_NITS);
+        BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        assertNotNull("BrightnessMappingStrategy should not be null", physical);
+        for (int i = 0; i < LUX_LEVELS.length; i++) {
+            final float expectedLevel = MathUtils.map(DISPLAY_RANGE_NITS[0], DISPLAY_RANGE_NITS[1],
+                    DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT[0],
+                    DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT[1],
+                    DISPLAY_LEVELS_NITS[i]);
+            assertEquals(expectedLevel,
+                    physical.getBrightness(LUX_LEVELS[i]),
+                    0.0001f /*tolerance*/);
+        }
+    }
+
+    @Test
+    public void testPhysicalStrategyMappingBetweenControlPoints() {
+        Resources res = createResources(EMPTY_INT_ARRAY);
+        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE,
+                LUX_LEVELS, DISPLAY_LEVELS_NITS);
+        BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        assertNotNull("BrightnessMappingStrategy should not be null", physical);
+        Spline brightnessToNits =
+                Spline.createSpline(BACKLIGHT_RANGE_ZERO_TO_ONE, DISPLAY_RANGE_NITS);
+        for (int i = 1; i < LUX_LEVELS.length; i++) {
+            final float lux = (LUX_LEVELS[i - 1] + LUX_LEVELS[i]) / 2.0f;
+            final float brightness = physical.getBrightness(lux);
+            final float nits = brightnessToNits.interpolate(brightness);
+            assertTrue("Desired brightness should be between adjacent control points: " + nits,
+                    nits > DISPLAY_LEVELS_NITS[i - 1] && nits < DISPLAY_LEVELS_NITS[i]);
+        }
+    }
+
+    @Test
+    public void testPhysicalStrategyUsesNewConfigurations() {
+        Resources res = createResources(EMPTY_INT_ARRAY);
+        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
+                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS);
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+
+        final float[] lux = {0f, 1f};
+        final float[] nits = {
+                DISPLAY_RANGE_NITS[0],
+                DISPLAY_RANGE_NITS[DISPLAY_RANGE_NITS.length - 1]
+        };
+
+        BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
+                .build();
+        strategy.setBrightnessConfiguration(config);
+        assertEquals(1.0f, strategy.getBrightness(1f), 0.0001f /*tolerance*/);
+
+        // Check that null returns us to the default configuration.
+        strategy.setBrightnessConfiguration(null);
+        final int n = DISPLAY_LEVELS_NITS.length;
+        final float expectedBrightness = DISPLAY_LEVELS_NITS[n - 1] / DISPLAY_RANGE_NITS[1];
+        assertEquals(expectedBrightness,
+                strategy.getBrightness(LUX_LEVELS[n - 1]), 0.0001f /*tolerance*/);
+    }
+
+    @Test
+    public void testPhysicalStrategyRecalculateSplines() {
+        Resources res = createResources(EMPTY_INT_ARRAY);
+        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
+                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS);
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        float[] adjustedNits50p = new float[DISPLAY_RANGE_NITS.length];
+        for (int i = 0; i < DISPLAY_RANGE_NITS.length; i++) {
+            adjustedNits50p[i] = DISPLAY_RANGE_NITS[i] * 0.5f;
+        }
+
+        // Default
+        assertEquals(DISPLAY_RANGE_NITS[0], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]),
+                TOLERANCE);
+        assertEquals(DISPLAY_RANGE_NITS[1], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]),
+                TOLERANCE);
+        assertEquals(DISPLAY_RANGE_NITS[0],
+                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]), TOLERANCE);
+        assertEquals(DISPLAY_RANGE_NITS[1],
+                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]), TOLERANCE);
+
+        // Adjustment is turned on
+        strategy.recalculateSplines(true, adjustedNits50p);
+        assertEquals(DISPLAY_RANGE_NITS[0], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]),
+                TOLERANCE);
+        assertEquals(DISPLAY_RANGE_NITS[1], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]),
+                TOLERANCE);
+        assertEquals(DISPLAY_RANGE_NITS[0] / 2,
+                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]), TOLERANCE);
+        assertEquals(DISPLAY_RANGE_NITS[1] / 2,
+                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]), TOLERANCE);
+
+        // Adjustment is turned off
+        strategy.recalculateSplines(false, adjustedNits50p);
+        assertEquals(DISPLAY_RANGE_NITS[0], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]),
+                TOLERANCE);
+        assertEquals(DISPLAY_RANGE_NITS[1], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]),
+                TOLERANCE);
+        assertEquals(DISPLAY_RANGE_NITS[0],
+                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]), TOLERANCE);
+        assertEquals(DISPLAY_RANGE_NITS[1],
+                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]), TOLERANCE);
+    }
+
+    @Test
+    public void testDefaultStrategyIsPhysical() {
+        Resources res = createResources(DISPLAY_LEVELS_BACKLIGHT);
+        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
+                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS);
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        assertTrue(strategy instanceof BrightnessMappingStrategy.PhysicalMappingStrategy);
+    }
+
+    @Test
+    public void testNonStrictlyIncreasingLuxLevelsFails() {
+        final float[] lux = Arrays.copyOf(LUX_LEVELS, LUX_LEVELS.length);
+        final int idx = lux.length / 2;
+        float tmp = lux[idx];
+        lux[idx] = lux[idx + 1];
+        lux[idx + 1] = tmp;
+        Resources res = createResources(EMPTY_INT_ARRAY);
+        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
+                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux, DISPLAY_LEVELS_NITS);
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        assertNull(strategy);
+
+        // And make sure we get the same result even if it's monotone but not increasing.
+        lux[idx] = lux[idx + 1];
+        ddc = createDdc(DISPLAY_RANGE_NITS, DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux,
+                DISPLAY_LEVELS_NITS);
+        strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        assertNull(strategy);
+    }
+
+    @Test
+    public void testDifferentNumberOfControlPointValuesFails() {
+        //Extra lux level
+        final float[] lux = Arrays.copyOf(LUX_LEVELS, LUX_LEVELS.length + 1);
+        // Make sure it's strictly increasing so that the only failure is the differing array
+        // lengths
+        lux[lux.length - 1] = lux[lux.length - 2] + 1;
+        Resources res = createResources(EMPTY_INT_ARRAY);
+        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
+                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux, DISPLAY_LEVELS_NITS);
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        assertNull(strategy);
+
+        res = createResources(DISPLAY_LEVELS_BACKLIGHT);
+        strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        assertNull(strategy);
+
+        // Extra backlight level
+        final int[] backlight = Arrays.copyOf(
+                DISPLAY_LEVELS_BACKLIGHT, DISPLAY_LEVELS_BACKLIGHT.length + 1);
+        backlight[backlight.length - 1] = backlight[backlight.length - 2] + 1;
+        res = createResources(backlight);
+        ddc = createDdc(DISPLAY_RANGE_NITS,
+                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, EMPTY_FLOAT_ARRAY);
+        strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        assertNull(strategy);
+
+        // Extra nits level
+        final float[] nits = Arrays.copyOf(DISPLAY_RANGE_NITS, DISPLAY_LEVELS_NITS.length + 1);
+        nits[nits.length - 1] = nits[nits.length - 2] + 1;
+        res = createResources(EMPTY_INT_ARRAY);
+        ddc = createDdc(DISPLAY_RANGE_NITS,
+                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, nits);
+        strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        assertNull(strategy);
+    }
+
+    @Test
+    public void testPhysicalStrategyRequiresNitsMapping() {
+        Resources res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
+        DisplayDeviceConfig ddc = createDdc(EMPTY_FLOAT_ARRAY /*nitsRange*/);
+        BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        assertNull(physical);
+
+        res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
+        physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        assertNull(physical);
+
+        res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
+        physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
+        assertNull(physical);
+    }
+
+    @Test
+    public void testStrategiesAdaptToUserDataPoint() {
+        Resources res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
+        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE,
+                LUX_LEVELS, DISPLAY_LEVELS_NITS);
+        assertStrategyAdaptsToUserDataPoints(BrightnessMappingStrategy.create(res, ddc, mMockDwbc));
+        ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE);
+        res = createResources(DISPLAY_LEVELS_BACKLIGHT);
+        assertStrategyAdaptsToUserDataPoints(BrightnessMappingStrategy.create(res, ddc, mMockDwbc));
+    }
+
+    @Test
+    public void testIdleModeConfigLoadsCorrectly() {
+        Resources res = createResourcesIdle(LUX_LEVELS_IDLE, DISPLAY_LEVELS_NITS_IDLE);
+        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE);
+
+        // Create an idle mode bms
+        // This will fail if it tries to fetch the wrong configuration.
+        BrightnessMappingStrategy bms = BrightnessMappingStrategy.createForIdleMode(res, ddc,
+                mMockDwbc);
+        assertNotNull("BrightnessMappingStrategy should not be null", bms);
+
+        // Ensure that the config is the one we set
+        // Ensure that the lux -> brightness -> nits path works. ()
+        for (int i = 0; i < DISPLAY_LEVELS_NITS_IDLE.length; i++) {
+            assertEquals(LUX_LEVELS_IDLE[i], bms.getDefaultConfig().getCurve().first[i], TOLERANCE);
+            assertEquals(DISPLAY_LEVELS_NITS_IDLE[i], bms.getDefaultConfig().getCurve().second[i],
+                    TOLERANCE);
+            assertEquals(bms.convertToNits(bms.getBrightness(LUX_LEVELS_IDLE[i])),
+                    DISPLAY_LEVELS_NITS_IDLE[i], TOLERANCE);
+        }
+    }
+
+    private static void assertStrategyAdaptsToUserDataPoints(BrightnessMappingStrategy strategy) {
+        // Save out all of the initial brightness data for comparison after reset.
+        float[] initialBrightnessLevels = new float[LUX_LEVELS.length];
+        for (int i = 0; i < LUX_LEVELS.length; i++) {
+            initialBrightnessLevels[i] = strategy.getBrightness(LUX_LEVELS[i]);
+        }
+
+        // Add a data point in the middle of the curve where the user has set the brightness max
+        final int idx = LUX_LEVELS.length / 2;
+        strategy.addUserDataPoint(LUX_LEVELS[idx], 1.0f);
+
+        // Then make sure that all control points after the middle lux level are also set to max...
+        for (int i = idx; i < LUX_LEVELS.length; i++) {
+            assertEquals(strategy.getBrightness(LUX_LEVELS[idx]), 1.0, 0.0001f /*tolerance*/);
+        }
+
+        // ...and that all control points before the middle lux level are strictly less than the
+        // previous one still.
+        float prevBrightness = strategy.getBrightness(LUX_LEVELS[idx]);
+        for (int i = idx - 1; i >= 0; i--) {
+            float brightness = strategy.getBrightness(LUX_LEVELS[i]);
+            assertTrue("Brightness levels must be monotonic after adapting to user data",
+                    prevBrightness >= brightness);
+            prevBrightness = brightness;
+        }
+
+        // Now reset the curve and make sure we go back to the initial brightness levels recorded
+        // before adding the user data point.
+        strategy.clearUserDataPoints();
+        for (int i = 0; i < LUX_LEVELS.length; i++) {
+            assertEquals(initialBrightnessLevels[i], strategy.getBrightness(LUX_LEVELS[i]),
+                    0.0001f /*tolerance*/);
+        }
+
+        // Now set the middle of the lux range to something just above the minimum.
+        float minBrightness = strategy.getBrightness(LUX_LEVELS[0]);
+        strategy.addUserDataPoint(LUX_LEVELS[idx], minBrightness + 0.0001f);
+
+        // Then make sure the curve is still monotonic.
+        prevBrightness = 0f;
+        for (float lux : LUX_LEVELS) {
+            float brightness = strategy.getBrightness(lux);
+            assertTrue("Brightness levels must be monotonic after adapting to user data",
+                    prevBrightness <= brightness);
+            prevBrightness = brightness;
+        }
+
+        // And that the lowest lux level still gives the absolute minimum brightness. This should
+        // be true assuming that there are more than two lux levels in the curve since we picked a
+        // brightness just barely above the minimum for the middle of the curve.
+        minBrightness = (float) MathUtils.pow(minBrightness, MAXIMUM_GAMMA); // Gamma correction.
+        assertEquals(minBrightness, strategy.getBrightness(LUX_LEVELS[0]), 0.0001f /*tolerance*/);
+    }
+
+    private Resources createResources(int[] brightnessLevelsBacklight) {
+        return createResources(brightnessLevelsBacklight, EMPTY_INT_ARRAY, EMPTY_FLOAT_ARRAY);
+    }
+
+    private Resources createResourcesIdle(int[] luxLevelsIdle, float[] brightnessLevelsNitsIdle) {
+        return createResources(EMPTY_INT_ARRAY,
+                luxLevelsIdle, brightnessLevelsNitsIdle);
+    }
+
+    private Resources createResources(int[] brightnessLevelsBacklight, int[] luxLevelsIdle,
+            float[] brightnessLevelsNitsIdle) {
+
+        Resources mockResources = mock(Resources.class);
+        if (luxLevelsIdle.length > 0) {
+            int[] luxLevelsIdleResource = Arrays.copyOfRange(luxLevelsIdle, 1,
+                    luxLevelsIdle.length);
+            when(mockResources.getIntArray(
+                    com.android.internal.R.array.config_autoBrightnessLevelsIdle))
+                    .thenReturn(luxLevelsIdleResource);
+        }
+
+        when(mockResources.getIntArray(
+                com.android.internal.R.array.config_autoBrightnessLcdBacklightValues))
+                .thenReturn(brightnessLevelsBacklight);
+
+        TypedArray mockBrightnessLevelNitsIdle = createFloatTypedArray(brightnessLevelsNitsIdle);
+        when(mockResources.obtainTypedArray(
+                com.android.internal.R.array.config_autoBrightnessDisplayValuesNitsIdle))
+                .thenReturn(mockBrightnessLevelNitsIdle);
+
+        when(mockResources.getInteger(
+                com.android.internal.R.integer.config_screenBrightnessSettingMinimum))
+                .thenReturn(1);
+        when(mockResources.getInteger(
+                com.android.internal.R.integer.config_screenBrightnessSettingMaximum))
+                .thenReturn(255);
+        when(mockResources.getFraction(
+                com.android.internal.R.fraction.config_autoBrightnessAdjustmentMaxGamma, 1, 1))
+                .thenReturn(MAXIMUM_GAMMA);
+        return mockResources;
+    }
+
+    private DisplayDeviceConfig createDdc() {
+        return createDdc(DISPLAY_RANGE_NITS);
+    }
+
+    private DisplayDeviceConfig createDdc(float[] nitsArray) {
+        return createDdc(nitsArray, DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT);
+    }
+
+    private DisplayDeviceConfig createDdc(float[] nitsArray, float[] backlightArray) {
+        DisplayDeviceConfig mockDdc = mock(DisplayDeviceConfig.class);
+        when(mockDdc.getNits()).thenReturn(nitsArray);
+        when(mockDdc.getBrightness()).thenReturn(backlightArray);
+        when(mockDdc.getAutoBrightnessBrighteningLevelsLux()).thenReturn(LUX_LEVELS);
+        when(mockDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(EMPTY_FLOAT_ARRAY);
+        return mockDdc;
+    }
+
+    private DisplayDeviceConfig createDdc(float[] nitsArray, float[] backlightArray,
+            float[] luxLevelsFloat, float[] brightnessLevelsNits) {
+        DisplayDeviceConfig mockDdc = mock(DisplayDeviceConfig.class);
+        when(mockDdc.getNits()).thenReturn(nitsArray);
+        when(mockDdc.getBrightness()).thenReturn(backlightArray);
+        when(mockDdc.getAutoBrightnessBrighteningLevelsLux()).thenReturn(luxLevelsFloat);
+        when(mockDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(brightnessLevelsNits);
+        return mockDdc;
+    }
+
+    private TypedArray createFloatTypedArray(float[] vals) {
+        TypedArray mockArray = mock(TypedArray.class);
+        when(mockArray.length()).thenAnswer(invocation -> {
+            return vals.length;
+        });
+        when(mockArray.getFloat(anyInt(), anyFloat())).thenAnswer(invocation -> {
+            final float def = (float) invocation.getArguments()[1];
+            if (vals == null) {
+                return def;
+            }
+            int idx = (int) invocation.getArguments()[0];
+            if (idx >= 0 && idx < vals.length) {
+                return vals[idx];
+            } else {
+                return def;
+            }
+        });
+        return mockArray;
+    }
+
+    // Gamma correction tests.
+    // x0 = 100   y0 = ~0.01
+    // x1 = 1000  y1 = ~0.20
+    // x2 = 2500  y2 = ~0.50
+    // x3 = 4000  y3 = ~0.80
+    // x4 = 4900  y4 = ~0.99
+
+    @Test
+    public void testGammaCorrectionLowChangeAtCenter() {
+        // If we set a user data point at (x2, y2^0.5), i.e. gamma = 0.5, it should bump the rest
+        // of the spline accordingly.
+        final int x1 = 1000;
+        final int x2 = 2500;
+        final int x3 = 4000;
+        final float y1 = GAMMA_CORRECTION_SPLINE.interpolate(x1);
+        final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2);
+        final float y3 = GAMMA_CORRECTION_SPLINE.interpolate(x3);
+
+        Resources resources = createResources(EMPTY_INT_ARRAY);
+        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
+                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
+                GAMMA_CORRECTION_NITS);
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
+                mMockDwbc);
+        // Let's start with a validity check:
+        assertEquals(y1, strategy.getBrightness(x1), 0.0001f /* tolerance */);
+        assertEquals(y2, strategy.getBrightness(x2), 0.0001f /* tolerance */);
+        assertEquals(y3, strategy.getBrightness(x3), 0.0001f /* tolerance */);
+        // OK, let's roll:
+        float gamma = 0.5f;
+        strategy.addUserDataPoint(x2, (float) MathUtils.pow(y2, gamma));
+        assertEquals(MathUtils.pow(y1, gamma), strategy.getBrightness(x1), 0.0001f /* tolerance */);
+        assertEquals(MathUtils.pow(y2, gamma), strategy.getBrightness(x2), 0.0001f /* tolerance */);
+        assertEquals(MathUtils.pow(y3, gamma), strategy.getBrightness(x3), 0.0001f /* tolerance */);
+        // The adjustment should be +0.6308 (manual calculation).
+        assertEquals(+0.6308f, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
+    }
+
+    @Test
+    public void testGammaCorrectionHighChangeAtCenter() {
+        // This time we set a user data point at (x2, y2^0.25), i.e. gamma = 0.3 (the minimum),
+        // which should bump the rest of the spline accordingly, and further correct x2 to hit
+        // y2^0.25 (not y2^0.3).
+        final int x1 = 1000;
+        final int x2 = 2500;
+        final int x3 = 4000;
+        final float y1 = GAMMA_CORRECTION_SPLINE.interpolate(x1);
+        final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2);
+        final float y3 = GAMMA_CORRECTION_SPLINE.interpolate(x3);
+        Resources resources = createResources(EMPTY_INT_ARRAY);
+        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
+                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
+                GAMMA_CORRECTION_NITS);
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
+                mMockDwbc);
+        // Validity check:
+        assertEquals(y1, strategy.getBrightness(x1), 0.0001f /* tolerance */);
+        assertEquals(y2, strategy.getBrightness(x2), 0.0001f /* tolerance */);
+        assertEquals(y3, strategy.getBrightness(x3), 0.0001f /* tolerance */);
+        // Let's roll:
+        float gamma = 0.25f;
+        final float minGamma = 1.0f / MAXIMUM_GAMMA;
+        strategy.addUserDataPoint(x2, (float) MathUtils.pow(y2, gamma));
+        assertEquals(MathUtils.pow(y1, minGamma),
+                strategy.getBrightness(x1), 0.0001f /* tolerance */);
+        assertEquals(MathUtils.pow(y2, gamma),
+                strategy.getBrightness(x2), 0.0001f /* tolerance */);
+        assertEquals(MathUtils.pow(y3, minGamma),
+                strategy.getBrightness(x3), 0.0001f /* tolerance */);
+        // The adjustment should be +1.0 (maximum adjustment).
+        assertEquals(+1.0f, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
+    }
+
+    @Test
+    public void testGammaCorrectionExtremeChangeAtCenter() {
+        // Extreme changes (e.g. setting brightness to 0.0 or 1.0) can't be gamma corrected, so we
+        // just make sure the adjustment reflects the change.
+        Resources resources = createResources(EMPTY_INT_ARRAY);
+        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
+                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
+                GAMMA_CORRECTION_NITS);
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
+                mMockDwbc);
+        assertEquals(0.0f, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
+        strategy.addUserDataPoint(2500, 1.0f);
+        assertEquals(+1.0f, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
+        strategy.addUserDataPoint(2500, 0.0f);
+        assertEquals(-1.0f, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
+    }
+
+    @Test
+    public void testGammaCorrectionChangeAtEdges() {
+        // The algorithm behaves differently at the edges, because gamma correction there tends to
+        // be extreme. If we add a user data point at (x0, y0+0.3), the adjustment should be 0.3,
+        // resulting in a gamma of 3**-0.6 = ~0.52.
+        final int x0 = 100;
+        final int x2 = 2500;
+        final int x4 = 4900;
+        final float y0 = GAMMA_CORRECTION_SPLINE.interpolate(x0);
+        final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2);
+        final float y4 = GAMMA_CORRECTION_SPLINE.interpolate(x4);
+        Resources resources = createResources(EMPTY_INT_ARRAY);
+        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
+                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
+                GAMMA_CORRECTION_NITS);
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
+                mMockDwbc);
+        // Validity, as per tradition:
+        assertEquals(y0, strategy.getBrightness(x0), 0.0001f /* tolerance */);
+        assertEquals(y2, strategy.getBrightness(x2), 0.0001f /* tolerance */);
+        assertEquals(y4, strategy.getBrightness(x4), 0.0001f /* tolerance */);
+        // Rollin':
+        float adjustment = 0.3f;
+        float gamma = (float) MathUtils.pow(MAXIMUM_GAMMA, -adjustment);
+        strategy.addUserDataPoint(x0, y0 + adjustment);
+        assertEquals(y0 + adjustment, strategy.getBrightness(x0), 0.0001f /* tolerance */);
+        assertEquals(MathUtils.pow(y2, gamma), strategy.getBrightness(x2), 0.0001f /* tolerance */);
+        assertEquals(MathUtils.pow(y4, gamma), strategy.getBrightness(x4), 0.0001f /* tolerance */);
+        assertEquals(adjustment, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
+        // Similarly, if we set a user data point at (x4, 1.0), the adjustment should be 1 - y4.
+        adjustment = 1.0f - y4;
+        gamma = (float) MathUtils.pow(MAXIMUM_GAMMA, -adjustment);
+        strategy.addUserDataPoint(x4, 1.0f);
+        assertEquals(MathUtils.pow(y0, gamma), strategy.getBrightness(x0), 0.0001f /* tolerance */);
+        assertEquals(MathUtils.pow(y2, gamma), strategy.getBrightness(x2), 0.0001f /* tolerance */);
+        assertEquals(1.0f, strategy.getBrightness(x4), 0.0001f /* tolerance */);
+        assertEquals(adjustment, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
+    }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java b/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java
new file mode 100644
index 0000000..8faaf59
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java
@@ -0,0 +1,493 @@
+/*
+ * 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 static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+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.hardware.display.BrightnessInfo;
+import android.os.Handler;
+import android.os.IThermalEventListener;
+import android.os.IThermalService;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.Temperature;
+import android.os.Temperature.ThrottlingStatus;
+import android.os.test.TestLooper;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.os.BackgroundThread;
+import com.android.server.display.BrightnessThrottler.Injector;
+import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData;
+import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel;
+import com.android.server.display.mode.DisplayModeDirectorTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BrightnessThrottlerTest {
+    private static final float EPSILON = 0.000001f;
+
+    private Handler mHandler;
+    private TestLooper mTestLooper;
+
+    @Mock IThermalService mThermalServiceMock;
+    @Mock Injector mInjectorMock;
+
+    DisplayModeDirectorTest.FakeDeviceConfig mDeviceConfigFake;
+
+    @Captor ArgumentCaptor<IThermalEventListener> mThermalEventListenerCaptor;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mInjectorMock.getThermalService()).thenReturn(mThermalServiceMock);
+        mTestLooper = new TestLooper();
+        mHandler = new Handler(mTestLooper.getLooper(), new Handler.Callback() {
+            @Override
+            public boolean handleMessage(Message msg) {
+                return true;
+            }
+        });
+        mDeviceConfigFake = new DisplayModeDirectorTest.FakeDeviceConfig();
+        when(mInjectorMock.getDeviceConfig()).thenReturn(mDeviceConfigFake);
+
+    }
+
+    /////////////////
+    // Test Methods
+    /////////////////
+
+    @Test
+    public void testThermalBrightnessThrottlingData() {
+        List<ThrottlingLevel> singleLevel = new ArrayList<>();
+        singleLevel.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.25f));
+
+        List<ThrottlingLevel> validLevels = new ArrayList<>();
+        validLevels.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE, 0.62f));
+        validLevels.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.25f));
+
+        List<ThrottlingLevel> unsortedThermalLevels = new ArrayList<>();
+        unsortedThermalLevels.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.62f));
+        unsortedThermalLevels.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE, 0.25f));
+
+        List<ThrottlingLevel> unsortedBrightnessLevels = new ArrayList<>();
+        unsortedBrightnessLevels.add(
+                new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE, 0.25f));
+        unsortedBrightnessLevels.add(
+                new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.62f));
+
+        List<ThrottlingLevel> unsortedLevels = new ArrayList<>();
+        unsortedLevels.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.25f));
+        unsortedLevels.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE, 0.62f));
+
+        List<ThrottlingLevel> invalidLevel = new ArrayList<>();
+        invalidLevel.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                PowerManager.BRIGHTNESS_MAX + EPSILON));
+
+        // Test invalid data
+        ThermalBrightnessThrottlingData data;
+        data = ThermalBrightnessThrottlingData.create((List<ThrottlingLevel>) null);
+        assertEquals(data, null);
+        data = ThermalBrightnessThrottlingData.create(new ArrayList<ThrottlingLevel>());
+        assertEquals(data, null);
+        data = ThermalBrightnessThrottlingData.create(unsortedThermalLevels);
+        assertEquals(data, null);
+        data = ThermalBrightnessThrottlingData.create(unsortedBrightnessLevels);
+        assertEquals(data, null);
+        data = ThermalBrightnessThrottlingData.create(unsortedLevels);
+        assertEquals(data, null);
+        data = ThermalBrightnessThrottlingData.create(invalidLevel);
+        assertEquals(data, null);
+
+        // Test valid data
+        data = ThermalBrightnessThrottlingData.create(singleLevel);
+        assertNotEquals(data, null);
+        assertThrottlingLevelsEquals(singleLevel, data.throttlingLevels);
+
+        data = ThermalBrightnessThrottlingData.create(validLevels);
+        assertNotEquals(data, null);
+        assertThrottlingLevelsEquals(validLevels, data.throttlingLevels);
+    }
+
+    @Test
+    public void testThermalThrottlingUnsupported() {
+        final BrightnessThrottler throttler = createThrottlerUnsupported();
+        assertFalse(throttler.deviceSupportsThrottling());
+
+        // Thermal listener shouldn't be registered if throttling is unsupported
+        verify(mInjectorMock, never()).getThermalService();
+
+        // Ensure that brightness is uncapped when the device doesn't support throttling
+        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
+    }
+
+    @Test
+    public void testThermalThrottlingSingleLevel() throws Exception {
+        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                0.25f);
+
+        List<ThrottlingLevel> levels = new ArrayList<>();
+        levels.add(level);
+        final ThermalBrightnessThrottlingData data = ThermalBrightnessThrottlingData.create(levels);
+        final BrightnessThrottler throttler = createThrottlerSupported(data);
+        assertTrue(throttler.deviceSupportsThrottling());
+
+        verify(mThermalServiceMock).registerThermalEventListenerWithType(
+                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
+        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
+
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
+        assertFalse(throttler.isThrottled());
+        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, throttler.getBrightnessMaxReason());
+
+        // Set status just high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(level.brightness, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
+                throttler.getBrightnessMaxReason());
+
+        // Set status more than high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus + 1));
+        mTestLooper.dispatchAll();
+        assertEquals(level.brightness, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
+                throttler.getBrightnessMaxReason());
+
+        // Return to the lower throttling level
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(level.brightness, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
+                throttler.getBrightnessMaxReason());
+
+        // Cool down
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
+        assertFalse(throttler.isThrottled());
+        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE,
+                throttler.getBrightnessMaxReason());
+    }
+
+    @Test
+    public void testThermalThrottlingMultiLevel() throws Exception {
+        final ThrottlingLevel levelLo = new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE,
+                0.62f);
+        final ThrottlingLevel levelHi = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                0.25f);
+
+        List<ThrottlingLevel> levels = new ArrayList<>();
+        levels.add(levelLo);
+        levels.add(levelHi);
+        final ThermalBrightnessThrottlingData data = ThermalBrightnessThrottlingData.create(levels);
+        final BrightnessThrottler throttler = createThrottlerSupported(data);
+        assertTrue(throttler.deviceSupportsThrottling());
+
+        verify(mThermalServiceMock).registerThermalEventListenerWithType(
+                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
+        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
+
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelLo.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
+        assertFalse(throttler.isThrottled());
+        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, throttler.getBrightnessMaxReason());
+
+        // Set status just high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelLo.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(levelLo.brightness, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
+                throttler.getBrightnessMaxReason());
+
+        // Set status to an intermediate throttling level
+        listener.notifyThrottling(getSkinTemp(levelLo.thermalStatus + 1));
+        mTestLooper.dispatchAll();
+        assertEquals(levelLo.brightness, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
+                throttler.getBrightnessMaxReason());
+
+        // Set status to the highest configured throttling level
+        listener.notifyThrottling(getSkinTemp(levelHi.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(levelHi.brightness, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
+                throttler.getBrightnessMaxReason());
+
+        // Set status to exceed the highest configured throttling level
+        listener.notifyThrottling(getSkinTemp(levelHi.thermalStatus + 1));
+        mTestLooper.dispatchAll();
+        assertEquals(levelHi.brightness, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
+                throttler.getBrightnessMaxReason());
+
+        // Return to an intermediate throttling level
+        listener.notifyThrottling(getSkinTemp(levelLo.thermalStatus + 1));
+        mTestLooper.dispatchAll();
+        assertEquals(levelLo.brightness, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
+                throttler.getBrightnessMaxReason());
+
+        // Return to the lowest configured throttling level
+        listener.notifyThrottling(getSkinTemp(levelLo.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(levelLo.brightness, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
+                throttler.getBrightnessMaxReason());
+
+        // Cool down
+        listener.notifyThrottling(getSkinTemp(levelLo.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
+        assertFalse(throttler.isThrottled());
+        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, throttler.getBrightnessMaxReason());
+    }
+
+    @Test public void testUpdateThermalThrottlingData() throws Exception {
+        // Initialise brightness throttling levels
+        // Ensure that they are overridden by setting the data through device config.
+        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                0.25f);
+        List<ThrottlingLevel> levels = new ArrayList<>();
+        levels.add(level);
+        final ThermalBrightnessThrottlingData data = ThermalBrightnessThrottlingData.create(levels);
+        mDeviceConfigFake.setThermalBrightnessThrottlingData("123,1,critical,0.4");
+        final BrightnessThrottler throttler = createThrottlerSupported(data);
+
+        verify(mThermalServiceMock).registerThermalEventListenerWithType(
+                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
+        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.4f);
+
+        // Set new (valid) data from device config
+        mDeviceConfigFake.setThermalBrightnessThrottlingData("123,1,critical,0.8");
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.8f);
+
+        mDeviceConfigFake.setThermalBrightnessThrottlingData(
+                "123,1,critical,0.75;123,1,critical,0.99,id_2");
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.75f);
+        mDeviceConfigFake.setThermalBrightnessThrottlingData(
+                "123,1,critical,0.8,default;123,1,critical,0.99,id_2");
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.8f);
+    }
+
+    @Test public void testInvalidThrottlingStrings() throws Exception {
+        // Initialise brightness throttling levels
+        // Ensure that they are not overridden by invalid data through device config.
+        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                0.25f);
+        List<ThrottlingLevel> levels = new ArrayList<>();
+        levels.add(level);
+        final ThermalBrightnessThrottlingData data = ThermalBrightnessThrottlingData.create(levels);
+        final BrightnessThrottler throttler = createThrottlerSupported(data);
+        verify(mThermalServiceMock).registerThermalEventListenerWithType(
+                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
+        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
+
+        // None of these are valid so shouldn't override the original data
+
+        // Not the current id
+        mDeviceConfigFake.setThermalBrightnessThrottlingData("321,1,critical,0.4");
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        // Incorrect number
+        mDeviceConfigFake.setThermalBrightnessThrottlingData("123,0,critical,0.4");
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        // Incorrect number
+        mDeviceConfigFake.setThermalBrightnessThrottlingData("123,2,critical,0.4");
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        // Invalid level
+        mDeviceConfigFake.setThermalBrightnessThrottlingData("123,1,invalid,0.4");
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        // Invalid brightness
+        mDeviceConfigFake.setThermalBrightnessThrottlingData("123,1,critical,none");
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        // Invalid brightness
+        mDeviceConfigFake.setThermalBrightnessThrottlingData("123,1,critical,-3");
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        // Invalid format
+        mDeviceConfigFake.setThermalBrightnessThrottlingData("invalid string");
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        // Invalid format
+        mDeviceConfigFake.setThermalBrightnessThrottlingData("");
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        // Invalid string format
+        mDeviceConfigFake.setThermalBrightnessThrottlingData(
+                "123,default,1,critical,0.75,1,critical,0.99");
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        // Invalid level string and number string
+        mDeviceConfigFake.setThermalBrightnessThrottlingData(
+                "123,1,1,critical,0.75,id_2,1,critical,0.99");
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        // Invalid format - (two default ids for same display)
+        mDeviceConfigFake.setThermalBrightnessThrottlingData(
+                "123,1,critical,0.75,default;123,1,critical,0.99");
+        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+    }
+
+    private void testThermalThrottling(BrightnessThrottler throttler,
+            IThermalEventListener listener, float tooLowCap, float tooHighCap) throws Exception {
+        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                tooHighCap);
+
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(tooLowCap, throttler.getBrightnessCap(), 0f);
+        assertFalse(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(tooHighCap, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+    }
+
+    @Test public void testMultipleConfigPoints() throws Exception {
+        // Initialise brightness throttling levels
+        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                0.25f);
+        List<ThrottlingLevel> levels = new ArrayList<>();
+        levels.add(level);
+        final ThermalBrightnessThrottlingData data = ThermalBrightnessThrottlingData.create(levels);
+
+        // These are identical to the string set below
+        final ThrottlingLevel levelSevere = new ThrottlingLevel(PowerManager.THERMAL_STATUS_SEVERE,
+                0.9f);
+        final ThrottlingLevel levelCritical = new ThrottlingLevel(
+                PowerManager.THERMAL_STATUS_CRITICAL, 0.5f);
+        final ThrottlingLevel levelEmergency = new ThrottlingLevel(
+                PowerManager.THERMAL_STATUS_EMERGENCY, 0.1f);
+
+        mDeviceConfigFake.setThermalBrightnessThrottlingData(
+                "123,3,severe,0.9,critical,0.5,emergency,0.1");
+        final BrightnessThrottler throttler = createThrottlerSupported(data);
+
+        verify(mThermalServiceMock).registerThermalEventListenerWithType(
+                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
+        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
+
+        // Ensure that the multiple levels set via the string through the device config correctly
+        // override the original display device config ones.
+
+        // levelSevere
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelSevere.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
+        assertFalse(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelSevere.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(0.9f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+
+        // levelCritical
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelCritical.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(0.9f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelCritical.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(0.5f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+
+        //levelEmergency
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelEmergency.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(0.5f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelEmergency.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(0.1f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+    }
+
+    private void assertThrottlingLevelsEquals(
+            List<ThrottlingLevel> expected,
+            List<ThrottlingLevel> actual) {
+        assertEquals(expected.size(), actual.size());
+
+        for (int i = 0; i < expected.size(); i++) {
+            ThrottlingLevel expectedLevel = expected.get(i);
+            ThrottlingLevel actualLevel = actual.get(i);
+
+            assertEquals(expectedLevel.thermalStatus, actualLevel.thermalStatus);
+            assertEquals(expectedLevel.brightness, actualLevel.brightness, 0.0f);
+        }
+    }
+
+    private BrightnessThrottler createThrottlerUnsupported() {
+        return new BrightnessThrottler(mInjectorMock, mHandler, mHandler,
+                /* throttlingChangeCallback= */ () -> {}, /* uniqueDisplayId= */ null,
+                /* thermalThrottlingDataId= */ null,
+                /* thermalThrottlingDataMap= */ new HashMap<>(1));
+    }
+
+    private BrightnessThrottler createThrottlerSupported(ThermalBrightnessThrottlingData data) {
+        assertNotNull(data);
+        HashMap<String, ThermalBrightnessThrottlingData> throttlingDataMap = new HashMap<>(1);
+        throttlingDataMap.put("default", data);
+        return new BrightnessThrottler(mInjectorMock, mHandler, BackgroundThread.getHandler(),
+                () -> {}, "123", "default", throttlingDataMap);
+    }
+
+    private Temperature getSkinTemp(@ThrottlingStatus int status) {
+        return new Temperature(30.0f, Temperature.TYPE_SKIN, "test_skin_temp", status);
+    }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessTrackerTest.java b/services/tests/displayservicetests/src/com/android/server/display/BrightnessTrackerTest.java
new file mode 100644
index 0000000..44c7dec
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessTrackerTest.java
@@ -0,0 +1,1247 @@
+/*
+ * 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.
+ */
+
+package com.android.server.display;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.ActivityTaskManager.RootTaskInfo;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ParceledListSlice;
+import android.database.ContentObserver;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.display.AmbientBrightnessDayStats;
+import android.hardware.display.BrightnessChangeEvent;
+import android.hardware.display.BrightnessConfiguration;
+import android.hardware.display.ColorDisplayManager;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayedContentSample;
+import android.hardware.display.DisplayedContentSamplingAttributes;
+import android.hardware.input.InputSensorInfo;
+import android.os.BatteryManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.MessageQueue;
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.util.AtomicFile;
+import android.view.Display;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BrightnessTrackerTest {
+    private static final float DEFAULT_INITIAL_BRIGHTNESS = 2.5f;
+    private static final boolean DEFAULT_COLOR_SAMPLING_ENABLED = true;
+    private static final String DEFAULT_DISPLAY_ID = "123";
+    private static final float FLOAT_DELTA = 0.01f;
+
+    @Mock private InputSensorInfo mInputSensorInfoMock;
+
+    private BrightnessTracker mTracker;
+    private TestInjector mInjector;
+    private Sensor mLightSensorFake;
+
+    private static Object sHandlerLock = new Object();
+    private static Handler sHandler;
+    private static HandlerThread sThread =
+            new HandlerThread("brightness.test", android.os.Process.THREAD_PRIORITY_BACKGROUND);
+
+    private int mDefaultNightModeColorTemperature;
+    private float mRbcOffsetFactor;
+
+    private static Handler ensureHandler() {
+        synchronized (sHandlerLock) {
+            if (sHandler == null) {
+                sThread.start();
+                sHandler = new Handler(sThread.getLooper());
+            }
+            return sHandler;
+        }
+    }
+
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mInjector = new TestInjector(ensureHandler());
+        mLightSensorFake = new Sensor(mInputSensorInfoMock);
+
+        mTracker = new BrightnessTracker(InstrumentationRegistry.getContext(), mInjector);
+        mTracker.setLightSensor(mLightSensorFake);
+        mDefaultNightModeColorTemperature =
+                InstrumentationRegistry.getContext().getResources().getInteger(
+                R.integer.config_nightDisplayColorTemperatureDefault);
+        mRbcOffsetFactor = InstrumentationRegistry.getContext()
+                .getSystemService(ColorDisplayManager.class).getReduceBrightColorsOffsetFactor();
+    }
+
+    @Test
+    public void testStartStopTrackerScreenOnOff() {
+        mInjector.mInteractive = false;
+        startTracker(mTracker);
+        assertNull(mInjector.mSensorListener);
+        assertNotNull(mInjector.mBroadcastReceiver);
+        assertTrue(mInjector.mIdleScheduled);
+        mInjector.sendScreenChange(/* screenOn= */ true);
+        assertNotNull(mInjector.mSensorListener);
+        assertTrue(mInjector.mColorSamplingEnabled);
+
+        mInjector.sendScreenChange(/* screenOn= */ false);
+        assertNull(mInjector.mSensorListener);
+        assertFalse(mInjector.mColorSamplingEnabled);
+
+        // Turn screen on while brightness mode is manual
+        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ false);
+        mInjector.sendScreenChange(/* screenOn= */ true);
+        assertNull(mInjector.mSensorListener);
+        assertFalse(mInjector.mColorSamplingEnabled);
+
+        // Set brightness mode to automatic while screen is off.
+        mInjector.sendScreenChange(/* screenOn= */ false);
+        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ true);
+        assertNull(mInjector.mSensorListener);
+        assertFalse(mInjector.mColorSamplingEnabled);
+
+        // Turn on screen while brightness mode is automatic.
+        mInjector.sendScreenChange(/* screenOn= */ true);
+        assertNotNull(mInjector.mSensorListener);
+        assertTrue(mInjector.mColorSamplingEnabled);
+
+        mTracker.stop();
+        assertNull(mInjector.mSensorListener);
+        assertNull(mInjector.mBroadcastReceiver);
+        assertFalse(mInjector.mIdleScheduled);
+        assertFalse(mInjector.mColorSamplingEnabled);
+    }
+
+    @Test
+    public void testModifyBrightnessConfiguration() {
+        mInjector.mInteractive = true;
+        // Start with tracker not listening for color samples.
+        startTracker(mTracker, DEFAULT_INITIAL_BRIGHTNESS, /* collectColorSamples= */ false);
+        assertFalse(mInjector.mColorSamplingEnabled);
+
+        // Update brightness config to enabled color sampling.
+        mTracker.setShouldCollectColorSample(/* collectColorSamples= */ true);
+        mInjector.waitForHandler();
+        assertTrue(mInjector.mColorSamplingEnabled);
+
+        // Update brightness config to disable color sampling.
+        mTracker.setShouldCollectColorSample(/* collectColorSamples= */ false);
+        mInjector.waitForHandler();
+        assertFalse(mInjector.mColorSamplingEnabled);
+
+        // Pretend screen is off, update config to turn on color sampling.
+        mInjector.sendScreenChange(/* screenOn= */ false);
+        mTracker.setShouldCollectColorSample(/* collectColorSamples= */ true);
+        mInjector.waitForHandler();
+        assertFalse(mInjector.mColorSamplingEnabled);
+
+        // Pretend screen is on.
+        mInjector.sendScreenChange(/* screenOn= */ true);
+        assertTrue(mInjector.mColorSamplingEnabled);
+
+        mTracker.stop();
+        assertFalse(mInjector.mColorSamplingEnabled);
+    }
+
+    @Test
+    public void testNoColorSampling_WrongPixelFormat() {
+        mInjector.mDefaultSamplingAttributes =
+                new DisplayedContentSamplingAttributes(
+                        0x23,
+                        mInjector.mDefaultSamplingAttributes.getDataspace(),
+                        mInjector.mDefaultSamplingAttributes.getComponentMask());
+        startTracker(mTracker);
+        assertFalse(mInjector.mColorSamplingEnabled);
+        assertNull(mInjector.mDisplayListener);
+    }
+
+    @Test
+    public void testNoColorSampling_MissingComponent() {
+        mInjector.mDefaultSamplingAttributes =
+                new DisplayedContentSamplingAttributes(
+                        mInjector.mDefaultSamplingAttributes.getPixelFormat(),
+                        mInjector.mDefaultSamplingAttributes.getDataspace(),
+                        0x2);
+        startTracker(mTracker);
+        assertFalse(mInjector.mColorSamplingEnabled);
+        assertNull(mInjector.mDisplayListener);
+    }
+
+    @Test
+    public void testNoColorSampling_NoSupport() {
+        mInjector.mDefaultSamplingAttributes = null;
+        startTracker(mTracker);
+        assertFalse(mInjector.mColorSamplingEnabled);
+        assertNull(mInjector.mDisplayListener);
+    }
+
+    @Test
+    public void testColorSampling_FrameRateChange() {
+        startTracker(mTracker);
+        assertTrue(mInjector.mColorSamplingEnabled);
+        assertNotNull(mInjector.mDisplayListener);
+        int noFramesSampled = mInjector.mNoColorSamplingFrames;
+        mInjector.mFrameRate = 120.0f;
+        // Wrong display
+        mInjector.mDisplayListener.onDisplayChanged(Display.DEFAULT_DISPLAY + 10);
+        assertEquals(noFramesSampled, mInjector.mNoColorSamplingFrames);
+        // Correct display
+        mInjector.mDisplayListener.onDisplayChanged(Display.DEFAULT_DISPLAY);
+        assertEquals(noFramesSampled * 2, mInjector.mNoColorSamplingFrames);
+    }
+
+    @Test
+    public void testAdaptiveOnOff() {
+        mInjector.mInteractive = true;
+        mInjector.mIsBrightnessModeAutomatic = false;
+        startTracker(mTracker);
+        assertNull(mInjector.mSensorListener);
+        assertNotNull(mInjector.mBroadcastReceiver);
+        assertNotNull(mInjector.mContentObserver);
+        assertTrue(mInjector.mIdleScheduled);
+        assertFalse(mInjector.mColorSamplingEnabled);
+        assertNull(mInjector.mDisplayListener);
+
+        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ true);
+        assertNotNull(mInjector.mSensorListener);
+        assertTrue(mInjector.mColorSamplingEnabled);
+        assertNotNull(mInjector.mDisplayListener);
+
+        SensorEventListener listener = mInjector.mSensorListener;
+        DisplayManager.DisplayListener displayListener = mInjector.mDisplayListener;
+        mInjector.mSensorListener = null;
+        mInjector.mColorSamplingEnabled = false;
+        mInjector.mDisplayListener = null;
+        // Duplicate notification
+        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ true);
+        // Sensor shouldn't have been registered as it was already registered.
+        assertNull(mInjector.mSensorListener);
+        assertFalse(mInjector.mColorSamplingEnabled);
+        assertNull(mInjector.mDisplayListener);
+        mInjector.mDisplayListener = displayListener;
+        mInjector.mColorSamplingEnabled = true;
+
+        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ false);
+        assertNull(mInjector.mSensorListener);
+        assertFalse(mInjector.mColorSamplingEnabled);
+        assertNull(mInjector.mDisplayListener);
+
+        mTracker.stop();
+        assertNull(mInjector.mSensorListener);
+        assertNull(mInjector.mBroadcastReceiver);
+        assertNull(mInjector.mContentObserver);
+        assertFalse(mInjector.mIdleScheduled);
+        assertFalse(mInjector.mColorSamplingEnabled);
+        assertNull(mInjector.mDisplayListener);
+    }
+
+    @Test
+    public void testBrightnessEvent() {
+        final float brightness = 0.5f;
+        final String displayId = "1234";
+
+        startTracker(mTracker);
+        final long sensorTime = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos());
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
+        final long currentTime = mInjector.currentTimeMillis();
+        notifyBrightnessChanged(mTracker, brightness, displayId, new float[] {1.0f},
+                new long[] {sensorTime});
+        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
+        mTracker.stop();
+
+        assertEquals(1, events.size());
+        BrightnessChangeEvent event = events.get(0);
+        assertEquals(currentTime, event.timeStamp);
+        assertEquals(displayId, event.uniqueDisplayId);
+        assertEquals(1, event.luxValues.length);
+        assertEquals(1.0f, event.luxValues[0], FLOAT_DELTA);
+        assertEquals(currentTime - TimeUnit.SECONDS.toMillis(2),
+                event.luxTimestamps[0]);
+        assertEquals(brightness, event.brightness, FLOAT_DELTA);
+        assertEquals(DEFAULT_INITIAL_BRIGHTNESS, event.lastBrightness, FLOAT_DELTA);
+
+        // System had no data so these should all be at defaults.
+        assertEquals(Float.NaN, event.batteryLevel, 0.0);
+        assertFalse(event.nightMode);
+        assertEquals(mDefaultNightModeColorTemperature, event.colorTemperature);
+    }
+
+    @Test
+    public void testMultipleBrightnessEvents() {
+        final float brightnessOne = 0.2f;
+        final float brightnessTwo = 0.4f;
+        final float brightnessThree = 0.6f;
+        final float brightnessFour = 0.3f;
+        final String displayId = "1234";
+        final float[] luxValues = new float[]{1.0f};
+
+        startTracker(mTracker);
+        final long sensorTime = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos());
+        final long sensorTime2 = sensorTime + TimeUnit.SECONDS.toMillis(20);
+        final long sensorTime3 = sensorTime2 + TimeUnit.SECONDS.toMillis(30);
+        final long sensorTime4 = sensorTime3 + TimeUnit.SECONDS.toMillis(40);
+        final long originalTime = mInjector.currentTimeMillis();
+
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
+        notifyBrightnessChanged(mTracker, brightnessOne, displayId, luxValues,
+                new long[] {sensorTime});
+
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(20));
+        notifyBrightnessChanged(mTracker, brightnessTwo, displayId, luxValues,
+                new long[] {sensorTime2});
+
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(30));
+        notifyBrightnessChanged(mTracker, brightnessThree, displayId, luxValues,
+                new long[] {sensorTime3});
+
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(40));
+        notifyBrightnessChanged(mTracker, brightnessFour, displayId, luxValues,
+                new long[] {sensorTime4});
+        mTracker.stop();
+        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
+        assertEquals(4, events.size());
+        BrightnessChangeEvent eventOne = events.get(0);
+        assertEquals(brightnessOne, eventOne.brightness, FLOAT_DELTA);
+        assertEquals(originalTime,
+                eventOne.luxTimestamps[0]);
+
+        BrightnessChangeEvent eventTwo = events.get(1);
+        assertEquals(brightnessTwo, eventTwo.brightness, FLOAT_DELTA);
+        assertEquals(originalTime + TimeUnit.SECONDS.toMillis(20),
+                eventTwo.luxTimestamps[0]);
+
+        BrightnessChangeEvent eventThree = events.get(2);
+        assertEquals(brightnessThree, eventThree.brightness, FLOAT_DELTA);
+        assertEquals(originalTime + TimeUnit.SECONDS.toMillis(50),
+                eventThree.luxTimestamps[0]);
+
+        BrightnessChangeEvent eventFour = events.get(3);
+        assertEquals(brightnessFour, eventFour.brightness, FLOAT_DELTA);
+        assertEquals(originalTime + TimeUnit.SECONDS.toMillis(90),
+                eventFour.luxTimestamps[0]);
+    }
+
+    @Test
+    public void testBrightnessFullPopulatedEvent() {
+        final int initialBrightness = 230;
+        final int brightness = 130;
+        final String displayId = "1234";
+
+        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_ACTIVATED, 1);
+        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE, 3333);
+
+        mInjector.mSecureIntSettings.put(Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, 1);
+        mInjector.mSecureIntSettings.put(Settings.Secure.REDUCE_BRIGHT_COLORS_LEVEL, 40);
+
+        startTracker(mTracker, initialBrightness, DEFAULT_COLOR_SAMPLING_ENABLED);
+        mInjector.mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(),
+                batteryChangeEvent(30, 60));
+        final long currentTime = mInjector.currentTimeMillis();
+        notifyBrightnessChanged(mTracker, brightness, displayId, new float[] {1000.0f},
+                new long[] {TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos())});
+        List<BrightnessChangeEvent> eventsNoPackage =
+                mTracker.getEvents(0, false).getList();
+        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
+        mTracker.stop();
+
+        assertEquals(1, events.size());
+        BrightnessChangeEvent event = events.get(0);
+        assertEquals(event.timeStamp, currentTime);
+        assertEquals(displayId, event.uniqueDisplayId);
+        assertArrayEquals(new float[] {1000.0f}, event.luxValues, FLOAT_DELTA);
+        assertArrayEquals(new long[] {currentTime}, event.luxTimestamps);
+        assertEquals(brightness, event.brightness, FLOAT_DELTA);
+        assertEquals(initialBrightness, event.lastBrightness, FLOAT_DELTA);
+        assertEquals(0.5, event.batteryLevel, FLOAT_DELTA);
+        assertTrue(event.nightMode);
+        assertEquals(3333, event.colorTemperature);
+        assertTrue(event.reduceBrightColors);
+        assertEquals(40, event.reduceBrightColorsStrength);
+        assertEquals(brightness * mRbcOffsetFactor, event.reduceBrightColorsOffset, FLOAT_DELTA);
+        assertEquals("a.package", event.packageName);
+        assertEquals(0, event.userId);
+        assertArrayEquals(new long[] {1, 10, 100, 1000, 300, 30, 10, 1}, event.colorValueBuckets);
+        assertEquals(10000, event.colorSampleDuration);
+
+        assertEquals(1, eventsNoPackage.size());
+        assertNull(eventsNoPackage.get(0).packageName);
+    }
+
+    @Test
+    public void testIgnoreAutomaticBrightnessChange() {
+        final int initialBrightness = 30;
+        startTracker(mTracker, initialBrightness, DEFAULT_COLOR_SAMPLING_ENABLED);
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(1));
+
+        final int systemUpdatedBrightness = 20;
+        notifyBrightnessChanged(mTracker, systemUpdatedBrightness, /* userInitiated= */ false,
+                /* powerBrightnessFactor= */ 0.5f, /* isUserSetBrightness= */ false,
+                /* isDefaultBrightnessConfig= */ false, DEFAULT_DISPLAY_ID);
+        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
+        // No events because we filtered out our change.
+        assertEquals(0, events.size());
+
+        final int firstUserUpdateBrightness = 20;
+        // Then change comes from somewhere else so we shouldn't filter.
+        notifyBrightnessChanged(mTracker, firstUserUpdateBrightness);
+
+        // and with a different brightness value.
+        final int secondUserUpdateBrightness = 34;
+        notifyBrightnessChanged(mTracker, secondUserUpdateBrightness);
+        events = mTracker.getEvents(0, true).getList();
+
+        assertEquals(2, events.size());
+        // First event is change from system update (20) to first user update (20)
+        assertEquals(systemUpdatedBrightness, events.get(0).lastBrightness, FLOAT_DELTA);
+        assertEquals(firstUserUpdateBrightness, events.get(0).brightness, FLOAT_DELTA);
+        // Second event is from first to second user update.
+        assertEquals(firstUserUpdateBrightness, events.get(1).lastBrightness, FLOAT_DELTA);
+        assertEquals(secondUserUpdateBrightness, events.get(1).brightness, FLOAT_DELTA);
+
+        mTracker.stop();
+    }
+
+    @Test
+    public void testLimitedBufferSize() {
+        startTracker(mTracker);
+
+        for (int brightness = 0; brightness <= 255; ++brightness) {
+            mInjector.incrementTime(TimeUnit.SECONDS.toNanos(1));
+            notifyBrightnessChanged(mTracker, brightness);
+        }
+        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
+        mTracker.stop();
+
+        // Should be capped at 100 events, and they should be the most recent 100.
+        assertEquals(100, events.size());
+        for (int i = 0; i < events.size(); i++) {
+            BrightnessChangeEvent event = events.get(i);
+            assertEquals(156 + i, event.brightness, FLOAT_DELTA);
+        }
+    }
+
+    @Test
+    public void testReadEvents() throws Exception {
+        BrightnessTracker tracker = new BrightnessTracker(InstrumentationRegistry.getContext(),
+                mInjector);
+        mInjector.mCurrentTimeMillis = System.currentTimeMillis();
+        long someTimeAgo = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(12);
+        long twoMonthsAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60);
+        // 3 Events in the file but one too old to read.
+        String eventFile =
+                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                + "<events>\n"
+                + "<event nits=\"194.2\" timestamp=\""
+                + Long.toString(someTimeAgo) + "\" packageName=\""
+                + "com.example.app\" user=\"10\" "
+                + "lastNits=\"32.333\" "
+                + "batteryLevel=\"1.0\" nightMode=\"false\" colorTemperature=\"0\" "
+                + "reduceBrightColors=\"false\" reduceBrightColorsStrength=\"40\" "
+                + "reduceBrightColorsOffset=\"0\"\n"
+                + "uniqueDisplayId=\"123\""
+                + "lux=\"32.2,31.1\" luxTimestamps=\""
+                + Long.toString(someTimeAgo) + "," + Long.toString(someTimeAgo) + "\""
+                + "defaultConfig=\"true\" powerSaveFactor=\"0.5\" userPoint=\"true\" />"
+                + "<event nits=\"71\" timestamp=\""
+                + Long.toString(someTimeAgo) + "\" packageName=\""
+                + "com.android.anapp\" user=\"11\" "
+                + "lastNits=\"32\" "
+                + "batteryLevel=\"0.5\" nightMode=\"true\" colorTemperature=\"3235\" "
+                + "reduceBrightColors=\"true\" reduceBrightColorsStrength=\"40\" "
+                + "reduceBrightColorsOffset=\"0\"\n"
+                + "uniqueDisplayId=\"456\""
+                + "lux=\"132.2,131.1\" luxTimestamps=\""
+                + Long.toString(someTimeAgo) + "," + Long.toString(someTimeAgo) + "\""
+                + "colorSampleDuration=\"3456\" colorValueBuckets=\"123,598,23,19\"/>"
+                // Event that is too old so shouldn't show up.
+                + "<event nits=\"142\" timestamp=\""
+                + Long.toString(twoMonthsAgo) + "\" packageName=\""
+                + "com.example.app\" user=\"10\" "
+                + "lastNits=\"32\" "
+                + "batteryLevel=\"1.0\" nightMode=\"false\" colorTemperature=\"0\" "
+                + "reduceBrightColors=\"false\" reduceBrightColorsStrength=\"40\" "
+                + "reduceBrightColorsOffset=\"0\"\n"
+                + "uniqueDisplayId=\"789\""
+                + "lux=\"32.2,31.1\" luxTimestamps=\""
+                + Long.toString(twoMonthsAgo) + "," + Long.toString(twoMonthsAgo) + "\"/>"
+                + "</events>";
+        tracker.readEventsLocked(getInputStream(eventFile));
+        List<BrightnessChangeEvent> events = tracker.getEvents(0, true).getList();
+        assertEquals(1, events.size());
+        BrightnessChangeEvent event = events.get(0);
+        assertEquals(someTimeAgo, event.timeStamp);
+        assertEquals(194.2, event.brightness, FLOAT_DELTA);
+        assertEquals("123", event.uniqueDisplayId);
+        assertArrayEquals(new float[] {32.2f, 31.1f}, event.luxValues, FLOAT_DELTA);
+        assertArrayEquals(new long[] {someTimeAgo, someTimeAgo}, event.luxTimestamps);
+        assertEquals(32.333, event.lastBrightness, FLOAT_DELTA);
+        assertEquals(0, event.userId);
+        assertFalse(event.nightMode);
+        assertFalse(event.reduceBrightColors);
+        assertEquals(1.0f, event.batteryLevel, FLOAT_DELTA);
+        assertEquals("com.example.app", event.packageName);
+        assertTrue(event.isDefaultBrightnessConfig);
+        assertEquals(0.5f, event.powerBrightnessFactor, FLOAT_DELTA);
+        assertTrue(event.isUserSetBrightness);
+        assertNull(event.colorValueBuckets);
+
+        events = tracker.getEvents(1, true).getList();
+        assertEquals(1, events.size());
+        event = events.get(0);
+        assertEquals(someTimeAgo, event.timeStamp);
+        assertEquals(71, event.brightness, FLOAT_DELTA);
+        assertEquals("456", event.uniqueDisplayId);
+        assertArrayEquals(new float[] {132.2f, 131.1f}, event.luxValues, FLOAT_DELTA);
+        assertArrayEquals(new long[] {someTimeAgo, someTimeAgo}, event.luxTimestamps);
+        assertEquals(32, event.lastBrightness, FLOAT_DELTA);
+        assertEquals(1, event.userId);
+        assertTrue(event.nightMode);
+        assertEquals(3235, event.colorTemperature);
+        assertTrue(event.reduceBrightColors);
+        assertEquals(0.5f, event.batteryLevel, FLOAT_DELTA);
+        assertEquals("com.android.anapp", event.packageName);
+        // Not present in the event so default to false.
+        assertFalse(event.isDefaultBrightnessConfig);
+        assertEquals(1.0, event.powerBrightnessFactor, FLOAT_DELTA);
+        assertFalse(event.isUserSetBrightness);
+        assertEquals(3456L, event.colorSampleDuration);
+        assertArrayEquals(new long[] {123L, 598L, 23L, 19L}, event.colorValueBuckets);
+
+        // Pretend user 1 is a profile of user 0.
+        mInjector.mProfiles = new int[]{0, 1};
+        events = tracker.getEvents(0, true).getList();
+        // Both events should now be returned.
+        assertEquals(2, events.size());
+        BrightnessChangeEvent userZeroEvent;
+        BrightnessChangeEvent userOneEvent;
+        if (events.get(0).userId == 0) {
+            userZeroEvent = events.get(0);
+            userOneEvent = events.get(1);
+        } else {
+            userZeroEvent = events.get(1);
+            userOneEvent = events.get(0);
+        }
+        assertEquals(0, userZeroEvent.userId);
+        assertEquals("com.example.app", userZeroEvent.packageName);
+        assertEquals(1, userOneEvent.userId);
+        // Events from user 1 should have the package name redacted
+        assertNull(userOneEvent.packageName);
+    }
+
+    @Test
+    public void testFailedRead() {
+        String someTimeAgo =
+                Long.toString(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(12));
+        mInjector.mCurrentTimeMillis = System.currentTimeMillis();
+
+        BrightnessTracker tracker = new BrightnessTracker(InstrumentationRegistry.getContext(),
+                mInjector);
+        String eventFile = "junk in the file";
+        try {
+            tracker.readEventsLocked(getInputStream(eventFile));
+        } catch (IOException e) {
+            // Expected;
+        }
+        assertEquals(0, tracker.getEvents(0, true).getList().size());
+
+        // Missing lux value.
+        eventFile =
+                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                        + "<events>\n"
+                        + "<event nits=\"194\" timestamp=\"" + someTimeAgo + "\" packageName=\""
+                        + "com.example.app\" user=\"10\" "
+                        + "batteryLevel=\"0.7\" nightMode=\"false\" colorTemperature=\"0\" />\n"
+                        + "</events>";
+        try {
+            tracker.readEventsLocked(getInputStream(eventFile));
+        } catch (IOException e) {
+            // Expected;
+        }
+        assertEquals(0, tracker.getEvents(0, true).getList().size());
+    }
+
+    @Test
+    public void testWriteThenRead() throws Exception {
+        final int brightness = 20;
+        final String displayId = "1234";
+
+        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_ACTIVATED, 1);
+        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE, 3339);
+
+        mInjector.mSecureIntSettings.put(Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, 1);
+        mInjector.mSecureIntSettings.put(Settings.Secure.REDUCE_BRIGHT_COLORS_LEVEL, 40);
+
+        startTracker(mTracker);
+        mInjector.mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(),
+                batteryChangeEvent(30, 100));
+        final long elapsedTime1 = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos());
+        final long currentTime1 = mInjector.currentTimeMillis();
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
+        final long elapsedTime2 = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos());
+        final long currentTime2 = mInjector.currentTimeMillis();
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(3));
+        notifyBrightnessChanged(mTracker, brightness, /* userInitiated= */ true,
+                /* powerBrightnessFactor= */ 0.5f, /* isUserSetBrightness= */ true,
+                /* isDefaultBrightnessConfig= */ false, displayId, new float[] {2000.0f, 3000.0f},
+                new long[] {elapsedTime1, elapsedTime2});
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        mTracker.writeEventsLocked(baos);
+        mTracker.stop();
+
+        baos.flush();
+        ByteArrayInputStream input = new ByteArrayInputStream(baos.toByteArray());
+        BrightnessTracker tracker = new BrightnessTracker(InstrumentationRegistry.getContext(),
+                mInjector);
+        tracker.readEventsLocked(input);
+        List<BrightnessChangeEvent> events = tracker.getEvents(0, true).getList();
+
+        assertEquals(1, events.size());
+        BrightnessChangeEvent event = events.get(0);
+        assertEquals(displayId, event.uniqueDisplayId);
+        assertArrayEquals(new float[] {2000.0f, 3000.0f}, event.luxValues, FLOAT_DELTA);
+        assertArrayEquals(new long[] {currentTime1, currentTime2}, event.luxTimestamps);
+        assertEquals(brightness, event.brightness, FLOAT_DELTA);
+        assertEquals(0.3, event.batteryLevel, FLOAT_DELTA);
+        assertTrue(event.nightMode);
+        assertEquals(3339, event.colorTemperature);
+        assertTrue(event.reduceBrightColors);
+        assertEquals(40, event.reduceBrightColorsStrength);
+        assertEquals(brightness * mRbcOffsetFactor, event.reduceBrightColorsOffset, FLOAT_DELTA);
+        assertEquals(0.5f, event.powerBrightnessFactor, FLOAT_DELTA);
+        assertTrue(event.isUserSetBrightness);
+        assertFalse(event.isDefaultBrightnessConfig);
+        assertArrayEquals(new long[] {1, 10, 100, 1000, 300, 30, 10, 1}, event.colorValueBuckets);
+        assertEquals(10000, event.colorSampleDuration);
+    }
+
+    @Test
+    public void testParcelUnParcel() {
+        Parcel parcel = Parcel.obtain();
+        BrightnessChangeEvent.Builder builder = new BrightnessChangeEvent.Builder();
+        builder.setBrightness(23f);
+        builder.setTimeStamp(345L);
+        builder.setPackageName("com.example");
+        builder.setUserId(12);
+        builder.setUniqueDisplayId("9876");
+        float[] luxValues = new float[2];
+        luxValues[0] = 3000.0f;
+        luxValues[1] = 4000.0f;
+        builder.setLuxValues(luxValues);
+        long[] luxTimestamps = new long[2];
+        luxTimestamps[0] = 325L;
+        luxTimestamps[1] = 315L;
+        builder.setLuxTimestamps(luxTimestamps);
+        builder.setBatteryLevel(0.7f);
+        builder.setNightMode(false);
+        builder.setColorTemperature(345);
+        builder.setReduceBrightColors(false);
+        builder.setReduceBrightColorsStrength(40);
+        builder.setReduceBrightColorsOffset(20f);
+        builder.setLastBrightness(50f);
+        builder.setColorValues(new long[] {23, 34, 45}, 1000L);
+        BrightnessChangeEvent event = builder.build();
+
+        event.writeToParcel(parcel, 0);
+        byte[] parceled = parcel.marshall();
+        parcel.recycle();
+
+        parcel = Parcel.obtain();
+        parcel.unmarshall(parceled, 0, parceled.length);
+        parcel.setDataPosition(0);
+
+        BrightnessChangeEvent event2 = BrightnessChangeEvent.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+        assertEquals(event.brightness, event2.brightness, FLOAT_DELTA);
+        assertEquals(event.timeStamp, event2.timeStamp);
+        assertEquals(event.packageName, event2.packageName);
+        assertEquals(event.userId, event2.userId);
+        assertEquals(event.uniqueDisplayId, event2.uniqueDisplayId);
+        assertArrayEquals(event.luxValues, event2.luxValues, FLOAT_DELTA);
+        assertArrayEquals(event.luxTimestamps, event2.luxTimestamps);
+        assertEquals(event.batteryLevel, event2.batteryLevel, FLOAT_DELTA);
+        assertEquals(event.nightMode, event2.nightMode);
+        assertEquals(event.colorTemperature, event2.colorTemperature);
+        assertEquals(event.reduceBrightColors, event2.reduceBrightColors);
+        assertEquals(event.reduceBrightColorsStrength, event2.reduceBrightColorsStrength);
+        assertEquals(event.reduceBrightColorsOffset, event2.reduceBrightColorsOffset, FLOAT_DELTA);
+        assertEquals(event.lastBrightness, event2.lastBrightness, FLOAT_DELTA);
+        assertArrayEquals(event.colorValueBuckets, event2.colorValueBuckets);
+        assertEquals(event.colorSampleDuration, event2.colorSampleDuration);
+
+        parcel = Parcel.obtain();
+        builder.setBatteryLevel(Float.NaN);
+        event = builder.build();
+        event.writeToParcel(parcel, 0);
+        parceled = parcel.marshall();
+        parcel.recycle();
+
+        parcel = Parcel.obtain();
+        parcel.unmarshall(parceled, 0, parceled.length);
+        parcel.setDataPosition(0);
+        event2 = BrightnessChangeEvent.CREATOR.createFromParcel(parcel);
+        assertEquals(event.batteryLevel, event2.batteryLevel, FLOAT_DELTA);
+    }
+
+    @Test
+    public void testNonNullAmbientStats() {
+        // getAmbientBrightnessStats should return an empty list rather than null when
+        // tracker isn't started or hasn't collected any data.
+        ParceledListSlice<AmbientBrightnessDayStats> slice = mTracker.getAmbientBrightnessStats(0);
+        assertNotNull(slice);
+        assertTrue(slice.getList().isEmpty());
+        startTracker(mTracker);
+        slice = mTracker.getAmbientBrightnessStats(0);
+        assertNotNull(slice);
+        assertTrue(slice.getList().isEmpty());
+    }
+
+    @Test
+    public void testBackgroundHandlerDelay() {
+        final int brightness = 20;
+
+        // Setup tracker.
+        startTracker(mTracker);
+        mInjector.mSensorListener.onSensorChanged(createSensorEvent(1.0f));
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
+
+        // Block handler from running.
+        final CountDownLatch latch = new CountDownLatch(1);
+        mInjector.mHandler.post(
+                () -> {
+                    try {
+                        latch.await();
+                    } catch (InterruptedException e) {
+                        fail(e.getMessage());
+                    }
+                });
+
+        // Send an event.
+        long eventTime = mInjector.currentTimeMillis();
+        mTracker.notifyBrightnessChanged(brightness, /* userInitiated= */ true,
+                /* powerBrightnessFactor= */ 1.0f, /* isUserSetBrightness= */ false,
+                /* isDefaultBrightnessConfig= */ false, DEFAULT_DISPLAY_ID, new float[10],
+                new long[10]);
+
+        // Time passes before handler can run.
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
+
+        // Let the handler run.
+        latch.countDown();
+        mInjector.waitForHandler();
+
+        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
+        mTracker.stop();
+
+        // Check event was recorded with time it was sent rather than handler ran.
+        assertEquals(1, events.size());
+        BrightnessChangeEvent event = events.get(0);
+        assertEquals(eventTime, event.timeStamp);
+    }
+
+    @Test
+    public void testDisplayIdChange() {
+        float firstBrightness = 0.5f;
+        float secondBrightness = 0.75f;
+        String firstDisplayId = "123";
+        String secondDisplayId = "456";
+
+        startTracker(mTracker);
+        mInjector.mSensorListener.onSensorChanged(createSensorEvent(1000.0f));
+
+        notifyBrightnessChanged(mTracker, firstBrightness, firstDisplayId);
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
+        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
+        assertEquals(1, events.size());
+        BrightnessChangeEvent firstEvent = events.get(0);
+        assertEquals(firstDisplayId, firstEvent.uniqueDisplayId);
+        assertEquals(firstBrightness, firstEvent.brightness, 0.001f);
+
+        notifyBrightnessChanged(mTracker, secondBrightness, secondDisplayId);
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
+        events = mTracker.getEvents(0, true).getList();
+        assertEquals(2, events.size());
+        BrightnessChangeEvent secondEvent = events.get(1);
+        assertEquals(secondDisplayId, secondEvent.uniqueDisplayId);
+        assertEquals(secondBrightness, secondEvent.brightness, 0.001f);
+
+        mTracker.stop();
+    }
+
+    @Test
+    public void testLightSensorChange() {
+        // verify the tracker started correctly and a listener registered
+        startTracker(mTracker);
+        assertNotNull(mInjector.mSensorListener);
+        assertEquals(mInjector.mLightSensor, mLightSensorFake);
+
+        // Setting the sensor to null should stop the registered listener.
+        mTracker.setLightSensor(null);
+        mInjector.waitForHandler();
+        assertNull(mInjector.mSensorListener);
+        assertNull(mInjector.mLightSensor);
+
+        // Resetting sensor should start listener again
+        mTracker.setLightSensor(mLightSensorFake);
+        mInjector.waitForHandler();
+        assertNotNull(mInjector.mSensorListener);
+        assertEquals(mInjector.mLightSensor, mLightSensorFake);
+
+        Sensor secondSensor = new Sensor(mInputSensorInfoMock);
+        // Setting a different listener should keep things working
+        mTracker.setLightSensor(secondSensor);
+        mInjector.waitForHandler();
+        assertNotNull(mInjector.mSensorListener);
+        assertEquals(mInjector.mLightSensor, secondSensor);
+    }
+
+    @Test
+    public void testSetLightSensorDoesntStartListener() {
+        mTracker.setLightSensor(mLightSensorFake);
+        assertNull(mInjector.mSensorListener);
+    }
+
+    @Test
+    public void testNullLightSensorWontRegister() {
+        mTracker.setLightSensor(null);
+        startTracker(mTracker);
+        assertNull(mInjector.mSensorListener);
+        assertNull(mInjector.mLightSensor);
+    }
+
+    @Test
+    public void testOnlyOneReceiverRegistered() {
+        assertNull(mInjector.mLightSensor);
+        assertNull(mInjector.mSensorListener);
+        assertNull(mInjector.mContentObserver);
+        assertNull(mInjector.mBroadcastReceiver);
+        assertFalse(mInjector.mIdleScheduled);
+        startTracker(mTracker, 0.3f, false);
+
+        assertNotNull(mInjector.mLightSensor);
+        assertNotNull(mInjector.mSensorListener);
+        assertNotNull(mInjector.mContentObserver);
+        assertNotNull(mInjector.mBroadcastReceiver);
+        assertTrue(mInjector.mIdleScheduled);
+        Sensor registeredLightSensor = mInjector.mLightSensor;
+        SensorEventListener registeredSensorListener = mInjector.mSensorListener;
+        ContentObserver registeredContentObserver = mInjector.mContentObserver;
+        BroadcastReceiver registeredBroadcastReceiver = mInjector.mBroadcastReceiver;
+
+        mTracker.start(0.3f);
+        assertSame(registeredLightSensor, mInjector.mLightSensor);
+        assertSame(registeredSensorListener, mInjector.mSensorListener);
+        assertSame(registeredContentObserver, mInjector.mContentObserver);
+        assertSame(registeredBroadcastReceiver, mInjector.mBroadcastReceiver);
+
+        mTracker.stop();
+        assertNull(mInjector.mLightSensor);
+        assertNull(mInjector.mSensorListener);
+        assertNull(mInjector.mContentObserver);
+        assertNull(mInjector.mBroadcastReceiver);
+        assertFalse(mInjector.mIdleScheduled);
+
+        // mInjector asserts that we aren't removing a null receiver
+        mTracker.stop();
+    }
+
+    private InputStream getInputStream(String data) {
+        return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
+    }
+
+    private Intent batteryChangeEvent(int level, int scale) {
+        Intent intent = new Intent();
+        intent.setAction(Intent.ACTION_BATTERY_CHANGED);
+        intent.putExtra(BatteryManager.EXTRA_LEVEL, level);
+        intent.putExtra(BatteryManager.EXTRA_SCALE, scale);
+        return intent;
+    }
+
+    private SensorEvent createSensorEvent(float lux) {
+        SensorEvent event;
+        try {
+            Constructor<SensorEvent> constr =
+                    SensorEvent.class.getDeclaredConstructor(Integer.TYPE);
+            constr.setAccessible(true);
+            event = constr.newInstance(1);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        event.values[0] = lux;
+        event.timestamp = mInjector.mElapsedRealtimeNanos;
+
+        return event;
+    }
+
+    private void startTracker(BrightnessTracker tracker) {
+        startTracker(tracker, DEFAULT_INITIAL_BRIGHTNESS,  DEFAULT_COLOR_SAMPLING_ENABLED);
+    }
+
+    private void startTracker(BrightnessTracker tracker, float initialBrightness,
+            boolean collectColorSamples) {
+        tracker.start(initialBrightness);
+        tracker.setShouldCollectColorSample(collectColorSamples);
+        mInjector.waitForHandler();
+    }
+
+    private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness) {
+        notifyBrightnessChanged(tracker, brightness, DEFAULT_DISPLAY_ID);
+    }
+
+    private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness,
+            String displayId) {
+        notifyBrightnessChanged(tracker, brightness, /* userInitiated= */ true,
+                /* powerBrightnessFactor= */ 1.0f, /* isUserSetBrightness= */ false,
+                /* isDefaultBrightnessConfig= */ false, displayId, new float[10], new long[10]);
+    }
+
+    private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness,
+            String displayId, float[] luxValues, long[] luxTimestamps) {
+        notifyBrightnessChanged(tracker, brightness, /* userInitiated= */ true,
+                /* powerBrightnessFactor= */ 1.0f, /* isUserSetBrightness= */ false,
+                /* isDefaultBrightnessConfig= */ false, displayId, luxValues, luxTimestamps);
+    }
+
+    private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness,
+            boolean userInitiated, float powerBrightnessFactor, boolean isUserSetBrightness,
+            boolean isDefaultBrightnessConfig, String displayId) {
+        tracker.notifyBrightnessChanged(brightness, userInitiated, powerBrightnessFactor,
+                isUserSetBrightness, isDefaultBrightnessConfig, displayId, new float[10],
+                new long[10]);
+        mInjector.waitForHandler();
+    }
+
+    private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness,
+            boolean userInitiated, float powerBrightnessFactor, boolean isUserSetBrightness,
+            boolean isDefaultBrightnessConfig, String displayId, float[] luxValues,
+            long[] luxTimestamps) {
+        tracker.notifyBrightnessChanged(brightness, userInitiated, powerBrightnessFactor,
+                isUserSetBrightness, isDefaultBrightnessConfig, displayId, luxValues,
+                luxTimestamps);
+        mInjector.waitForHandler();
+    }
+
+    private BrightnessConfiguration buildBrightnessConfiguration(boolean collectColorSamples) {
+        BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder(
+                /* lux= */ new float[] {0f, 10f, 100f},
+                /* nits= */ new float[] {1f, 90f, 100f});
+        builder.setShouldCollectColorSamples(collectColorSamples);
+        return builder.build();
+    }
+
+    private static final class Idle implements MessageQueue.IdleHandler {
+        private boolean mIdle;
+
+        @Override
+        public boolean queueIdle() {
+            synchronized (this) {
+                mIdle = true;
+                notifyAll();
+            }
+            return false;
+        }
+
+        public synchronized void waitForIdle() {
+            while (!mIdle) {
+                try {
+                    wait();
+                } catch (InterruptedException e) {
+                }
+            }
+        }
+    }
+
+    private class TestInjector extends BrightnessTracker.Injector {
+        SensorEventListener mSensorListener;
+        Sensor mLightSensor;
+        BroadcastReceiver mBroadcastReceiver;
+        DisplayManager.DisplayListener mDisplayListener;
+        Map<String, Integer> mSecureIntSettings = new HashMap<>();
+        long mCurrentTimeMillis = System.currentTimeMillis();
+        long mElapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos();
+        Handler mHandler;
+        boolean mIdleScheduled;
+        boolean mInteractive = true;
+        int[] mProfiles;
+        ContentObserver mContentObserver;
+        boolean mIsBrightnessModeAutomatic = true;
+        boolean mColorSamplingEnabled = false;
+        DisplayedContentSamplingAttributes mDefaultSamplingAttributes =
+                new DisplayedContentSamplingAttributes(0x37, 0, 0x4);
+        float mFrameRate = 60.0f;
+        int mNoColorSamplingFrames;
+
+
+        public TestInjector(Handler handler) {
+            mHandler = handler;
+        }
+
+        void incrementTime(long timeMillis) {
+            mCurrentTimeMillis += timeMillis;
+            mElapsedRealtimeNanos += TimeUnit.MILLISECONDS.toNanos(timeMillis);
+        }
+
+        void setBrightnessMode(boolean isBrightnessModeAutomatic) {
+            mIsBrightnessModeAutomatic = isBrightnessModeAutomatic;
+            mContentObserver.dispatchChange(false, null);
+            waitForHandler();
+        }
+
+        void sendScreenChange(boolean screenOn) {
+            mInteractive = screenOn;
+            Intent intent = new Intent();
+            intent.setAction(screenOn ? Intent.ACTION_SCREEN_ON : Intent.ACTION_SCREEN_OFF);
+            mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(), intent);
+            waitForHandler();
+        }
+
+        void waitForHandler() {
+            Idle idle = new Idle();
+            mHandler.getLooper().getQueue().addIdleHandler(idle);
+            mHandler.post(() -> {});
+            idle.waitForIdle();
+        }
+
+        @Override
+        public void registerSensorListener(Context context,
+                SensorEventListener sensorListener, Sensor lightSensor, Handler handler) {
+            mSensorListener = sensorListener;
+            mLightSensor = lightSensor;
+        }
+
+        @Override
+        public void unregisterSensorListener(Context context,
+                SensorEventListener sensorListener) {
+            mSensorListener = null;
+            mLightSensor = null;
+        }
+
+        @Override
+        public void registerBrightnessModeObserver(ContentResolver resolver,
+                ContentObserver settingsObserver) {
+            mContentObserver = settingsObserver;
+        }
+
+        @Override
+        public void unregisterBrightnessModeObserver(Context context,
+                ContentObserver settingsObserver) {
+            mContentObserver = null;
+        }
+
+        @Override
+        public void registerReceiver(Context context,
+                BroadcastReceiver shutdownReceiver, IntentFilter shutdownFilter) {
+            mBroadcastReceiver = shutdownReceiver;
+        }
+
+        @Override
+        public void unregisterReceiver(Context context,
+                BroadcastReceiver broadcastReceiver) {
+            assertEquals(mBroadcastReceiver, broadcastReceiver);
+            mBroadcastReceiver = null;
+        }
+
+        @Override
+        public Handler getBackgroundHandler() {
+            return mHandler;
+        }
+
+        @Override
+        public boolean isBrightnessModeAutomatic(ContentResolver resolver) {
+            return mIsBrightnessModeAutomatic;
+        }
+
+        @Override
+        public int getSecureIntForUser(ContentResolver resolver, String setting, int defaultValue,
+                int userId) {
+            Integer value = mSecureIntSettings.get(setting);
+            if (value == null) {
+                return defaultValue;
+            } else {
+                return value;
+            }
+        }
+
+        @Override
+        public AtomicFile getFile(String filename) {
+            // Don't have the test write / read from anywhere.
+            return null;
+        }
+
+        @Override
+        public AtomicFile getLegacyFile(String filename) {
+            // Don't have the test write / read from anywhere.
+            return null;
+        }
+
+        @Override
+        public long currentTimeMillis() {
+            return mCurrentTimeMillis;
+        }
+
+        @Override
+        public long elapsedRealtimeNanos() {
+            return mElapsedRealtimeNanos;
+        }
+
+        @Override
+        public int getUserSerialNumber(UserManager userManager, int userId) {
+            return userId + 10;
+        }
+
+        @Override
+        public int getUserId(UserManager userManager, int userSerialNumber) {
+            return userSerialNumber - 10;
+        }
+
+        @Override
+        public int[] getProfileIds(UserManager userManager, int userId) {
+            if (mProfiles != null) {
+                return mProfiles;
+            } else {
+                return new int[]{userId};
+            }
+        }
+
+        @Override
+        public RootTaskInfo getFocusedStack() throws RemoteException {
+            RootTaskInfo focusedStack = new RootTaskInfo();
+            focusedStack.userId = 0;
+            focusedStack.topActivity = new ComponentName("a.package", "a.class");
+            return focusedStack;
+        }
+
+        @Override
+        public void scheduleIdleJob(Context context) {
+            // Don't actually schedule jobs during unit tests.
+            mIdleScheduled = true;
+        }
+
+        @Override
+        public void cancelIdleJob(Context context) {
+            mIdleScheduled = false;
+        }
+
+        @Override
+        public boolean isInteractive(Context context) {
+            return mInteractive;
+        }
+
+        @Override
+        public int getNightDisplayColorTemperature(Context context) {
+            return mSecureIntSettings.getOrDefault(Settings.Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE,
+                    mDefaultNightModeColorTemperature);
+        }
+
+        @Override
+        public boolean isNightDisplayActivated(Context context) {
+            return mSecureIntSettings.getOrDefault(Settings.Secure.NIGHT_DISPLAY_ACTIVATED,
+                    0) == 1;
+        }
+
+        @Override
+        public int getReduceBrightColorsStrength(Context context) {
+            return mSecureIntSettings.getOrDefault(Settings.Secure.REDUCE_BRIGHT_COLORS_LEVEL,
+                    0);
+        }
+
+        @Override
+        public boolean isReduceBrightColorsActivated(Context context) {
+            return mSecureIntSettings.getOrDefault(Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
+                    0) == 1;
+        }
+
+        @Override
+        public DisplayedContentSample sampleColor(int noFramesToSample) {
+            return new DisplayedContentSample(600L,
+                    null,
+                    null,
+                     new long[] {1, 10, 100, 1000, 300, 30, 10, 1},
+                    null);
+        }
+
+        @Override
+        public float getFrameRate(Context context) {
+            return mFrameRate;
+        }
+
+        @Override
+        public DisplayedContentSamplingAttributes getSamplingAttributes() {
+            return mDefaultSamplingAttributes;
+        }
+
+        @Override
+        public boolean enableColorSampling(boolean enable, int noFrames) {
+            mColorSamplingEnabled = enable;
+            mNoColorSamplingFrames = noFrames;
+            return true;
+        }
+
+        @Override
+        public void registerDisplayListener(Context context,
+                DisplayManager.DisplayListener listener, Handler handler) {
+            mDisplayListener = listener;
+        }
+
+        @Override
+        public void unRegisterDisplayListener(Context context,
+                DisplayManager.DisplayListener listener) {
+            mDisplayListener = null;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/ColorFadeTest.java b/services/tests/displayservicetests/src/com/android/server/display/ColorFadeTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/ColorFadeTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/ColorFadeTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/DeviceStateToLayoutMapTest.java b/services/tests/displayservicetests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
new file mode 100644
index 0000000..8b04eca
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -0,0 +1,953 @@
+/*
+ * 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.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Temperature;
+import android.util.SparseArray;
+import android.view.SurfaceControl;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.R;
+import com.android.server.display.config.ThermalStatus;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+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};
+    private static final int[] HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{30000};
+
+    private DisplayDeviceConfig mDisplayDeviceConfig;
+    private static final float ZERO_DELTA = 0.0f;
+    private static final float SMALL_DELTA = 0.0001f;
+    @Mock
+    private Context mContext;
+
+    @Mock
+    private Resources mResources;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mContext.getResources()).thenReturn(mResources);
+        mockDeviceConfigs();
+    }
+
+    @Test
+    public void testConfigValuesFromDisplayConfig() throws IOException {
+        setupDisplayDeviceConfigFromDisplayConfigFile();
+
+        assertEquals(mDisplayDeviceConfig.getName(), "Example Display");
+        assertEquals(mDisplayDeviceConfig.getAmbientHorizonLong(), 5000);
+        assertEquals(mDisplayDeviceConfig.getAmbientHorizonShort(), 50);
+        assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000);
+        assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000);
+        assertEquals(mDisplayDeviceConfig.getBrightnessRampFastDecrease(), 0.01f, ZERO_DELTA);
+        assertEquals(mDisplayDeviceConfig.getBrightnessRampFastIncrease(), 0.02f, ZERO_DELTA);
+        assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowIncrease(), 0.04f, ZERO_DELTA);
+        assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowDecrease(), 0.03f, ZERO_DELTA);
+        assertEquals(mDisplayDeviceConfig.getBrightnessDefault(), 0.5f, ZERO_DELTA);
+        assertArrayEquals(mDisplayDeviceConfig.getBrightness(), new float[]{0.0f, 0.62f, 1.0f},
+                ZERO_DELTA);
+        assertArrayEquals(mDisplayDeviceConfig.getNits(), new float[]{2.0f, 500.0f, 800.0f},
+                ZERO_DELTA);
+        assertArrayEquals(mDisplayDeviceConfig.getBacklight(), new float[]{0.0f, 0.62f, 1.0f},
+                ZERO_DELTA);
+        assertEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLightDebounce(), 2000);
+        assertEquals(mDisplayDeviceConfig.getAutoBrightnessDarkeningLightDebounce(), 1000);
+        assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new
+                float[]{0.0f, 50.0f, 80.0f}, ZERO_DELTA);
+        assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new
+                float[]{45.32f, 75.43f}, ZERO_DELTA);
+
+        // Test thresholds
+        assertEquals(10, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(),
+                ZERO_DELTA);
+        assertEquals(20, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(),
+                ZERO_DELTA);
+        assertEquals(30, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), ZERO_DELTA);
+        assertEquals(40, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+        assertEquals(0.1f, mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), ZERO_DELTA);
+        assertEquals(0.2f, mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle(), ZERO_DELTA);
+        assertEquals(0.3f, mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), ZERO_DELTA);
+        assertEquals(0.4f, mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 0.10f, 0.20f},
+                mDisplayDeviceConfig.getScreenBrighteningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{9, 10, 11},
+                mDisplayDeviceConfig.getScreenBrighteningPercentages(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 0.11f, 0.21f},
+                mDisplayDeviceConfig.getScreenDarkeningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{11, 12, 13},
+                mDisplayDeviceConfig.getScreenDarkeningPercentages(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 100, 200},
+                mDisplayDeviceConfig.getAmbientBrighteningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{13, 14, 15},
+                mDisplayDeviceConfig.getAmbientBrighteningPercentages(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 300, 400},
+                mDisplayDeviceConfig.getAmbientDarkeningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{15, 16, 17},
+                mDisplayDeviceConfig.getAmbientDarkeningPercentages(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 0.12f, 0.22f},
+                mDisplayDeviceConfig.getScreenBrighteningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{17, 18, 19},
+                mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 0.13f, 0.23f},
+                mDisplayDeviceConfig.getScreenDarkeningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{19, 20, 21},
+                mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 500, 600},
+                mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{21, 22, 23},
+                mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 700, 800},
+                mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{23, 24, 25},
+                mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
+
+        assertEquals(75, mDisplayDeviceConfig.getDefaultLowBlockingZoneRefreshRate());
+        assertEquals(90, mDisplayDeviceConfig.getDefaultHighBlockingZoneRefreshRate());
+        assertEquals(85, mDisplayDeviceConfig.getDefaultPeakRefreshRate());
+        assertEquals(45, mDisplayDeviceConfig.getDefaultRefreshRate());
+        assertEquals(2, mDisplayDeviceConfig.getRefreshRangeProfiles().size());
+        assertEquals(60, mDisplayDeviceConfig.getRefreshRange("test1").min, SMALL_DELTA);
+        assertEquals(60, mDisplayDeviceConfig.getRefreshRange("test1").max, SMALL_DELTA);
+        assertEquals(80, mDisplayDeviceConfig.getRefreshRange("test2").min, SMALL_DELTA);
+        assertEquals(90, mDisplayDeviceConfig.getRefreshRange("test2").max, SMALL_DELTA);
+        assertEquals(82, mDisplayDeviceConfig.getDefaultRefreshRateInHbmHdr());
+        assertEquals(83, mDisplayDeviceConfig.getDefaultRefreshRateInHbmSunlight());
+        assertArrayEquals(new int[]{45, 55},
+                mDisplayDeviceConfig.getLowDisplayBrightnessThresholds());
+        assertArrayEquals(new int[]{50, 60},
+                mDisplayDeviceConfig.getLowAmbientBrightnessThresholds());
+        assertArrayEquals(new int[]{65, 75},
+                mDisplayDeviceConfig.getHighDisplayBrightnessThresholds());
+        assertArrayEquals(new int[]{70, 80},
+                mDisplayDeviceConfig.getHighAmbientBrightnessThresholds());
+
+        assertEquals("sensor_12345",
+                mDisplayDeviceConfig.getScreenOffBrightnessSensor().type);
+        assertEquals("Sensor 12345",
+                mDisplayDeviceConfig.getScreenOffBrightnessSensor().name);
+
+        assertArrayEquals(new int[]{-1, 10, 20, 30, 40},
+                mDisplayDeviceConfig.getScreenOffBrightnessSensorValueToLux());
+
+        List<DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel>
+                defaultThrottlingLevels = new ArrayList<>();
+        defaultThrottlingLevels.add(
+                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
+                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.light), 0.4f
+        ));
+        defaultThrottlingLevels.add(
+                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
+                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.moderate), 0.3f
+        ));
+        defaultThrottlingLevels.add(
+                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
+                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.severe), 0.2f
+        ));
+        defaultThrottlingLevels.add(
+                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
+                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.critical), 0.1f
+        ));
+        defaultThrottlingLevels.add(
+                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
+                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.emergency), 0.05f
+        ));
+        defaultThrottlingLevels.add(
+                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
+                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.shutdown), 0.025f
+        ));
+
+        DisplayDeviceConfig.ThermalBrightnessThrottlingData defaultThrottlingData =
+                new DisplayDeviceConfig.ThermalBrightnessThrottlingData(defaultThrottlingLevels);
+
+        List<DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel>
+                concurrentThrottlingLevels = new ArrayList<>();
+        concurrentThrottlingLevels.add(
+                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
+                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.light), 0.2f
+        ));
+        concurrentThrottlingLevels.add(
+                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
+                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.moderate), 0.15f
+        ));
+        concurrentThrottlingLevels.add(
+                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
+                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.severe), 0.1f
+        ));
+        concurrentThrottlingLevels.add(
+                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
+                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.critical), 0.05f
+        ));
+        concurrentThrottlingLevels.add(
+                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
+                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.emergency), 0.025f
+        ));
+        concurrentThrottlingLevels.add(
+                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
+                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.shutdown), 0.0125f
+        ));
+        DisplayDeviceConfig.ThermalBrightnessThrottlingData concurrentThrottlingData =
+                new DisplayDeviceConfig.ThermalBrightnessThrottlingData(concurrentThrottlingLevels);
+
+        HashMap<String, DisplayDeviceConfig.ThermalBrightnessThrottlingData> throttlingDataMap =
+                new HashMap<>(2);
+        throttlingDataMap.put("default", defaultThrottlingData);
+        throttlingDataMap.put("concurrent", concurrentThrottlingData);
+
+        assertEquals(throttlingDataMap,
+                mDisplayDeviceConfig.getThermalBrightnessThrottlingDataMapByThrottlingId());
+
+        assertNotNull(mDisplayDeviceConfig.getHostUsiVersion());
+        assertEquals(mDisplayDeviceConfig.getHostUsiVersion().getMajorVersion(), 2);
+        assertEquals(mDisplayDeviceConfig.getHostUsiVersion().getMinorVersion(), 0);
+
+        // Max desired Hdr/SDR ratio upper-bounds the HDR brightness.
+        assertEquals(1.0f,
+                mDisplayDeviceConfig.getHdrBrightnessFromSdr(0.62f, Float.POSITIVE_INFINITY),
+                ZERO_DELTA);
+        assertEquals(0.62f,
+                mDisplayDeviceConfig.getHdrBrightnessFromSdr(0.62f, 1.0f),
+                ZERO_DELTA);
+        assertEquals(0.77787f,
+                mDisplayDeviceConfig.getHdrBrightnessFromSdr(0.62f, 1.25f),
+                SMALL_DELTA);
+
+        // Todo: Add asserts for DensityMapping,
+        // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
+    }
+
+    @Test
+    public void testConfigValuesFromConfigResource() {
+        setupDisplayDeviceConfigFromConfigResourceFile();
+        assertNull(mDisplayDeviceConfig.getName());
+        assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new
+                float[]{2.0f, 200.0f, 600.0f}, ZERO_DELTA);
+        assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new
+                float[]{0.0f, 0.0f, 110.0f, 500.0f}, ZERO_DELTA);
+
+        // Test thresholds
+        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(),
+                ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+        assertEquals(0, mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), ZERO_DELTA);
+        assertEquals(0, mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle(), ZERO_DELTA);
+
+        // screen levels will be considered "old screen brightness scale"
+        // and therefore will divide by 255
+        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+                mDisplayDeviceConfig.getScreenBrighteningLevels(), SMALL_DELTA);
+        assertArrayEquals(new float[]{35, 36, 37},
+                mDisplayDeviceConfig.getScreenBrighteningPercentages(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+                mDisplayDeviceConfig.getScreenDarkeningLevels(), SMALL_DELTA);
+        assertArrayEquals(new float[]{37, 38, 39},
+                mDisplayDeviceConfig.getScreenDarkeningPercentages(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 30, 31},
+                mDisplayDeviceConfig.getAmbientBrighteningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{27, 28, 29},
+                mDisplayDeviceConfig.getAmbientBrighteningPercentages(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 30, 31},
+                mDisplayDeviceConfig.getAmbientDarkeningLevels(), ZERO_DELTA);
+        assertArrayEquals(new float[]{29, 30, 31},
+                mDisplayDeviceConfig.getAmbientDarkeningPercentages(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+                mDisplayDeviceConfig.getScreenBrighteningLevelsIdle(), SMALL_DELTA);
+        assertArrayEquals(new float[]{35, 36, 37},
+                mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
+                mDisplayDeviceConfig.getScreenDarkeningLevelsIdle(), SMALL_DELTA);
+        assertArrayEquals(new float[]{37, 38, 39},
+                mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle(), ZERO_DELTA);
+
+        assertArrayEquals(new float[]{0, 30, 31},
+                mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{27, 28, 29},
+                mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{0, 30, 31},
+                mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA);
+        assertArrayEquals(new float[]{29, 30, 31},
+                mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
+        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(0, mDisplayDeviceConfig.getRefreshRangeProfiles().size());
+        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(),
+                LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
+        assertArrayEquals(mDisplayDeviceConfig.getHighDisplayBrightnessThresholds(),
+                HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
+        assertArrayEquals(mDisplayDeviceConfig.getHighAmbientBrightnessThresholds(),
+                HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
+
+        // Todo: Add asserts for ThermalBrightnessThrottlingData, DensityMapping,
+        // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
+    }
+
+    @Test
+    public void testThermalRefreshRateThrottlingFromDisplayConfig() throws IOException {
+        setupDisplayDeviceConfigFromDisplayConfigFile();
+
+        SparseArray<SurfaceControl.RefreshRateRange> defaultMap =
+                mDisplayDeviceConfig.getThermalRefreshRateThrottlingData(null);
+        assertNotNull(defaultMap);
+        assertEquals(2, defaultMap.size());
+        assertEquals(30, defaultMap.get(Temperature.THROTTLING_CRITICAL).min, SMALL_DELTA);
+        assertEquals(60, defaultMap.get(Temperature.THROTTLING_CRITICAL).max, SMALL_DELTA);
+        assertEquals(0, defaultMap.get(Temperature.THROTTLING_SHUTDOWN).min, SMALL_DELTA);
+        assertEquals(30, defaultMap.get(Temperature.THROTTLING_SHUTDOWN).max, SMALL_DELTA);
+
+        SparseArray<SurfaceControl.RefreshRateRange> testMap =
+                mDisplayDeviceConfig.getThermalRefreshRateThrottlingData("test");
+        assertNotNull(testMap);
+        assertEquals(1, testMap.size());
+        assertEquals(60, testMap.get(Temperature.THROTTLING_EMERGENCY).min, SMALL_DELTA);
+        assertEquals(90, testMap.get(Temperature.THROTTLING_EMERGENCY).max, SMALL_DELTA);
+    }
+
+    @Test
+    public void testValidLuxThrottling() throws Exception {
+        setupDisplayDeviceConfigFromDisplayConfigFile();
+
+        Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData =
+                mDisplayDeviceConfig.getLuxThrottlingData();
+        assertEquals(2, luxThrottlingData.size());
+
+        Map<Float, Float> adaptiveOnBrightnessPoints = luxThrottlingData.get(
+                DisplayDeviceConfig.BrightnessLimitMapType.ADAPTIVE);
+        assertEquals(2, adaptiveOnBrightnessPoints.size());
+        assertEquals(0.3f, adaptiveOnBrightnessPoints.get(1000f), SMALL_DELTA);
+        assertEquals(0.5f, adaptiveOnBrightnessPoints.get(5000f), SMALL_DELTA);
+
+        Map<Float, Float> adaptiveOffBrightnessPoints = luxThrottlingData.get(
+                DisplayDeviceConfig.BrightnessLimitMapType.DEFAULT);
+        assertEquals(2, adaptiveOffBrightnessPoints.size());
+        assertEquals(0.35f, adaptiveOffBrightnessPoints.get(1500f), SMALL_DELTA);
+        assertEquals(0.55f, adaptiveOffBrightnessPoints.get(5500f), SMALL_DELTA);
+    }
+
+    @Test
+    public void testInvalidLuxThrottling() throws Exception {
+        setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getInvalidLuxThrottling()));
+
+        Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData =
+                mDisplayDeviceConfig.getLuxThrottlingData();
+        assertEquals(1, luxThrottlingData.size());
+
+        Map<Float, Float> adaptiveOnBrightnessPoints = luxThrottlingData.get(
+                DisplayDeviceConfig.BrightnessLimitMapType.ADAPTIVE);
+        assertEquals(1, adaptiveOnBrightnessPoints.size());
+        assertEquals(0.3f, adaptiveOnBrightnessPoints.get(1000f), SMALL_DELTA);
+    }
+
+    private String getValidLuxThrottling() {
+        return "<luxThrottling>\n"
+                + "    <brightnessLimitMap>\n"
+                + "        <type>adaptive</type>\n"
+                + "        <map>\n"
+                + "            <point>"
+                + "                <first>1000</first>\n"
+                + "                <second>0.3</second>\n"
+                + "            </point>"
+                + "            <point>"
+                + "                <first>5000</first>\n"
+                + "                <second>0.5</second>\n"
+                + "            </point>"
+                + "        </map>\n"
+                + "    </brightnessLimitMap>\n"
+                + "    <brightnessLimitMap>\n"
+                + "        <type>default</type>\n"
+                + "        <map>\n"
+                + "            <point>"
+                + "                <first>1500</first>\n"
+                + "                <second>0.35</second>\n"
+                + "            </point>"
+                + "            <point>"
+                + "                <first>5500</first>\n"
+                + "                <second>0.55</second>\n"
+                + "            </point>"
+                + "        </map>\n"
+                + "    </brightnessLimitMap>\n"
+                + "</luxThrottling>";
+    }
+
+    private String getInvalidLuxThrottling() {
+        return "<luxThrottling>\n"
+                + "    <brightnessLimitMap>\n"
+                + "        <type>adaptive</type>\n"
+                + "        <map>\n"
+                + "            <point>"
+                + "                <first>1000</first>\n"
+                + "                <second>0.3</second>\n"
+                + "            </point>"
+                + "            <point>" // second > hbm.transitionPoint, skipped
+                + "                <first>1500</first>\n"
+                + "                <second>0.9</second>\n"
+                + "            </point>"
+                + "            <point>" // same lux value, skipped
+                + "                <first>1000</first>\n"
+                + "                <second>0.5</second>\n"
+                + "            </point>"
+                + "        </map>\n"
+                + "    </brightnessLimitMap>\n"
+                + "    <brightnessLimitMap>\n" // Same type, skipped
+                + "        <type>adaptive</type>\n"
+                + "        <map>\n"
+                + "            <point>"
+                + "                <first>2000</first>\n"
+                + "                <second>0.35</second>\n"
+                + "            </point>"
+                + "            <point>"
+                + "                <first>6000</first>\n"
+                + "                <second>0.55</second>\n"
+                + "            </point>"
+                + "        </map>\n"
+                + "    </brightnessLimitMap>\n"
+                + "    <brightnessLimitMap>\n" // Invalid points only, skipped
+                + "        <type>default</type>\n"
+                + "        <map>\n"
+                + "            <point>"
+                + "                <first>2500</first>\n"
+                + "                <second>0.99</second>\n"
+                + "            </point>"
+                + "        </map>\n"
+                + "    </brightnessLimitMap>\n"
+                + "</luxThrottling>";
+    }
+
+    private String getRefreshThermalThrottlingMaps() {
+        return "<refreshRateThrottlingMap>\n"
+               + "    <refreshRateThrottlingPoint>\n"
+               + "        <thermalStatus>critical</thermalStatus>\n"
+               + "        <refreshRateRange>\n"
+               + "            <minimum>30</minimum>\n"
+               + "            <maximum>60</maximum>\n"
+               + "        </refreshRateRange>\n"
+               + "    </refreshRateThrottlingPoint>\n"
+               + "    <refreshRateThrottlingPoint>\n"
+               + "        <thermalStatus>shutdown</thermalStatus>\n"
+               + "        <refreshRateRange>\n"
+               + "            <minimum>0</minimum>\n"
+               + "            <maximum>30</maximum>\n"
+               + "        </refreshRateRange>\n"
+               + "    </refreshRateThrottlingPoint>\n"
+               + "</refreshRateThrottlingMap>\n"
+               + "<refreshRateThrottlingMap id=\"test\">\n"
+               + "    <refreshRateThrottlingPoint>\n"
+               + "        <thermalStatus>emergency</thermalStatus>\n"
+               + "        <refreshRateRange>\n"
+               + "            <minimum>60</minimum>\n"
+               + "            <maximum>90</maximum>\n"
+               + "        </refreshRateRange>\n"
+               + "    </refreshRateThrottlingPoint>\n"
+               + "</refreshRateThrottlingMap>\n";
+    }
+
+    private String getContent() {
+        return getContent(getValidLuxThrottling());
+    }
+
+    private String getContent(String brightnessCapConfig) {
+        return "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                + "<displayConfiguration>\n"
+                +   "<name>Example Display</name>"
+                +   "<screenBrightnessMap>\n"
+                +       "<point>\n"
+                +           "<value>0.0</value>\n"
+                +           "<nits>2.0</nits>\n"
+                +       "</point>\n"
+                +       "<point>\n"
+                +           "<value>0.62</value>\n"
+                +           "<nits>500.0</nits>\n"
+                +       "</point>\n"
+                +       "<point>\n"
+                +           "<value>1.0</value>\n"
+                +           "<nits>800.0</nits>\n"
+                +       "</point>\n"
+                +   "</screenBrightnessMap>\n"
+                +   "<autoBrightness>\n"
+                +       "<brighteningLightDebounceMillis>2000</brighteningLightDebounceMillis>\n"
+                +       "<darkeningLightDebounceMillis>1000</darkeningLightDebounceMillis>\n"
+                +       "<displayBrightnessMapping>\n"
+                +            "<displayBrightnessPoint>\n"
+                +                "<lux>50</lux>\n"
+                +                "<nits>45.32</nits>\n"
+                +            "</displayBrightnessPoint>\n"
+                +            "<displayBrightnessPoint>\n"
+                +                "<lux>80</lux>\n"
+                +                "<nits>75.43</nits>\n"
+                +            "</displayBrightnessPoint>\n"
+                +       "</displayBrightnessMapping>\n"
+                +   "</autoBrightness>\n"
+                +   "<highBrightnessMode enabled=\"true\">\n"
+                +       "<transitionPoint>0.62</transitionPoint>\n"
+                +       "<minimumLux>10000</minimumLux>\n"
+                +       "<timing>\n"
+                +           "<!-- allow for 5 minutes out of every 30 minutes -->\n"
+                +           "<timeWindowSecs>1800</timeWindowSecs>\n"
+                +           "<timeMaxSecs>300</timeMaxSecs>\n"
+                +           "<timeMinSecs>60</timeMinSecs>\n"
+                +       "</timing>\n"
+                +       "<refreshRate>\n"
+                +           "<minimum>120</minimum>\n"
+                +           "<maximum>120</maximum>\n"
+                +       "</refreshRate>\n"
+                +       "<thermalStatusLimit>light</thermalStatusLimit>\n"
+                +       "<allowInLowPowerMode>false</allowInLowPowerMode>\n"
+                +       "<sdrHdrRatioMap>\n"
+                +            "<point>\n"
+                +                "<sdrNits>2.000</sdrNits>\n"
+                +                "<hdrRatio>4.000</hdrRatio>\n"
+                +            "</point>\n"
+                +            "<point>\n"
+                +                "<sdrNits>500.0</sdrNits>\n"
+                +                "<hdrRatio>1.6</hdrRatio>\n"
+                +            "</point>\n"
+                +       "</sdrHdrRatioMap>\n"
+                +   "</highBrightnessMode>\n"
+                + brightnessCapConfig
+                +   "<screenOffBrightnessSensor>\n"
+                +       "<type>sensor_12345</type>\n"
+                +       "<name>Sensor 12345</name>\n"
+                +   "</screenOffBrightnessSensor>\n"
+                +   "<ambientBrightnessChangeThresholds>\n"
+                +       "<brighteningThresholds>\n"
+                +           "<minimum>10</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>13</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>100</threshold><percentage>14</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>200</threshold><percentage>15</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</brighteningThresholds>\n"
+                +       "<darkeningThresholds>\n"
+                +           "<minimum>30</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>15</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>300</threshold><percentage>16</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>400</threshold><percentage>17</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</darkeningThresholds>\n"
+                +   "</ambientBrightnessChangeThresholds>\n"
+                +   "<displayBrightnessChangeThresholds>\n"
+                +       "<brighteningThresholds>\n"
+                +           "<minimum>0.1</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold>\n"
+                +                   "<percentage>9</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.10</threshold>\n"
+                +                   "<percentage>10</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.20</threshold>\n"
+                +                   "<percentage>11</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</brighteningThresholds>\n"
+                +       "<darkeningThresholds>\n"
+                +           "<minimum>0.3</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>11</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.11</threshold><percentage>12</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.21</threshold><percentage>13</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</darkeningThresholds>\n"
+                +   "</displayBrightnessChangeThresholds>\n"
+                +   "<ambientBrightnessChangeThresholdsIdle>\n"
+                +       "<brighteningThresholds>\n"
+                +           "<minimum>20</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>21</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>500</threshold><percentage>22</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>600</threshold><percentage>23</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</brighteningThresholds>\n"
+                +       "<darkeningThresholds>\n"
+                +           "<minimum>40</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>23</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>700</threshold><percentage>24</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>800</threshold><percentage>25</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</darkeningThresholds>\n"
+                +   "</ambientBrightnessChangeThresholdsIdle>\n"
+                +   "<displayBrightnessChangeThresholdsIdle>\n"
+                +       "<brighteningThresholds>\n"
+                +           "<minimum>0.2</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>17</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.12</threshold><percentage>18</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.22</threshold><percentage>19</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</brighteningThresholds>\n"
+                +       "<darkeningThresholds>\n"
+                +           "<minimum>0.4</minimum>\n"
+                +           "<brightnessThresholdPoints>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0</threshold><percentage>19</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.13</threshold><percentage>20</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +               "<brightnessThresholdPoint>\n"
+                +                   "<threshold>0.23</threshold><percentage>21</percentage>\n"
+                +               "</brightnessThresholdPoint>\n"
+                +           "</brightnessThresholdPoints>\n"
+                +       "</darkeningThresholds>\n"
+                +   "</displayBrightnessChangeThresholdsIdle>\n"
+                +   "<screenBrightnessRampFastDecrease>0.01</screenBrightnessRampFastDecrease> "
+                +   "<screenBrightnessRampFastIncrease>0.02</screenBrightnessRampFastIncrease>  "
+                +   "<screenBrightnessRampSlowDecrease>0.03</screenBrightnessRampSlowDecrease>"
+                +   "<screenBrightnessRampSlowIncrease>0.04</screenBrightnessRampSlowIncrease>"
+                +   "<screenBrightnessRampIncreaseMaxMillis>"
+                +       "2000"
+                +   "</screenBrightnessRampIncreaseMaxMillis>"
+                +   "<screenBrightnessRampDecreaseMaxMillis>"
+                +       "3000"
+                +   "</screenBrightnessRampDecreaseMaxMillis>"
+                +   "<ambientLightHorizonLong>5000</ambientLightHorizonLong>\n"
+                +   "<ambientLightHorizonShort>50</ambientLightHorizonShort>\n"
+                +   "<screenBrightnessRampIncreaseMaxMillis>"
+                +       "2000"
+                +   "</screenBrightnessRampIncreaseMaxMillis>\n"
+                +   "<thermalThrottling>\n"
+                +       "<brightnessThrottlingMap>\n"
+                +           "<brightnessThrottlingPoint>\n"
+                +               "<thermalStatus>light</thermalStatus>\n"
+                +               "<brightness>0.4</brightness>\n"
+                +           "</brightnessThrottlingPoint>\n"
+                +           "<brightnessThrottlingPoint>\n"
+                +               "<thermalStatus>moderate</thermalStatus>\n"
+                +               "<brightness>0.3</brightness>\n"
+                +           "</brightnessThrottlingPoint>\n"
+                +           "<brightnessThrottlingPoint>\n"
+                +               "<thermalStatus>severe</thermalStatus>\n"
+                +               "<brightness>0.2</brightness>\n"
+                +           "</brightnessThrottlingPoint>\n"
+                +           "<brightnessThrottlingPoint>\n"
+                +               "<thermalStatus>critical</thermalStatus>\n"
+                +               "<brightness>0.1</brightness>\n"
+                +           "</brightnessThrottlingPoint>\n"
+                +           "<brightnessThrottlingPoint>\n"
+                +               "<thermalStatus>emergency</thermalStatus>\n"
+                +               "<brightness>0.05</brightness>\n"
+                +           "</brightnessThrottlingPoint>\n"
+                +           "<brightnessThrottlingPoint>\n"
+                +               "<thermalStatus>shutdown</thermalStatus>\n"
+                +               "<brightness>0.025</brightness>\n"
+                +           "</brightnessThrottlingPoint>\n"
+                +       "</brightnessThrottlingMap>\n"
+                +       "<brightnessThrottlingMap id=\"concurrent\">\n"
+                +           "<brightnessThrottlingPoint>\n"
+                +               "<thermalStatus>light</thermalStatus>\n"
+                +               "<brightness>0.2</brightness>\n"
+                +           "</brightnessThrottlingPoint>\n"
+                +           "<brightnessThrottlingPoint>\n"
+                +               "<thermalStatus>moderate</thermalStatus>\n"
+                +               "<brightness>0.15</brightness>\n"
+                +           "</brightnessThrottlingPoint>\n"
+                +           "<brightnessThrottlingPoint>\n"
+                +               "<thermalStatus>severe</thermalStatus>\n"
+                +               "<brightness>0.1</brightness>\n"
+                +           "</brightnessThrottlingPoint>\n"
+                +           "<brightnessThrottlingPoint>\n"
+                +               "<thermalStatus>critical</thermalStatus>\n"
+                +               "<brightness>0.05</brightness>\n"
+                +           "</brightnessThrottlingPoint>\n"
+                +           "<brightnessThrottlingPoint>\n"
+                +               "<thermalStatus>emergency</thermalStatus>\n"
+                +               "<brightness>0.025</brightness>\n"
+                +           "</brightnessThrottlingPoint>\n"
+                +           "<brightnessThrottlingPoint>\n"
+                +               "<thermalStatus>shutdown</thermalStatus>\n"
+                +               "<brightness>0.0125</brightness>\n"
+                +           "</brightnessThrottlingPoint>\n"
+                +       "</brightnessThrottlingMap>\n"
+                +  getRefreshThermalThrottlingMaps()
+                +   "</thermalThrottling>\n"
+                +   "<refreshRate>\n"
+                +       "<defaultRefreshRate>45</defaultRefreshRate>\n"
+                +       "<defaultPeakRefreshRate>85</defaultPeakRefreshRate>\n"
+                +       "<refreshRateZoneProfiles>"
+                +           "<refreshRateZoneProfile id=\"test1\">"
+                +               "<refreshRateRange>\n"
+                +                   "<minimum>60</minimum>\n"
+                +                   "<maximum>60</maximum>\n"
+                +               "</refreshRateRange>\n"
+                +           "</refreshRateZoneProfile>\n"
+                +           "<refreshRateZoneProfile id=\"test2\">"
+                +               "<refreshRateRange>\n"
+                +                   "<minimum>80</minimum>\n"
+                +                   "<maximum>90</maximum>\n"
+                +               "</refreshRateRange>\n"
+                +           "</refreshRateZoneProfile>\n"
+                +       "</refreshRateZoneProfiles>"
+                +       "<defaultRefreshRateInHbmHdr>82</defaultRefreshRateInHbmHdr>\n"
+                +       "<defaultRefreshRateInHbmSunlight>83</defaultRefreshRateInHbmSunlight>\n"
+                +       "<lowerBlockingZoneConfigs>\n"
+                +           "<defaultRefreshRate>75</defaultRefreshRate>\n"
+                +           "<blockingZoneThreshold>\n"
+                +               "<displayBrightnessPoint>\n"
+                +                   "<lux>50</lux>\n"
+                // This number will be rounded to integer when read by the system
+                +                   "<nits>45.3</nits>\n"
+                +               "</displayBrightnessPoint>\n"
+                +               "<displayBrightnessPoint>\n"
+                +                   "<lux>60</lux>\n"
+                // This number will be rounded to integer when read by the system
+                +                   "<nits>55.2</nits>\n"
+                +               "</displayBrightnessPoint>\n"
+                +           "</blockingZoneThreshold>\n"
+                +       "</lowerBlockingZoneConfigs>\n"
+                +       "<higherBlockingZoneConfigs>\n"
+                +           "<defaultRefreshRate>90</defaultRefreshRate>\n"
+                +           "<blockingZoneThreshold>\n"
+                +               "<displayBrightnessPoint>\n"
+                +                   "<lux>70</lux>\n"
+                // This number will be rounded to integer when read by the system
+                +                   "<nits>65.6</nits>\n"
+                +               "</displayBrightnessPoint>\n"
+                +               "<displayBrightnessPoint>\n"
+                +                   "<lux>80</lux>\n"
+                // This number will be rounded to integer when read by the system
+                +                   "<nits>75</nits>\n"
+                +               "</displayBrightnessPoint>\n"
+                +           "</blockingZoneThreshold>\n"
+                +       "</higherBlockingZoneConfigs>\n"
+                +   "</refreshRate>\n"
+                +   "<screenOffBrightnessSensorValueToLux>\n"
+                +       "<item>-1</item>\n"
+                +       "<item>10</item>\n"
+                +       "<item>20</item>\n"
+                +       "<item>30</item>\n"
+                +       "<item>40</item>\n"
+                +   "</screenOffBrightnessSensorValueToLux>\n"
+                +   "<usiVersion>\n"
+                +       "<majorVersion>2</majorVersion>\n"
+                +       "<minorVersion>0</minorVersion>\n"
+                +   "</usiVersion>\n"
+                + "</displayConfiguration>\n";
+    }
+
+    private void mockDeviceConfigs() {
+        when(mResources.getFloat(com.android.internal.R.dimen
+                .config_screenBrightnessSettingDefaultFloat)).thenReturn(0.5f);
+        when(mResources.getFloat(com.android.internal.R.dimen
+                .config_screenBrightnessSettingMaximumFloat)).thenReturn(1.0f);
+    }
+
+    private void setupDisplayDeviceConfigFromDisplayConfigFile() throws IOException {
+        setupDisplayDeviceConfigFromDisplayConfigFile(getContent());
+    }
+
+    private void setupDisplayDeviceConfigFromDisplayConfigFile(String content) throws IOException {
+        Path tempFile = Files.createTempFile("display_config", ".tmp");
+        Files.write(tempFile, content.getBytes(StandardCharsets.UTF_8));
+        mDisplayDeviceConfig = new DisplayDeviceConfig(mContext);
+        mDisplayDeviceConfig.initFromFile(tempFile.toFile());
+    }
+
+    private void setupDisplayDeviceConfigFromConfigResourceFile() {
+        TypedArray screenBrightnessNits = createFloatTypedArray(new float[]{2.0f, 250.0f, 650.0f});
+        when(mResources.obtainTypedArray(
+                com.android.internal.R.array.config_screenBrightnessNits))
+                .thenReturn(screenBrightnessNits);
+        TypedArray screenBrightnessBacklight = createFloatTypedArray(new
+                float[]{0.0f, 120.0f, 255.0f});
+        when(mResources.obtainTypedArray(
+                com.android.internal.R.array.config_screenBrightnessBacklight))
+                .thenReturn(screenBrightnessBacklight);
+        when(mResources.getIntArray(com.android.internal.R.array
+                .config_screenBrightnessBacklight)).thenReturn(new int[]{0, 120, 255});
+
+        when(mResources.getIntArray(com.android.internal.R.array
+                .config_autoBrightnessLevels)).thenReturn(new int[]{30, 80});
+        when(mResources.getIntArray(com.android.internal.R.array
+                .config_autoBrightnessDisplayValuesNits)).thenReturn(new int[]{25, 55});
+
+        TypedArray screenBrightnessLevelNits = createFloatTypedArray(new
+                float[]{2.0f, 200.0f, 600.0f});
+        when(mResources.obtainTypedArray(
+                com.android.internal.R.array.config_autoBrightnessDisplayValuesNits))
+                .thenReturn(screenBrightnessLevelNits);
+        int[] screenBrightnessLevelLux = new int[]{0, 110, 500};
+        when(mResources.getIntArray(
+                com.android.internal.R.array.config_autoBrightnessLevels))
+                .thenReturn(screenBrightnessLevelLux);
+
+        // Thresholds
+        // Config.xml requires the levels arrays to be of length N and the thresholds arrays to be
+        // of length N+1
+        when(mResources.getIntArray(com.android.internal.R.array.config_ambientThresholdLevels))
+                .thenReturn(new int[]{30, 31});
+        when(mResources.getIntArray(com.android.internal.R.array.config_screenThresholdLevels))
+                .thenReturn(new int[]{42, 43});
+        when(mResources.getIntArray(
+                com.android.internal.R.array.config_ambientBrighteningThresholds))
+                .thenReturn(new int[]{270, 280, 290});
+        when(mResources.getIntArray(com.android.internal.R.array.config_ambientDarkeningThresholds))
+                .thenReturn(new int[]{290, 300, 310});
+        when(mResources.getIntArray(R.array.config_screenBrighteningThresholds))
+                .thenReturn(new int[]{350, 360, 370});
+        when(mResources.getIntArray(R.array.config_screenDarkeningThresholds))
+                .thenReturn(new int[]{370, 380, 390});
+
+        // Configs related to refresh rates and blocking zones
+        when(mResources.getInteger(R.integer.config_defaultPeakRefreshRate))
+                .thenReturn(DEFAULT_PEAK_REFRESH_RATE);
+        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))
+                .thenReturn(LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
+        when(mResources.getIntArray(
+                R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate))
+                .thenReturn(HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
+        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);
+    }
+
+    private TypedArray createFloatTypedArray(float[] vals) {
+        TypedArray mockArray = mock(TypedArray.class);
+        when(mockArray.length()).thenAnswer(invocation -> {
+            return vals.length;
+        });
+        when(mockArray.getFloat(anyInt(), anyFloat())).thenAnswer(invocation -> {
+            final float def = (float) invocation.getArguments()[1];
+            if (vals == null) {
+                return def;
+            }
+            int idx = (int) invocation.getArguments()[0];
+            if (idx >= 0 && idx < vals.length) {
+                return vals[idx];
+            } else {
+                return def;
+            }
+        });
+        return mockArray;
+    }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
new file mode 100644
index 0000000..d16c9c5
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -0,0 +1,2319 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY;
+import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
+import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
+import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
+
+import static com.android.server.display.VirtualDisplayAdapter.UNIQUE_ID_PREFIX;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.PropertyInvalidatedCache;
+import android.companion.virtual.IVirtualDevice;
+import android.companion.virtual.IVirtualDeviceManager;
+import android.companion.virtual.VirtualDeviceManager;
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.hardware.display.BrightnessConfiguration;
+import android.hardware.display.Curve;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManagerGlobal;
+import android.hardware.display.DisplayManagerInternal;
+import android.hardware.display.DisplayViewport;
+import android.hardware.display.DisplayedContentSample;
+import android.hardware.display.DisplayedContentSamplingAttributes;
+import android.hardware.display.HdrConversionMode;
+import android.hardware.display.IDisplayManagerCallback;
+import android.hardware.display.IVirtualDisplayCallback;
+import android.hardware.display.VirtualDisplayConfig;
+import android.media.projection.IMediaProjection;
+import android.media.projection.IMediaProjectionManager;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.MessageQueue;
+import android.os.Process;
+import android.os.RemoteException;
+import android.view.ContentRecordingSession;
+import android.view.Display;
+import android.view.DisplayCutout;
+import android.view.DisplayEventReceiver;
+import android.view.DisplayInfo;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.window.DisplayWindowPolicyController;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
+import com.android.server.display.DisplayManagerService.DeviceStateListener;
+import com.android.server.display.DisplayManagerService.SyncRoot;
+import com.android.server.input.InputManagerInternal;
+import com.android.server.lights.LightsManager;
+import com.android.server.sensors.SensorManagerInternal;
+import com.android.server.wm.WindowManagerInternal;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.LongStream;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DisplayManagerServiceTest {
+    private static final int MSG_REGISTER_DEFAULT_DISPLAY_ADAPTERS = 1;
+    private static final long SHORT_DEFAULT_DISPLAY_TIMEOUT_MILLIS = 10;
+
+    private static final float FLOAT_TOLERANCE = 0.01f;
+
+    private static final String VIRTUAL_DISPLAY_NAME = "Test Virtual Display";
+    private static final String PACKAGE_NAME = "com.android.frameworks.displayservicetests";
+    private static final long STANDARD_DISPLAY_EVENTS = DisplayManager.EVENT_FLAG_DISPLAY_ADDED
+                    | DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
+                    | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED;
+
+    @Rule
+    public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
+    private Context mContext;
+
+    private int mHdrConversionMode;
+
+    private int mPreferredHdrOutputType;
+
+    private final DisplayManagerService.Injector mShortMockedInjector =
+            new DisplayManagerService.Injector() {
+                @Override
+                VirtualDisplayAdapter getVirtualDisplayAdapter(SyncRoot syncRoot,
+                        Context context, Handler handler, DisplayAdapter.Listener listener) {
+                    return mMockVirtualDisplayAdapter;
+                }
+
+                @Override
+                LocalDisplayAdapter getLocalDisplayAdapter(SyncRoot syncRoot, Context context,
+                        Handler handler, DisplayAdapter.Listener displayAdapterListener) {
+                    return new LocalDisplayAdapter(syncRoot, context, handler,
+                            displayAdapterListener, new LocalDisplayAdapter.Injector() {
+                        @Override
+                        public LocalDisplayAdapter.SurfaceControlProxy getSurfaceControlProxy() {
+                            return mSurfaceControlProxy;
+                        }
+                    });
+                }
+
+                @Override
+                long getDefaultDisplayDelayTimeout() {
+                    return SHORT_DEFAULT_DISPLAY_TIMEOUT_MILLIS;
+                }
+            };
+
+    class BasicInjector extends DisplayManagerService.Injector {
+        @Override
+        IMediaProjectionManager getProjectionService() {
+            return mMockProjectionService;
+        }
+
+        @Override
+        VirtualDisplayAdapter getVirtualDisplayAdapter(SyncRoot syncRoot, Context context,
+                Handler handler, DisplayAdapter.Listener displayAdapterListener) {
+            return new VirtualDisplayAdapter(syncRoot, context, handler, displayAdapterListener,
+                    new VirtualDisplayAdapter.SurfaceControlDisplayFactory() {
+                        @Override
+                        public IBinder createDisplay(String name, boolean secure,
+                                float requestedRefreshRate) {
+                            return mMockDisplayToken;
+                        }
+
+                        @Override
+                        public void destroyDisplay(IBinder displayToken) {
+                        }
+                    });
+        }
+
+        @Override
+        LocalDisplayAdapter getLocalDisplayAdapter(SyncRoot syncRoot, Context context,
+                Handler handler, DisplayAdapter.Listener displayAdapterListener) {
+            return new LocalDisplayAdapter(syncRoot, context, handler,
+                    displayAdapterListener, new LocalDisplayAdapter.Injector() {
+                        @Override
+                        public LocalDisplayAdapter.SurfaceControlProxy getSurfaceControlProxy() {
+                            return mSurfaceControlProxy;
+                        }
+                    });
+        }
+
+        @Override
+        int setHdrConversionMode(int conversionMode, int preferredHdrOutputType,
+                int[] autoHdrTypes) {
+            mHdrConversionMode = conversionMode;
+            mPreferredHdrOutputType = preferredHdrOutputType;
+            return Display.HdrCapabilities.HDR_TYPE_INVALID;
+        }
+
+        @Override
+        int[] getSupportedHdrOutputTypes() {
+            return new int[]{};
+        }
+
+        @Override
+        int[] getHdrOutputTypesWithLatency() {
+            return new int[]{Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION};
+        }
+
+        boolean getHdrOutputConversionSupport() {
+            return true;
+        }
+    }
+
+    private final DisplayManagerService.Injector mBasicInjector = new BasicInjector();
+
+    @Mock IMediaProjectionManager mMockProjectionService;
+    @Mock IVirtualDeviceManager mIVirtualDeviceManager;
+    @Mock InputManagerInternal mMockInputManagerInternal;
+    @Mock VirtualDeviceManagerInternal mMockVirtualDeviceManagerInternal;
+    @Mock IVirtualDisplayCallback.Stub mMockAppToken;
+    @Mock IVirtualDisplayCallback.Stub mMockAppToken2;
+    @Mock WindowManagerInternal mMockWindowManagerInternal;
+    @Mock LightsManager mMockLightsManager;
+    @Mock VirtualDisplayAdapter mMockVirtualDisplayAdapter;
+    @Mock LocalDisplayAdapter.SurfaceControlProxy mSurfaceControlProxy;
+    @Mock IBinder mMockDisplayToken;
+    @Mock SensorManagerInternal mMockSensorManagerInternal;
+
+    @Mock SensorManager mSensorManager;
+
+    @Mock DisplayDeviceConfig mMockDisplayDeviceConfig;
+
+    @Captor ArgumentCaptor<ContentRecordingSession> mContentRecordingSessionCaptor;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        LocalServices.removeServiceForTest(InputManagerInternal.class);
+        LocalServices.addService(InputManagerInternal.class, mMockInputManagerInternal);
+        LocalServices.removeServiceForTest(WindowManagerInternal.class);
+        LocalServices.addService(WindowManagerInternal.class, mMockWindowManagerInternal);
+        LocalServices.removeServiceForTest(LightsManager.class);
+        LocalServices.addService(LightsManager.class, mMockLightsManager);
+        LocalServices.removeServiceForTest(SensorManagerInternal.class);
+        LocalServices.addService(SensorManagerInternal.class, mMockSensorManagerInternal);
+        LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class);
+        LocalServices.addService(
+                VirtualDeviceManagerInternal.class, mMockVirtualDeviceManagerInternal);
+        // TODO: b/287945043
+        mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
+
+        VirtualDeviceManager vdm = new VirtualDeviceManager(mIVirtualDeviceManager, mContext);
+        when(mContext.getSystemService(VirtualDeviceManager.class)).thenReturn(vdm);
+        // Disable binder caches in this process.
+        PropertyInvalidatedCache.disableForTestMode();
+        setUpDisplay();
+    }
+
+    private void setUpDisplay() {
+        long[] ids = new long[] {100};
+        when(mSurfaceControlProxy.getPhysicalDisplayIds()).thenReturn(ids);
+        when(mSurfaceControlProxy.getPhysicalDisplayToken(anyLong()))
+                .thenReturn(mMockDisplayToken);
+        SurfaceControl.StaticDisplayInfo staticDisplayInfo = new SurfaceControl.StaticDisplayInfo();
+        staticDisplayInfo.isInternal = true;
+        when(mSurfaceControlProxy.getStaticDisplayInfo(anyLong()))
+                .thenReturn(staticDisplayInfo);
+        SurfaceControl.DynamicDisplayInfo dynamicDisplayMode =
+                new SurfaceControl.DynamicDisplayInfo();
+        SurfaceControl.DisplayMode displayMode = new SurfaceControl.DisplayMode();
+        displayMode.width = 100;
+        displayMode.height = 200;
+        displayMode.supportedHdrTypes = new int[]{1, 2};
+        dynamicDisplayMode.supportedDisplayModes = new SurfaceControl.DisplayMode[] {displayMode};
+        when(mSurfaceControlProxy.getDynamicDisplayInfo(anyLong()))
+                .thenReturn(dynamicDisplayMode);
+        when(mSurfaceControlProxy.getDesiredDisplayModeSpecs(mMockDisplayToken))
+                .thenReturn(new SurfaceControl.DesiredDisplayModeSpecs());
+    }
+
+    @Test
+    public void testCreateVirtualDisplay_sentToInputManager() throws RemoteException {
+        // This is to update the display device config such that DisplayManagerService can ignore
+        // the usage of SensorManager, which is available only after the PowerManagerService
+        // is ready.
+        resetConfigToIgnoreSensorManager(mContext);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+        displayManager.systemReady(false /* safeMode */);
+        displayManager.windowManagerAndInputReady();
+
+        // This is effectively the DisplayManager service published to ServiceManager.
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+
+        String uniqueId = "uniqueId --- Test";
+        String uniqueIdPrefix = UNIQUE_ID_PREFIX + mContext.getPackageName() + ":";
+        int width = 600;
+        int height = 800;
+        int dpi = 320;
+        int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH;
+
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, width, height, dpi);
+        builder.setUniqueId(uniqueId);
+        builder.setFlags(flags);
+        int displayId = bs.createVirtualDisplay(builder.build(), mMockAppToken /* callback */,
+                null /* projection */, PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+
+        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+
+        // flush the handler
+        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
+
+        ArgumentCaptor<List<DisplayViewport>> viewportCaptor = ArgumentCaptor.forClass(List.class);
+        verify(mMockInputManagerInternal).setDisplayViewports(viewportCaptor.capture());
+        List<DisplayViewport> viewports = viewportCaptor.getValue();
+
+        // Expect to receive at least 2 viewports: at least 1 internal, and 1 virtual
+        assertTrue(viewports.size() >= 2);
+
+        DisplayViewport virtualViewport = null;
+        DisplayViewport internalViewport = null;
+        for (int i = 0; i < viewports.size(); i++) {
+            DisplayViewport v = viewports.get(i);
+            switch (v.type) {
+                case DisplayViewport.VIEWPORT_INTERNAL: {
+                    // If more than one internal viewport, this will get overwritten several times,
+                    // which for the purposes of this test is fine.
+                    internalViewport = v;
+                    assertTrue(internalViewport.valid);
+                    break;
+                }
+                case DisplayViewport.VIEWPORT_EXTERNAL: {
+                    // External view port is present for auto devices in the form of instrument
+                    // cluster.
+                    break;
+                }
+                case DisplayViewport.VIEWPORT_VIRTUAL: {
+                    virtualViewport = v;
+                    break;
+                }
+            }
+        }
+        // INTERNAL viewport gets created upon access.
+        assertNotNull(internalViewport);
+        assertNotNull(virtualViewport);
+
+        // VIRTUAL
+        assertEquals(height, virtualViewport.deviceHeight);
+        assertEquals(width, virtualViewport.deviceWidth);
+        assertEquals(uniqueIdPrefix + uniqueId, virtualViewport.uniqueId);
+        assertEquals(displayId, virtualViewport.displayId);
+    }
+
+    @Test
+    public void testPhysicalViewports() {
+        // This is to update the display device config such that DisplayManagerService can ignore
+        // the usage of SensorManager, which is available only after the PowerManagerService
+        // is ready.
+        resetConfigToIgnoreSensorManager(mContext);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+        displayManager.systemReady(false /* safeMode */);
+        displayManager.windowManagerAndInputReady();
+
+        // This is effectively the DisplayManager service published to ServiceManager.
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        final int[] displayIds = bs.getDisplayIds(/* includeDisabled= */ true);
+        final int size = displayIds.length;
+        assertTrue(size > 0);
+
+        Map<Integer, Integer> expectedDisplayTypeToViewPortTypeMapping = Map.of(
+                Display.TYPE_INTERNAL, DisplayViewport.VIEWPORT_INTERNAL,
+                Display.TYPE_EXTERNAL, DisplayViewport.VIEWPORT_EXTERNAL
+        );
+        for (int i = 0; i < size; i++) {
+            DisplayInfo info = bs.getDisplayInfo(displayIds[i]);
+            assertTrue(expectedDisplayTypeToViewPortTypeMapping.keySet().contains(info.type));
+        }
+
+        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+
+        // flush the handler
+        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
+
+        ArgumentCaptor<List<DisplayViewport>> viewportCaptor = ArgumentCaptor.forClass(List.class);
+        verify(mMockInputManagerInternal).setDisplayViewports(viewportCaptor.capture());
+        List<DisplayViewport> viewports = viewportCaptor.getValue();
+
+        // Due to the nature of foldables, we may have a different number of viewports than
+        // displays, just verify there's at least one.
+        final int viewportSize = viewports.size();
+        assertTrue(viewportSize > 0);
+
+        // Now verify that each viewport's displayId is valid.
+        Arrays.sort(displayIds);
+        for (int i = 0; i < viewportSize; i++) {
+            DisplayViewport viewport = viewports.get(i);
+            assertNotNull(viewport);
+            DisplayInfo displayInfo = bs.getDisplayInfo(viewport.displayId);
+            assertTrue(expectedDisplayTypeToViewPortTypeMapping.get(displayInfo.type)
+                    == viewport.type);
+            assertTrue(viewport.valid);
+            assertTrue(Arrays.binarySearch(displayIds, viewport.displayId) >= 0);
+        }
+    }
+
+    @Test
+    public void testCreateVirtualDisplayRotatesWithContent() throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+
+        // This is effectively the DisplayManager service published to ServiceManager.
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+
+        String uniqueId = "uniqueId --- Rotates With Content Test";
+        int width = 600;
+        int height = 800;
+        int dpi = 320;
+        int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT;
+
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, width, height, dpi);
+        builder.setFlags(flags);
+        builder.setUniqueId(uniqueId);
+        int displayId = bs.createVirtualDisplay(builder.build(), mMockAppToken /* callback */,
+                null /* projection */, PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+
+        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+
+        // flush the handler
+        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
+
+        DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
+        assertNotNull(ddi);
+        assertTrue((ddi.flags & DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT) != 0);
+    }
+
+    @Test
+    public void testCreateVirtualDisplayOwnFocus() throws RemoteException {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+
+        // This is effectively the DisplayManager service published to ServiceManager.
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+
+        String uniqueId = "uniqueId --- Own Focus Test";
+        int width = 600;
+        int height = 800;
+        int dpi = 320;
+        int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_FOCUS
+                | DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
+
+        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)).thenReturn(
+                PackageManager.PERMISSION_GRANTED);
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, width, height, dpi);
+        builder.setFlags(flags);
+        builder.setUniqueId(uniqueId);
+        int displayId = bs.createVirtualDisplay(builder.build(), /* callback= */ mMockAppToken,
+                /* projection= */ null, PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+
+        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+
+        // flush the handler
+        displayManager.getDisplayHandler().runWithScissors(() -> {}, /* now= */ 0);
+
+        DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
+        assertNotNull(ddi);
+        assertTrue((ddi.flags & DisplayDeviceInfo.FLAG_OWN_FOCUS) != 0);
+    }
+
+    @Test
+    public void testCreateVirtualDisplayOwnFocus_nonTrustedDisplay() throws RemoteException {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+
+        // This is effectively the DisplayManager service published to ServiceManager.
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+
+        String uniqueId = "uniqueId --- Own Focus Test -- nonTrustedDisplay";
+        int width = 600;
+        int height = 800;
+        int dpi = 320;
+        int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_FOCUS;
+
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, width, height, dpi);
+        builder.setFlags(flags);
+        builder.setUniqueId(uniqueId);
+        int displayId = bs.createVirtualDisplay(builder.build(), /* callback= */ mMockAppToken,
+                /* projection= */ null, PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+
+        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+
+        // flush the handler
+        displayManager.getDisplayHandler().runWithScissors(() -> {}, /* now= */ 0);
+
+        DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
+        assertNotNull(ddi);
+        assertTrue((ddi.flags & DisplayDeviceInfo.FLAG_OWN_FOCUS) == 0);
+    }
+
+    /**
+     * Tests that the virtual display is created along-side the default display.
+     */
+    @Test
+    public void testStartVirtualDisplayWithDefaultDisplay_Succeeds() throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+    }
+
+    /**
+     * Tests that we send the device state to window manager
+     */
+    @Test
+    public void testOnStateChanged_sendsStateChangedEventToWm() throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        registerDefaultDisplays(displayManager);
+        displayManager.windowManagerAndInputReady();
+        displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+        DeviceStateListener listener = displayManager.new DeviceStateListener();
+        Handler handler = displayManager.getDisplayHandler();
+        IDisplayManagerCallback displayChangesCallback = registerDisplayChangeCallback(
+                displayManager);
+
+        listener.onStateChanged(123);
+        waitForIdleHandler(handler);
+
+        InOrder inOrder = inOrder(mMockWindowManagerInternal, displayChangesCallback);
+        // Verify there are no display events before WM call
+        inOrder.verify(displayChangesCallback, never()).onDisplayEvent(anyInt(), anyInt());
+        inOrder.verify(mMockWindowManagerInternal).onDisplayManagerReceivedDeviceState(123);
+    }
+
+    /**
+     * Tests that there should be a display change notification to WindowManager to update its own
+     * internal state for things like display cutout when nonOverrideDisplayInfo is changed.
+     */
+    @Test
+    public void testShouldNotifyChangeWhenNonOverrideDisplayInfoChanged() 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();
+        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
+        displayDeviceInfo.width = 100;
+        displayDeviceInfo.height = 200;
+        displayDeviceInfo.supportedModes = new Display.Mode[1];
+        displayDeviceInfo.supportedModes[0] = new Display.Mode(1, 100, 200, 60f);
+        displayDeviceInfo.modeId = 1;
+        final Rect zeroRect = new Rect();
+        displayDeviceInfo.displayCutout = new DisplayCutout(
+                Insets.of(0, 10, 0, 0),
+                zeroRect, new Rect(0, 0, 10, 10), zeroRect, zeroRect);
+        displayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY;
+        displayDeviceInfo.address = new TestUtils.TestDisplayAddress();
+        displayDevice.setDisplayDeviceInfo(displayDeviceInfo);
+        displayManager.getDisplayDeviceRepository()
+                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
+
+        // Find the display id of the added FakeDisplayDevice
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        int displayId = getDisplayIdForDisplayDevice(displayManager, bs, displayDevice);
+        // Setup override DisplayInfo
+        DisplayInfo overrideInfo = bs.getDisplayInfo(displayId);
+        displayManager.setDisplayInfoOverrideFromWindowManagerInternal(displayId, overrideInfo);
+
+        FakeDisplayManagerCallback callback = registerDisplayListenerCallback(
+                displayManager, bs, displayDevice);
+
+        // Simulate DisplayDevice change
+        DisplayDeviceInfo displayDeviceInfo2 = new DisplayDeviceInfo();
+        displayDeviceInfo2.copyFrom(displayDeviceInfo);
+        displayDeviceInfo2.displayCutout = null;
+        displayDevice.setDisplayDeviceInfo(displayDeviceInfo2);
+        displayManager.getDisplayDeviceRepository()
+                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_CHANGED);
+
+        Handler handler = displayManager.getDisplayHandler();
+        waitForIdleHandler(handler);
+        assertTrue(callback.mDisplayChangedCalled);
+    }
+
+    /**
+     * Tests that we get a Runtime exception when we cannot initialize the default display.
+     */
+    @Test
+    public void testStartVirtualDisplayWithDefDisplay_NoDefaultDisplay() throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        Handler handler = displayManager.getDisplayHandler();
+        waitForIdleHandler(handler);
+
+        try {
+            displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+        } catch (RuntimeException e) {
+            return;
+        }
+        fail("Expected DisplayManager to throw RuntimeException when it cannot initialize the"
+                + " default display");
+    }
+
+    /**
+     * Tests that we get a Runtime exception when we cannot initialize the virtual display.
+     */
+    @Test
+    public void testStartVirtualDisplayWithDefDisplay_NoVirtualDisplayAdapter() throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext,
+                new DisplayManagerService.Injector() {
+                    @Override
+                    VirtualDisplayAdapter getVirtualDisplayAdapter(SyncRoot syncRoot,
+                            Context context, Handler handler, DisplayAdapter.Listener listener) {
+                        return null;  // return null for the adapter.  This should cause a failure.
+                    }
+
+                    @Override
+                    long getDefaultDisplayDelayTimeout() {
+                        return SHORT_DEFAULT_DISPLAY_TIMEOUT_MILLIS;
+                    }
+                });
+        try {
+            displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+        } catch (RuntimeException e) {
+            return;
+        }
+        fail("Expected DisplayManager to throw RuntimeException when it cannot initialize the"
+                + " virtual display adapter");
+    }
+
+    /**
+     * Tests that an exception is raised for too dark a brightness configuration.
+     */
+    @Test
+    public void testTooDarkBrightnessConfigurationThrowException() {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        Curve minimumBrightnessCurve = displayManager.getMinimumBrightnessCurveInternal();
+        float[] lux = minimumBrightnessCurve.getX();
+        float[] minimumNits = minimumBrightnessCurve.getY();
+        float[] nits = new float[minimumNits.length];
+        // For every control point, assert that making it slightly lower than the minimum throws an
+        // exception.
+        for (int i = 0; i < nits.length; i++) {
+            for (int j = 0; j < nits.length; j++) {
+                nits[j] = minimumNits[j];
+                if (j == i) {
+                    nits[j] -= 0.1f;
+                }
+                if (nits[j] < 0) {
+                    nits[j] = 0;
+                }
+            }
+            BrightnessConfiguration config =
+                    new BrightnessConfiguration.Builder(lux, nits).build();
+            Exception thrown = null;
+            try {
+                displayManager.validateBrightnessConfiguration(config);
+            } catch (IllegalArgumentException e) {
+                thrown = e;
+            }
+            assertNotNull("Building too dark a brightness configuration must throw an exception");
+        }
+    }
+
+    /**
+     * Tests that no exception is raised for not too dark a brightness configuration.
+     */
+    @Test
+    public void testBrightEnoughBrightnessConfigurationDoesNotThrowException() {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        Curve minimumBrightnessCurve = displayManager.getMinimumBrightnessCurveInternal();
+        float[] lux = minimumBrightnessCurve.getX();
+        float[] nits = minimumBrightnessCurve.getY();
+        BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits).build();
+        displayManager.validateBrightnessConfiguration(config);
+    }
+
+    /**
+     * Tests that null brightness configurations are alright.
+     */
+    @Test
+    public void testNullBrightnessConfiguration() {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        displayManager.validateBrightnessConfiguration(null);
+    }
+
+    /**
+     * Tests that collection of display color sampling results are sensible.
+     */
+    @Test
+    public void testDisplayedContentSampling() {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        registerDefaultDisplays(displayManager);
+
+        DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(0);
+        assertNotNull(ddi);
+
+        DisplayedContentSamplingAttributes attr =
+                displayManager.getDisplayedContentSamplingAttributesInternal(0);
+        if (attr == null) return; //sampling not supported on device, skip remainder of test.
+
+        boolean enabled = displayManager.setDisplayedContentSamplingEnabledInternal(0, true, 0, 0);
+        assertTrue(enabled);
+
+        displayManager.setDisplayedContentSamplingEnabledInternal(0, false, 0, 0);
+        DisplayedContentSample sample = displayManager.getDisplayedContentSampleInternal(0, 0, 0);
+        assertNotNull(sample);
+
+        long numPixels = ddi.width * ddi.height * sample.getNumFrames();
+        long[] samples = sample.getSampleComponent(DisplayedContentSample.ColorComponent.CHANNEL0);
+        assertTrue(samples.length == 0 || LongStream.of(samples).sum() == numPixels);
+
+        samples = sample.getSampleComponent(DisplayedContentSample.ColorComponent.CHANNEL1);
+        assertTrue(samples.length == 0 || LongStream.of(samples).sum() == numPixels);
+
+        samples = sample.getSampleComponent(DisplayedContentSample.ColorComponent.CHANNEL2);
+        assertTrue(samples.length == 0 || LongStream.of(samples).sum() == numPixels);
+
+        samples = sample.getSampleComponent(DisplayedContentSample.ColorComponent.CHANNEL3);
+        assertTrue(samples.length == 0 || LongStream.of(samples).sum() == numPixels);
+    }
+
+    /**
+     * Tests that the virtual display is created with
+     * {@link VirtualDisplayConfig.Builder#setDisplayIdToMirror(int)}
+     */
+    @Test
+    @FlakyTest(bugId = 127687569)
+    public void testCreateVirtualDisplay_displayIdToMirror() throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+
+        // This is effectively the DisplayManager service published to ServiceManager.
+        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
+        DisplayManagerService.LocalService localDisplayManager = displayManager.new LocalService();
+
+        final String uniqueId = "uniqueId --- displayIdToMirrorTest";
+        final int width = 600;
+        final int height = 800;
+        final int dpi = 320;
+
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, width, height, dpi);
+        builder.setUniqueId(uniqueId);
+        final int firstDisplayId = binderService.createVirtualDisplay(builder.build(),
+                mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+
+        // The second virtual display requests to mirror the first virtual display.
+        final String uniqueId2 = "uniqueId --- displayIdToMirrorTest #2";
+        when(mMockAppToken2.asBinder()).thenReturn(mMockAppToken2);
+        final VirtualDisplayConfig.Builder builder2 = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, width, height, dpi).setUniqueId(uniqueId2);
+        builder2.setUniqueId(uniqueId2);
+        builder2.setDisplayIdToMirror(firstDisplayId);
+        final int secondDisplayId = binderService.createVirtualDisplay(builder2.build(),
+                mMockAppToken2 /* callback */, null /* projection */,
+                PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+
+        // flush the handler
+        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
+
+        // The displayId to mirror should be a default display if there is none initially.
+        assertEquals(localDisplayManager.getDisplayIdToMirror(firstDisplayId),
+                Display.DEFAULT_DISPLAY);
+        assertEquals(localDisplayManager.getDisplayIdToMirror(secondDisplayId),
+                firstDisplayId);
+    }
+
+    /** Tests that the virtual device is created in a device display group. */
+    @Test
+    public void createVirtualDisplay_addsDisplaysToDeviceDisplayGroups() throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+
+        registerDefaultDisplays(displayManager);
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+        when(virtualDevice.getDeviceId()).thenReturn(1);
+        when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+        // Create a first virtual display. A display group should be created for this display on the
+        // virtual device.
+        final VirtualDisplayConfig.Builder builder1 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group 1");
+
+        int displayId1 =
+                localService.createVirtualDisplay(
+                        builder1.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+        int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
+
+        // Create a second virtual display. This should be added to the previously created display
+        // group.
+        final VirtualDisplayConfig.Builder builder2 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group 1");
+
+        int displayId2 =
+                localService.createVirtualDisplay(
+                        builder2.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+        int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
+
+        assertEquals(
+                "Both displays should be added to the same displayGroup.",
+                displayGroupId1,
+                displayGroupId2);
+    }
+
+    /**
+     * Tests that the virtual display is not added to the device display group when
+     * VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is set.
+     */
+    @Test
+    public void createVirtualDisplay_addsDisplaysToOwnDisplayGroups() throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+
+        registerDefaultDisplays(displayManager);
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+        when(virtualDevice.getDeviceId()).thenReturn(1);
+        when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+
+        // Create a first virtual display. A display group should be created for this display on the
+        // virtual device.
+        final VirtualDisplayConfig.Builder builder1 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group");
+
+        int displayId1 =
+                localService.createVirtualDisplay(
+                        builder1.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+        int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
+
+        // Create a second virtual display. With the flag VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP,
+        // the display should not be added to the previously created display group.
+        final VirtualDisplayConfig.Builder builder2 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
+                        .setUniqueId("uniqueId --- own display group");
+
+        when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+
+        int displayId2 =
+                localService.createVirtualDisplay(
+                        builder2.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+        int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
+
+        assertNotEquals(
+                "Display 1 should be in the device display group and display 2 in its own display"
+                        + " group.",
+                displayGroupId1,
+                displayGroupId2);
+    }
+
+    @Test
+    public void displaysInDeviceOrOwnDisplayGroupShouldPreserveAlwaysUnlockedFlag()
+            throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+
+        registerDefaultDisplays(displayManager);
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+        when(virtualDevice.getDeviceId()).thenReturn(1);
+        when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+
+        // Allow an ALWAYS_UNLOCKED display to be created.
+        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
+                .thenReturn(PackageManager.PERMISSION_GRANTED);
+
+        when(mContext.checkCallingPermission(ADD_ALWAYS_UNLOCKED_DISPLAY))
+                .thenReturn(PackageManager.PERMISSION_GRANTED);
+
+        // Create a virtual display in a device display group.
+        final VirtualDisplayConfig deviceDisplayGroupDisplayConfig =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group 1")
+                        .setFlags(VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED)
+                        .build();
+
+        int deviceDisplayGroupDisplayId =
+                localService.createVirtualDisplay(
+                        deviceDisplayGroupDisplayConfig,
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+
+        // Check that FLAG_ALWAYS_UNLOCKED is set.
+        assertNotEquals(
+                "FLAG_ALWAYS_UNLOCKED should be set for displays created in a device display"
+                        + " group.",
+                (displayManager.getDisplayDeviceInfoInternal(deviceDisplayGroupDisplayId).flags
+                        & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED),
+                0);
+
+        // Create a virtual display in its own display group.
+        final VirtualDisplayConfig ownDisplayGroupConfig =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- own display group 1")
+                        .setFlags(
+                                VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED
+                                        | VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
+                        .build();
+
+        int ownDisplayGroupDisplayId =
+                localService.createVirtualDisplay(
+                        ownDisplayGroupConfig,
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+
+        // Check that FLAG_ALWAYS_UNLOCKED is set.
+        assertNotEquals(
+                "FLAG_ALWAYS_UNLOCKED should be set for displays created in their own display"
+                        + " group.",
+                (displayManager.getDisplayDeviceInfoInternal(ownDisplayGroupDisplayId).flags
+                        & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED),
+                0);
+
+        // Create a virtual display in a device display group.
+        final VirtualDisplayConfig defaultDisplayGroupConfig =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- default display group 1")
+                        .setFlags(VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED)
+                        .build();
+
+        int defaultDisplayGroupDisplayId =
+                localService.createVirtualDisplay(
+                        defaultDisplayGroupConfig,
+                        mMockAppToken /* callback */,
+                        null /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+
+        // Check that FLAG_ALWAYS_UNLOCKED is not set.
+        assertEquals(
+                "FLAG_ALWAYS_UNLOCKED should not be set for displays created in the default"
+                        + " display group.",
+                (displayManager.getDisplayDeviceInfoInternal(defaultDisplayGroupDisplayId).flags
+                        & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED),
+                0);
+    }
+
+    @Test
+    public void testGetDisplayIdToMirror() throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+
+        // This is effectively the DisplayManager service published to ServiceManager.
+        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
+        DisplayManagerService.LocalService localDisplayManager = displayManager.new LocalService();
+
+        final String uniqueId = "uniqueId --- displayIdToMirrorTest";
+        final int width = 600;
+        final int height = 800;
+        final int dpi = 320;
+
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, width, height, dpi)
+                .setUniqueId(uniqueId)
+                .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
+        final int firstDisplayId = binderService.createVirtualDisplay(builder.build(),
+                mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+
+        // The second virtual display requests to mirror the first virtual display.
+        final String uniqueId2 = "uniqueId --- displayIdToMirrorTest #2";
+        when(mMockAppToken2.asBinder()).thenReturn(mMockAppToken2);
+        final VirtualDisplayConfig.Builder builder2 = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, width, height, dpi)
+                .setUniqueId(uniqueId2)
+                .setWindowManagerMirroringEnabled(true);
+        final int secondDisplayId = binderService.createVirtualDisplay(builder2.build(),
+                mMockAppToken2 /* callback */, null /* projection */,
+                PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+
+        // flush the handler
+        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
+
+        // The displayId to mirror should be a invalid since the display had flag OWN_CONTENT_ONLY
+        assertEquals(localDisplayManager.getDisplayIdToMirror(firstDisplayId),
+                Display.INVALID_DISPLAY);
+        // The second display has mirroring managed by WindowManager so the mirror displayId should
+        // be invalid.
+        assertEquals(localDisplayManager.getDisplayIdToMirror(secondDisplayId),
+                Display.INVALID_DISPLAY);
+    }
+
+    @Test
+    public void testCreateVirtualDisplay_isValidProjection_notValid()
+            throws RemoteException {
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+        IMediaProjection projection = mock(IMediaProjection.class);
+        doReturn(false).when(projection).isValid();
+        when(mMockProjectionService
+                .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
+                .thenReturn(true);
+        doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
+
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
+        builder.setUniqueId("uniqueId --- isValid false");
+
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+        displayManager.windowManagerAndInputReady();
+
+        // Pass in a non-null projection.
+        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
+        final int displayId = binderService.createVirtualDisplay(builder.build(),
+                mMockAppToken /* callback */, projection, PACKAGE_NAME);
+
+        // VirtualDisplay is created for mirroring.
+        assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
+        verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
+                any(ContentRecordingSession.class), nullable(IMediaProjection.class));
+        // But mirroring doesn't begin.
+        verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
+                mContentRecordingSessionCaptor.capture(), nullable(IMediaProjection.class));
+        ContentRecordingSession session = mContentRecordingSessionCaptor.getValue();
+        assertThat(session.isWaitingForConsent()).isTrue();
+    }
+
+    @Test
+    public void testCreateVirtualDisplay_setContentRecordingSessionSuccess()
+            throws RemoteException {
+        final int displayToRecord = 50;
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+        IMediaProjection projection = mock(IMediaProjection.class);
+        doReturn(true).when(projection).isValid();
+        when(mMockProjectionService
+                .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
+                .thenReturn(true);
+        doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
+
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
+        builder.setUniqueId("uniqueId --- setContentRecordingSession true");
+        builder.setDisplayIdToMirror(displayToRecord);
+
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+        displayManager.windowManagerAndInputReady();
+
+        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
+        final int displayId = binderService.createVirtualDisplay(builder.build(),
+                mMockAppToken /* callback */, projection, PACKAGE_NAME);
+
+        assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
+        verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
+                mContentRecordingSessionCaptor.capture(), nullable(IMediaProjection.class));
+        ContentRecordingSession session = mContentRecordingSessionCaptor.getValue();
+        assertThat(session.getContentToRecord()).isEqualTo(RECORD_CONTENT_DISPLAY);
+        assertThat(session.getVirtualDisplayId()).isEqualTo(displayId);
+        assertThat(session.getDisplayToRecord()).isEqualTo(displayToRecord);
+        assertThat(session.isWaitingForConsent()).isFalse();
+    }
+
+    @Test
+    public void testCreateVirtualDisplay_setContentRecordingSessionFail() throws RemoteException {
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+        IMediaProjection projection = mock(IMediaProjection.class);
+        doReturn(true).when(projection).isValid();
+        when(mMockProjectionService
+                .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
+                .thenReturn(false);
+        doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
+
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
+        builder.setUniqueId("uniqueId --- setContentRecordingSession false");
+
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+        displayManager.windowManagerAndInputReady();
+
+        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
+        final int displayId = binderService.createVirtualDisplay(builder.build(),
+                mMockAppToken /* callback */, projection, PACKAGE_NAME);
+
+        assertThat(displayId).isEqualTo(Display.INVALID_DISPLAY);
+    }
+
+    @Test
+    public void testCreateVirtualDisplay_setContentRecordingSession_taskSession()
+            throws RemoteException {
+        final int displayToRecord = 50;
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+        IMediaProjection projection = mock(IMediaProjection.class);
+        doReturn(true).when(projection).isValid();
+        when(mMockProjectionService
+                .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
+                .thenReturn(true);
+        doReturn(mock(IBinder.class)).when(projection).getLaunchCookie();
+        doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
+
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
+        builder.setUniqueId("uniqueId --- setContentRecordingSession false");
+        builder.setDisplayIdToMirror(displayToRecord);
+
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+        displayManager.windowManagerAndInputReady();
+
+        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
+        final int displayId = binderService.createVirtualDisplay(builder.build(),
+                mMockAppToken /* callback */, projection, PACKAGE_NAME);
+
+        assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
+        verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
+                mContentRecordingSessionCaptor.capture(), nullable(IMediaProjection.class));
+        ContentRecordingSession session = mContentRecordingSessionCaptor.getValue();
+        assertThat(session.getContentToRecord()).isEqualTo(RECORD_CONTENT_TASK);
+        assertThat(session.getVirtualDisplayId()).isEqualTo(displayId);
+        assertThat(session.getTokenToRecord()).isNotNull();
+    }
+
+    @Test
+    public void testCreateVirtualDisplay_setContentRecordingSession_noProjection_noFlags()
+            throws RemoteException {
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        // Set no flags for the VirtualDisplay.
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
+        builder.setUniqueId("uniqueId --- setContentRecordingSession false");
+
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+        displayManager.windowManagerAndInputReady();
+
+        // Pass in a null projection.
+        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
+        final int displayId = binderService.createVirtualDisplay(builder.build(),
+                mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME);
+
+        // VirtualDisplay is created but not for mirroring.
+        assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
+        verify(mMockProjectionService, never()).setContentRecordingSession(
+                any(ContentRecordingSession.class), nullable(IMediaProjection.class));
+    }
+
+    @Test
+    public void testCreateVirtualDisplay_setContentRecordingSession_noProjection_noMirroringFlag()
+            throws RemoteException {
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        // Set a non-mirroring flag for the VirtualDisplay.
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
+        builder.setUniqueId("uniqueId --- setContentRecordingSession false");
+        builder.setFlags(VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
+
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+        displayManager.windowManagerAndInputReady();
+
+        // Pass in a null projection.
+        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
+        final int displayId = binderService.createVirtualDisplay(builder.build(),
+                mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME);
+
+        // VirtualDisplay is created but not for mirroring.
+        assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
+        verify(mMockProjectionService, never()).setContentRecordingSession(
+                any(ContentRecordingSession.class), nullable(IMediaProjection.class));
+    }
+
+    @Test
+    public void testCreateVirtualDisplay_setContentRecordingSession_projection_noMirroringFlag()
+            throws RemoteException {
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+        IMediaProjection projection = mock(IMediaProjection.class);
+        doReturn(true).when(projection).isValid();
+        when(mMockProjectionService
+                .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
+                .thenReturn(true);
+        doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
+
+        // Set no flags for the VirtualDisplay.
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
+        builder.setUniqueId("uniqueId --- setContentRecordingSession false");
+
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+        displayManager.windowManagerAndInputReady();
+
+        // Pass in a non-null projection.
+        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
+        final int displayId = binderService.createVirtualDisplay(builder.build(),
+                mMockAppToken /* callback */, projection, PACKAGE_NAME);
+
+        // VirtualDisplay is created for mirroring.
+        assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
+        verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
+                any(ContentRecordingSession.class), nullable(IMediaProjection.class));
+    }
+
+    /**
+     * Tests that the virtual display is created with
+     * {@link VirtualDisplayConfig.Builder#setSurface(Surface)}
+     */
+    @Test
+    @FlakyTest(bugId = 127687569)
+    public void testCreateVirtualDisplay_setSurface() throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+
+        // This is effectively the DisplayManager service published to ServiceManager.
+        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
+
+        final String uniqueId = "uniqueId --- setSurface";
+        final int width = 600;
+        final int height = 800;
+        final int dpi = 320;
+        final Surface surface = new Surface();
+
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, width, height, dpi);
+        builder.setSurface(surface);
+        builder.setUniqueId(uniqueId);
+        final int displayId = binderService.createVirtualDisplay(builder.build(),
+                mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+
+        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+
+        // flush the handler
+        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
+
+        assertEquals(displayManager.getVirtualDisplaySurfaceInternal(mMockAppToken), surface);
+    }
+
+    /**
+     * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is allowed when the permission
+     * ADD_TRUSTED_DISPLAY is granted.
+     */
+    @Test
+    public void testOwnDisplayGroup_allowCreationWithAddTrustedDisplayPermission()
+            throws RemoteException {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)).thenReturn(
+                PackageManager.PERMISSION_GRANTED);
+
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
+        builder.setFlags(DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP);
+        builder.setUniqueId("uniqueId --- OWN_DISPLAY_GROUP");
+
+        int displayId = bs.createVirtualDisplay(builder.build(), mMockAppToken /* callback */,
+                null /* projection */, PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
+        DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
+        assertNotNull(ddi);
+        assertNotEquals(0, ddi.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
+    }
+
+    /**
+     * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is blocked when the permission
+     * ADD_TRUSTED_DISPLAY is denied.
+     */
+    @Test
+    public void testOwnDisplayGroup_withoutAddTrustedDisplayPermission_throwsSecurityException() {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mBasicInjector);
+        registerDefaultDisplays(displayManager);
+
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)).thenReturn(
+                PackageManager.PERMISSION_DENIED);
+
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
+        builder.setFlags(DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP);
+        builder.setUniqueId("uniqueId --- OWN_DISPLAY_GROUP");
+
+        try {
+            bs.createVirtualDisplay(builder.build(), mMockAppToken /* callback */,
+                    null /* projection */, PACKAGE_NAME);
+            fail("Creating virtual display with VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP without "
+                    + "ADD_TRUSTED_DISPLAY permission should throw SecurityException.");
+        } catch (SecurityException e) {
+            // SecurityException is expected
+        }
+    }
+
+    /**
+     * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is allowed when called with
+     * a virtual device, even if ADD_TRUSTED_DISPLAY is not granted.
+     */
+    @Test
+    public void testOwnDisplayGroup_allowCreationWithVirtualDevice()  throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+
+        registerDefaultDisplays(displayManager);
+
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)).thenReturn(
+                PackageManager.PERMISSION_DENIED);
+
+        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
+        builder.setFlags(DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP);
+        builder.setUniqueId("uniqueId --- OWN_DISPLAY_GROUP");
+
+        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+        when(virtualDevice.getDeviceId()).thenReturn(1);
+        when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+
+        int displayId = localService.createVirtualDisplay(builder.build(),
+                mMockAppToken /* callback */, virtualDevice /* virtualDeviceToken */,
+                mock(DisplayWindowPolicyController.class), PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
+        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
+        DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
+        assertNotNull(ddi);
+        assertNotEquals(0, ddi.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
+    }
+
+    /**
+     * Tests that there is a display change notification if the frame rate override
+     * list is updated.
+     */
+    @Test
+    public void testShouldNotifyChangeWhenDisplayInfoFrameRateOverrideChanged() throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager, new float[]{60f});
+        FakeDisplayManagerCallback callback = registerDisplayListenerCallback(displayManager,
+                displayManagerBinderService, displayDevice);
+
+        int myUid = Process.myUid();
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(myUid, 30f),
+                });
+        assertTrue(callback.mDisplayChangedCalled);
+        callback.clear();
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(myUid, 30f),
+                        new DisplayEventReceiver.FrameRateOverride(1234, 30f),
+                });
+        assertFalse(callback.mDisplayChangedCalled);
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(myUid, 20f),
+                        new DisplayEventReceiver.FrameRateOverride(1234, 30f),
+                        new DisplayEventReceiver.FrameRateOverride(5678, 30f),
+                });
+        assertTrue(callback.mDisplayChangedCalled);
+        callback.clear();
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(1234, 30f),
+                        new DisplayEventReceiver.FrameRateOverride(5678, 30f),
+                });
+        assertTrue(callback.mDisplayChangedCalled);
+        callback.clear();
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(5678, 30f),
+                });
+        assertFalse(callback.mDisplayChangedCalled);
+    }
+
+    /**
+     * Tests that the DisplayInfo is updated correctly with a frame rate override
+     */
+    @Test
+    public void testDisplayInfoFrameRateOverride() throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f, 30f, 20f});
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(
+                                Process.myUid(), 20f),
+                        new DisplayEventReceiver.FrameRateOverride(
+                                Process.myUid() + 1, 30f)
+                });
+        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
+
+        // Changing the mode to 30Hz should not override the refresh rate to 20Hz anymore
+        // as 20 is not a divider of 30.
+        updateModeId(displayManager, displayDevice, 2);
+        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(30f, displayInfo.getRefreshRate(), 0.01f);
+    }
+
+    /**
+     * Tests that the frame rate override is returning the correct value from
+     * DisplayInfo#getRefreshRate
+     */
+    @Test
+    public void testDisplayInfoNonNativeFrameRateOverride() throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f});
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(
+                                Process.myUid(), 20f)
+                });
+        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
+    }
+
+    /**
+     * Tests that the mode reflects the frame rate override is in compat mode
+     */
+    @Test
+    @DisableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
+    public  void testDisplayInfoFrameRateOverrideModeCompat() throws Exception {
+        testDisplayInfoFrameRateOverrideModeCompat(/*compatChangeEnabled*/ false);
+    }
+
+    /**
+     * Tests that the mode reflects the physical display refresh rate when not in compat mode.
+     */
+    @Test
+    @EnableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
+    public  void testDisplayInfoFrameRateOverrideMode() throws Exception {
+        testDisplayInfoFrameRateOverrideModeCompat(/*compatChangeEnabled*/ true);
+    }
+
+    /**
+     * Tests that the mode reflects the frame rate override is in compat mode and accordingly to the
+     * allowNonNativeRefreshRateOverride policy.
+     */
+    @Test
+    @DisableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
+    public void testDisplayInfoNonNativeFrameRateOverrideModeCompat() throws Exception {
+        testDisplayInfoNonNativeFrameRateOverrideMode(/*compatChangeEnabled*/ false);
+    }
+
+    /**
+     * Tests that the mode reflects the physical display refresh rate when not in compat mode.
+     */
+    @Test
+    @EnableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
+    public void testDisplayInfoNonNativeFrameRateOverrideMode() throws Exception {
+        testDisplayInfoNonNativeFrameRateOverrideMode(/*compatChangeEnabled*/ true);
+    }
+
+    /**
+     * Tests that there is a display change notification if the render frame rate is updated
+     */
+    @Test
+    public void testShouldNotifyChangeWhenDisplayInfoRenderFrameRateChanged() throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager, new float[]{60f});
+        FakeDisplayManagerCallback callback = registerDisplayListenerCallback(displayManager,
+                displayManagerBinderService, displayDevice);
+
+        updateRenderFrameRate(displayManager, displayDevice, 30f);
+        assertTrue(callback.mDisplayChangedCalled);
+        callback.clear();
+
+        updateRenderFrameRate(displayManager, displayDevice, 30f);
+        assertFalse(callback.mDisplayChangedCalled);
+
+        updateRenderFrameRate(displayManager, displayDevice, 20f);
+        assertTrue(callback.mDisplayChangedCalled);
+        callback.clear();
+    }
+
+    /**
+     * Tests that the DisplayInfo is updated correctly with a render frame rate
+     */
+    @Test
+    public void testDisplayInfoRenderFrameRate() throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f, 30f, 20f});
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
+
+        updateRenderFrameRate(displayManager, displayDevice, 20f);
+        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
+    }
+
+    /**
+     * Tests that the mode reflects the render frame rate is in compat mode
+     */
+    @Test
+    @DisableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
+    public  void testDisplayInfoRenderFrameRateModeCompat() throws Exception {
+        testDisplayInfoRenderFrameRateModeCompat(/*compatChangeEnabled*/ false);
+    }
+
+    /**
+     * Tests that the mode reflects the physical display refresh rate when not in compat mode.
+     */
+    @Test
+    @EnableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
+    public  void testDisplayInfoRenderFrameRateMode() throws Exception {
+        testDisplayInfoRenderFrameRateModeCompat(/*compatChangeEnabled*/ true);
+    }
+
+    /**
+     * Tests that EVENT_DISPLAY_ADDED is sent when a display is added.
+     */
+    @Test
+    public void testShouldNotifyDisplayAdded_WhenNewDisplayDeviceIsAdded() {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+
+        Handler handler = displayManager.getDisplayHandler();
+        waitForIdleHandler(handler);
+
+        // register display listener callback
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        displayManagerBinderService.registerCallbackWithEventMask(
+                callback, STANDARD_DISPLAY_EVENTS);
+
+        waitForIdleHandler(handler);
+
+        createFakeDisplayDevice(displayManager, new float[]{60f});
+
+        waitForIdleHandler(handler);
+
+        assertFalse(callback.mDisplayChangedCalled);
+        assertFalse(callback.mDisplayRemovedCalled);
+        assertTrue(callback.mDisplayAddedCalled);
+    }
+
+    /**
+     * Tests that EVENT_DISPLAY_ADDED is not sent when a display is added and the
+     * client has a callback which is not subscribed to this event type.
+     */
+    @Test
+    public void testShouldNotNotifyDisplayAdded_WhenClientIsNotSubscribed() {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+
+        Handler handler = displayManager.getDisplayHandler();
+        waitForIdleHandler(handler);
+
+        // register display listener callback
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        long allEventsExceptDisplayAdded = STANDARD_DISPLAY_EVENTS
+                & ~DisplayManager.EVENT_FLAG_DISPLAY_ADDED;
+        displayManagerBinderService.registerCallbackWithEventMask(callback,
+                allEventsExceptDisplayAdded);
+
+        waitForIdleHandler(handler);
+
+        createFakeDisplayDevice(displayManager, new float[]{60f});
+
+        waitForIdleHandler(handler);
+
+        assertFalse(callback.mDisplayChangedCalled);
+        assertFalse(callback.mDisplayRemovedCalled);
+        assertFalse(callback.mDisplayAddedCalled);
+    }
+
+    /**
+     * Tests that EVENT_DISPLAY_REMOVED is sent when a display is removed.
+     */
+    @Test
+    public void testShouldNotifyDisplayRemoved_WhenDisplayDeviceIsRemoved() {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+
+        Handler handler = displayManager.getDisplayHandler();
+        waitForIdleHandler(handler);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f});
+
+        waitForIdleHandler(handler);
+
+        FakeDisplayManagerCallback callback = registerDisplayListenerCallback(
+                displayManager, displayManagerBinderService, displayDevice);
+
+        waitForIdleHandler(handler);
+
+        displayManager.getDisplayDeviceRepository()
+                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED);
+
+        waitForIdleHandler(handler);
+
+        assertFalse(callback.mDisplayChangedCalled);
+        assertTrue(callback.mDisplayRemovedCalled);
+        assertFalse(callback.mDisplayAddedCalled);
+    }
+
+    /**
+     * Tests that EVENT_DISPLAY_REMOVED is not sent when a display is added and the
+     * client has a callback which is not subscribed to this event type.
+     */
+    @Test
+    public void testShouldNotNotifyDisplayRemoved_WhenClientIsNotSubscribed() {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+
+        Handler handler = displayManager.getDisplayHandler();
+        waitForIdleHandler(handler);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f});
+
+        waitForIdleHandler(handler);
+
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        long allEventsExceptDisplayRemoved = STANDARD_DISPLAY_EVENTS
+                & ~DisplayManager.EVENT_FLAG_DISPLAY_REMOVED;
+        displayManagerBinderService.registerCallbackWithEventMask(callback,
+                allEventsExceptDisplayRemoved);
+
+        waitForIdleHandler(handler);
+
+        displayManager.getDisplayDeviceRepository()
+                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED);
+
+        waitForIdleHandler(handler);
+
+        assertFalse(callback.mDisplayChangedCalled);
+        assertFalse(callback.mDisplayRemovedCalled);
+        assertFalse(callback.mDisplayAddedCalled);
+    }
+
+
+
+    @Test
+    public void testSettingTwoBrightnessConfigurationsOnMultiDisplay() {
+        Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+
+        // get the first two internal displays
+        Display[] displays = displayManager.getDisplays();
+        Display internalDisplayOne = null;
+        Display internalDisplayTwo = null;
+        for (Display display : displays) {
+            if (display.getType() == Display.TYPE_INTERNAL) {
+                if (internalDisplayOne == null) {
+                    internalDisplayOne = display;
+                } else {
+                    internalDisplayTwo = display;
+                    break;
+                }
+            }
+        }
+
+        // return if there are fewer than 2 displays on this device
+        if (internalDisplayOne == null || internalDisplayTwo == null) {
+            return;
+        }
+
+        final String uniqueDisplayIdOne = internalDisplayOne.getUniqueId();
+        final String uniqueDisplayIdTwo = internalDisplayTwo.getUniqueId();
+
+        BrightnessConfiguration configOne =
+                new BrightnessConfiguration.Builder(
+                        new float[]{0.0f, 12345.0f}, new float[]{15.0f, 400.0f})
+                        .setDescription("model:1").build();
+        BrightnessConfiguration configTwo =
+                new BrightnessConfiguration.Builder(
+                        new float[]{0.0f, 6789.0f}, new float[]{12.0f, 300.0f})
+                        .setDescription("model:2").build();
+
+        displayManager.setBrightnessConfigurationForDisplay(configOne,
+                uniqueDisplayIdOne);
+        displayManager.setBrightnessConfigurationForDisplay(configTwo,
+                uniqueDisplayIdTwo);
+
+        BrightnessConfiguration configFromOne =
+                displayManager.getBrightnessConfigurationForDisplay(uniqueDisplayIdOne);
+        BrightnessConfiguration configFromTwo =
+                displayManager.getBrightnessConfigurationForDisplay(uniqueDisplayIdTwo);
+
+        assertNotNull(configFromOne);
+        assertEquals(configOne, configFromOne);
+        assertEquals(configTwo, configFromTwo);
+
+    }
+
+    @Test
+    public void testHdrConversionModeEquals() {
+        assertEquals(
+                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 2),
+                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 2));
+        assertNotEquals(
+                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 2),
+                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 3));
+        assertEquals(
+                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM),
+                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM));
+        assertNotEquals(
+                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 2),
+                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM));
+    }
+
+    @Test
+    public void testCreateHdrConversionMode_withInvalidArguments_throwsException() {
+        assertThrows(
+                "preferredHdrOutputType must not be set if the conversion mode is "
+                        + "HDR_CONVERSION_PASSTHROUGH",
+                IllegalArgumentException.class,
+                () -> new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_PASSTHROUGH,
+                        Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION));
+    }
+
+    @Test
+    public void testSetHdrConversionModeInternal_withInvalidArguments_throwsException() {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        assertThrows("Expected DisplayManager to throw IllegalArgumentException when "
+                        + "preferredHdrOutputType is set and the conversion mode is "
+                        + "HDR_CONVERSION_SYSTEM",
+                IllegalArgumentException.class,
+                () -> displayManager.setHdrConversionModeInternal(new HdrConversionMode(
+                        HdrConversionMode.HDR_CONVERSION_SYSTEM,
+                        Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION)));
+    }
+
+    @Test
+    public void testSetAndGetHdrConversionModeInternal() {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        final HdrConversionMode mode = new HdrConversionMode(
+                HdrConversionMode.HDR_CONVERSION_FORCE,
+                Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION);
+        displayManager.setHdrConversionModeInternal(mode);
+        assertEquals(mode, displayManager.getHdrConversionModeSettingInternal());
+        assertEquals(mode.getConversionMode(), mHdrConversionMode);
+        assertEquals(mode.getPreferredHdrOutputType(), mPreferredHdrOutputType);
+    }
+
+    @Test
+    public void testHdrConversionMode_withMinimalPostProcessing() {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f, 30f, 20f});
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+
+        final HdrConversionMode mode = new HdrConversionMode(
+                HdrConversionMode.HDR_CONVERSION_FORCE,
+                Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION);
+        displayManager.setHdrConversionModeInternal(mode);
+        assertEquals(mode, displayManager.getHdrConversionModeSettingInternal());
+
+        displayManager.setMinimalPostProcessingAllowed(true);
+        displayManager.setDisplayPropertiesInternal(displayId, false /* hasContent */,
+                30.0f /* requestedRefreshRate */,
+                displayDevice.getDisplayDeviceInfoLocked().modeId /* requestedModeId */,
+                30.0f /* requestedMinRefreshRate */, 120.0f /* requestedMaxRefreshRate */,
+                true /* preferMinimalPostProcessing */, false /* disableHdrConversion */,
+                true /* inTraversal */);
+
+        assertEquals(new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_PASSTHROUGH),
+                displayManager.getHdrConversionModeInternal());
+
+        displayManager.setDisplayPropertiesInternal(displayId, false /* hasContent */,
+                30.0f /* requestedRefreshRate */,
+                displayDevice.getDisplayDeviceInfoLocked().modeId /* requestedModeId */,
+                30.0f /* requestedMinRefreshRate */, 120.0f /* requestedMaxRefreshRate */,
+                false /* preferMinimalPostProcessing */, false /* disableHdrConversion */,
+                true /* inTraversal */);
+        assertEquals(mode, displayManager.getHdrConversionModeInternal());
+    }
+
+    @Test
+    public void testReturnsRefreshRateForDisplayAndSensor_proximitySensorSet() {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        displayManager.overrideSensorManager(mSensorManager);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager, new float[]{60f});
+        displayDevice.mDisplayDeviceConfig = mMockDisplayDeviceConfig;
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+
+        String testSensorName = "testName";
+        String testSensorType = "testType";
+        Sensor testSensor = TestUtils.createSensor(testSensorType, testSensorName);
+
+        DisplayDeviceConfig.SensorData sensorData = new DisplayDeviceConfig.SensorData();
+        sensorData.type = testSensorType;
+        sensorData.name = testSensorName;
+        sensorData.minRefreshRate = 10f;
+        sensorData.maxRefreshRate = 100f;
+
+        when(mMockDisplayDeviceConfig.getProximitySensor()).thenReturn(sensorData);
+        when(mSensorManager.getSensorList(Sensor.TYPE_ALL)).thenReturn(Collections.singletonList(
+                testSensor));
+
+        SurfaceControl.RefreshRateRange result = localService.getRefreshRateForDisplayAndSensor(
+                displayId, testSensorName, testSensorType);
+
+        assertNotNull(result);
+        assertEquals(result.min, sensorData.minRefreshRate, FLOAT_TOLERANCE);
+        assertEquals(result.max, sensorData.maxRefreshRate, FLOAT_TOLERANCE);
+    }
+
+    @Test
+    public void testReturnsRefreshRateForDisplayAndSensor_proximitySensorNotSet() {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        displayManager.overrideSensorManager(mSensorManager);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager, new float[]{60f});
+        displayDevice.mDisplayDeviceConfig = mMockDisplayDeviceConfig;
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+
+        String testSensorName = "testName";
+        String testSensorType = "testType";
+        Sensor testSensor = TestUtils.createSensor(testSensorType, testSensorName);
+
+        DisplayDeviceConfig.SensorData sensorData = new DisplayDeviceConfig.SensorData();
+        sensorData.type = testSensorType;
+        sensorData.name = testSensorName;
+        sensorData.minRefreshRate = 10f;
+        sensorData.maxRefreshRate = 100f;
+
+        when(mMockDisplayDeviceConfig.getProximitySensor()).thenReturn(null);
+        when(mSensorManager.getSensorList(Sensor.TYPE_ALL)).thenReturn(Collections.singletonList(
+                testSensor));
+
+        SurfaceControl.RefreshRateRange result = localService.getRefreshRateForDisplayAndSensor(
+                displayId, testSensorName, testSensorType);
+
+        assertNull(result);
+    }
+
+    private void testDisplayInfoFrameRateOverrideModeCompat(boolean compatChangeEnabled)
+            throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f, 30f, 20f});
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(
+                                Process.myUid(), 20f),
+                        new DisplayEventReceiver.FrameRateOverride(
+                                Process.myUid() + 1, 30f)
+                });
+        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
+        Display.Mode expectedMode;
+        if (compatChangeEnabled) {
+            expectedMode = new Display.Mode(1, 100, 200, 60f);
+        } else {
+            expectedMode = new Display.Mode(3, 100, 200, 20f);
+        }
+        assertEquals(expectedMode, displayInfo.getMode());
+    }
+
+    private void testDisplayInfoRenderFrameRateModeCompat(boolean compatChangeEnabled)
+            throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f, 30f, 20f});
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
+
+        updateRenderFrameRate(displayManager, displayDevice, 20f);
+        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
+        Display.Mode expectedMode;
+        if (compatChangeEnabled) {
+            expectedMode = new Display.Mode(1, 100, 200, 60f);
+        } else {
+            expectedMode = new Display.Mode(3, 100, 200, 20f);
+        }
+        assertEquals(expectedMode, displayInfo.getMode());
+    }
+
+    private void testDisplayInfoNonNativeFrameRateOverrideMode(boolean compatChangeEnabled) {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f});
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(
+                                Process.myUid(), 20f)
+                });
+        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        Display.Mode expectedMode;
+        if (compatChangeEnabled) {
+            expectedMode = new Display.Mode(1, 100, 200, 60f);
+        } else {
+            expectedMode = new Display.Mode(255, 100, 200, 20f);
+        }
+        assertEquals(expectedMode, displayInfo.getMode());
+    }
+
+    private int getDisplayIdForDisplayDevice(
+            DisplayManagerService displayManager,
+            DisplayManagerService.BinderService displayManagerBinderService,
+            FakeDisplayDevice displayDevice) {
+
+        final int[] displayIds = displayManagerBinderService.getDisplayIds(
+                /* includeDisabled= */ true);
+        assertTrue(displayIds.length > 0);
+        int displayId = Display.INVALID_DISPLAY;
+        for (int i = 0; i < displayIds.length; i++) {
+            DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayIds[i]);
+            if (displayDevice.getDisplayDeviceInfoLocked().equals(ddi)) {
+                displayId = displayIds[i];
+                break;
+            }
+        }
+        assertFalse(displayId == Display.INVALID_DISPLAY);
+        return displayId;
+    }
+
+    private void updateDisplayDeviceInfo(DisplayManagerService displayManager,
+            FakeDisplayDevice displayDevice,
+            DisplayDeviceInfo displayDeviceInfo) {
+        displayDevice.setDisplayDeviceInfo(displayDeviceInfo);
+        displayManager.getDisplayDeviceRepository()
+                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_CHANGED);
+        Handler handler = displayManager.getDisplayHandler();
+        waitForIdleHandler(handler);
+    }
+
+    private void updateFrameRateOverride(DisplayManagerService displayManager,
+            FakeDisplayDevice displayDevice,
+            DisplayEventReceiver.FrameRateOverride[] frameRateOverrides) {
+        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
+        displayDeviceInfo.copyFrom(displayDevice.getDisplayDeviceInfoLocked());
+        displayDeviceInfo.frameRateOverrides = frameRateOverrides;
+        updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo);
+    }
+
+    private void updateRenderFrameRate(DisplayManagerService displayManager,
+            FakeDisplayDevice displayDevice,
+            float renderFrameRate) {
+        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
+        displayDeviceInfo.copyFrom(displayDevice.getDisplayDeviceInfoLocked());
+        displayDeviceInfo.renderFrameRate = renderFrameRate;
+        updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo);
+    }
+
+    private void updateModeId(DisplayManagerService displayManager,
+            FakeDisplayDevice displayDevice,
+            int modeId) {
+        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
+        displayDeviceInfo.copyFrom(displayDevice.getDisplayDeviceInfoLocked());
+        displayDeviceInfo.modeId = modeId;
+        updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo);
+    }
+
+    private IDisplayManagerCallback registerDisplayChangeCallback(
+            DisplayManagerService displayManager) {
+        IDisplayManagerCallback displayChangesCallback = mock(IDisplayManagerCallback.class);
+        when(displayChangesCallback.asBinder()).thenReturn(new Binder());
+        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
+        binderService.registerCallback(displayChangesCallback);
+        return displayChangesCallback;
+    }
+
+    private FakeDisplayManagerCallback registerDisplayListenerCallback(
+            DisplayManagerService displayManager,
+            DisplayManagerService.BinderService displayManagerBinderService,
+            FakeDisplayDevice displayDevice) {
+        // Find the display id of the added FakeDisplayDevice
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+
+        Handler handler = displayManager.getDisplayHandler();
+        waitForIdleHandler(handler);
+
+        // register display listener callback
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(displayId);
+        displayManagerBinderService.registerCallbackWithEventMask(
+                callback, STANDARD_DISPLAY_EVENTS);
+        return callback;
+    }
+
+    private FakeDisplayDevice createFakeDisplayDevice(DisplayManagerService displayManager,
+            float[] refreshRates) {
+        FakeDisplayDevice displayDevice = new FakeDisplayDevice();
+        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
+        int width = 100;
+        int height = 200;
+        displayDeviceInfo.supportedModes = new Display.Mode[refreshRates.length];
+        for (int i = 0; i < refreshRates.length; i++) {
+            displayDeviceInfo.supportedModes[i] =
+                    new Display.Mode(i + 1, width, height, refreshRates[i]);
+        }
+        displayDeviceInfo.modeId = 1;
+        displayDeviceInfo.renderFrameRate = displayDeviceInfo.supportedModes[0].getRefreshRate();
+        displayDeviceInfo.width = width;
+        displayDeviceInfo.height = height;
+        final Rect zeroRect = new Rect();
+        displayDeviceInfo.displayCutout = new DisplayCutout(
+                Insets.of(0, 10, 0, 0),
+                zeroRect, new Rect(0, 0, 10, 10), zeroRect, zeroRect);
+        displayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY;
+        displayDeviceInfo.address = new TestUtils.TestDisplayAddress();
+        displayDevice.setDisplayDeviceInfo(displayDeviceInfo);
+        displayManager.getDisplayDeviceRepository()
+                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
+        return displayDevice;
+    }
+
+    private void registerDefaultDisplays(DisplayManagerService displayManager) {
+        Handler handler = displayManager.getDisplayHandler();
+        // Would prefer to call displayManager.onStart() directly here but it performs binderService
+        // registration which triggers security exceptions when running from a test.
+        handler.sendEmptyMessage(MSG_REGISTER_DEFAULT_DISPLAY_ADAPTERS);
+        waitForIdleHandler(handler);
+    }
+
+    private void waitForIdleHandler(Handler handler) {
+        waitForIdleHandler(handler, Duration.ofSeconds(1));
+    }
+
+    private void waitForIdleHandler(Handler handler, Duration timeout) {
+        final MessageQueue queue = handler.getLooper().getQueue();
+        final CountDownLatch latch = new CountDownLatch(1);
+        queue.addIdleHandler(() -> {
+            latch.countDown();
+            // Remove idle handler
+            return false;
+        });
+        try {
+            latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            fail("Interrupted unexpectedly: " + e);
+        }
+    }
+
+    private void resetConfigToIgnoreSensorManager(Context context) {
+        final Resources res = Mockito.spy(context.getResources());
+        doReturn(new int[]{-1}).when(res).getIntArray(R.array
+                .config_ambientThresholdsOfPeakRefreshRate);
+        doReturn(new int[]{-1}).when(res).getIntArray(R.array
+                .config_brightnessThresholdsOfPeakRefreshRate);
+        doReturn(new int[]{-1}).when(res).getIntArray(R.array
+                .config_highDisplayBrightnessThresholdsOfFixedRefreshRate);
+        doReturn(new int[]{-1}).when(res).getIntArray(R.array
+                .config_highAmbientBrightnessThresholdsOfFixedRefreshRate);
+
+        when(context.getResources()).thenReturn(res);
+    }
+
+    private class FakeDisplayManagerCallback extends IDisplayManagerCallback.Stub {
+        int mDisplayId;
+        boolean mDisplayAddedCalled = false;
+        boolean mDisplayChangedCalled = false;
+        boolean mDisplayRemovedCalled = false;
+
+        FakeDisplayManagerCallback(int displayId) {
+            mDisplayId = displayId;
+        }
+
+        FakeDisplayManagerCallback() {
+            mDisplayId = -1;
+        }
+
+        @Override
+        public void onDisplayEvent(int displayId, int event) {
+            if (mDisplayId != -1 && displayId != mDisplayId) {
+                return;
+            }
+
+            if (event == DisplayManagerGlobal.EVENT_DISPLAY_ADDED) {
+                mDisplayAddedCalled = true;
+            }
+
+            if (event == DisplayManagerGlobal.EVENT_DISPLAY_CHANGED) {
+                mDisplayChangedCalled = true;
+            }
+
+            if (event == DisplayManagerGlobal.EVENT_DISPLAY_REMOVED) {
+                mDisplayRemovedCalled = true;
+            }
+        }
+
+        public void clear() {
+            mDisplayAddedCalled = false;
+            mDisplayChangedCalled = false;
+            mDisplayRemovedCalled = false;
+        }
+    }
+
+    private class FakeDisplayDevice extends DisplayDevice {
+        private DisplayDeviceInfo mDisplayDeviceInfo;
+
+        FakeDisplayDevice() {
+            super(null, null, "", mContext);
+        }
+
+        public void setDisplayDeviceInfo(DisplayDeviceInfo displayDeviceInfo) {
+            mDisplayDeviceInfo = displayDeviceInfo;
+        }
+
+        @Override
+        public boolean hasStableUniqueId() {
+            return false;
+        }
+
+        @Override
+        public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+            return mDisplayDeviceInfo;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/HbmEventTest.java b/services/tests/displayservicetests/src/com/android/server/display/HbmEventTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/HbmEventTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/HbmEventTest.java
diff --git a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeControllerTest.java
new file mode 100644
index 0000000..76e6ec7
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeControllerTest.java
@@ -0,0 +1,756 @@
+/*
+ * 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.server.display;
+
+import static android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR;
+import static android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF;
+import static android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT;
+
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED;
+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;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.anyFloat;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.hardware.display.BrightnessInfo;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Message;
+import android.os.test.TestLooper;
+import android.test.mock.MockContentResolver;
+import android.util.MathUtils;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.internal.util.test.FakeSettingsProviderRule;
+import com.android.server.display.DisplayDeviceConfig.HighBrightnessModeData;
+import com.android.server.display.HighBrightnessModeController.Injector;
+import com.android.server.testutils.OffsettableClock;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class HighBrightnessModeControllerTest {
+
+    private static final int MINIMUM_LUX = 100;
+    private static final float TRANSITION_POINT = 0.763f;
+    private static final long TIME_WINDOW_MILLIS = 55 * 1000;
+    private static final long TIME_ALLOWED_IN_WINDOW_MILLIS = 12 * 1000;
+    private static final long TIME_MINIMUM_AVAILABLE_TO_ENABLE_MILLIS = 5 * 1000;
+    private static final boolean ALLOW_IN_LOW_POWER_MODE = false;
+
+    private static final float DEFAULT_MIN = 0.01f;
+    private static final float DEFAULT_MAX = 0.80f;
+
+    private static final int DISPLAY_WIDTH = 900;
+    private static final int DISPLAY_HEIGHT = 1600;
+
+    private static final float EPSILON = 0.000001f;
+
+    private OffsettableClock mClock;
+    private TestLooper mTestLooper;
+    private Handler mHandler;
+    private Binder mDisplayToken;
+    private String mDisplayUniqueId;
+    private Context mContextSpy;
+    private HighBrightnessModeMetadata mHighBrightnessModeMetadata;
+
+    @Rule
+    public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
+
+    @Mock Injector mInjectorMock;
+    @Mock HighBrightnessModeController.HdrBrightnessDeviceConfig mHdrBrightnessDeviceConfigMock;
+
+    private static final HighBrightnessModeData DEFAULT_HBM_DATA =
+            new HighBrightnessModeData(MINIMUM_LUX, TRANSITION_POINT, TIME_WINDOW_MILLIS,
+                    TIME_ALLOWED_IN_WINDOW_MILLIS, TIME_MINIMUM_AVAILABLE_TO_ENABLE_MILLIS,
+                    ALLOW_IN_LOW_POWER_MODE, HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT);
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mClock = new OffsettableClock.Stopped();
+        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);
+    }
+
+    /////////////////
+    // Test Methods
+    /////////////////
+
+    @Test
+    public void testNoHbmData() {
+        initHandler(null);
+        final HighBrightnessModeController hbmc = new HighBrightnessModeController(
+                mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken,
+                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);
+    }
+
+    @Test
+    public void testNoHbmData_Enabled() {
+        initHandler(null);
+        final HighBrightnessModeController hbmc = new HighBrightnessModeController(
+                mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken,
+                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);
+        assertEquals(hbmc.getTransitionPoint(), HBM_TRANSITION_POINT_INVALID, 0.0f);
+    }
+
+    @Test
+    public void testOffByDefault() {
+        final HighBrightnessModeController hbmc = createDefaultHbm();
+
+        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
+    }
+
+    @Test
+    public void testAutoBrightnessEnabled_NoLux() {
+        final HighBrightnessModeController hbmc = createDefaultHbm();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
+    }
+
+    @Test
+    public void testAutoBrightnessEnabled_LowLux() {
+        final HighBrightnessModeController hbmc = createDefaultHbm();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX - 1); // below allowed range
+        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
+    }
+
+    @Test
+    public void testAutoBrightnessEnabled_HighLux() {
+        final HighBrightnessModeController hbmc = createDefaultHbm();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
+    }
+
+    @Test
+    public void testAutoBrightnessEnabled_HighLux_ThenDisable() {
+        final HighBrightnessModeController hbmc = createDefaultHbm();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_DISABLED);
+
+        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
+    }
+
+    @Test
+    public void testWithinHighRange_thenOverTime_thenEarnBackTime() {
+        final HighBrightnessModeController hbmc = createDefaultHbm();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
+
+        // Verify we are in HBM
+        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
+
+        // Use up all the time in the window.
+        advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS);
+
+        // Verify we are not out of HBM
+        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
+
+        // Shift time so that the HBM event is at the beginning of the current window
+        advanceTime(TIME_WINDOW_MILLIS - TIME_ALLOWED_IN_WINDOW_MILLIS);
+        // Shift time again so that we are just below the minimum allowable
+        advanceTime(TIME_MINIMUM_AVAILABLE_TO_ENABLE_MILLIS - 1);
+
+        // Verify we are not out of HBM
+        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
+
+        // Advance the necessary millisecond
+        advanceTime(1);
+
+        // Verify we are allowed HBM again.
+        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
+    }
+
+    @Test
+    public void testInHBM_ThenLowLux() {
+        final HighBrightnessModeController hbmc = createDefaultHbm();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
+
+        // Verify we are in HBM
+        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
+
+        advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2);
+
+        // Verify we are in HBM
+        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
+
+        hbmc.onAmbientLuxChange(1);
+        advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2 + 1);
+
+        // Verify we are out of HBM
+        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
+
+    }
+
+    @Test
+    public void testInHBM_TestMultipleEvents_DueToAutoBrightness() {
+        final HighBrightnessModeController hbmc = createDefaultHbm();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+
+        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
+        advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2);
+
+        // Verify we are in HBM
+        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
+
+        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT - 0.01f);
+        advanceTime(1);
+
+        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
+
+        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
+        advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2);
+
+        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
+
+        advanceTime(2);
+
+        // Now we should be out again.
+        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
+    }
+
+    @Test
+    public void testInHBM_TestMultipleEvents_DueToLux() {
+        final HighBrightnessModeController hbmc = createDefaultHbm();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+
+        // Go into HBM for half the allowed window
+        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
+        advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2);
+        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
+
+        // Move lux below threshold (ending first event);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX - 1);
+        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT);
+        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
+
+        // Move up some amount of time so that there's still time in the window even after a
+        // second event.
+        advanceTime((TIME_WINDOW_MILLIS - TIME_ALLOWED_IN_WINDOW_MILLIS) / 2);
+        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
+
+        // Go into HBM for just under the second half of allowed window
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 1);
+        advanceTime((TIME_ALLOWED_IN_WINDOW_MILLIS / 2) - 1);
+
+        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
+
+        // Now exhaust the time
+        advanceTime(2);
+        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
+    }
+
+    @Test
+    public void testHbmIsNotTurnedOffInHighThermalState() throws Exception {
+        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
+
+        // Disabled thermal throttling
+        hbmc.onBrightnessChanged(/*brightness=*/ 1f, /*unthrottledBrightness*/ 1f,
+                BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE);
+
+        assertFalse(hbmc.isThermalThrottlingActive());
+
+        // Try to go into HBM mode
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+        advanceTime(1);
+
+        assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
+
+        // Enable thermal throttling
+        hbmc.onBrightnessChanged(/*brightness=*/ TRANSITION_POINT - 0.01f,
+                /*unthrottledBrightness*/ 1f, BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL);
+        advanceTime(10);
+        assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
+        assertTrue(hbmc.isThermalThrottlingActive());
+
+        // Disabled thermal throttling
+        hbmc.onBrightnessChanged(/*brightness=*/ 1f, /*unthrottledBrightness*/ 1f,
+                BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE);
+        advanceTime(1);
+        assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
+        assertFalse(hbmc.isThermalThrottlingActive());
+    }
+
+    @Test
+    public void testHdrRequires50PercentOfScreen() {
+        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
+
+        final int layerWidth = DISPLAY_WIDTH;
+        final int smallLayerHeight = DISPLAY_HEIGHT / 2 - 1; // height to use for <50%
+        final int largeLayerHeight = DISPLAY_HEIGHT / 2 + 1; // height to use for >50%
+
+        // ensure hdr doesn't turn on if layer is too small
+        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
+                layerWidth, smallLayerHeight, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
+        advanceTime(0);
+        assertEquals(HIGH_BRIGHTNESS_MODE_OFF, hbmc.getHighBrightnessMode());
+
+        // Now check with layer larger than 50%
+        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
+                layerWidth, largeLayerHeight, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
+        advanceTime(0);
+        assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
+    }
+
+    @Test
+    public void testHdrRespectsMaxDesiredHdrSdrRatio() {
+        final HighBrightnessModeController hbmc = new TestHbmBuilder()
+                .setClock(new OffsettableClock())
+                .setHdrBrightnessConfig(mHdrBrightnessDeviceConfigMock)
+                .build();
+
+        // Passthrough return the max desired hdr/sdr ratio
+        when(mHdrBrightnessDeviceConfigMock.getHdrBrightnessFromSdr(anyFloat(), anyFloat()))
+                .thenAnswer(i -> i.getArgument(1));
+
+        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
+                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 2.0f /*maxDesiredHdrSdrRatio*/);
+        advanceTime(0);
+        assertEquals(2.0f, hbmc.getHdrBrightnessValue(), EPSILON);
+
+        // The hdr ratio cannot be less than 1.
+        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
+                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 0.5f /*maxDesiredHdrSdrRatio*/);
+        advanceTime(0);
+        assertEquals(1.0f, hbmc.getHdrBrightnessValue(), EPSILON);
+
+        // The hdr ratio can be as much as positive infinity
+        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
+                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/,
+                Float.POSITIVE_INFINITY /*maxDesiredHdrSdrRatio*/);
+        advanceTime(0);
+        assertEquals(Float.POSITIVE_INFINITY, hbmc.getHdrBrightnessValue(), 0.0);
+    }
+
+
+    @Test
+    public void testHdrTrumpsSunlight() {
+        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
+
+        // Turn on sunlight
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+        advanceTime(0);
+        assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
+        assertEquals(DEFAULT_MAX, hbmc.getCurrentBrightnessMax(), EPSILON);
+
+        // turn on hdr
+        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
+                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
+        advanceTime(0);
+        assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
+        assertEquals(TRANSITION_POINT, hbmc.getCurrentBrightnessMax(), EPSILON);
+    }
+
+    @Test
+    public void testHdrBrightnessLimitSameAsNormalLimit() {
+        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
+
+        // Check limit when HBM is off
+        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
+                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
+        advanceTime(0);
+        assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
+        assertEquals(TRANSITION_POINT, hbmc.getCurrentBrightnessMax(), EPSILON);
+
+        // Check limit with HBM is set to HDR
+        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 0 /*numberOfHdrLayers*/,
+                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
+        advanceTime(0);
+        assertEquals(HIGH_BRIGHTNESS_MODE_OFF, hbmc.getHighBrightnessMode());
+        assertEquals(TRANSITION_POINT, hbmc.getCurrentBrightnessMax(), EPSILON);
+    }
+
+    @Test
+    public void testHdrBrightnessScaledNormalBrightness() {
+        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
+
+        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
+                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
+        advanceTime(0);
+        assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
+
+        // verify things are scaled for 0.5f
+        float brightness = 0.5f;
+        float expectedHdrBrightness = MathUtils.map(DEFAULT_MIN, TRANSITION_POINT,
+                DEFAULT_MIN, DEFAULT_MAX, brightness); // map value from normal range to hdr range
+        hbmcOnBrightnessChanged(hbmc, brightness);
+        advanceTime(0);
+        assertEquals(expectedHdrBrightness, hbmc.getHdrBrightnessValue(), EPSILON);
+
+        // Try another value
+        brightness = 0.33f;
+        expectedHdrBrightness = MathUtils.map(DEFAULT_MIN, TRANSITION_POINT,
+                DEFAULT_MIN, DEFAULT_MAX, brightness); // map value from normal range to hdr range
+        hbmcOnBrightnessChanged(hbmc, brightness);
+        advanceTime(0);
+        assertEquals(expectedHdrBrightness, hbmc.getHdrBrightnessValue(), EPSILON);
+
+        // Try the min value
+        brightness = DEFAULT_MIN;
+        expectedHdrBrightness = DEFAULT_MIN;
+        hbmcOnBrightnessChanged(hbmc, brightness);
+        advanceTime(0);
+        assertEquals(expectedHdrBrightness, hbmc.getHdrBrightnessValue(), EPSILON);
+
+        // Try the max value
+        brightness = TRANSITION_POINT;
+        expectedHdrBrightness = DEFAULT_MAX;
+        hbmcOnBrightnessChanged(hbmc, brightness);
+        advanceTime(0);
+        assertEquals(expectedHdrBrightness, hbmc.getHdrBrightnessValue(), EPSILON);
+    }
+
+    @Test
+    public void testHbmStats_StateChange() {
+        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
+        final int displayStatsId = mDisplayUniqueId.hashCode();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT);
+        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
+                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
+        advanceTime(0);
+        assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
+
+        // Verify Stats HBM_ON_HDR
+        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_HDR),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
+
+        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 0 /*numberOfHdrLayers*/,
+                0, 0, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
+        advanceTime(0);
+
+        // Verify Stats HBM_OFF
+        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
+
+        // Verify Stats HBM_ON_SUNLIGHT
+        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_SUNLIGHT),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
+
+        hbmc.onAmbientLuxChange(1);
+        advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2 + 1);
+
+        // Verify Stats HBM_OFF
+        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__REASON__HBM_SV_OFF_LUX_DROP));
+    }
+
+    @Test
+    public void testHbmStats_NbmHdrNoReport() {
+        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
+        final int displayStatsId = mDisplayUniqueId.hashCode();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmcOnBrightnessChanged(hbmc, DEFAULT_MIN);
+        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
+                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
+        advanceTime(0);
+        assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
+
+        // Verify Stats HBM_ON_HDR not report
+        verify(mInjectorMock, never()).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_HDR),
+                anyInt());
+    }
+
+    @Test
+    public void testHbmStats_HighLuxLowBrightnessNoReport() {
+        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
+        final int displayStatsId = mDisplayUniqueId.hashCode();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+        hbmcOnBrightnessChanged(hbmc, DEFAULT_MIN);
+        advanceTime(0);
+        // verify in HBM sunlight mode
+        assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
+
+        // Verify Stats HBM_ON_SUNLIGHT not report
+        verify(mInjectorMock, never()).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_SUNLIGHT),
+                anyInt());
+    }
+
+    // Test reporting of thermal throttling when triggered externally through
+    // HighBrightnessModeController.onBrightnessChanged()
+    @Test
+    public void testHbmStats_ExternalThermalOff() throws Exception {
+        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
+        final int displayStatsId = mDisplayUniqueId.hashCode();
+        final float hbmBrightness = TRANSITION_POINT + 0.01f;
+        final float nbmBrightness = TRANSITION_POINT - 0.01f;
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+        // Brightness is unthrottled, HBM brightness granted
+        hbmc.onBrightnessChanged(hbmBrightness, hbmBrightness,
+                BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE);
+        advanceTime(1);
+        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_SUNLIGHT),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
+
+        // Brightness is thermally throttled, HBM brightness denied (NBM brightness granted)
+        hbmc.onBrightnessChanged(nbmBrightness, hbmBrightness,
+                BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL);
+        advanceTime(1);
+        // We expect HBM mode to remain set to sunlight, indicating that HBMC *allows* this mode.
+        // However, we expect the HBM state reported by HBMC to be off, since external thermal
+        // throttling (reported to HBMC through onBrightnessChanged()) lowers brightness to below
+        // the HBM transition point.
+        assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
+        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF),
+                eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_SV_OFF_THERMAL_LIMIT));
+    }
+
+    @Test
+    public void testHbmStats_TimeOut() {
+        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
+        final int displayStatsId = mDisplayUniqueId.hashCode();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
+        advanceTime(0);
+        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_SUNLIGHT),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
+
+        // Use up all the time in the window.
+        advanceTime(TIME_WINDOW_MILLIS + 1);
+
+        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF),
+                eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_SV_OFF_TIME_LIMIT));
+    }
+
+    @Test
+    public void testHbmStats_DisplayOff() {
+        final HighBrightnessModeController hbmc = createDefaultHbm();
+        final int displayStatsId = mDisplayUniqueId.hashCode();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
+        advanceTime(0);
+        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_SUNLIGHT),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE);
+        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__REASON__HBM_SV_OFF_DISPLAY_OFF));
+    }
+
+    @Test
+    public void testHbmStats_HdrPlaying() {
+        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
+        final int displayStatsId = mDisplayUniqueId.hashCode();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
+        advanceTime(0);
+        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_SUNLIGHT),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
+
+        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
+                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
+        advanceTime(0);
+
+        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_HDR),
+                eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_SV_OFF_HDR_PLAYING));
+    }
+
+    @Test
+    public void tetHbmStats_LowRequestedBrightness() {
+        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
+        final int displayStatsId = mDisplayUniqueId.hashCode();
+
+        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
+        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
+        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
+        advanceTime(0);
+        // verify in HBM sunlight mode
+        assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
+        // verify HBM_ON_SUNLIGHT
+        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_SUNLIGHT),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
+
+        hbmcOnBrightnessChanged(hbmc, DEFAULT_MIN);
+        // verify HBM_SV_OFF due to LOW_REQUESTED_BRIGHTNESS
+        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
+                eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF),
+                eq(FrameworkStatsLog
+                        .DISPLAY_HBM_STATE_CHANGED__REASON__HBM_SV_OFF_LOW_REQUESTED_BRIGHTNESS));
+    }
+
+    private void assertState(HighBrightnessModeController hbmc,
+            float brightnessMin, float brightnessMax, int hbmMode) {
+        assertEquals(brightnessMin, hbmc.getCurrentBrightnessMin(), EPSILON);
+        assertEquals(brightnessMax, hbmc.getCurrentBrightnessMax(), EPSILON);
+        assertEquals(hbmMode, hbmc.getHighBrightnessMode());
+    }
+
+    private class TestHbmBuilder {
+        OffsettableClock mClock;
+        HighBrightnessModeController.HdrBrightnessDeviceConfig mHdrBrightnessCfg;
+
+        TestHbmBuilder setClock(OffsettableClock clock) {
+            mClock = clock;
+            return this;
+        }
+
+        TestHbmBuilder setHdrBrightnessConfig(
+                HighBrightnessModeController.HdrBrightnessDeviceConfig hdrBrightnessCfg
+        ) {
+            mHdrBrightnessCfg = hdrBrightnessCfg;
+            return this;
+        }
+
+        HighBrightnessModeController build() {
+            initHandler(mClock);
+            if (mHighBrightnessModeMetadata == null) {
+                mHighBrightnessModeMetadata = new HighBrightnessModeMetadata();
+            }
+            return new HighBrightnessModeController(mInjectorMock, mHandler, DISPLAY_WIDTH,
+                    DISPLAY_HEIGHT, mDisplayToken, mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX,
+                    DEFAULT_HBM_DATA, mHdrBrightnessCfg, () -> {}, mHighBrightnessModeMetadata,
+                    mContextSpy);
+        }
+
+    }
+
+    private HighBrightnessModeController createDefaultHbm() {
+        return new TestHbmBuilder().build();
+    }
+
+    // Creates instance with standard initialization values.
+    private HighBrightnessModeController createDefaultHbm(OffsettableClock clock) {
+        return new TestHbmBuilder().setClock(clock).build();
+    }
+
+    private void initHandler(OffsettableClock clock) {
+        mClock = clock != null ? clock : new OffsettableClock.Stopped();
+        when(mInjectorMock.getClock()).thenReturn(mClock::now);
+        mTestLooper = new TestLooper(mClock::now);
+        mHandler = new Handler(mTestLooper.getLooper(), new Handler.Callback() {
+            @Override
+            public boolean handleMessage(Message msg) {
+                return true;
+            }
+        });
+    }
+
+    private void advanceTime(long timeMs) {
+        mClock.fastForward(timeMs);
+        mTestLooper.dispatchAll();
+    }
+
+    private void hbmcOnBrightnessChanged(HighBrightnessModeController hbmc, float brightness) {
+        hbmc.onBrightnessChanged(brightness, brightness, BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeMetadataTest.java b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/HighBrightnessModeMetadataTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
new file mode 100644
index 0000000..2065479
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
@@ -0,0 +1,290 @@
+/*
+ * 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.server.display;
+
+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.Mockito.any;
+import static org.mockito.Mockito.eq;
+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.app.PropertyInvalidatedCache;
+import android.graphics.Point;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.DisplayInfo;
+import android.view.Surface;
+import android.view.SurfaceControl;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.display.layout.Layout;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+@SmallTest
+public class LogicalDisplayTest {
+    private static final int DISPLAY_ID = 0;
+    private static final int LAYER_STACK = 0;
+    private static final int DISPLAY_WIDTH = 100;
+    private static final int DISPLAY_HEIGHT = 200;
+    private static final int MODE_ID = 1;
+
+    private LogicalDisplay mLogicalDisplay;
+    private DisplayDevice mDisplayDevice;
+    private DisplayDeviceRepository mDeviceRepo;
+    private final DisplayDeviceInfo mDisplayDeviceInfo = new DisplayDeviceInfo();
+
+    @Before
+    public void setUp() {
+        // Share classloader to allow package private access.
+        System.setProperty("dexmaker.share_classloader", "true");
+        mDisplayDevice = mock(DisplayDevice.class);
+        mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice);
+
+        mDisplayDeviceInfo.copyFrom(new DisplayDeviceInfo());
+        mDisplayDeviceInfo.width = DISPLAY_WIDTH;
+        mDisplayDeviceInfo.height = DISPLAY_HEIGHT;
+        mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT;
+        mDisplayDeviceInfo.touch = DisplayDeviceInfo.TOUCH_INTERNAL;
+        mDisplayDeviceInfo.modeId = MODE_ID;
+        mDisplayDeviceInfo.supportedModes = new Display.Mode[] {new Display.Mode(MODE_ID,
+                DISPLAY_WIDTH, DISPLAY_HEIGHT, /* refreshRate= */ 60)};
+        when(mDisplayDevice.getDisplayDeviceInfoLocked()).thenReturn(mDisplayDeviceInfo);
+
+        // Disable binder caches in this process.
+        PropertyInvalidatedCache.disableForTestMode();
+
+        mDeviceRepo = new DisplayDeviceRepository(
+                new DisplayManagerService.SyncRoot(),
+                new PersistentDataStore(new PersistentDataStore.Injector() {
+                    @Override
+                    public InputStream openRead() {
+                        return null;
+                    }
+
+                    @Override
+                    public OutputStream startWrite() {
+                        return null;
+                    }
+
+                    @Override
+                    public void finishWrite(OutputStream os, boolean success) {}
+                }));
+        mDeviceRepo.onDisplayDeviceEvent(mDisplayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+    }
+
+    @Test
+    public void testGetDisplayPosition() {
+        Point expectedPosition = new Point();
+
+        SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+        assertEquals(expectedPosition, mLogicalDisplay.getDisplayPosition());
+
+        expectedPosition.set(20, 40);
+        mLogicalDisplay.setDisplayOffsetsLocked(20, 40);
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+        assertEquals(expectedPosition, mLogicalDisplay.getDisplayPosition());
+
+        expectedPosition.set(40, -20);
+        DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.logicalWidth = DISPLAY_HEIGHT;
+        displayInfo.logicalHeight = DISPLAY_WIDTH;
+        displayInfo.rotation = Surface.ROTATION_90;
+        mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo);
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+        assertEquals(expectedPosition, mLogicalDisplay.getDisplayPosition());
+    }
+
+    // TODO: b/288880734 - fix test after display tests migration
+    @Test
+    @Ignore
+    public void testDisplayInputFlags() {
+        SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+        verify(t).setDisplayFlags(any(), eq(SurfaceControl.DISPLAY_RECEIVES_INPUT));
+        reset(t);
+
+        mDisplayDeviceInfo.touch = DisplayDeviceInfo.TOUCH_NONE;
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+        verify(t).setDisplayFlags(any(), eq(0));
+        reset(t);
+
+        mDisplayDeviceInfo.touch = DisplayDeviceInfo.TOUCH_VIRTUAL;
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+        verify(t).setDisplayFlags(any(), eq(SurfaceControl.DISPLAY_RECEIVES_INPUT));
+        reset(t);
+
+        mLogicalDisplay.setEnabledLocked(false);
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+        verify(t).setDisplayFlags(any(), eq(0));
+        reset(t);
+
+        mLogicalDisplay.setEnabledLocked(true);
+        mDisplayDeviceInfo.touch = DisplayDeviceInfo.TOUCH_EXTERNAL;
+        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+        verify(t).setDisplayFlags(any(), eq(SurfaceControl.DISPLAY_RECEIVES_INPUT));
+        reset(t);
+    }
+
+    @Test
+    public void testRearDisplaysArePresentationDisplaysThatDestroyContentOnRemoval() {
+        // Assert that the display isn't a presentation display by default, with a default remove
+        // mode
+        assertEquals(0, mLogicalDisplay.getDisplayInfoLocked().flags);
+        assertEquals(Display.REMOVE_MODE_MOVE_CONTENT_TO_PRIMARY,
+                mLogicalDisplay.getDisplayInfoLocked().removeMode);
+
+        // Update position and test to see that it's been updated to a rear, presentation display
+        // that destroys content on removal
+        mLogicalDisplay.setDevicePositionLocked(Layout.Display.POSITION_REAR);
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+        assertEquals(Display.FLAG_REAR | Display.FLAG_PRESENTATION,
+                mLogicalDisplay.getDisplayInfoLocked().flags);
+        assertEquals(Display.REMOVE_MODE_DESTROY_CONTENT,
+                mLogicalDisplay.getDisplayInfoLocked().removeMode);
+
+        // And then check the unsetting the position resets both
+        mLogicalDisplay.setDevicePositionLocked(Layout.Display.POSITION_UNKNOWN);
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+        assertEquals(0, mLogicalDisplay.getDisplayInfoLocked().flags);
+        assertEquals(Display.REMOVE_MODE_MOVE_CONTENT_TO_PRIMARY,
+                mLogicalDisplay.getDisplayInfoLocked().removeMode);
+    }
+
+    @Test
+    public void testUpdateLayoutLimitedRefreshRate() {
+        SurfaceControl.RefreshRateRange layoutLimitedRefreshRate =
+                new SurfaceControl.RefreshRateRange(0, 120);
+        DisplayInfo info1 = mLogicalDisplay.getDisplayInfoLocked();
+        mLogicalDisplay.updateLayoutLimitedRefreshRateLocked(layoutLimitedRefreshRate);
+        DisplayInfo info2 = mLogicalDisplay.getDisplayInfoLocked();
+        // Display info should only be updated when updateLocked is called
+        assertEquals(info2, info1);
+
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+        DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
+        assertNotEquals(info3, info2);
+        assertEquals(layoutLimitedRefreshRate, info3.layoutLimitedRefreshRate);
+    }
+
+    @Test
+    public void testUpdateLayoutLimitedRefreshRate_setsDirtyFlag() {
+        SurfaceControl.RefreshRateRange layoutLimitedRefreshRate =
+                new SurfaceControl.RefreshRateRange(0, 120);
+        assertFalse(mLogicalDisplay.isDirtyLocked());
+
+        mLogicalDisplay.updateLayoutLimitedRefreshRateLocked(layoutLimitedRefreshRate);
+        assertTrue(mLogicalDisplay.isDirtyLocked());
+
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+        assertFalse(mLogicalDisplay.isDirtyLocked());
+    }
+
+    @Test
+    public void testUpdateRefreshRateThermalThrottling() {
+        SparseArray<SurfaceControl.RefreshRateRange> refreshRanges = new SparseArray<>();
+        refreshRanges.put(0, new SurfaceControl.RefreshRateRange(0, 120));
+        DisplayInfo info1 = mLogicalDisplay.getDisplayInfoLocked();
+        mLogicalDisplay.updateThermalRefreshRateThrottling(refreshRanges);
+        DisplayInfo info2 = mLogicalDisplay.getDisplayInfoLocked();
+        // Display info should only be updated when updateLocked is called
+        assertEquals(info2, info1);
+
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+        DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
+        assertNotEquals(info3, info2);
+        assertTrue(refreshRanges.contentEquals(info3.thermalRefreshRateThrottling));
+    }
+
+    @Test
+    public void testUpdateRefreshRateThermalThrottling_setsDirtyFlag() {
+        SparseArray<SurfaceControl.RefreshRateRange> refreshRanges = new SparseArray<>();
+        refreshRanges.put(0, new SurfaceControl.RefreshRateRange(0, 120));
+        assertFalse(mLogicalDisplay.isDirtyLocked());
+
+        mLogicalDisplay.updateThermalRefreshRateThrottling(refreshRanges);
+        assertTrue(mLogicalDisplay.isDirtyLocked());
+
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+        assertFalse(mLogicalDisplay.isDirtyLocked());
+    }
+
+    @Test
+    public void testUpdateDisplayGroupIdLocked() {
+        int newId = 999;
+        DisplayInfo info1 = mLogicalDisplay.getDisplayInfoLocked();
+        mLogicalDisplay.updateDisplayGroupIdLocked(newId);
+        DisplayInfo info2 = mLogicalDisplay.getDisplayInfoLocked();
+        // Display info should only be updated when updateLocked is called
+        assertEquals(info2, info1);
+
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+        DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
+        assertNotEquals(info3, info2);
+        assertEquals(newId, info3.displayGroupId);
+    }
+
+    @Test
+    public void testUpdateDisplayGroupIdLocked_setsDirtyFlag() {
+        assertFalse(mLogicalDisplay.isDirtyLocked());
+
+        mLogicalDisplay.updateDisplayGroupIdLocked(99);
+        assertTrue(mLogicalDisplay.isDirtyLocked());
+
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+        assertFalse(mLogicalDisplay.isDirtyLocked());
+    }
+
+    @Test
+    public void testSetThermalBrightnessThrottlingDataId() {
+        String brightnessThrottlingDataId = "throttling_data_id";
+        DisplayInfo info1 = mLogicalDisplay.getDisplayInfoLocked();
+        mLogicalDisplay.setThermalBrightnessThrottlingDataIdLocked(brightnessThrottlingDataId);
+        DisplayInfo info2 = mLogicalDisplay.getDisplayInfoLocked();
+        // Display info should only be updated when updateLocked is called
+        assertEquals(info2, info1);
+
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+        DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
+        assertNotEquals(info3, info2);
+        assertEquals(brightnessThrottlingDataId, info3.thermalBrightnessThrottlingDataId);
+    }
+
+    @Test
+    public void testSetThermalBrightnessThrottlingDataId_setsDirtyFlag() {
+        assertFalse(mLogicalDisplay.isDirtyLocked());
+
+        mLogicalDisplay.setThermalBrightnessThrottlingDataIdLocked("99");
+        assertTrue(mLogicalDisplay.isDirtyLocked());
+
+        mLogicalDisplay.updateLocked(mDeviceRepo);
+        assertFalse(mLogicalDisplay.isDirtyLocked());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/NormalBrightnessModeControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/NormalBrightnessModeControllerTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/NormalBrightnessModeControllerTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/NormalBrightnessModeControllerTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/OWNERS b/services/tests/displayservicetests/src/com/android/server/display/OWNERS
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/OWNERS
rename to services/tests/displayservicetests/src/com/android/server/display/OWNERS
diff --git a/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java b/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java
new file mode 100644
index 0000000..9f91916
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java
@@ -0,0 +1,570 @@
+/*
+ * 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.
+ */
+
+package com.android.server.display;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.hardware.display.BrightnessConfiguration;
+import android.os.Handler;
+import android.os.test.TestLooper;
+import android.util.Pair;
+
+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;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PersistentDataStoreTest {
+    private PersistentDataStore mDataStore;
+    private TestInjector mInjector;
+    private TestLooper mTestLooper;
+
+    @Before
+    public void setUp() {
+        mInjector = new TestInjector();
+        mTestLooper = new TestLooper();
+        Handler handler = new Handler(mTestLooper.getLooper());
+        mDataStore = new PersistentDataStore(mInjector, handler);
+    }
+
+    @Test
+    public void testLoadBrightness() {
+        final String uniqueDisplayId = "test:123";
+        final DisplayDevice testDisplayDevice = new DisplayDevice(
+                null, null, uniqueDisplayId, null) {
+            @Override
+            public boolean hasStableUniqueId() {
+                return true;
+            }
+
+            @Override
+            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+                return null;
+            }
+        };
+
+        String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                + "<display-manager-state>\n"
+                + "  <display-states>\n"
+                + "    <display unique-id=\"test:123\">\n"
+                + "      <brightness-value user-serial=\"1\">0.1</brightness-value>\n"
+                + "      <brightness-value user-serial=\"2\">0.2</brightness-value>\n"
+                + "    </display>\n"
+                + "  </display-states>\n"
+                + "</display-manager-state>\n";
+
+        InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8));
+        mInjector.setReadStream(is);
+        mDataStore.loadIfNeeded();
+
+        float brightness = mDataStore.getBrightness(testDisplayDevice, 1);
+        assertEquals(0.1, brightness, 0.01);
+
+        brightness = mDataStore.getBrightness(testDisplayDevice, 2);
+        assertEquals(0.2, brightness, 0.01);
+    }
+
+    @Test
+    public void testSetBrightness_brightnessTagWithNoUserId_updatesToBrightnessTagWithUserId() {
+        final String uniqueDisplayId = "test:123";
+        final DisplayDevice testDisplayDevice =
+                new DisplayDevice(null, null, uniqueDisplayId, null) {
+            @Override
+            public boolean hasStableUniqueId() {
+                return true;
+            }
+
+            @Override
+            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+                return null;
+            }
+        };
+
+        String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                + "<display-manager-state>\n"
+                + "  <display-states>\n"
+                + "    <color-mode>0</color-mode>\n"
+                + "    <display unique-id=\"test:123\">\n"
+                + "      <brightness-value>0.5</brightness-value>\n"
+                + "    </display>\n"
+                + "  </display-states>\n"
+                + "</display-manager-state>\n";
+
+        InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8));
+        mInjector.setReadStream(is);
+        mDataStore.loadIfNeeded();
+
+        float user1Brightness = mDataStore.getBrightness(testDisplayDevice, 1 /* userSerial */);
+        float user2Brightness = mDataStore.getBrightness(testDisplayDevice, 2 /* userSerial */);
+        assertEquals(0.5, user1Brightness, 0.01);
+        assertEquals(0.5, user2Brightness, 0.01);
+
+        // Override the value for user 2. Default user must have been removed.
+        mDataStore.setBrightness(testDisplayDevice, 0.2f, 2 /* userSerial */  /* brightness*/);
+
+        user1Brightness = mDataStore.getBrightness(testDisplayDevice, 1 /* userSerial */);
+        user2Brightness = mDataStore.getBrightness(testDisplayDevice, 2 /* userSerial */);
+        assertTrue(Float.isNaN(user1Brightness));
+        assertEquals(0.2f, user2Brightness, 0.01);
+
+        // Override the value for user 1. User-specific brightness values should co-exist.
+        mDataStore.setBrightness(testDisplayDevice, 0.1f, 1 /* userSerial */  /* brightness*/);
+        user1Brightness = mDataStore.getBrightness(testDisplayDevice, 1 /* userSerial */);
+        user2Brightness = mDataStore.getBrightness(testDisplayDevice, 2 /* userSerial */);
+        assertEquals(0.1f, user1Brightness, 0.01);
+        assertEquals(0.2f, user2Brightness, 0.01);
+
+        // Validate saveIfNeeded writes user-specific brightnes.
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        mInjector.setWriteStream(baos);
+        mDataStore.saveIfNeeded();
+        mTestLooper.dispatchAll();
+        assertTrue(mInjector.wasWriteSuccessful());
+        TestInjector newInjector = new TestInjector();
+        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        newInjector.setReadStream(bais);
+        newDataStore.loadIfNeeded();
+
+        user1Brightness = newDataStore.getBrightness(testDisplayDevice, 1 /* userSerial */);
+        user2Brightness = newDataStore.getBrightness(testDisplayDevice, 2 /* userSerial */);
+        float unknownUserBrightness =
+                newDataStore.getBrightness(testDisplayDevice, 999 /* userSerial */);
+        assertEquals(0.1f, user1Brightness, 0.01);
+        assertEquals(0.2f, user2Brightness, 0.01);
+        assertTrue(Float.isNaN(unknownUserBrightness));
+    }
+
+    @Test
+    public void testLoadingBrightnessConfigurations() {
+        String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                + "<display-manager-state>\n"
+                + "  <brightness-configurations>\n"
+                + "    <brightness-configuration"
+                + "         user-serial=\"1\""
+                + "         package-name=\"example.com\""
+                + "         timestamp=\"123456\">\n"
+                + "      <brightness-curve description=\"something\">\n"
+                + "        <brightness-point lux=\"0\" nits=\"13.25\"/>\n"
+                + "        <brightness-point lux=\"25\" nits=\"35.94\"/>\n"
+                + "      </brightness-curve>\n"
+                + "    </brightness-configuration>\n"
+                + "    <brightness-configuration user-serial=\"3\">\n"
+                + "      <brightness-curve>\n"
+                + "        <brightness-point lux=\"0\" nits=\"13.25\"/>\n"
+                + "        <brightness-point lux=\"10.2\" nits=\"15\"/>\n"
+                + "      </brightness-curve>\n"
+                + "    </brightness-configuration>\n"
+                + "  </brightness-configurations>\n"
+                + "</display-manager-state>\n";
+        InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8));
+        mInjector.setReadStream(is);
+        mDataStore.loadIfNeeded();
+        BrightnessConfiguration config = mDataStore.getBrightnessConfiguration(1 /*userSerial*/);
+        Pair<float[], float[]> curve = config.getCurve();
+        float[] expectedLux = { 0f, 25f };
+        float[] expectedNits = { 13.25f, 35.94f };
+        assertArrayEquals(expectedLux, curve.first, "lux");
+        assertArrayEquals(expectedNits, curve.second, "nits");
+        assertEquals("something", config.getDescription());
+
+        config = mDataStore.getBrightnessConfiguration(3 /*userSerial*/);
+        curve = config.getCurve();
+        expectedLux = new float[] { 0f, 10.2f };
+        expectedNits = new float[] { 13.25f, 15f };
+        assertArrayEquals(expectedLux, curve.first, "lux");
+        assertArrayEquals(expectedNits, curve.second, "nits");
+        assertNull(config.getDescription());
+    }
+
+    @Test
+    public void testBrightnessConfigWithInvalidCurveIsIgnored() {
+        String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                + "<display-manager-state>\n"
+                + "  <brightness-configurations>\n"
+                + "    <brightness-configuration user-serial=\"0\">\n"
+                + "      <brightness-curve>\n"
+                + "        <brightness-point lux=\"1\" nits=\"13.25\"/>\n"
+                + "        <brightness-point lux=\"25\" nits=\"35.94\"/>\n"
+                + "      </brightness-curve>\n"
+                + "    </brightness-configuration>\n"
+                + "  </brightness-configurations>\n"
+                + "</display-manager-state>\n";
+        InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8));
+        mInjector.setReadStream(is);
+        mDataStore.loadIfNeeded();
+        assertNull(mDataStore.getBrightnessConfiguration(0 /*userSerial*/));
+    }
+
+    @Test
+    public void testBrightnessConfigWithInvalidFloatsIsIgnored() {
+        String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                + "<display-manager-state>\n"
+                + "  <brightness-configurations>\n"
+                + "    <brightness-configuration user-serial=\"0\">\n"
+                + "      <brightness-curve>\n"
+                + "        <brightness-point lux=\"0\" nits=\"13.25\"/>\n"
+                + "        <brightness-point lux=\"0xFF\" nits=\"foo\"/>\n"
+                + "      </brightness-curve>\n"
+                + "    </brightness-configuration>\n"
+                + "  </brightness-configurations>\n"
+                + "</display-manager-state>\n";
+        InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8));
+        mInjector.setReadStream(is);
+        mDataStore.loadIfNeeded();
+        assertNull(mDataStore.getBrightnessConfiguration(0 /*userSerial*/));
+    }
+
+    @Test
+    public void testEmptyBrightnessConfigurationsDoesntCrash() {
+        String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                + "<display-manager-state>\n"
+                + "  <brightness-configurations />\n"
+                + "</display-manager-state>\n";
+        InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8));
+        mInjector.setReadStream(is);
+        mDataStore.loadIfNeeded();
+        assertNull(mDataStore.getBrightnessConfiguration(0 /*userSerial*/));
+    }
+
+    @Test
+    public void testStoreAndReloadOfDisplayBrightnessConfigurations() throws InterruptedException {
+        final String uniqueDisplayId = "test:123";
+        int userSerial = 0;
+        String packageName = "pdsTestPackage";
+        final float[] lux = { 0f, 10f };
+        final float[] nits = {1f, 100f };
+        final BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
+                .setDescription("a description")
+                .build();
+        mDataStore.loadIfNeeded();
+        assertNull(mDataStore.getBrightnessConfigurationForDisplayLocked(uniqueDisplayId,
+                userSerial));
+
+        DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
+            @Override
+            public boolean hasStableUniqueId() {
+                return true;
+            }
+
+            @Override
+            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+                return null;
+            }
+        };
+
+        mDataStore.setBrightnessConfigurationForDisplayLocked(config, testDisplayDevice, userSerial,
+                packageName);
+
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        mInjector.setWriteStream(baos);
+        mDataStore.saveIfNeeded();
+        mTestLooper.dispatchAll();
+        assertTrue(mInjector.wasWriteSuccessful());
+        TestInjector newInjector = new TestInjector();
+        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        newInjector.setReadStream(bais);
+        newDataStore.loadIfNeeded();
+        assertNotNull(newDataStore.getBrightnessConfigurationForDisplayLocked(uniqueDisplayId,
+                userSerial));
+        assertEquals(mDataStore.getBrightnessConfigurationForDisplayLocked(uniqueDisplayId,
+                userSerial), newDataStore.getBrightnessConfigurationForDisplayLocked(
+                        uniqueDisplayId, userSerial));
+    }
+
+    @Test
+    public void testSetBrightnessConfigurationFailsWithUnstableId() {
+        final String uniqueDisplayId = "test:123";
+        int userSerial = 0;
+        String packageName = "pdsTestPackage";
+        final float[] lux = { 0f, 10f };
+        final float[] nits = {1f, 100f };
+        final BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
+                .setDescription("a description")
+                .build();
+        mDataStore.loadIfNeeded();
+        assertNull(mDataStore.getBrightnessConfigurationForDisplayLocked(uniqueDisplayId,
+                userSerial));
+
+        DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
+            @Override
+            public boolean hasStableUniqueId() {
+                return false;
+            }
+
+            @Override
+            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+                return null;
+            }
+        };
+
+        assertFalse(mDataStore.setBrightnessConfigurationForDisplayLocked(
+                config, testDisplayDevice, userSerial, packageName));
+    }
+
+    @Test
+    public void testStoreAndReloadOfBrightnessConfigurations() throws InterruptedException {
+        final float[] lux = { 0f, 10f };
+        final float[] nits = {1f, 100f };
+        final BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
+                .setDescription("a description")
+                .build();
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        String packageName = context.getPackageName();
+
+        mDataStore.loadIfNeeded();
+        assertNull(mDataStore.getBrightnessConfiguration(0 /*userSerial*/));
+        mDataStore.setBrightnessConfigurationForUser(config, 0, packageName);
+
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        mInjector.setWriteStream(baos);
+        mDataStore.saveIfNeeded();
+        mTestLooper.dispatchAll();
+        assertTrue(mInjector.wasWriteSuccessful());
+
+        TestInjector newInjector = new TestInjector();
+        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        newInjector.setReadStream(bais);
+        newDataStore.loadIfNeeded();
+        assertNotNull(newDataStore.getBrightnessConfiguration(0 /*userSerial*/));
+        assertEquals(mDataStore.getBrightnessConfiguration(0 /*userSerial*/),
+                newDataStore.getBrightnessConfiguration(0 /*userSerial*/));
+    }
+
+    @Test
+    public void testNullBrightnessConfiguration() {
+        final float[] lux = { 0f, 10f };
+        final float[] nits = {1f, 100f };
+        int userSerial = 0;
+        final BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
+                .setDescription("a description")
+                .build();
+        mDataStore.loadIfNeeded();
+        assertNull(mDataStore.getBrightnessConfiguration(userSerial));
+
+        mDataStore.setBrightnessConfigurationForUser(config, userSerial, "packagename");
+        assertNotNull(mDataStore.getBrightnessConfiguration(userSerial));
+
+        mDataStore.setBrightnessConfigurationForUser(null, userSerial, "packagename");
+        assertNull(mDataStore.getBrightnessConfiguration(userSerial));
+    }
+
+    @Test
+    public void testStoreAndRestoreResolution() {
+        final String uniqueDisplayId = "test:123";
+        DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
+            @Override
+            public boolean hasStableUniqueId() {
+                return true;
+            }
+
+            @Override
+            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+                return null;
+            }
+        };
+        int width = 35;
+        int height = 45;
+        mDataStore.loadIfNeeded();
+        mDataStore.setUserPreferredResolution(testDisplayDevice, width, height);
+
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        mInjector.setWriteStream(baos);
+        mDataStore.saveIfNeeded();
+        mTestLooper.dispatchAll();
+        assertTrue(mInjector.wasWriteSuccessful());
+        TestInjector newInjector = new TestInjector();
+        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        newInjector.setReadStream(bais);
+        newDataStore.loadIfNeeded();
+        assertNotNull(newDataStore.getUserPreferredResolution(testDisplayDevice));
+        assertEquals(35, newDataStore.getUserPreferredResolution(testDisplayDevice).x);
+        assertEquals(35, mDataStore.getUserPreferredResolution(testDisplayDevice).x);
+        assertEquals(45, newDataStore.getUserPreferredResolution(testDisplayDevice).y);
+        assertEquals(45, mDataStore.getUserPreferredResolution(testDisplayDevice).y);
+    }
+
+    @Test
+    public void testStoreAndRestoreRefreshRate() {
+        final String uniqueDisplayId = "test:123";
+        DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
+            @Override
+            public boolean hasStableUniqueId() {
+                return true;
+            }
+
+            @Override
+            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+                return null;
+            }
+        };
+        float refreshRate = 85.3f;
+        mDataStore.loadIfNeeded();
+        mDataStore.setUserPreferredRefreshRate(testDisplayDevice, refreshRate);
+
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        mInjector.setWriteStream(baos);
+        mDataStore.saveIfNeeded();
+        mTestLooper.dispatchAll();
+        assertTrue(mInjector.wasWriteSuccessful());
+        TestInjector newInjector = new TestInjector();
+        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        newInjector.setReadStream(bais);
+        newDataStore.loadIfNeeded();
+        assertNotNull(newDataStore.getUserPreferredRefreshRate(testDisplayDevice));
+        assertEquals(85.3f, mDataStore.getUserPreferredRefreshRate(testDisplayDevice), 01.f);
+        assertEquals(85.3f, newDataStore.getUserPreferredRefreshRate(testDisplayDevice), 0.1f);
+    }
+
+    @Test
+    public void testBrightnessInitialisesWithInvalidFloat() {
+        final String uniqueDisplayId = "test:123";
+        DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
+            @Override
+            public boolean hasStableUniqueId() {
+                return true;
+            }
+
+            @Override
+            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+                return null;
+            }
+        };
+
+        // Set any value which initialises Display state
+        float refreshRate = 85.3f;
+        mDataStore.loadIfNeeded();
+        mDataStore.setUserPreferredRefreshRate(testDisplayDevice, refreshRate);
+
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        mInjector.setWriteStream(baos);
+        mDataStore.saveIfNeeded();
+        mTestLooper.dispatchAll();
+        assertTrue(mInjector.wasWriteSuccessful());
+        TestInjector newInjector = new TestInjector();
+        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        newInjector.setReadStream(bais);
+        newDataStore.loadIfNeeded();
+        assertTrue(Float.isNaN(mDataStore.getBrightness(testDisplayDevice, 1 /* userSerial */)));
+    }
+
+    @Test
+    public void testStoreAndRestoreBrightnessNitsForDefaultDisplay() {
+        float brightnessNitsForDefaultDisplay = 190;
+        mDataStore.loadIfNeeded();
+        mDataStore.setBrightnessNitsForDefaultDisplay(brightnessNitsForDefaultDisplay);
+
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        mInjector.setWriteStream(baos);
+        mDataStore.saveIfNeeded();
+        mTestLooper.dispatchAll();
+        assertTrue(mInjector.wasWriteSuccessful());
+        TestInjector newInjector = new TestInjector();
+        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        newInjector.setReadStream(bais);
+        newDataStore.loadIfNeeded();
+        assertEquals(brightnessNitsForDefaultDisplay,
+                mDataStore.getBrightnessNitsForDefaultDisplay(), 0);
+        assertEquals(brightnessNitsForDefaultDisplay,
+                newDataStore.getBrightnessNitsForDefaultDisplay(), 0);
+    }
+
+    @Test
+    public void testInitialBrightnessNitsForDefaultDisplay() {
+        mDataStore.loadIfNeeded();
+        assertEquals(-1, mDataStore.getBrightnessNitsForDefaultDisplay(), 0);
+    }
+
+    public class TestInjector extends PersistentDataStore.Injector {
+        private InputStream mReadStream;
+        private OutputStream mWriteStream;
+
+        private boolean mWasSuccessful;
+
+        @Override
+        public InputStream openRead() throws FileNotFoundException {
+            if (mReadStream != null) {
+                return mReadStream;
+            } else {
+                throw new FileNotFoundException();
+            }
+        }
+
+        @Override
+        public OutputStream startWrite() {
+            return mWriteStream;
+        }
+
+        @Override
+        public void finishWrite(OutputStream os, boolean success) {
+            mWasSuccessful = success;
+            try {
+                os.close();
+            } catch (IOException e) {
+                // This method can't throw IOException since the super implementation doesn't, so
+                // we just wrap it in a RuntimeException so we end up crashing the test all the
+                // same.
+                throw new RuntimeException(e);
+            }
+        }
+
+        public void setReadStream(InputStream is) {
+            mReadStream = is;
+        }
+
+        public void setWriteStream(OutputStream os) {
+            mWriteStream = os;
+        }
+
+        public boolean wasWriteSuccessful() {
+            return mWasSuccessful;
+        }
+    }
+
+    private static void assertArrayEquals(float[] expected, float[] actual, String name) {
+        assertEquals("Expected " + name + " arrays to be the same length!",
+                expected.length, actual.length);
+        for (int i = 0; i < expected.length; i++) {
+            assertEquals("Expected " + name + " arrays to be equivalent when value " + i
+                    + "differs", expected[i], actual[i], 0.01 /*tolerance*/);
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java
diff --git a/services/tests/displayservicetests/src/com/android/server/display/SensorUtilsTest.java b/services/tests/displayservicetests/src/com/android/server/display/SensorUtilsTest.java
new file mode 100644
index 0000000..4494b0c
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/SensorUtilsTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.hardware.input.InputSensorInfo;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.annotations.Keep;
+import com.android.server.display.DisplayDeviceConfig.SensorData;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Collections;
+import java.util.List;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+@SmallTest
+@RunWith(JUnitParamsRunner.class)
+public class SensorUtilsTest {
+
+    private static final String TEST_SENSOR_NAME = "test_sensor_name";
+    private static final String TEST_SENSOR_TYPE = "test_sensor_type";
+    private static final Sensor TEST_SENSOR = createSensor();
+    @Mock
+    private SensorManager mSensorManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testNoSensorData() {
+        Sensor result = SensorUtils.findSensor(mSensorManager, null, Sensor.TYPE_LIGHT);
+        assertNull(result);
+    }
+
+    @Test
+    public void testNoSensorManager() {
+        Sensor result = SensorUtils.findSensor(null, new SensorData(), Sensor.TYPE_LIGHT);
+        assertNull(result);
+    }
+
+    @Keep
+    private static Object[][] findSensorData() {
+        // sensorName, sensorType, fallbackType, allSensors, defaultSensor, expectedResult
+        return new Object[][]{
+                // no data, no default
+                {null, null, Sensor.TYPE_LIGHT,
+                        Collections.singletonList(TEST_SENSOR), null, null},
+                // matching name, matching type, no default
+                {TEST_SENSOR_NAME, TEST_SENSOR_TYPE, Sensor.TYPE_LIGHT,
+                        Collections.singletonList(TEST_SENSOR), null, TEST_SENSOR},
+                // matching name, no default
+                {TEST_SENSOR_NAME, null, Sensor.TYPE_LIGHT,
+                        Collections.singletonList(TEST_SENSOR), null, TEST_SENSOR},
+                // not matching name, no default
+                {"not_matching_name", null, Sensor.TYPE_LIGHT,
+                        Collections.singletonList(TEST_SENSOR), null, null},
+                // matching type, no default
+                {null, TEST_SENSOR_TYPE, Sensor.TYPE_LIGHT,
+                        Collections.singletonList(TEST_SENSOR), null, TEST_SENSOR},
+                // not matching type, no default
+                {null, "not_matching_type", Sensor.TYPE_LIGHT,
+                        Collections.singletonList(TEST_SENSOR), null, null},
+                // not matching type, matching name, no default
+                {TEST_SENSOR_NAME, "not_matching_type", Sensor.TYPE_LIGHT,
+                        Collections.singletonList(TEST_SENSOR), null, null},
+                // not matching name, matching type, no default
+                {"not_matching_name", TEST_SENSOR_TYPE, Sensor.TYPE_LIGHT,
+                        Collections.singletonList(TEST_SENSOR), null, null},
+                // not matching type, not matching name, no default
+                {"not_matching_name", "not_matching_type", Sensor.TYPE_LIGHT,
+                        Collections.singletonList(TEST_SENSOR), null, null},
+                // not matching type, not matching name, with default
+                {"not_matching_name", "not_matching_type", Sensor.TYPE_LIGHT,
+                        Collections.singletonList(TEST_SENSOR), TEST_SENSOR, TEST_SENSOR},
+                // no data, with default
+                {null, null, Sensor.TYPE_LIGHT,
+                        Collections.singletonList(TEST_SENSOR), TEST_SENSOR, TEST_SENSOR},
+                // empty data, with default
+                {"", "", Sensor.TYPE_LIGHT,
+                        Collections.singletonList(TEST_SENSOR), TEST_SENSOR, TEST_SENSOR},
+                // empty data, with default, no fallback
+                {"", "", SensorUtils.NO_FALLBACK,
+                        Collections.singletonList(TEST_SENSOR), TEST_SENSOR, null},
+        };
+    }
+
+    @Test
+    @Parameters(method = "findSensorData")
+    public void testFindSensor(@Nullable String sensorName, @Nullable String sensorType,
+            int fallbackType, List<Sensor> allSensors, @Nullable Sensor defaultSensor,
+            @Nullable Sensor expectedResult) {
+        when(mSensorManager.getSensorList(Sensor.TYPE_ALL)).thenReturn(allSensors);
+        when(mSensorManager.getDefaultSensor(fallbackType)).thenReturn(defaultSensor);
+
+        SensorData sensorData = new SensorData();
+        sensorData.name = sensorName;
+        sensorData.type = sensorType;
+
+        Sensor result = SensorUtils.findSensor(mSensorManager, sensorData, fallbackType);
+
+        assertEquals(expectedResult, result);
+    }
+
+    private static Sensor createSensor() {
+        return new Sensor(new InputSensorInfo(
+                TEST_SENSOR_NAME, "vendor", 0, 0, 0, 1f, 1f, 1, 1, 1, 1,
+                TEST_SENSOR_TYPE, "", 0, 0, 0));
+    }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/TestUtils.java b/services/tests/displayservicetests/src/com/android/server/display/TestUtils.java
new file mode 100644
index 0000000..8b45145
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/TestUtils.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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 android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.input.InputSensorInfo;
+import android.os.Parcel;
+import android.os.SystemClock;
+import android.view.DisplayAddress;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+public final class TestUtils {
+
+    public static SensorEvent createSensorEvent(Sensor sensor, int value) throws Exception {
+        final Constructor<SensorEvent> constructor =
+                SensorEvent.class.getDeclaredConstructor(int.class);
+        constructor.setAccessible(true);
+        final SensorEvent event = constructor.newInstance(1);
+        event.sensor = sensor;
+        event.values[0] = value;
+        event.timestamp = SystemClock.elapsedRealtimeNanos();
+        return event;
+    }
+
+
+    public static void setSensorType(Sensor sensor, int type, String strType) throws Exception {
+        Method setter = Sensor.class.getDeclaredMethod("setType", Integer.TYPE);
+        setter.setAccessible(true);
+        setter.invoke(sensor, type);
+        if (strType != null) {
+            Field f = sensor.getClass().getDeclaredField("mStringType");
+            f.setAccessible(true);
+            f.set(sensor, strType);
+        }
+    }
+
+    public static void setMaximumRange(Sensor sensor, float maximumRange) throws Exception {
+        Method setter = Sensor.class.getDeclaredMethod("setRange", Float.TYPE, Float.TYPE);
+        setter.setAccessible(true);
+        setter.invoke(sensor, maximumRange, 1);
+    }
+
+    public static Sensor createSensor(int type, String strType) throws Exception {
+        Constructor<Sensor> constr = Sensor.class.getDeclaredConstructor();
+        constr.setAccessible(true);
+        Sensor sensor = constr.newInstance();
+        setSensorType(sensor, type, strType);
+        return sensor;
+    }
+
+    public static Sensor createSensor(int type, String strType, float maximumRange)
+            throws Exception {
+        Constructor<Sensor> constr = Sensor.class.getDeclaredConstructor();
+        constr.setAccessible(true);
+        Sensor sensor = constr.newInstance();
+        setSensorType(sensor, type, strType);
+        setMaximumRange(sensor, maximumRange);
+        return sensor;
+    }
+
+    public static Sensor createSensor(String type, String name) {
+        return new Sensor(new InputSensorInfo(
+                name, "vendor", 0, 0, 0, 1f, 1f, 1, 1, 1, 1,
+                type, "", 0, 0, 0));
+    }
+
+    /**
+     * Create a custom {@link DisplayAddress} to ensure we're not relying on any specific
+     * display-address implementation in our code. Intentionally uses default object (reference)
+     * equality rules.
+     */
+    public static class TestDisplayAddress extends DisplayAddress {
+        @Override
+        public void writeToParcel(Parcel out, int flags) { }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessReasonTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/brightness/BrightnessReasonTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
new file mode 100644
index 0000000..c4f4838
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
@@ -0,0 +1,387 @@
+/*
+ * 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.brightness;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
+import android.os.HandlerExecutor;
+import android.os.PowerManager;
+import android.view.Display;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.display.AutomaticBrightnessController;
+import com.android.server.display.BrightnessSetting;
+import com.android.server.display.brightness.strategy.DisplayBrightnessStrategy;
+import com.android.server.display.brightness.strategy.TemporaryBrightnessStrategy;
+
+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)
+public final class DisplayBrightnessControllerTest {
+    private static final int DISPLAY_ID = Display.DEFAULT_DISPLAY;
+    private static final float DEFAULT_BRIGHTNESS = 0.15f;
+
+    @Mock
+    private DisplayBrightnessStrategySelector mDisplayBrightnessStrategySelector;
+    @Mock
+    private Context mContext;
+    @Mock
+    private Resources mResources;
+    @Mock
+    private BrightnessSetting mBrightnessSetting;
+    @Mock
+    private Runnable mOnBrightnessChangeRunnable;
+
+    @Mock
+    private HandlerExecutor mBrightnessChangeExecutor;
+
+    private final DisplayBrightnessController.Injector mInjector = new
+            DisplayBrightnessController.Injector() {
+        @Override
+        DisplayBrightnessStrategySelector getDisplayBrightnessStrategySelector(
+                Context context, int displayId) {
+            return mDisplayBrightnessStrategySelector;
+        }
+    };
+
+    private DisplayBrightnessController mDisplayBrightnessController;
+
+    @Before
+    public void before() {
+        MockitoAnnotations.initMocks(this);
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mBrightnessSetting.getBrightness()).thenReturn(Float.NaN);
+        when(mBrightnessSetting.getBrightnessNitsForDefaultDisplay()).thenReturn(-1f);
+        when(mResources.getBoolean(
+                com.android.internal.R.bool.config_persistBrightnessNitsForDefaultDisplay))
+                .thenReturn(true);
+        mDisplayBrightnessController = new DisplayBrightnessController(mContext, mInjector,
+                DISPLAY_ID, DEFAULT_BRIGHTNESS, mBrightnessSetting, mOnBrightnessChangeRunnable,
+                mBrightnessChangeExecutor);
+    }
+
+    @Test
+    public void testIfFirstScreenBrightnessIsDefault() {
+        assertEquals(DEFAULT_BRIGHTNESS, mDisplayBrightnessController.getCurrentBrightness(),
+                /* delta= */ 0.0f);
+    }
+
+    @Test
+    public void testUpdateBrightness() {
+        DisplayPowerRequest displayPowerRequest = mock(DisplayPowerRequest.class);
+        DisplayBrightnessStrategy displayBrightnessStrategy = mock(DisplayBrightnessStrategy.class);
+        int targetDisplayState = Display.STATE_DOZE;
+        when(mDisplayBrightnessStrategySelector.selectStrategy(displayPowerRequest,
+                targetDisplayState)).thenReturn(displayBrightnessStrategy);
+        mDisplayBrightnessController.updateBrightness(displayPowerRequest, targetDisplayState);
+        verify(displayBrightnessStrategy).updateBrightness(displayPowerRequest);
+        assertEquals(mDisplayBrightnessController.getCurrentDisplayBrightnessStrategy(),
+                displayBrightnessStrategy);
+    }
+
+    @Test
+    public void isAllowAutoBrightnessWhileDozingConfigDelegatesToDozeBrightnessStrategy() {
+        mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig();
+        verify(mDisplayBrightnessStrategySelector).isAllowAutoBrightnessWhileDozingConfig();
+    }
+
+    @Test
+    public void setTemporaryBrightness() {
+        float temporaryBrightness = 0.4f;
+        TemporaryBrightnessStrategy temporaryBrightnessStrategy = mock(
+                TemporaryBrightnessStrategy.class);
+        when(mDisplayBrightnessStrategySelector.getTemporaryDisplayBrightnessStrategy()).thenReturn(
+                temporaryBrightnessStrategy);
+        mDisplayBrightnessController.setTemporaryBrightness(temporaryBrightness);
+        verify(temporaryBrightnessStrategy).setTemporaryScreenBrightness(temporaryBrightness);
+    }
+
+    @Test
+    public void setCurrentScreenBrightness() {
+        // Current Screen brightness is set as expected when a different value than the current
+        // is set
+        float currentScreenBrightness = 0.4f;
+        mDisplayBrightnessController.setAndNotifyCurrentScreenBrightness(currentScreenBrightness);
+        assertEquals(mDisplayBrightnessController.getCurrentBrightness(),
+                currentScreenBrightness, /* delta= */ 0.0f);
+        verify(mBrightnessChangeExecutor).execute(mOnBrightnessChangeRunnable);
+
+        // No change to the current screen brightness is same as the existing one
+        mDisplayBrightnessController.setAndNotifyCurrentScreenBrightness(currentScreenBrightness);
+        verifyNoMoreInteractions(mBrightnessChangeExecutor);
+    }
+
+    @Test
+    public void setPendingScreenBrightnessSetting() {
+        float pendingScreenBrightness = 0.4f;
+        mDisplayBrightnessController.setPendingScreenBrightness(pendingScreenBrightness);
+        assertEquals(mDisplayBrightnessController.getPendingScreenBrightness(),
+                pendingScreenBrightness, /* delta= */ 0.0f);
+    }
+
+    @Test
+    public void updateUserSetScreenBrightness() {
+        // No brightness is set if the pending brightness is invalid
+        mDisplayBrightnessController.setPendingScreenBrightness(Float.NaN);
+        assertFalse(mDisplayBrightnessController.updateUserSetScreenBrightness());
+
+        // user set brightness is not set if the current and the pending brightness are same.
+        float currentBrightness = 0.4f;
+        TemporaryBrightnessStrategy temporaryBrightnessStrategy = mock(
+                TemporaryBrightnessStrategy.class);
+        when(mDisplayBrightnessStrategySelector.getTemporaryDisplayBrightnessStrategy()).thenReturn(
+                temporaryBrightnessStrategy);
+        mDisplayBrightnessController.setAndNotifyCurrentScreenBrightness(currentBrightness);
+        mDisplayBrightnessController.setPendingScreenBrightness(currentBrightness);
+        mDisplayBrightnessController.setTemporaryBrightness(currentBrightness);
+        assertFalse(mDisplayBrightnessController.updateUserSetScreenBrightness());
+        verify(temporaryBrightnessStrategy).setTemporaryScreenBrightness(
+                PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        assertEquals(mDisplayBrightnessController.getPendingScreenBrightness(),
+                PowerManager.BRIGHTNESS_INVALID_FLOAT, /* delta= */ 0.0f);
+
+        // user set brightness is set as expected
+        currentBrightness = 0.4f;
+        float pendingScreenBrightness = 0.3f;
+        float temporaryScreenBrightness = 0.2f;
+        mDisplayBrightnessController.setAndNotifyCurrentScreenBrightness(currentBrightness);
+        mDisplayBrightnessController.setPendingScreenBrightness(pendingScreenBrightness);
+        mDisplayBrightnessController.setTemporaryBrightness(temporaryScreenBrightness);
+        assertTrue(mDisplayBrightnessController.updateUserSetScreenBrightness());
+        assertEquals(mDisplayBrightnessController.getCurrentBrightness(),
+                pendingScreenBrightness, /* delta= */ 0.0f);
+        assertEquals(mDisplayBrightnessController.getLastUserSetScreenBrightness(),
+                pendingScreenBrightness, /* delta= */ 0.0f);
+        verify(mBrightnessChangeExecutor, times(2))
+                .execute(mOnBrightnessChangeRunnable);
+        verify(temporaryBrightnessStrategy, times(2))
+                .setTemporaryScreenBrightness(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        assertEquals(mDisplayBrightnessController.getPendingScreenBrightness(),
+                PowerManager.BRIGHTNESS_INVALID_FLOAT, /* delta= */ 0.0f);
+    }
+
+    @Test
+    public void registerBrightnessSettingChangeListener() {
+        BrightnessSetting.BrightnessSettingListener brightnessSettingListener = mock(
+                BrightnessSetting.BrightnessSettingListener.class);
+        mDisplayBrightnessController.registerBrightnessSettingChangeListener(
+                brightnessSettingListener);
+        verify(mBrightnessSetting).registerListener(brightnessSettingListener);
+        assertEquals(mDisplayBrightnessController.getBrightnessSettingListener(),
+                brightnessSettingListener);
+    }
+
+    @Test
+    public void getScreenBrightnessSetting() {
+        // getScreenBrightnessSetting returns the value relayed by BrightnessSetting, if the
+        // valid is valid and in range
+        float brightnessSetting = 0.2f;
+        when(mBrightnessSetting.getBrightness()).thenReturn(brightnessSetting);
+        assertEquals(mDisplayBrightnessController.getScreenBrightnessSetting(), brightnessSetting,
+                /* delta= */ 0.0f);
+
+        // getScreenBrightnessSetting value is clamped if BrightnessSetting returns value beyond max
+        brightnessSetting = 1.1f;
+        when(mBrightnessSetting.getBrightness()).thenReturn(brightnessSetting);
+        assertEquals(mDisplayBrightnessController.getScreenBrightnessSetting(), 1.0f,
+                /* delta= */ 0.0f);
+
+        // getScreenBrightnessSetting returns default value is BrightnessSetting returns invalid
+        // value.
+        brightnessSetting = Float.NaN;
+        when(mBrightnessSetting.getBrightness()).thenReturn(brightnessSetting);
+        assertEquals(mDisplayBrightnessController.getScreenBrightnessSetting(), DEFAULT_BRIGHTNESS,
+                /* delta= */ 0.0f);
+    }
+
+    @Test
+    public void setBrightnessSetsInBrightnessSetting() {
+        float brightnessValue = 0.3f;
+        mDisplayBrightnessController.setBrightness(brightnessValue);
+        verify(mBrightnessSetting).setBrightness(brightnessValue);
+    }
+
+    @Test
+    public void updateScreenBrightnessSetting() {
+        // This interaction happens in the constructor itself
+        verify(mBrightnessSetting).getBrightness();
+
+        // Sets the appropriate value when valid, and not equal to the current brightness
+        float brightnessValue = 0.3f;
+        mDisplayBrightnessController.updateScreenBrightnessSetting(brightnessValue);
+        assertEquals(mDisplayBrightnessController.getCurrentBrightness(), brightnessValue,
+                0.0f);
+        verify(mBrightnessChangeExecutor).execute(mOnBrightnessChangeRunnable);
+        verify(mBrightnessSetting).setBrightness(brightnessValue);
+        verify(mBrightnessSetting).setUserSerial(anyInt());
+
+        // Does nothing if the value is invalid
+        mDisplayBrightnessController.updateScreenBrightnessSetting(Float.NaN);
+        verifyNoMoreInteractions(mBrightnessChangeExecutor, mBrightnessSetting);
+
+        // Does nothing if the value is same as the current brightness
+        brightnessValue = 0.2f;
+        mDisplayBrightnessController.setAndNotifyCurrentScreenBrightness(brightnessValue);
+        verify(mBrightnessChangeExecutor, times(2))
+                .execute(mOnBrightnessChangeRunnable);
+        mDisplayBrightnessController.updateScreenBrightnessSetting(brightnessValue);
+        verifyNoMoreInteractions(mBrightnessChangeExecutor, mBrightnessSetting);
+    }
+
+    @Test
+    public void testConvertToNits() {
+        final float brightness = 0.5f;
+        final float nits = 300;
+        final float adjustedNits = 200;
+
+        // ABC is null
+        assertEquals(-1f, mDisplayBrightnessController.convertToNits(brightness),
+                /* delta= */ 0);
+        assertEquals(-1f, mDisplayBrightnessController.convertToAdjustedNits(brightness),
+                /* delta= */ 0);
+
+        AutomaticBrightnessController automaticBrightnessController =
+                mock(AutomaticBrightnessController.class);
+        when(automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
+        when(automaticBrightnessController.convertToAdjustedNits(brightness))
+                .thenReturn(adjustedNits);
+        mDisplayBrightnessController.setAutomaticBrightnessController(
+                automaticBrightnessController);
+
+        assertEquals(nits, mDisplayBrightnessController.convertToNits(brightness), /* delta= */ 0);
+        assertEquals(adjustedNits, mDisplayBrightnessController.convertToAdjustedNits(brightness),
+                /* delta= */ 0);
+    }
+
+    @Test
+    public void testConvertToFloatScale() {
+        float brightness = 0.5f;
+        float nits = 300;
+
+        // ABC is null
+        assertEquals(PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                mDisplayBrightnessController.convertToFloatScale(nits), /* delta= */ 0);
+
+        AutomaticBrightnessController automaticBrightnessController =
+                mock(AutomaticBrightnessController.class);
+        when(automaticBrightnessController.convertToFloatScale(nits)).thenReturn(brightness);
+        mDisplayBrightnessController.setAutomaticBrightnessController(
+                automaticBrightnessController);
+
+        assertEquals(brightness, mDisplayBrightnessController.convertToFloatScale(nits),
+                /* delta= */ 0);
+    }
+
+    @Test
+    public void stop() {
+        BrightnessSetting.BrightnessSettingListener brightnessSettingListener = mock(
+                BrightnessSetting.BrightnessSettingListener.class);
+        mDisplayBrightnessController.registerBrightnessSettingChangeListener(
+                brightnessSettingListener);
+        mDisplayBrightnessController.stop();
+        verify(mBrightnessSetting).unregisterListener(brightnessSettingListener);
+    }
+
+    @Test
+    public void testLoadNitBasedBrightnessSetting() {
+        // When the nits value is valid, the brightness is set from the old default display nits
+        // value
+        float nits = 200f;
+        float brightness = 0.3f;
+        AutomaticBrightnessController automaticBrightnessController =
+                mock(AutomaticBrightnessController.class);
+        when(automaticBrightnessController.convertToFloatScale(nits)).thenReturn(brightness);
+        when(mBrightnessSetting.getBrightnessNitsForDefaultDisplay()).thenReturn(nits);
+        mDisplayBrightnessController.setAutomaticBrightnessController(
+                automaticBrightnessController);
+        verify(mBrightnessSetting).setBrightness(brightness);
+        assertEquals(brightness, mDisplayBrightnessController.getCurrentBrightness(), 0.01f);
+        clearInvocations(automaticBrightnessController, mBrightnessSetting);
+
+        // When the nits value is invalid, the brightness is resumed from where it was last set
+        nits = -1;
+        brightness = 0.4f;
+        when(automaticBrightnessController.convertToFloatScale(nits)).thenReturn(brightness);
+        when(mBrightnessSetting.getBrightnessNitsForDefaultDisplay()).thenReturn(nits);
+        when(mBrightnessSetting.getBrightness()).thenReturn(brightness);
+        mDisplayBrightnessController.setAutomaticBrightnessController(
+                automaticBrightnessController);
+        verify(mBrightnessSetting, never()).setBrightness(brightness);
+        assertEquals(brightness, mDisplayBrightnessController.getCurrentBrightness(), 0.01f);
+        clearInvocations(automaticBrightnessController, mBrightnessSetting);
+
+        // When the display is a non-default display, the brightness is resumed from where it was
+        // last set
+        int nonDefaultDisplayId = 1;
+        mDisplayBrightnessController = new DisplayBrightnessController(mContext, mInjector,
+                nonDefaultDisplayId, DEFAULT_BRIGHTNESS, mBrightnessSetting,
+                mOnBrightnessChangeRunnable, mBrightnessChangeExecutor);
+        brightness = 0.5f;
+        when(mBrightnessSetting.getBrightness()).thenReturn(brightness);
+        mDisplayBrightnessController.setAutomaticBrightnessController(
+                automaticBrightnessController);
+        assertEquals(brightness, mDisplayBrightnessController.getCurrentBrightness(), 0.01f);
+        verifyZeroInteractions(automaticBrightnessController);
+        verify(mBrightnessSetting, never()).getBrightnessNitsForDefaultDisplay();
+        verify(mBrightnessSetting, never()).setBrightness(brightness);
+    }
+
+    @Test
+    public void testChangeBrightnessNitsWhenUserChanges() {
+        float brightnessValue1 = 0.3f;
+        float nits1 = 200f;
+        float brightnessValue2 = 0.5f;
+        float nits2 = 300f;
+        AutomaticBrightnessController automaticBrightnessController =
+                mock(AutomaticBrightnessController.class);
+        when(automaticBrightnessController.convertToNits(brightnessValue1)).thenReturn(nits1);
+        when(automaticBrightnessController.convertToNits(brightnessValue2)).thenReturn(nits2);
+        mDisplayBrightnessController.setAutomaticBrightnessController(
+                automaticBrightnessController);
+
+        mDisplayBrightnessController.setBrightness(brightnessValue1, 1 /* user-serial */);
+        verify(mBrightnessSetting).setUserSerial(1);
+        verify(mBrightnessSetting).setBrightness(brightnessValue1);
+        verify(mBrightnessSetting).setBrightnessNitsForDefaultDisplay(nits1);
+
+        mDisplayBrightnessController.setBrightness(brightnessValue2, 2 /* user-serial */);
+        verify(mBrightnessSetting).setUserSerial(2);
+        verify(mBrightnessSetting).setBrightness(brightnessValue2);
+        verify(mBrightnessSetting).setBrightnessNitsForDefaultDisplay(nits2);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
new file mode 100644
index 0000000..b652576
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.brightness.strategy;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.hardware.display.BrightnessConfiguration;
+import android.hardware.display.DisplayManagerInternal;
+import android.os.PowerManager;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.test.mock.MockContentResolver;
+import android.view.Display;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.internal.util.test.FakeSettingsProviderRule;
+import com.android.server.display.AutomaticBrightnessController;
+import com.android.server.display.brightness.BrightnessEvent;
+import com.android.server.display.brightness.BrightnessReason;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AutomaticBrightnessStrategyTest {
+    private static final int DISPLAY_ID = 0;
+    @Rule
+    public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
+
+    @Mock
+    private AutomaticBrightnessController mAutomaticBrightnessController;
+
+    private BrightnessConfiguration mBrightnessConfiguration;
+    private float mDefaultScreenAutoBrightnessAdjustment;
+    private Context mContext;
+    private AutomaticBrightnessStrategy mAutomaticBrightnessStrategy;
+
+    @Before
+    public void before() {
+        MockitoAnnotations.initMocks(this);
+        mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
+        final MockContentResolver resolver = mSettingsProviderRule.mockContentResolver(mContext);
+        when(mContext.getContentResolver()).thenReturn(resolver);
+        mDefaultScreenAutoBrightnessAdjustment = Settings.System.getFloat(
+                mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, Float.NaN);
+        Settings.System.putFloat(mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.5f);
+        mAutomaticBrightnessStrategy = new AutomaticBrightnessStrategy(mContext, DISPLAY_ID);
+
+        mBrightnessConfiguration = new BrightnessConfiguration.Builder(
+                new float[]{0f, 1f}, new float[]{0, PowerManager.BRIGHTNESS_ON}).build();
+        when(mAutomaticBrightnessController.hasUserDataPoints()).thenReturn(true);
+        mAutomaticBrightnessStrategy.setAutomaticBrightnessController(
+                mAutomaticBrightnessController);
+        mAutomaticBrightnessStrategy.setBrightnessConfiguration(mBrightnessConfiguration,
+                true);
+    }
+
+    @After
+    public void after() {
+        Settings.System.putFloat(mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, mDefaultScreenAutoBrightnessAdjustment);
+    }
+
+    @Test
+    public void testAutoBrightnessState_AutoBrightnessDisabled() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(false);
+        int targetDisplayState = Display.STATE_ON;
+        boolean allowAutoBrightnessWhileDozing = false;
+        int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
+        float lastUserSetBrightness = 0.2f;
+        boolean userSetBrightnessChanged = true;
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(true);
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+                userSetBrightnessChanged);
+        verify(mAutomaticBrightnessController)
+                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
+                        mBrightnessConfiguration,
+                        lastUserSetBrightness,
+                        userSetBrightnessChanged, 0.5f,
+                        false, policy, true);
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+    }
+
+    @Test
+    public void testAutoBrightnessState_DisplayIsOff() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+        int targetDisplayState = Display.STATE_OFF;
+        boolean allowAutoBrightnessWhileDozing = false;
+        int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF;
+        float lastUserSetBrightness = 0.2f;
+        boolean userSetBrightnessChanged = true;
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(true);
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+                userSetBrightnessChanged);
+        verify(mAutomaticBrightnessController)
+                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                        mBrightnessConfiguration,
+                        lastUserSetBrightness,
+                        userSetBrightnessChanged, 0.5f,
+                        false, policy, true);
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+        assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+    }
+
+    @Test
+    public void testAutoBrightnessState_DisplayIsInDoze_ConfigDoesNotAllow() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+        int targetDisplayState = Display.STATE_DOZE;
+        boolean allowAutoBrightnessWhileDozing = false;
+        int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+        float lastUserSetBrightness = 0.2f;
+        boolean userSetBrightnessChanged = true;
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(true);
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+                userSetBrightnessChanged);
+        verify(mAutomaticBrightnessController)
+                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                        mBrightnessConfiguration,
+                        lastUserSetBrightness,
+                        userSetBrightnessChanged, 0.5f,
+                        false, policy, true);
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+        assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+    }
+
+    @Test
+    public void testAutoBrightnessState_BrightnessReasonIsOverride() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+        int targetDisplayState = Display.STATE_ON;
+        boolean allowAutoBrightnessWhileDozing = false;
+        int brightnessReason = BrightnessReason.REASON_OVERRIDE;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
+        float lastUserSetBrightness = 0.2f;
+        boolean userSetBrightnessChanged = true;
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(true);
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+                userSetBrightnessChanged);
+        verify(mAutomaticBrightnessController)
+                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
+                        mBrightnessConfiguration,
+                        lastUserSetBrightness,
+                        userSetBrightnessChanged, 0.5f,
+                        false, policy, true);
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+    }
+
+    @Test
+    public void testAutoBrightnessState_DisplayIsInDoze_ConfigDoesAllow() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+        int targetDisplayState = Display.STATE_DOZE;
+        boolean allowAutoBrightnessWhileDozing = true;
+        int brightnessReason = BrightnessReason.REASON_DOZE;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+        float lastUserSetBrightness = 0.2f;
+        boolean userSetBrightnessChanged = true;
+        Settings.System.putFloat(mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.4f);
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(false);
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+                userSetBrightnessChanged);
+        verify(mAutomaticBrightnessController)
+                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED,
+                        mBrightnessConfiguration,
+                        lastUserSetBrightness,
+                        userSetBrightnessChanged, 0.4f,
+                        true, policy, true);
+        assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+    }
+
+    @Test
+    public void testAutoBrightnessState_DisplayIsOn() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+        int targetDisplayState = Display.STATE_ON;
+        boolean allowAutoBrightnessWhileDozing = false;
+        int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+        float lastUserSetBrightness = 0.2f;
+        boolean userSetBrightnessChanged = true;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
+        float pendingBrightnessAdjustment = 0.1f;
+        Settings.System.putFloat(mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingBrightnessAdjustment);
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(false);
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+                userSetBrightnessChanged);
+        verify(mAutomaticBrightnessController)
+                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED,
+                        mBrightnessConfiguration,
+                        lastUserSetBrightness,
+                        userSetBrightnessChanged, pendingBrightnessAdjustment,
+                        true, policy, true);
+        assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+    }
+
+    @Test
+    public void accommodateUserBrightnessChangesWorksAsExpected() {
+        // Verify the state if automaticBrightnessController is configured.
+        assertFalse(mAutomaticBrightnessStrategy.isShortTermModelActive());
+        boolean userSetBrightnessChanged = true;
+        float lastUserSetScreenBrightness = 0.2f;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
+        BrightnessConfiguration brightnessConfiguration = new BrightnessConfiguration.Builder(
+                new float[]{0f, 1f}, new float[]{0, PowerManager.BRIGHTNESS_ON}).build();
+        int autoBrightnessState = AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED;
+        float temporaryAutoBrightnessAdjustments = 0.4f;
+        mAutomaticBrightnessStrategy.setShouldResetShortTermModel(true);
+        setTemporaryAutoBrightnessAdjustment(temporaryAutoBrightnessAdjustments);
+        mAutomaticBrightnessStrategy.accommodateUserBrightnessChanges(userSetBrightnessChanged,
+                lastUserSetScreenBrightness, policy, brightnessConfiguration,
+                autoBrightnessState);
+        verify(mAutomaticBrightnessController).configure(autoBrightnessState,
+                brightnessConfiguration,
+                lastUserSetScreenBrightness,
+                userSetBrightnessChanged, temporaryAutoBrightnessAdjustments,
+                /* userChangedAutoBrightnessAdjustment= */ false, policy,
+                /* shouldResetShortTermModel= */ true);
+        assertTrue(mAutomaticBrightnessStrategy.isTemporaryAutoBrightnessAdjustmentApplied());
+        assertFalse(mAutomaticBrightnessStrategy.shouldResetShortTermModel());
+        assertTrue(mAutomaticBrightnessStrategy.isShortTermModelActive());
+        // Verify the state when automaticBrightnessController is not configured
+        setTemporaryAutoBrightnessAdjustment(Float.NaN);
+        mAutomaticBrightnessStrategy.setAutomaticBrightnessController(null);
+        mAutomaticBrightnessStrategy.setShouldResetShortTermModel(true);
+        mAutomaticBrightnessStrategy.accommodateUserBrightnessChanges(userSetBrightnessChanged,
+                lastUserSetScreenBrightness, policy, brightnessConfiguration,
+                autoBrightnessState);
+        assertFalse(mAutomaticBrightnessStrategy.isTemporaryAutoBrightnessAdjustmentApplied());
+        assertTrue(mAutomaticBrightnessStrategy.shouldResetShortTermModel());
+        assertFalse(mAutomaticBrightnessStrategy.isShortTermModelActive());
+    }
+
+    @Test
+    public void adjustAutomaticBrightnessStateIfValid() throws Settings.SettingNotFoundException {
+        float brightnessState = 0.3f;
+        float autoBrightnessAdjustment = 0.2f;
+        when(mAutomaticBrightnessController.getAutomaticScreenBrightnessAdjustment()).thenReturn(
+                autoBrightnessAdjustment);
+        mAutomaticBrightnessStrategy.adjustAutomaticBrightnessStateIfValid(brightnessState);
+        assertEquals(autoBrightnessAdjustment,
+                mAutomaticBrightnessStrategy.getAutoBrightnessAdjustment(), 0.0f);
+        assertEquals(autoBrightnessAdjustment, Settings.System.getFloatForUser(
+                mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ,
+                UserHandle.USER_CURRENT), 0.0f);
+        assertEquals(BrightnessReason.ADJUSTMENT_AUTO,
+                mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags());
+        float invalidBrightness = -0.5f;
+        mAutomaticBrightnessStrategy
+                .adjustAutomaticBrightnessStateIfValid(invalidBrightness);
+        assertEquals(autoBrightnessAdjustment,
+                mAutomaticBrightnessStrategy.getAutoBrightnessAdjustment(), 0.0f);
+        assertEquals(0,
+                mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags());
+    }
+
+    @Test
+    public void updatePendingAutoBrightnessAdjustments() {
+        // Verify the state when the pendingAutoBrightnessAdjustments are not present
+        setPendingAutoBrightnessAdjustment(Float.NaN);
+        assertFalse(mAutomaticBrightnessStrategy.processPendingAutoBrightnessAdjustments());
+        assertFalse(mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged());
+        // Verify the state when the pendingAutoBrightnessAdjustments are present, but
+        // pendingAutoBrightnessAdjustments and autoBrightnessAdjustments are the same
+        float autoBrightnessAdjustment = 0.3f;
+        setPendingAutoBrightnessAdjustment(autoBrightnessAdjustment);
+        setAutoBrightnessAdjustment(autoBrightnessAdjustment);
+        assertFalse(mAutomaticBrightnessStrategy.processPendingAutoBrightnessAdjustments());
+        assertFalse(mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged());
+        assertEquals(Float.NaN, mAutomaticBrightnessStrategy.getPendingAutoBrightnessAdjustment(),
+                0.0f);
+        // Verify the state when the pendingAutoBrightnessAdjustments are present, and
+        // pendingAutoBrightnessAdjustments and autoBrightnessAdjustments are not the same
+        float pendingAutoBrightnessAdjustment = 0.2f;
+        setPendingAutoBrightnessAdjustment(pendingAutoBrightnessAdjustment);
+        setTemporaryAutoBrightnessAdjustment(0.1f);
+        assertTrue(mAutomaticBrightnessStrategy.processPendingAutoBrightnessAdjustments());
+        assertTrue(mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged());
+        assertEquals(pendingAutoBrightnessAdjustment,
+                mAutomaticBrightnessStrategy.getAutoBrightnessAdjustment(), 0.0f);
+        assertEquals(Float.NaN, mAutomaticBrightnessStrategy.getPendingAutoBrightnessAdjustment(),
+                0.0f);
+        assertEquals(Float.NaN, mAutomaticBrightnessStrategy.getTemporaryAutoBrightnessAdjustment(),
+                0.0f);
+    }
+
+    @Test
+    public void setAutomaticBrightnessWorksAsExpected() {
+        float automaticScreenBrightness = 0.3f;
+        AutomaticBrightnessController automaticBrightnessController = mock(
+                AutomaticBrightnessController.class);
+        when(automaticBrightnessController.getAutomaticScreenBrightness(any(BrightnessEvent.class)))
+                .thenReturn(automaticScreenBrightness);
+        mAutomaticBrightnessStrategy.setAutomaticBrightnessController(
+                automaticBrightnessController);
+        assertEquals(automaticScreenBrightness,
+                mAutomaticBrightnessStrategy.getAutomaticScreenBrightness(
+                        new BrightnessEvent(DISPLAY_ID)), 0.0f);
+    }
+
+    @Test
+    public void shouldUseAutoBrightness() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+        assertTrue(mAutomaticBrightnessStrategy.shouldUseAutoBrightness());
+    }
+
+    @Test
+    public void setPendingAutoBrightnessAdjustments() throws Settings.SettingNotFoundException {
+        float pendingAutoBrightnessAdjustments = 0.3f;
+        setPendingAutoBrightnessAdjustment(pendingAutoBrightnessAdjustments);
+        assertEquals(pendingAutoBrightnessAdjustments,
+                mAutomaticBrightnessStrategy.getPendingAutoBrightnessAdjustment(), 0.0f);
+        assertEquals(pendingAutoBrightnessAdjustments, Settings.System.getFloatForUser(
+                mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ,
+                UserHandle.USER_CURRENT), 0.0f);
+    }
+
+    @Test
+    public void setTemporaryAutoBrightnessAdjustment() {
+        float temporaryAutoBrightnessAdjustment = 0.3f;
+        mAutomaticBrightnessStrategy.setTemporaryAutoBrightnessAdjustment(
+                temporaryAutoBrightnessAdjustment);
+        assertEquals(temporaryAutoBrightnessAdjustment,
+                mAutomaticBrightnessStrategy.getTemporaryAutoBrightnessAdjustment(), 0.0f);
+    }
+
+    @Test
+    public void setAutoBrightnessApplied() {
+        mAutomaticBrightnessStrategy.setAutoBrightnessApplied(true);
+        assertTrue(mAutomaticBrightnessStrategy.hasAppliedAutoBrightness());
+    }
+
+    @Test
+    public void testVerifyNoAutoBrightnessAdjustmentsArePopulatedForNonDefaultDisplay() {
+        int newDisplayId = 1;
+        mAutomaticBrightnessStrategy = new AutomaticBrightnessStrategy(mContext, newDisplayId);
+        mAutomaticBrightnessStrategy.putAutoBrightnessAdjustmentSetting(0.3f);
+        assertEquals(0.5f, mAutomaticBrightnessStrategy.getAutoBrightnessAdjustment(),
+                0.0f);
+    }
+
+    private void setPendingAutoBrightnessAdjustment(float pendingAutoBrightnessAdjustment) {
+        Settings.System.putFloat(mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingAutoBrightnessAdjustment);
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(false);
+    }
+
+    private void setTemporaryAutoBrightnessAdjustment(float temporaryAutoBrightnessAdjustment) {
+        mAutomaticBrightnessStrategy.setTemporaryAutoBrightnessAdjustment(
+                temporaryAutoBrightnessAdjustment);
+    }
+
+    private void setAutoBrightnessAdjustment(float autoBrightnessAdjustment) {
+        mAutomaticBrightnessStrategy.putAutoBrightnessAdjustmentSetting(autoBrightnessAdjustment);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/strategy/DozeBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/DozeBrightnessStrategyTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/brightness/strategy/DozeBrightnessStrategyTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/DozeBrightnessStrategyTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/strategy/OverrideBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OverrideBrightnessStrategyTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/brightness/strategy/OverrideBrightnessStrategyTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OverrideBrightnessStrategyTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategyTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategyTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategyTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/color/AppSaturationControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/color/AppSaturationControllerTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/color/AppSaturationControllerTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/color/AppSaturationControllerTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/color/CctEvaluatorTest.java b/services/tests/displayservicetests/src/com/android/server/display/color/CctEvaluatorTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/color/CctEvaluatorTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/color/CctEvaluatorTest.java
diff --git a/services/tests/displayservicetests/src/com/android/server/display/color/ColorDisplayServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/color/ColorDisplayServiceTest.java
new file mode 100644
index 0000000..c7c09b5
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/color/ColorDisplayServiceTest.java
@@ -0,0 +1,1406 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.color;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.app.AlarmManager;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.Resources;
+import android.hardware.display.ColorDisplayManager;
+import android.hardware.display.DisplayManagerInternal;
+import android.hardware.display.Time;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.provider.Settings.Secure;
+import android.provider.Settings.System;
+import android.test.mock.MockContentResolver;
+import android.view.Display;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.twilight.TwilightListener;
+import com.android.server.twilight.TwilightManager;
+import com.android.server.twilight.TwilightState;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class ColorDisplayServiceTest {
+
+    private Context mContext;
+    private int mUserId;
+
+    private MockTwilightManager mTwilightManager;
+    private DisplayTransformManager mDisplayTransformManager;
+    private DisplayManagerInternal mDisplayManagerInternal;
+
+    private ColorDisplayService mCds;
+    private ColorDisplayService.BinderService mBinderService;
+
+    private Resources mResourcesSpy;
+
+    private static final int[] MINIMAL_COLOR_MODES = new int[] {
+        ColorDisplayManager.COLOR_MODE_NATURAL,
+        ColorDisplayManager.COLOR_MODE_BOOSTED,
+    };
+
+    @Before
+    public void setUp() {
+        mContext = Mockito.spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
+        doReturn(mContext).when(mContext).getApplicationContext();
+
+        final Resources res = Mockito.spy(mContext.getResources());
+        doReturn(MINIMAL_COLOR_MODES).when(res).getIntArray(R.array.config_availableColorModes);
+        doReturn(true).when(res).getBoolean(R.bool.config_nightDisplayAvailable);
+        doReturn(true).when(res).getBoolean(R.bool.config_displayWhiteBalanceAvailable);
+        when(mContext.getResources()).thenReturn(res);
+        mResourcesSpy = res;
+
+        mUserId = ActivityManager.getCurrentUser();
+
+        final MockContentResolver cr = new MockContentResolver(mContext);
+        cr.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+        doReturn(cr).when(mContext).getContentResolver();
+
+        final AlarmManager am = Mockito.mock(AlarmManager.class);
+        doReturn(am).when(mContext).getSystemService(Context.ALARM_SERVICE);
+
+        mTwilightManager = new MockTwilightManager();
+        LocalServices.addService(TwilightManager.class, mTwilightManager);
+
+        mDisplayTransformManager = Mockito.mock(DisplayTransformManager.class);
+        doReturn(true).when(mDisplayTransformManager).needsLinearColorMatrix();
+        LocalServices.addService(DisplayTransformManager.class, mDisplayTransformManager);
+
+        mDisplayManagerInternal = Mockito.mock(DisplayManagerInternal.class);
+        LocalServices.removeServiceForTest(DisplayManagerInternal.class);
+        LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternal);
+
+        mCds = new ColorDisplayService(mContext);
+        mBinderService = mCds.new BinderService();
+        LocalServices.addService(ColorDisplayService.ColorDisplayServiceInternal.class,
+                mCds.new ColorDisplayServiceInternal());
+    }
+
+    @After
+    public void tearDown() {
+        /*
+         * Wait for internal {@link Handler} to finish processing pending messages, so that test
+         * code can safelyremove {@link DisplayTransformManager} mock from {@link LocalServices}.
+         */
+        mCds.mHandler.runWithScissors(() -> { /* nop */ }, /* timeout */ 1000);
+        mCds = null;
+
+        LocalServices.removeServiceForTest(TwilightManager.class);
+        mTwilightManager = null;
+
+        LocalServices.removeServiceForTest(DisplayTransformManager.class);
+
+        mUserId = UserHandle.USER_NULL;
+        mContext = null;
+
+        FakeSettingsProvider.clearSettingsProvider();
+
+        LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class);
+        LocalServices.removeServiceForTest(DisplayManagerInternal.class);
+    }
+
+    @Test
+    public void customSchedule_whenStartedAfterNight_ifOffAfterNight_turnsOff() {
+        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
+        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedAfterNight_ifOffBeforeNight_turnsOff() {
+        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
+        setNightDisplayActivated(false /* activated */, -180 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedAfterNight_ifOffDuringNight_turnsOff() {
+        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
+        setNightDisplayActivated(false /* activated */, -90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedAfterNight_ifOffInFuture_turnsOff() {
+        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
+        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedAfterNight_ifOnAfterNight_turnsOn() {
+        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
+        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedAfterNight_ifOnBeforeNight_turnsOff() {
+        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
+        setNightDisplayActivated(true /* activated */, -180 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedAfterNight_ifOnDuringNight_turnsOff() {
+        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
+        setNightDisplayActivated(true /* activated */, -90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedAfterNight_ifOnInFuture_turnsOff() {
+        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
+        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedBeforeNight_ifOffAfterNight_turnsOff() {
+        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
+        setNightDisplayActivated(false /* activated */, 180 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedBeforeNight_ifOffBeforeNight_turnsOff() {
+        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
+        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedBeforeNight_ifOffDuringNight_turnsOff() {
+        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
+        setNightDisplayActivated(false /* activated */, 90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedBeforeNight_ifOffInPast_turnsOff() {
+        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
+        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedBeforeNight_ifOnAfterNight_turnsOff() {
+        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
+        setNightDisplayActivated(true /* activated */, 180 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedBeforeNight_ifOnBeforeNight_turnsOff() {
+        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
+        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedBeforeNight_ifOnDuringNight_turnsOff() {
+        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
+        setNightDisplayActivated(true /* activated */, 90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedBeforeNight_ifOnInPast_turnsOn() {
+        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
+        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedDuringNight_ifOffAfterNight_turnsOn() {
+        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
+        setNightDisplayActivated(false /* activated */, 90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedDuringNight_ifOffBeforeNight_turnsOn() {
+        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
+        setNightDisplayActivated(false /* activated */, -90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedDuringNight_ifOffDuringNightInFuture_turnsOn() {
+        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
+        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedDuringNight_ifOffDuringNightInPast_turnsOff() {
+        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
+        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedDuringNight_ifOnAfterNight_turnsOn() {
+        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
+        setNightDisplayActivated(true /* activated */, 90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedDuringNight_ifOnBeforeNight_turnsOn() {
+        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
+        setNightDisplayActivated(true /* activated */, -90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedDuringNight_ifOnDuringNightInFuture_turnsOn() {
+        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
+        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void customSchedule_whenStartedDuringNight_ifOnDuringNightInPast_turnsOn() {
+        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
+        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedAfterNight_ifOffAfterNight_turnsOff() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedAfterNight_ifOffBeforeNight_turnsOff() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, -180 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedAfterNight_ifOffDuringNight_turnsOff() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, -90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedAfterNight_ifOffInFuture_turnsOff() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedAfterNight_ifOnAfterNight_turnsOn() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedAfterNight_ifOnBeforeNight_turnsOff() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, -180 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedAfterNight_ifOnDuringNight_turnsOff() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, -90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedAfterNight_ifOnInFuture_turnsOff() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedBeforeNight_ifOffAfterNight_turnsOff() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, 180 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedBeforeNight_ifOffBeforeNight_turnsOff() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedBeforeNight_ifOffDuringNight_turnsOff() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, 90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedBeforeNight_ifOffInPast_turnsOff() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedBeforeNight_ifOnAfterNight_turnsOff() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, 180 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedBeforeNight_ifOnBeforeNight_turnsOff() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedBeforeNight_ifOnDuringNight_turnsOff() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, 90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedBeforeNight_ifOnInPast_turnsOn() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedDuringNight_ifOffAfterNight_turnsOn() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, 90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedDuringNight_ifOffBeforeNight_turnsOn() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, -90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedDuringNight_ifOffDuringNightInFuture_turnsOn() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedDuringNight_ifOffDuringNightInPast_turnsOff() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedDuringNight_ifOnAfterNight_turnsOn() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, 90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedDuringNight_ifOnBeforeNight_turnsOn() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, -90 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedDuringNight_ifOnDuringNightInFuture_turnsOn() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenStartedDuringNight_ifOnDuringNightInPast_turnsOn() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        startService();
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedAfterNight_ifOffAfterNight_turnsOff() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(false /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedAfterNight_ifOffBeforeNight_turnsOff() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, -180 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(false /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedAfterNight_ifOffDuringNight_turnsOff() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, -90 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(false /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedAfterNight_ifOffInFuture_turnsOff() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(false /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedAfterNight_ifOnAfterNight_turnsOn() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(true /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedAfterNight_ifOnBeforeNight_turnsOff() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, -180 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(true /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedAfterNight_ifOnDuringNight_turnsOff() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, -90 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(true /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedAfterNight_ifOnInFuture_turnsOff() {
+        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(true /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedBeforeNight_ifOffAfterNight_turnsOff() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, 180 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(false /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedBeforeNight_ifOffBeforeNight_turnsOff() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(false /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedBeforeNight_ifOffDuringNight_turnsOff() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, 90 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(false /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedBeforeNight_ifOffInPast_turnsOff() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(false /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedBeforeNight_ifOnAfterNight_turnsOff() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, 180 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(true /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedBeforeNight_ifOnBeforeNight_turnsOff() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(true /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedBeforeNight_ifOnDuringNight_turnsOff() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, 90 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(true /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedBeforeNight_ifOnInPast_turnsOn() {
+        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(true /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedDuringNight_ifOffAfterNight_turnsOn() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, 90 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(false /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedDuringNight_ifOffBeforeNight_turnsOn() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, -90 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(false /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedDuringNight_ifOffDuringNightInFuture_turnsOn() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(false /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedDuringNight_ifOffDuringNightInPast_turnsOff() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(false /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(false /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedDuringNight_ifOnAfterNight_turnsOn() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, 90 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(true /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedDuringNight_ifOnBeforeNight_turnsOn() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, -90 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(true /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedDuringNight_ifOnDuringNightInFuture_turnsOn() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(true /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void twilightSchedule_whenRebootedDuringNight_ifOnDuringNightInPast_turnsOn() {
+        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
+        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        final TwilightState state = mTwilightManager.getLastTwilightState();
+        mTwilightManager.setTwilightState(null);
+
+        startService();
+        assertActivated(true /* activated */);
+
+        mTwilightManager.setTwilightState(state);
+        assertActivated(true /* activated */);
+    }
+
+    @Test
+    public void accessibility_colorInversion_transformActivated() {
+        if (!mContext.getResources().getConfiguration().isScreenWideColorGamut()) {
+            return;
+        }
+
+        setAccessibilityColorInversion(true);
+        setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
+
+        startService();
+        assertUserColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
+        assertActiveColorMode(mContext.getResources().getInteger(
+                R.integer.config_accessibilityColorMode));
+    }
+
+    @Test
+    public void accessibility_colorCorrection_transformActivated() {
+        if (!mContext.getResources().getConfiguration().isScreenWideColorGamut()) {
+            return;
+        }
+
+        setAccessibilityColorCorrection(true);
+        setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
+
+        startService();
+        assertUserColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
+        assertActiveColorMode(mContext.getResources().getInteger(
+                R.integer.config_accessibilityColorMode));
+    }
+
+    @Test
+    public void accessibility_all_transformActivated() {
+        if (!mContext.getResources().getConfiguration().isScreenWideColorGamut()) {
+            return;
+        }
+
+        setAccessibilityColorCorrection(true);
+        setAccessibilityColorInversion(true);
+        setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
+
+        startService();
+        assertUserColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
+        assertActiveColorMode(mContext.getResources().getInteger(
+                R.integer.config_accessibilityColorMode));
+    }
+
+    @Test
+    public void accessibility_none_transformActivated() {
+        if (!mContext.getResources().getConfiguration().isScreenWideColorGamut()) {
+            return;
+        }
+
+        setAccessibilityColorCorrection(false);
+        setAccessibilityColorInversion(false);
+        setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
+
+        startService();
+        assertUserColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
+        assertActiveColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
+    }
+
+    @Test
+    public void displayWhiteBalance_enabled() {
+        setDisplayWhiteBalanceEnabled(true);
+        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
+        mBinderService.setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
+        startService();
+        assertDwbActive(true);
+    }
+
+    @Test
+    public void displayWhiteBalance_disabledAfterNightDisplayEnabled() {
+        setDisplayWhiteBalanceEnabled(true);
+        startService();
+        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        /* Since we are using FakeSettingsProvider which could not trigger observer change,
+         * force an update here.*/
+        mCds.updateDisplayWhiteBalanceStatus();
+        assertDwbActive(false);
+    }
+
+    @Test
+    public void displayWhiteBalance_enabledAfterNightDisplayDisabled() {
+        setDisplayWhiteBalanceEnabled(true);
+        startService();
+        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
+
+        mCds.updateDisplayWhiteBalanceStatus();
+        assertDwbActive(false);
+
+        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
+        mCds.updateDisplayWhiteBalanceStatus();
+        assertDwbActive(true);
+    }
+
+    @Test
+    public void displayWhiteBalance_enabledAfterLinearColorModeSelected() {
+        if (!isColorModeValid(ColorDisplayManager.COLOR_MODE_SATURATED)) {
+            return;
+        }
+        setDisplayWhiteBalanceEnabled(true);
+        mBinderService.setColorMode(ColorDisplayManager.COLOR_MODE_SATURATED);
+        startService();
+        assertDwbActive(false);
+
+        mBinderService.setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
+        mCds.updateDisplayWhiteBalanceStatus();
+        assertDwbActive(true);
+    }
+
+    @Test
+    public void displayWhiteBalance_disabledWhileAccessibilityColorCorrectionEnabled() {
+        setDisplayWhiteBalanceEnabled(true);
+        setAccessibilityColorCorrection(true);
+        startService();
+        assertDwbActive(false);
+
+        setAccessibilityColorCorrection(false);
+        mCds.updateDisplayWhiteBalanceStatus();
+        assertDwbActive(true);
+    }
+
+    @Test
+    public void displayWhiteBalance_disabledWhileAccessibilityColorInversionEnabled() {
+        setDisplayWhiteBalanceEnabled(true);
+        setAccessibilityColorInversion(true);
+        startService();
+        assertDwbActive(false);
+
+        setAccessibilityColorInversion(false);
+        mCds.updateDisplayWhiteBalanceStatus();
+        assertDwbActive(true);
+    }
+
+    @Test
+    public void compositionColorSpaces_noResources() {
+        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorModes))
+            .thenReturn(new int[] {});
+        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorSpaces))
+            .thenReturn(new int[] {});
+        setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
+        startService();
+        verify(mDisplayTransformManager).setColorMode(
+                eq(ColorDisplayManager.COLOR_MODE_NATURAL), any(), eq(Display.COLOR_MODE_INVALID));
+    }
+
+    @Test
+    public void compositionColorSpaces_invalidResources() {
+        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorModes))
+                .thenReturn(new int[] {
+                        ColorDisplayManager.COLOR_MODE_NATURAL,
+                        // Missing second color mode
+                });
+        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorSpaces))
+                .thenReturn(new int[] {
+                        Display.COLOR_MODE_SRGB,
+                        Display.COLOR_MODE_DISPLAY_P3
+                });
+        setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
+        startService();
+        verify(mDisplayTransformManager).setColorMode(
+                eq(ColorDisplayManager.COLOR_MODE_NATURAL), any(), eq(Display.COLOR_MODE_INVALID));
+    }
+
+    @Test
+    public void compositionColorSpaces_validResources_validColorMode() {
+        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorModes))
+                .thenReturn(new int[] {
+                        ColorDisplayManager.COLOR_MODE_NATURAL
+                });
+        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorSpaces))
+                .thenReturn(new int[] {
+                        Display.COLOR_MODE_SRGB,
+                });
+        setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
+        startService();
+        verify(mDisplayTransformManager).setColorMode(
+                eq(ColorDisplayManager.COLOR_MODE_NATURAL), any(), eq(Display.COLOR_MODE_SRGB));
+    }
+
+    @Test
+    public void compositionColorSpaces_validResources_invalidColorMode() {
+        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorModes))
+                .thenReturn(new int[] {
+                        ColorDisplayManager.COLOR_MODE_NATURAL
+                });
+        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorSpaces))
+                .thenReturn(new int[] {
+                        Display.COLOR_MODE_SRGB,
+                });
+        setColorMode(ColorDisplayManager.COLOR_MODE_BOOSTED);
+        startService();
+        verify(mDisplayTransformManager).setColorMode(
+                eq(ColorDisplayManager.COLOR_MODE_BOOSTED), any(), eq(Display.COLOR_MODE_INVALID));
+    }
+
+    @Test
+    public void getColorMode_noAvailableModes_returnsNotSet() {
+        when(mResourcesSpy.getIntArray(R.array.config_availableColorModes))
+                    .thenReturn(new int[] {});
+        startService();
+        verify(mDisplayTransformManager, never()).setColorMode(anyInt(), any(), anyInt());
+        assertThat(mBinderService.getColorMode()).isEqualTo(-1);
+    }
+
+    /**
+     * Configures Night display to use a custom schedule.
+     *
+     * @param startTimeOffset the offset relative to now to activate Night display (in minutes)
+     * @param endTimeOffset the offset relative to now to deactivate Night display (in minutes)
+     */
+    private void setAutoModeCustom(int startTimeOffset, int endTimeOffset) {
+        mBinderService.setNightDisplayAutoMode(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME);
+        mBinderService.setNightDisplayCustomStartTime(
+                new Time(getLocalTimeRelativeToNow(startTimeOffset)));
+        mBinderService
+                .setNightDisplayCustomEndTime(new Time(getLocalTimeRelativeToNow(endTimeOffset)));
+    }
+
+    /**
+     * Configures Night display to use the twilight schedule.
+     *
+     * @param sunsetOffset the offset relative to now for sunset (in minutes)
+     * @param sunriseOffset the offset relative to now for sunrise (in minutes)
+     */
+    private void setAutoModeTwilight(int sunsetOffset, int sunriseOffset) {
+        mBinderService.setNightDisplayAutoMode(ColorDisplayManager.AUTO_MODE_TWILIGHT);
+        mTwilightManager.setTwilightState(
+                getTwilightStateRelativeToNow(sunsetOffset, sunriseOffset));
+    }
+
+    /**
+     * Configures the Night display activated state.
+     *
+     * @param activated {@code true} if Night display should be activated
+     * @param lastActivatedTimeOffset the offset relative to now to record that Night display was
+     * activated (in minutes)
+     */
+    private void setNightDisplayActivated(boolean activated, int lastActivatedTimeOffset) {
+        mBinderService.setNightDisplayActivated(activated);
+        Secure.putStringForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_LAST_ACTIVATED_TIME,
+                LocalDateTime.now().plusMinutes(lastActivatedTimeOffset).toString(),
+                mUserId);
+    }
+
+    /**
+     * Configures the Accessibility color correction setting state.
+     *
+     * @param state {@code true} if color inversion should be activated
+     */
+    private void setAccessibilityColorCorrection(boolean state) {
+        Secure.putIntForUser(mContext.getContentResolver(),
+                Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, state ? 1 : 0, mUserId);
+    }
+
+    /**
+     * Configures the Accessibility color inversion setting state.
+     *
+     * @param state {@code true} if color inversion should be activated
+     */
+    private void setAccessibilityColorInversion(boolean state) {
+        Secure.putIntForUser(mContext.getContentResolver(),
+                Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, state ? 1 : 0, mUserId);
+    }
+
+    /**
+     * Configures the Display White Balance setting state.
+     *
+     * @param enabled {@code true} if display white balance should be enabled
+     */
+    private void setDisplayWhiteBalanceEnabled(boolean enabled) {
+        Secure.putIntForUser(mContext.getContentResolver(),
+                Secure.DISPLAY_WHITE_BALANCE_ENABLED, enabled ? 1 : 0, mUserId);
+    }
+
+    /**
+     * Configures color mode.
+     */
+    private void setColorMode(int colorMode) {
+        mBinderService.setColorMode(colorMode);
+    }
+
+    /**
+     * Returns whether the color mode is valid on the device the tests are running on.
+     */
+    private boolean isColorModeValid(int mode) {
+        final int[] availableColorModes = mContext.getResources().getIntArray(
+                R.array.config_availableColorModes);
+        if (availableColorModes != null) {
+            for (int availableMode : availableColorModes) {
+                if (mode == availableMode) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Convenience method to start {@link #mCds}.
+     */
+    private void startService() {
+        Secure.putIntForUser(mContext.getContentResolver(), Secure.USER_SETUP_COMPLETE, 1, mUserId);
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            mCds.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+            mCds.onUserChanged(mUserId);
+        });
+    }
+
+    /**
+     * Convenience method for asserting whether Night display should be activated.
+     *
+     * @param activated the expected activated state of Night display
+     */
+    private void assertActivated(boolean activated) {
+        assertWithMessage("Incorrect Night display activated state")
+                .that(mBinderService.isNightDisplayActivated())
+                .isEqualTo(activated);
+    }
+
+    /**
+     * Convenience method for asserting that the active color mode matches expectation.
+     *
+     * @param mode the expected active color mode.
+     */
+    private void assertActiveColorMode(int mode) {
+        assertWithMessage("Unexpected color mode setting")
+                .that(mBinderService.getColorMode())
+                .isEqualTo(mode);
+    }
+
+    /**
+     * Convenience method for asserting that the user chosen color mode matches expectation.
+     *
+     * @param mode the expected color mode setting.
+     */
+    private void assertUserColorMode(int mode) {
+        final int actualMode = System.getIntForUser(mContext.getContentResolver(),
+                System.DISPLAY_COLOR_MODE, -1, mUserId);
+        assertWithMessage("Unexpected color mode setting")
+                .that(actualMode)
+                .isEqualTo(mode);
+    }
+
+    /**
+     * Convenience method for asserting that the DWB active status matches expectation.
+     *
+     * @param enabled the expected active status.
+     */
+    private void assertDwbActive(boolean enabled) {
+        assertWithMessage("Incorrect Display White Balance state")
+                .that(mCds.mDisplayWhiteBalanceTintController.isActivated())
+                .isEqualTo(enabled);
+    }
+
+    /**
+     * Convenience for making a {@link LocalTime} instance with an offset relative to now.
+     *
+     * @param offsetMinutes the offset relative to now (in minutes)
+     * @return the LocalTime instance
+     */
+    private static LocalTime getLocalTimeRelativeToNow(int offsetMinutes) {
+        final Calendar c = Calendar.getInstance();
+        c.add(Calendar.MINUTE, offsetMinutes);
+        return LocalTime.of(c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE));
+    }
+
+    /**
+     * Convenience for making a {@link TwilightState} instance with sunrise/sunset relative to now.
+     *
+     * @param sunsetOffset the offset relative to now for sunset (in minutes)
+     * @param sunriseOffset the offset relative to now for sunrise (in minutes)
+     * @return the TwilightState instance
+     */
+    private static TwilightState getTwilightStateRelativeToNow(int sunsetOffset,
+            int sunriseOffset) {
+        final LocalTime sunset = getLocalTimeRelativeToNow(sunsetOffset);
+        final LocalTime sunrise = getLocalTimeRelativeToNow(sunriseOffset);
+
+        final LocalDateTime now = LocalDateTime.now();
+        final ZoneId zoneId = ZoneId.systemDefault();
+
+        long sunsetMillis = ColorDisplayService.getDateTimeBefore(sunset, now)
+                .atZone(zoneId)
+                .toInstant()
+                .toEpochMilli();
+        long sunriseMillis = ColorDisplayService.getDateTimeBefore(sunrise, now)
+                .atZone(zoneId)
+                .toInstant()
+                .toEpochMilli();
+        if (sunsetMillis < sunriseMillis) {
+            sunsetMillis = ColorDisplayService.getDateTimeAfter(sunset, now)
+                    .atZone(zoneId)
+                    .toInstant()
+                    .toEpochMilli();
+        } else {
+            sunriseMillis = ColorDisplayService.getDateTimeAfter(sunrise, now)
+                    .atZone(zoneId)
+                    .toInstant()
+                    .toEpochMilli();
+        }
+
+        return new TwilightState(sunriseMillis, sunsetMillis);
+    }
+
+    private static class MockTwilightManager implements TwilightManager {
+
+        private final Map<TwilightListener, Handler> mListeners = new HashMap<>();
+        private TwilightState mTwilightState;
+
+        /**
+         * Updates the TwilightState and notifies any registered listeners.
+         *
+         * @param state the new TwilightState to use
+         */
+        void setTwilightState(TwilightState state) {
+            synchronized (mListeners) {
+                mTwilightState = state;
+
+                final CountDownLatch latch = new CountDownLatch(mListeners.size());
+                for (Map.Entry<TwilightListener, Handler> entry : mListeners.entrySet()) {
+                    entry.getValue().post(new Runnable() {
+                        @Override
+                        public void run() {
+                            entry.getKey().onTwilightStateChanged(state);
+                            latch.countDown();
+                        }
+                    });
+                }
+
+                try {
+                    latch.await(5, TimeUnit.SECONDS);
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        }
+
+        @Override
+        public void registerListener(@NonNull TwilightListener listener, @NonNull Handler handler) {
+            synchronized (mListeners) {
+                mListeners.put(listener, handler);
+            }
+        }
+
+        @Override
+        public void unregisterListener(@NonNull TwilightListener listener) {
+            synchronized (mListeners) {
+                mListeners.remove(listener);
+            }
+        }
+
+        @Override
+        public TwilightState getLastTwilightState() {
+            return mTwilightState;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/color/GlobalSaturationTintControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/color/GlobalSaturationTintControllerTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/color/GlobalSaturationTintControllerTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/color/GlobalSaturationTintControllerTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/color/ReduceBrightColorsTintControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/color/ReduceBrightColorsTintControllerTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/color/ReduceBrightColorsTintControllerTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/color/ReduceBrightColorsTintControllerTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/mode/VotesStorageTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/VotesStorageTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/mode/VotesStorageTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/mode/VotesStorageTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/utils/AmbientFilterTest.java b/services/tests/displayservicetests/src/com/android/server/display/utils/AmbientFilterTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/utils/AmbientFilterTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/utils/AmbientFilterTest.java
diff --git a/services/tests/servicestests/src/com/android/server/display/whitebalance/AmbientFilterStubber.java b/services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientFilterStubber.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/whitebalance/AmbientFilterStubber.java
rename to services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientFilterStubber.java
diff --git a/services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientLuxTest.java b/services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientLuxTest.java
new file mode 100644
index 0000000..f975b6f
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientLuxTest.java
@@ -0,0 +1,559 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.whitebalance;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+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.content.ContextWrapper;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.TypedValue;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.R;
+import com.android.server.LocalServices;
+import com.android.server.display.TestUtils;
+import com.android.server.display.color.ColorDisplayService;
+import com.android.server.display.utils.AmbientFilter;
+import com.android.server.display.utils.AmbientFilterStubber;
+
+import com.google.common.collect.ImmutableList;
+
+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;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public final class AmbientLuxTest {
+
+    private static final float ALLOWED_ERROR_DELTA = 0.001f;
+    private static final int AMBIENT_COLOR_TYPE = 20705;
+    private static final String AMBIENT_COLOR_TYPE_STR = "colorSensoryDensoryDoc";
+    private static final float LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE = 5432.1f;
+    private static final float HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE = 3456.7f;
+
+    private Handler mHandler = new Handler(Looper.getMainLooper());
+    private Sensor mLightSensor;
+    private Sensor mAmbientColorSensor;
+    private ContextWrapper mContextSpy;
+    private Resources mResourcesSpy;
+
+    @Mock private SensorManager mSensorManagerMock;
+
+    @Mock private TypedArray mBrightnesses;
+    @Mock private TypedArray mBiases;
+    @Mock private TypedArray mHighLightBrightnesses;
+    @Mock private TypedArray mHighLightBiases;
+    @Mock private TypedArray mAmbientColorTemperatures;
+    @Mock private TypedArray mDisplayColorTemperatures;
+    @Mock private TypedArray mStrongAmbientColorTemperatures;
+    @Mock private TypedArray mStrongDisplayColorTemperatures;
+    @Mock private ColorDisplayService.ColorDisplayServiceInternal mColorDisplayServiceInternalMock;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mLightSensor = TestUtils.createSensor(Sensor.TYPE_LIGHT, null);
+        mAmbientColorSensor = TestUtils.createSensor(AMBIENT_COLOR_TYPE, AMBIENT_COLOR_TYPE_STR);
+        mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
+        mResourcesSpy = spy(mContextSpy.getResources());
+        when(mContextSpy.getResources()).thenReturn(mResourcesSpy);
+        when(mSensorManagerMock.getDefaultSensor(Sensor.TYPE_LIGHT)).thenReturn(mLightSensor);
+        final List<Sensor> sensorList = ImmutableList.of(mLightSensor, mAmbientColorSensor);
+        when(mSensorManagerMock.getSensorList(Sensor.TYPE_ALL)).thenReturn(sensorList);
+        when(mResourcesSpy.getString(
+                R.string.config_displayWhiteBalanceColorTemperatureSensorName))
+                .thenReturn(AMBIENT_COLOR_TYPE_STR);
+        when(mResourcesSpy.getInteger(
+                R.integer.config_displayWhiteBalanceDecreaseDebounce))
+                .thenReturn(0);
+        when(mResourcesSpy.getInteger(
+                R.integer.config_displayWhiteBalanceIncreaseDebounce))
+                .thenReturn(0);
+        mockResourcesFloat(R.dimen.config_displayWhiteBalanceLowLightAmbientColorTemperature,
+                LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE);
+        mockResourcesFloat(R.dimen.config_displayWhiteBalanceHighLightAmbientColorTemperature,
+                HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE);
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceAmbientColorTemperatures))
+                .thenReturn(mAmbientColorTemperatures);
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceDisplayColorTemperatures))
+                .thenReturn(mDisplayColorTemperatures);
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceStrongAmbientColorTemperatures))
+                .thenReturn(mStrongAmbientColorTemperatures);
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceStrongDisplayColorTemperatures))
+                .thenReturn(mStrongDisplayColorTemperatures);
+
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceLowLightAmbientBrightnesses))
+                .thenReturn(mBrightnesses);
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceLowLightAmbientBiases))
+                .thenReturn(mBiases);
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceHighLightAmbientBrightnesses))
+                .thenReturn(mHighLightBrightnesses);
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceHighLightAmbientBiases))
+                .thenReturn(mHighLightBiases);
+        mockThrottler();
+        LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class);
+        LocalServices.addService(ColorDisplayService.ColorDisplayServiceInternal.class,
+                mColorDisplayServiceInternalMock);
+    }
+
+    @Test
+    public void testCalculateAdjustedBrightnessNits() {
+        doReturn(0.9f).when(mColorDisplayServiceInternalMock).getDisplayWhiteBalanceLuminance();
+        DisplayWhiteBalanceController controller =
+                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
+        final float adjustedNits = controller.calculateAdjustedBrightnessNits(500f);
+        assertEquals(/* expected= */ 550f, adjustedNits, /* delta= */ 0.001);
+    }
+
+    @Test
+    public void testNoSpline() throws Exception {
+        setBrightnesses();
+        setBiases();
+
+        DisplayWhiteBalanceController controller =
+                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
+        final float ambientColorTemperature = 8000.0f;
+        setEstimatedColorTemperature(controller, ambientColorTemperature);
+        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
+
+        for (float luxOverride = 0.1f; luxOverride <= 10000; luxOverride *= 10) {
+            setEstimatedBrightnessAndUpdate(controller, luxOverride);
+            assertEquals(controller.mPendingAmbientColorTemperature,
+                    ambientColorTemperature, 0.001);
+        }
+    }
+
+    @Test
+    public void testSpline_OneSegment() throws Exception {
+        final float lowerBrightness = 10.0f;
+        final float upperBrightness = 50.0f;
+        setBrightnesses(lowerBrightness, upperBrightness);
+        setBiases(0.0f, 1.0f);
+
+        DisplayWhiteBalanceController controller =
+                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
+        final float ambientColorTemperature = 8000.0f;
+        setEstimatedColorTemperature(controller, ambientColorTemperature);
+        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
+
+        for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
+            setEstimatedBrightnessAndUpdate(controller,
+                    mix(lowerBrightness, upperBrightness, t));
+            assertEquals(controller.mPendingAmbientColorTemperature,
+                    mix(LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE, ambientColorTemperature, t), 0.001);
+        }
+
+        setEstimatedBrightnessAndUpdate(controller, 0.0f);
+        assertEquals(controller.mPendingAmbientColorTemperature,
+                LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE, 0.001);
+
+        setEstimatedBrightnessAndUpdate(controller, upperBrightness + 1.0f);
+        assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature, 0.001);
+    }
+
+    @Test
+    public void testSpline_TwoSegments() throws Exception {
+        final float brightness0 = 10.0f;
+        final float brightness1 = 50.0f;
+        final float brightness2 = 60.0f;
+        setBrightnesses(brightness0, brightness1, brightness2);
+        final float bias0 = 0.0f;
+        final float bias1 = 0.25f;
+        final float bias2 = 1.0f;
+        setBiases(bias0, bias1, bias2);
+
+        DisplayWhiteBalanceController controller =
+                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
+        final float ambientColorTemperature = 8000.0f;
+        setEstimatedColorTemperature(controller, ambientColorTemperature);
+        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
+
+        for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
+            float luxOverride = mix(brightness0, brightness1, t);
+            setEstimatedBrightnessAndUpdate(controller, luxOverride);
+            float bias = mix(bias0, bias1, t);
+            assertEquals(controller.mPendingAmbientColorTemperature,
+                    mix(LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE, ambientColorTemperature, bias), 0.001);
+        }
+
+        for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
+            float luxOverride = mix(brightness1, brightness2, t);
+            setEstimatedBrightnessAndUpdate(controller, luxOverride);
+            float bias = mix(bias1, bias2, t);
+            assertEquals(controller.mPendingAmbientColorTemperature,
+                    mix(LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE, ambientColorTemperature, bias), 0.001);
+        }
+
+        setEstimatedBrightnessAndUpdate(controller, 0.0f);
+        assertEquals(controller.mPendingAmbientColorTemperature,
+                LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE, 0.001);
+
+        setEstimatedBrightnessAndUpdate(controller, brightness2 + 1.0f);
+        assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature, 0.001);
+    }
+
+    @Test
+    public void testSpline_VerticalSegment() throws Exception {
+        final float lowerBrightness = 10.0f;
+        final float upperBrightness = 10.0f;
+        setBrightnesses(lowerBrightness, upperBrightness);
+        setBiases(0.0f, 1.0f);
+
+        DisplayWhiteBalanceController controller =
+                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
+        final float ambientColorTemperature = 8000.0f;
+        setEstimatedColorTemperature(controller, ambientColorTemperature);
+        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
+
+        setEstimatedBrightnessAndUpdate(controller, 0.0f);
+        assertEquals(controller.mPendingAmbientColorTemperature,
+                LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE, 0.001);
+
+        setEstimatedBrightnessAndUpdate(controller, upperBrightness + 1.0f);
+        assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature, 0.001);
+    }
+
+    @Test
+    public void testSpline_InvalidEndBias() throws Exception {
+        setBrightnesses(10.0f, 1000.0f);
+        setBiases(0.0f, 2.0f);
+
+        DisplayWhiteBalanceController controller =
+                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
+        final float ambientColorTemperature = 8000.0f;
+        setEstimatedColorTemperature(controller, ambientColorTemperature);
+        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
+
+        for (float luxOverride = 0.1f; luxOverride <= 10000; luxOverride *= 10) {
+            setEstimatedBrightnessAndUpdate(controller, luxOverride);
+            assertEquals(controller.mPendingAmbientColorTemperature,
+                    ambientColorTemperature, 0.001);
+        }
+    }
+
+    @Test
+    public void testSpline_InvalidBeginBias() throws Exception {
+        setBrightnesses(10.0f, 1000.0f);
+        setBiases(0.1f, 1.0f);
+
+        DisplayWhiteBalanceController controller =
+                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
+        final float ambientColorTemperature = 8000.0f;
+        setEstimatedColorTemperature(controller, ambientColorTemperature);
+        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
+
+        for (float luxOverride = 0.1f; luxOverride <= 10000; luxOverride *= 10) {
+            setEstimatedBrightnessAndUpdate(controller, luxOverride);
+            assertEquals(controller.mPendingAmbientColorTemperature,
+                    ambientColorTemperature, 0.001);
+        }
+    }
+
+    @Test
+    public void testSpline_OneSegmentHighLight() throws Exception {
+        final float lowerBrightness = 10.0f;
+        final float upperBrightness = 50.0f;
+        setHighLightBrightnesses(lowerBrightness, upperBrightness);
+        setHighLightBiases(0.0f, 1.0f);
+
+        DisplayWhiteBalanceController controller =
+                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
+        final float ambientColorTemperature = 8000.0f;
+        setEstimatedColorTemperature(controller, ambientColorTemperature);
+        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
+
+        for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
+            setEstimatedBrightnessAndUpdate(controller,
+                    mix(lowerBrightness, upperBrightness, t));
+            assertEquals(controller.mPendingAmbientColorTemperature,
+                    mix(HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE, ambientColorTemperature, 1.0f - t),
+                    0.001);
+        }
+
+        setEstimatedBrightnessAndUpdate(controller, upperBrightness + 1.0f);
+        assertEquals(controller.mPendingAmbientColorTemperature,
+                HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE, 0.001);
+
+        setEstimatedBrightnessAndUpdate(controller, 0.0f);
+        assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature, 0.001);
+    }
+
+    @Test
+    public void testSpline_TwoSegmentsHighLight() throws Exception {
+        final float brightness0 = 10.0f;
+        final float brightness1 = 50.0f;
+        final float brightness2 = 60.0f;
+        setHighLightBrightnesses(brightness0, brightness1, brightness2);
+        final float bias0 = 0.0f;
+        final float bias1 = 0.25f;
+        final float bias2 = 1.0f;
+        setHighLightBiases(bias0, bias1, bias2);
+
+        DisplayWhiteBalanceController controller =
+                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
+        final float ambientColorTemperature = 6000.0f;
+        setEstimatedColorTemperature(controller, ambientColorTemperature);
+        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
+
+        for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
+            float luxOverride = mix(brightness0, brightness1, t);
+            setEstimatedBrightnessAndUpdate(controller, luxOverride);
+            float bias = mix(bias0, bias1, t);
+            assertEquals(controller.mPendingAmbientColorTemperature,
+                    mix(HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE, ambientColorTemperature, 1.0f - bias),
+                    0.01);
+        }
+
+        for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
+            float luxOverride = mix(brightness1, brightness2, t);
+            setEstimatedBrightnessAndUpdate(controller, luxOverride);
+            float bias = mix(bias1, bias2, t);
+            assertEquals(controller.mPendingAmbientColorTemperature,
+                    mix(HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE, ambientColorTemperature, 1.0f - bias),
+                    0.01);
+        }
+
+        setEstimatedBrightnessAndUpdate(controller, brightness2 + 1.0f);
+        assertEquals(controller.mPendingAmbientColorTemperature,
+                HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE, 0.001);
+
+        setEstimatedBrightnessAndUpdate(controller, 0.0f);
+        assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature, 0.001);
+    }
+
+    @Test
+    public void testSpline_InvalidCombinations() throws Exception {
+        setBrightnesses(100.0f, 200.0f);
+        setBiases(0.0f, 1.0f);
+        setHighLightBrightnesses(150.0f, 250.0f);
+        setHighLightBiases(0.0f, 1.0f);
+
+        DisplayWhiteBalanceController controller =
+                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
+        final float ambientColorTemperature = 8000.0f;
+        setEstimatedColorTemperature(controller, ambientColorTemperature);
+        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
+
+        for (float luxOverride = 0.1f; luxOverride <= 10000; luxOverride *= 10) {
+            setEstimatedBrightnessAndUpdate(controller, luxOverride);
+            assertEquals(controller.mPendingAmbientColorTemperature,
+                    ambientColorTemperature, 0.001);
+        }
+    }
+
+    @Test
+    public void testStrongMode() {
+        final float lowerBrightness = 10.0f;
+        final float upperBrightness = 50.0f;
+        setBrightnesses(lowerBrightness, upperBrightness);
+        setBiases(0.0f, 1.0f);
+        final int ambientColorTempLow = 6000;
+        final int ambientColorTempHigh = 8000;
+        final int displayColorTempLow = 6400;
+        final int displayColorTempHigh = 7400;
+        setStrongAmbientColorTemperatures(ambientColorTempLow, ambientColorTempHigh);
+        setStrongDisplayColorTemperatures(displayColorTempLow, displayColorTempHigh);
+
+        DisplayWhiteBalanceController controller =
+                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
+        controller.setStrongModeEnabled(true);
+        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
+
+        for (float ambientTempFraction = 0.0f; ambientTempFraction <= 1.0f;
+                ambientTempFraction += 0.1f) {
+            final float ambientTemp =
+                    (ambientColorTempHigh - ambientColorTempLow) * ambientTempFraction
+                            + ambientColorTempLow;
+            setEstimatedColorTemperature(controller, ambientTemp);
+            for (float brightnessFraction = 0.0f; brightnessFraction <= 1.0f;
+                    brightnessFraction += 0.1f) {
+                setEstimatedBrightnessAndUpdate(controller,
+                        mix(lowerBrightness, upperBrightness, brightnessFraction));
+                assertEquals(controller.mPendingAmbientColorTemperature,
+                        mix(LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE,
+                                mix(displayColorTempLow, displayColorTempHigh, ambientTempFraction),
+                                brightnessFraction),
+                        ALLOWED_ERROR_DELTA);
+            }
+        }
+    }
+
+    @Test
+    public void testLowLight_DefaultAmbient() throws Exception {
+        final float lowerBrightness = 10.0f;
+        final float upperBrightness = 50.0f;
+        setBrightnesses(lowerBrightness, upperBrightness);
+        setBiases(0.0f, 1.0f);
+
+        DisplayWhiteBalanceController controller =
+                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
+        final float ambientColorTemperature = -1.0f;
+        setEstimatedColorTemperature(controller, ambientColorTemperature);
+        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
+
+        for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
+            setEstimatedBrightnessAndUpdate(controller,
+                    mix(lowerBrightness, upperBrightness, t));
+            assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature,
+                        0.001);
+        }
+
+        setEstimatedBrightnessAndUpdate(controller, 0.0f);
+        assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature, 0.001);
+
+        setEstimatedBrightnessAndUpdate(controller, upperBrightness + 1.0f);
+        assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature, 0.001);
+    }
+
+    @Test
+    public void testWhiteBalance_updateWithEmptyFilter() throws Exception {
+        setAmbientColorTemperatures(5300.0f, 6000.0f, 7000.0f, 8000.0f);
+        setDisplayColorTemperatures(6300.0f, 6400.0f, 6850.0f, 7450.0f);
+        DisplayWhiteBalanceController controller =
+                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
+        controller.updateAmbientColorTemperature();
+        assertEquals(-1.0f, controller.mPendingAmbientColorTemperature, 0);
+    }
+
+    void mockThrottler() {
+        when(mResourcesSpy.getInteger(
+                R.integer.config_displayWhiteBalanceDecreaseDebounce)).thenReturn(0);
+        when(mResourcesSpy.getInteger(
+                R.integer.config_displayWhiteBalanceIncreaseDebounce)).thenReturn(0);
+        TypedArray base = mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceBaseThresholds);
+        TypedArray inc = mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceIncreaseThresholds);
+        TypedArray dec = mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceDecreaseThresholds);
+        base = spy(base);
+        inc = spy(inc);
+        dec = spy(dec);
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceBaseThresholds)).thenReturn(base);
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceIncreaseThresholds)).thenReturn(inc);
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceDecreaseThresholds)).thenReturn(dec);
+        setFloatArrayResource(base, new float[]{0.0f});
+        setFloatArrayResource(inc, new float[]{0.0f});
+        setFloatArrayResource(dec, new float[]{0.0f});
+    }
+
+    private void mockResourcesFloat(int id, float floatValue) {
+        doAnswer(new Answer<Void>() {
+            public Void answer(InvocationOnMock invocation) {
+                TypedValue value = (TypedValue) invocation.getArgument(1);
+                value.type = TypedValue.TYPE_FLOAT;
+                value.data = Float.floatToIntBits(floatValue);
+                return null;
+            }
+        }).when(mResourcesSpy).getValue(
+                eq(id),
+                any(TypedValue.class), eq(true));
+    }
+
+    private void setEstimatedColorTemperature(DisplayWhiteBalanceController controller,
+                                              float ambientColorTemperature) {
+        AmbientFilter colorTemperatureFilter = spy(new AmbientFilterStubber());
+        controller.mColorTemperatureFilter = colorTemperatureFilter;
+        when(colorTemperatureFilter.getEstimate(anyLong())).thenReturn(ambientColorTemperature);
+    }
+
+    private void setEstimatedBrightnessAndUpdate(DisplayWhiteBalanceController controller,
+                                                 float brightness) {
+        when(controller.mBrightnessFilter.getEstimate(anyLong())).thenReturn(brightness);
+        controller.updateAmbientColorTemperature();
+    }
+
+    private void setBrightnesses(float... vals) {
+        setFloatArrayResource(mBrightnesses, vals);
+    }
+
+    private void setBiases(float... vals) {
+        setFloatArrayResource(mBiases, vals);
+    }
+
+    private void setHighLightBrightnesses(float... vals) {
+        setFloatArrayResource(mHighLightBrightnesses, vals);
+    }
+
+    private void setHighLightBiases(float... vals) {
+        setFloatArrayResource(mHighLightBiases, vals);
+    }
+
+    private void setAmbientColorTemperatures(float... vals) {
+        setFloatArrayResource(mAmbientColorTemperatures, vals);
+    }
+
+    private void setDisplayColorTemperatures(float... vals) {
+        setFloatArrayResource(mDisplayColorTemperatures, vals);
+    }
+
+    private void setStrongAmbientColorTemperatures(float... vals) {
+        setFloatArrayResource(mStrongAmbientColorTemperatures, vals);
+    }
+
+    private void setStrongDisplayColorTemperatures(float... vals) {
+        setFloatArrayResource(mStrongDisplayColorTemperatures, vals);
+    }
+
+    private void setFloatArrayResource(TypedArray array, float[] vals) {
+        when(array.length()).thenReturn(vals.length);
+        for (int i = 0; i < vals.length; i++) {
+            when(array.getFloat(i, Float.NaN)).thenReturn(vals[i]);
+        }
+    }
+
+    private TypedArray createTypedArray() throws Exception {
+        TypedArray mockArray = mock(TypedArray.class);
+        return mockArray;
+    }
+
+    private static float mix(float a, float b, float t) {
+        return (1.0f - t) * a + t * b;
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/whitebalance/AmbientSensorTest.java b/services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientSensorTest.java
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/display/whitebalance/AmbientSensorTest.java
rename to services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientSensorTest.java
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/affected_cpus b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/affected_cpus
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/affected_cpus
@@ -0,0 +1 @@
+
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/related_cpus b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/related_cpus
new file mode 100644
index 0000000..c227083
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/related_cpus
@@ -0,0 +1 @@
+0
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/scaling_cur_freq b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/scaling_cur_freq
new file mode 100644
index 0000000..dadd973
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/scaling_cur_freq
@@ -0,0 +1 @@
+1230000
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/scaling_max_freq b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/scaling_max_freq
new file mode 100644
index 0000000..a93d6f7
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/scaling_max_freq
@@ -0,0 +1 @@
+2500000
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/stats/time_in_state b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/stats/time_in_state
new file mode 100644
index 0000000..5121f666
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy0/stats/time_in_state
@@ -0,0 +1,4 @@
+200000 500
+350000 500
+500000 1000
+2500000 100
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/affected_cpus b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/affected_cpus
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/affected_cpus
@@ -0,0 +1 @@
+1
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/related_cpus b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/related_cpus
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/related_cpus
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/scaling_cur_freq b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/scaling_cur_freq
new file mode 100644
index 0000000..2bc4ce9
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/scaling_cur_freq
@@ -0,0 +1 @@
+1450000
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/scaling_max_freq b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/scaling_max_freq
new file mode 100644
index 0000000..c754f1a
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/scaling_max_freq
@@ -0,0 +1 @@
+2800000
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/stats/time_in_state b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/stats/time_in_state
new file mode 100644
index 0000000..7ed12cf
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_affected_cpus/policy1/stats/time_in_state
@@ -0,0 +1,4 @@
+200000 500
+350000 500
+500000 1000
+2800000 100
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/affected_cpus b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/affected_cpus
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/affected_cpus
@@ -0,0 +1 @@
+0
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/related_cpus b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/related_cpus
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/related_cpus
@@ -0,0 +1 @@
+
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/scaling_cur_freq b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/scaling_cur_freq
new file mode 100644
index 0000000..dadd973
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/scaling_cur_freq
@@ -0,0 +1 @@
+1230000
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/scaling_max_freq b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/scaling_max_freq
new file mode 100644
index 0000000..a93d6f7
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/scaling_max_freq
@@ -0,0 +1 @@
+2500000
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/stats/time_in_state b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/stats/time_in_state
new file mode 100644
index 0000000..5121f666
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy0/stats/time_in_state
@@ -0,0 +1,4 @@
+200000 500
+350000 500
+500000 1000
+2500000 100
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/affected_cpus b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/affected_cpus
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/affected_cpus
@@ -0,0 +1 @@
+1
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/related_cpus b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/related_cpus
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/related_cpus
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/scaling_cur_freq b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/scaling_cur_freq
new file mode 100644
index 0000000..2bc4ce9
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/scaling_cur_freq
@@ -0,0 +1 @@
+1450000
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/scaling_max_freq b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/scaling_max_freq
new file mode 100644
index 0000000..c754f1a
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/scaling_max_freq
@@ -0,0 +1 @@
+2800000
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/stats/time_in_state b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/stats/time_in_state
new file mode 100644
index 0000000..7ed12cf
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_empty_related_cpus/policy1/stats/time_in_state
@@ -0,0 +1,4 @@
+200000 500
+350000 500
+500000 1000
+2800000 100
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_time_in_state_2/policy0/scaling_max_freq b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_time_in_state_2/policy0/scaling_max_freq
index a93d6f7..d17275f 100644
--- a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_time_in_state_2/policy0/scaling_max_freq
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_time_in_state_2/policy0/scaling_max_freq
@@ -1 +1 @@
-2500000
+2600000
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_time_in_state_2/policy1/scaling_max_freq b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_time_in_state_2/policy1/scaling_max_freq
index c754f1a..79c41c7 100644
--- a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_time_in_state_2/policy1/scaling_max_freq
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_time_in_state_2/policy1/scaling_max_freq
@@ -1 +1 @@
-2800000
+2900000
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_time_in_state_2/policy2/scaling_max_freq b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_time_in_state_2/policy2/scaling_max_freq
index deebb18..526a717 100644
--- a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_time_in_state_2/policy2/scaling_max_freq
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpufreq_with_time_in_state_2/policy2/scaling_max_freq
@@ -1 +1 @@
-2000000
+2100000
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpuset_with_empty_cpus/background/cpus b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpuset_with_empty_cpus/background/cpus
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpuset_with_empty_cpus/background/cpus
@@ -0,0 +1 @@
+
diff --git a/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpuset_with_empty_cpus/top-app/cpus b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpuset_with_empty_cpus/top-app/cpus
new file mode 100644
index 0000000..40c7bb2
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/CpuInfoReaderTest/valid_cpuset_with_empty_cpus/top-app/cpus
@@ -0,0 +1 @@
+0-3
diff --git a/services/tests/mockingservicestests/src/android/service/games/GameSessionTrampolineActivityTest.java b/services/tests/mockingservicestests/src/android/service/games/GameSessionTrampolineActivityTest.java
index 353341e..82e02ca 100644
--- a/services/tests/mockingservicestests/src/android/service/games/GameSessionTrampolineActivityTest.java
+++ b/services/tests/mockingservicestests/src/android/service/games/GameSessionTrampolineActivityTest.java
@@ -47,11 +47,14 @@
 
 import com.android.internal.infra.AndroidFuture;
 
+import com.google.common.util.concurrent.Uninterruptibles;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.time.Duration;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -62,6 +65,8 @@
 @Presubmit
 public class GameSessionTrampolineActivityTest {
 
+    private static final Duration TEST_ACTIVITY_OPEN_DURATION = Duration.ofSeconds(5);
+
     @Before
     public void setUp() {
         setAlwaysFinishActivities(false);
@@ -145,10 +150,15 @@
             startTestActivityViaGameSessionTrampolineActivity() {
         Intent testActivityIntent = new Intent();
         testActivityIntent.setClass(getInstrumentation().getTargetContext(), TestActivity.class);
+        sleep(TEST_ACTIVITY_OPEN_DURATION);
 
         return startGameSessionTrampolineActivity(testActivityIntent);
     }
 
+    private static void sleep(Duration sleepDuration) {
+        Uninterruptibles.sleepUninterruptibly(sleepDuration.toMillis(), TimeUnit.MILLISECONDS);
+    }
+
     private static AndroidFuture<GameSessionActivityResult> startGameSessionTrampolineActivity(
             Intent targetIntent) {
         AndroidFuture<GameSessionActivityResult> resultFuture = new AndroidFuture<>();
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
index 7c5d96e..1fd7c4a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
@@ -131,6 +131,7 @@
 
         mRealAms = new ActivityManagerService(
                 new TestInjector(mContext), mServiceThreadRule.getThread());
+        mRealAms.mConstants.loadDeviceConfigConstants();
         mRealAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
         mRealAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
         mRealAms.mAtmInternal = mActivityTaskManagerInt;
@@ -195,7 +196,9 @@
             Log.v(TAG, "Intercepting bindApplication() for "
                     + Arrays.toString(invocation.getArguments()));
             if (!wedge) {
-                mRealAms.finishAttachApplication(0);
+                if (mRealAms.mConstants.mEnableWaitForFinishAttachApplication) {
+                    mRealAms.finishAttachApplication(0);
+                }
             }
             return null;
         }).when(thread).bindApplication(
@@ -237,9 +240,10 @@
      */
     @Test
     public void testNormal() throws Exception {
-        ProcessRecord app = startProcessAndWait(false);
-
-        verify(app, never()).killLocked(any(), anyInt(), anyBoolean());
+        if (mRealAms.mConstants.mEnableWaitForFinishAttachApplication) {
+            ProcessRecord app = startProcessAndWait(false);
+            verify(app, never()).killLocked(any(), anyInt(), anyBoolean());
+        }
     }
 
     /**
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 60e2af5..1f4563f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -1971,6 +1971,36 @@
 
     @SuppressWarnings("GuardedBy")
     @Test
+    public void testUpdateOomAdj_DoOne_PendingFinishAttach() {
+        ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+                MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+        app.setPendingFinishAttach(true);
+        app.mState.setHasForegroundActivities(false);
+
+        sService.mOomAdjuster.setAttachingProcessStatesLSP(app);
+        updateOomAdj(app);
+
+        assertProcStates(app, PROCESS_STATE_CACHED_EMPTY, FOREGROUND_APP_ADJ,
+                SCHED_GROUP_DEFAULT);
+    }
+
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testUpdateOomAdj_DoOne_TopApp_PendingFinishAttach() {
+        ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+                MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
+        app.setPendingFinishAttach(true);
+        app.mState.setHasForegroundActivities(true);
+
+        sService.mOomAdjuster.setAttachingProcessStatesLSP(app);
+        updateOomAdj(app);
+
+        assertProcStates(app, PROCESS_STATE_TOP, FOREGROUND_APP_ADJ,
+                SCHED_GROUP_TOP_APP);
+    }
+
+    @SuppressWarnings("GuardedBy")
+    @Test
     public void testUpdateOomAdj_UidIdle_StopService() {
         final ProcessRecord app1 = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
                 MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
diff --git a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java
index 04f6f8b..2fbe8aa 100644
--- a/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/cpu/CpuInfoReaderTest.java
@@ -48,6 +48,11 @@
     private static final String TAG = CpuInfoReaderTest.class.getSimpleName();
     private static final String ROOT_DIR_NAME = "CpuInfoReaderTest";
     private static final String VALID_CPUSET_DIR = "valid_cpuset";
+    private static final String VALID_CPUSET_WITH_EMPTY_CPUS = "valid_cpuset_with_empty_cpus";
+    private static final String VALID_CPUFREQ_WITH_EMPTY_AFFECTED_CPUS =
+            "valid_cpufreq_with_empty_affected_cpus";
+    private static final String VALID_CPUFREQ_WITH_EMPTY_RELATED_CPUS =
+            "valid_cpufreq_with_empty_related_cpus";
     private static final String VALID_CPUFREQ_WITH_TIME_IN_STATE_DIR =
             "valid_cpufreq_with_time_in_state";
     private static final String VALID_CPUFREQ_WITH_TIME_IN_STATE_2_DIR =
@@ -142,8 +147,8 @@
         expectedCpuInfos.clear();
         expectedCpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0,
                 FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000,
-                /* maxCpuFreqKHz= */ 2_500_000, /* avgTimeInStateCpuFreqKHz= */ 419_354,
-                /* normalizedAvailableCpuFreqKHz= */ 2_425_919,
+                /* maxCpuFreqKHz= */ 2_600_000, /* avgTimeInStateCpuFreqKHz= */ 419_354,
+                /* normalizedAvailableCpuFreqKHz= */ 2_525_919,
                 new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 10_000_000,
                         /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 10_000_000,
                         /* idleTimeMillis= */ 110_000_000, /* iowaitTimeMillis= */ 1_100_000,
@@ -152,8 +157,8 @@
                         /* guestNiceTimeMillis= */ 0)));
         expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1,
                 FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 2_800_000,
-                /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ 429_032,
-                /* normalizedAvailableCpuFreqKHz= */ 2_403_009,
+                /* maxCpuFreqKHz= */ 2_900_000, /* avgTimeInStateCpuFreqKHz= */ 429_032,
+                /* normalizedAvailableCpuFreqKHz= */ 2_503_009,
                 new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 900_000,
                         /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 10_000_000,
                         /* idleTimeMillis= */ 1_000_000, /* iowaitTimeMillis= */ 90_000,
@@ -163,8 +168,8 @@
         expectedCpuInfos.append(2, new CpuInfoReader.CpuInfo(/* cpuCore= */ 2,
                 FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND,
                 /* isOnline= */ true, /* curCpuFreqKHz= */ 2_000_000,
-                /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ 403_225,
-                /* normalizedAvailableCpuFreqKHz= */ 1_688_209,
+                /* maxCpuFreqKHz= */ 2_100_000, /* avgTimeInStateCpuFreqKHz= */ 403_225,
+                /* normalizedAvailableCpuFreqKHz= */ 1_788_209,
                 new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 10_000_000,
                         /* niceTimeMillis= */ 2_000_000, /* systemTimeMillis= */ 0,
                         /* idleTimeMillis= */ 10_000_000, /* iowaitTimeMillis= */ 1_000_000,
@@ -174,7 +179,7 @@
         expectedCpuInfos.append(3, new CpuInfoReader.CpuInfo(/* cpuCore= */ 3,
                 FLAG_CPUSET_CATEGORY_TOP_APP | FLAG_CPUSET_CATEGORY_BACKGROUND,
                 /* isOnline= */ false, /* curCpuFreqKHz= */ MISSING_FREQUENCY,
-                /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY,
+                /* maxCpuFreqKHz= */ 2_100_000, /* avgTimeInStateCpuFreqKHz= */ MISSING_FREQUENCY,
                 /* normalizedAvailableCpuFreqKHz= */ MISSING_FREQUENCY,
                 new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 2_000_000,
                         /* niceTimeMillis= */ 1_000_000, /* systemTimeMillis= */ 1_000_000,
@@ -403,6 +408,104 @@
     }
 
     @Test
+    public void testReadCpuInfoWithEmptyRelatedCpus() throws Exception {
+        CpuInfoReader cpuInfoReader = newCpuInfoReader(getCacheFile(VALID_CPUSET_DIR),
+                getCacheFile(VALID_CPUFREQ_WITH_EMPTY_RELATED_CPUS),
+                getCacheFile(VALID_PROC_STAT));
+
+        SparseArray<CpuInfoReader.CpuInfo> actualCpuInfos = cpuInfoReader.readCpuInfos();
+        SparseArray<CpuInfoReader.CpuInfo> expectedCpuInfos = new SparseArray<>();
+
+        expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1,
+                FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_450_000,
+                /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ 502_380,
+                /* normalizedAvailableCpuFreqKHz= */ 2_693_525,
+                new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_949_280,
+                        /* niceTimeMillis= */ 7_799_450, /* systemTimeMillis= */ 54_004_020,
+                        /* idleTimeMillis= */ 402_707_120, /* iowaitTimeMillis= */ 1_186_960,
+                        /* irqTimeMillis= */ 14_786_940, /* softirqTimeMillis= */ 1_498_130,
+                        /* stealTimeMillis= */ 78_780, /* guestTimeMillis= */ 0,
+                        /* guestNiceTimeMillis= */ 0)));
+
+        compareCpuInfos("CPU infos with policy 0 containing an empty related_cpus file",
+                expectedCpuInfos, actualCpuInfos);
+    }
+
+    @Test
+    public void testReadCpuInfoWithEmptyCpusetCpus() throws Exception {
+        CpuInfoReader cpuInfoReader = newCpuInfoReader(getCacheFile(VALID_CPUSET_WITH_EMPTY_CPUS),
+                getCacheFile(VALID_CPUFREQ_WITH_TIME_IN_STATE_DIR),
+                getCacheFile(VALID_PROC_STAT));
+
+        SparseArray<CpuInfoReader.CpuInfo> actualCpuInfos = cpuInfoReader.readCpuInfos();
+        SparseArray<CpuInfoReader.CpuInfo> expectedCpuInfos = new SparseArray<>();
+        expectedCpuInfos.append(0, new CpuInfoReader.CpuInfo(/* cpuCore= */ 0,
+                FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_230_000,
+                /* maxCpuFreqKHz= */ 2_500_000, /* avgTimeInStateCpuFreqKHz= */ 488_095,
+                new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_249_610,
+                        /* niceTimeMillis= */ 7_950_930, /* systemTimeMillis= */ 52_227_050,
+                        /* idleTimeMillis= */ 409_036_950, /* iowaitTimeMillis= */ 1_322_810,
+                        /* irqTimeMillis= */ 8_146_740, /* softirqTimeMillis= */ 428_970,
+                        /* stealTimeMillis= */ 81_950, /* guestTimeMillis= */ 0,
+                        /* guestNiceTimeMillis= */ 0)));
+        expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1,
+                FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_450_000,
+                /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ 502_380,
+                new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_949_280,
+                        /* niceTimeMillis= */ 7_799_450, /* systemTimeMillis= */ 54_004_020,
+                        /* idleTimeMillis= */ 402_707_120, /* iowaitTimeMillis= */ 1_186_960,
+                        /* irqTimeMillis= */ 14_786_940, /* softirqTimeMillis= */ 1_498_130,
+                        /* stealTimeMillis= */ 78_780, /* guestTimeMillis= */ 0,
+                        /* guestNiceTimeMillis= */ 0)));
+        expectedCpuInfos.append(2, new CpuInfoReader.CpuInfo(/* cpuCore= */ 2,
+                FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000,
+                /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ 464_285,
+                new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_959_280,
+                        /* niceTimeMillis= */ 7_789_450, /* systemTimeMillis= */ 54_014_020,
+                        /* idleTimeMillis= */ 402_717_120, /* iowaitTimeMillis= */ 1_166_960,
+                        /* irqTimeMillis= */ 14_796_940, /* softirqTimeMillis= */ 1_478_130,
+                        /* stealTimeMillis= */ 88_780, /* guestTimeMillis= */ 0,
+                        /* guestNiceTimeMillis= */ 0)));
+        expectedCpuInfos.append(3, new CpuInfoReader.CpuInfo(/* cpuCore= */ 3,
+                FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_000_000,
+                /* maxCpuFreqKHz= */ 2_000_000, /* avgTimeInStateCpuFreqKHz= */ 464_285,
+                /* normalizedAvailableCpuFreqKHz= */ 1_907_125,
+                new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 32_349_610,
+                        /* niceTimeMillis= */ 7_850_930, /* systemTimeMillis= */ 52_127_050,
+                        /* idleTimeMillis= */ 409_136_950, /* iowaitTimeMillis= */ 1_332_810,
+                        /* irqTimeMillis= */ 8_136_740, /* softirqTimeMillis= */ 438_970,
+                        /* stealTimeMillis= */ 71_950, /* guestTimeMillis= */ 0,
+                        /* guestNiceTimeMillis= */ 0)));
+
+        compareCpuInfos("CPU infos with empty background cpu set", expectedCpuInfos,
+                actualCpuInfos);
+    }
+
+    @Test
+    public void testReadCpuInfoWithEmptyAffectedCpus() throws Exception {
+        CpuInfoReader cpuInfoReader = newCpuInfoReader(getCacheFile(VALID_CPUSET_DIR),
+                getCacheFile(VALID_CPUFREQ_WITH_EMPTY_AFFECTED_CPUS),
+                getCacheFile(VALID_PROC_STAT));
+
+        SparseArray<CpuInfoReader.CpuInfo> actualCpuInfos = cpuInfoReader.readCpuInfos();
+        SparseArray<CpuInfoReader.CpuInfo> expectedCpuInfos = new SparseArray<>();
+
+        expectedCpuInfos.append(1, new CpuInfoReader.CpuInfo(/* cpuCore= */ 1,
+                FLAG_CPUSET_CATEGORY_TOP_APP, /* isOnline= */ true, /* curCpuFreqKHz= */ 1_450_000,
+                /* maxCpuFreqKHz= */ 2_800_000, /* avgTimeInStateCpuFreqKHz= */ 502_380,
+                /* normalizedAvailableCpuFreqKHz= */ 2_693_525,
+                new CpuInfoReader.CpuUsageStats(/* userTimeMillis= */ 28_949_280,
+                        /* niceTimeMillis= */ 7_799_450, /* systemTimeMillis= */ 54_004_020,
+                        /* idleTimeMillis= */ 402_707_120, /* iowaitTimeMillis= */ 1_186_960,
+                        /* irqTimeMillis= */ 14_786_940, /* softirqTimeMillis= */ 1_498_130,
+                        /* stealTimeMillis= */ 78_780, /* guestTimeMillis= */ 0,
+                        /* guestNiceTimeMillis= */ 0)));
+
+        compareCpuInfos("CPU infos with policy 0 containing an empty affected_cpus file",
+                expectedCpuInfos, actualCpuInfos);
+    }
+
+    @Test
     public void testReadCpuInfoWithEmptyProcStat() throws Exception {
         File emptyFile = getCacheFile(EMPTY_FILE);
         assertWithMessage("Create empty file %s", emptyFile).that(emptyFile.createNewFile())
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
index de5e6d7..434a75f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -46,6 +46,7 @@
 import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.PowerManager;
 import android.os.SystemProperties;
@@ -95,6 +96,10 @@
     private static final int SECOND_FOLLOWER_DISPLAY_ID = FOLLOWER_DISPLAY_ID + 1;
     private static final String SECOND_FOLLOWER_UNIQUE_DISPLAY_ID = "unique_id_789";
     private static final float PROX_SENSOR_MAX_RANGE = 5;
+    private static final float BRIGHTNESS_RAMP_RATE_FAST_DECREASE = 0.3f;
+    private static final float BRIGHTNESS_RAMP_RATE_FAST_INCREASE = 0.4f;
+    private static final float BRIGHTNESS_RAMP_RATE_SLOW_DECREASE = 0.1f;
+    private static final float BRIGHTNESS_RAMP_RATE_SLOW_INCREASE = 0.2f;
 
     private OffsettableClock mClock;
     private TestLooper mTestLooper;
@@ -296,6 +301,8 @@
     public void testDisplayBrightnessFollowers_BothDpcsSupportNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -334,14 +341,18 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
     }
 
     @Test
     public void testDisplayBrightnessFollowers_FollowerDoesNotSupportNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -362,14 +373,18 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
     }
 
     @Test
     public void testDisplayBrightnessFollowers_LeadDpcDoesNotSupportNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -388,14 +403,18 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
     }
 
     @Test
     public void testDisplayBrightnessFollowers_NeitherDpcSupportsNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -416,8 +435,10 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
     }
 
     @Test
@@ -425,24 +446,70 @@
         Settings.System.putInt(mContext.getContentResolver(),
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
                 Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        final float brightness = 0.4f;
-        final float nits = 300;
-        final float ambientLux = 3000;
-        when(mHolder.automaticBrightnessController.getRawAutomaticScreenBrightness())
-                .thenReturn(brightness);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness())
-                .thenReturn(0.3f);
-        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(mHolder.automaticBrightnessController.getAmbientLux()).thenReturn(ambientLux);
+        DisplayPowerControllerHolder followerDpc =
+                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
         when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        DisplayPowerController2 followerDpc = mock(DisplayPowerController2.class);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        float leadBrightness = 0.1f;
+        float rawLeadBrightness = 0.3f;
+        float followerBrightness = 0.4f;
+        float nits = 300;
+        float ambientLux = 3000;
+        when(mHolder.automaticBrightnessController.getRawAutomaticScreenBrightness())
+                .thenReturn(rawLeadBrightness);
+        when(mHolder.automaticBrightnessController
+                .getAutomaticScreenBrightness(any(BrightnessEvent.class)))
+                .thenReturn(leadBrightness);
+        when(mHolder.automaticBrightnessController.convertToNits(rawLeadBrightness))
+                .thenReturn(nits);
+        when(mHolder.automaticBrightnessController.getAmbientLux()).thenReturn(ambientLux);
+        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+                .thenReturn(followerBrightness);
 
-        mHolder.dpc.addDisplayBrightnessFollower(followerDpc);
+        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
+
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        verify(followerDpc).setBrightnessToFollow(brightness, nits, ambientLux);
+        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        // One triggered by handleBrightnessModeChange, another triggered by setBrightnessToFollow
+        verify(followerDpc.hbmController, times(2)).onAmbientLuxChange(ambientLux);
+        verify(followerDpc.animator, times(2)).animateTo(eq(followerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(leadBrightness);
+        when(followerDpc.displayPowerState.getScreenBrightness()).thenReturn(followerBrightness);
+        clearInvocations(mHolder.animator, followerDpc.animator);
+
+        leadBrightness = 0.05f;
+        rawLeadBrightness = 0.2f;
+        followerBrightness = 0.3f;
+        nits = 200;
+        ambientLux = 2000;
+        when(mHolder.automaticBrightnessController.getRawAutomaticScreenBrightness())
+                .thenReturn(rawLeadBrightness);
+        when(mHolder.automaticBrightnessController
+                .getAutomaticScreenBrightness(any(BrightnessEvent.class)))
+                .thenReturn(leadBrightness);
+        when(mHolder.automaticBrightnessController.convertToNits(rawLeadBrightness))
+                .thenReturn(nits);
+        when(mHolder.automaticBrightnessController.getAmbientLux()).thenReturn(ambientLux);
+        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+                .thenReturn(followerBrightness);
+
+        mHolder.dpc.updateBrightness();
+        advanceTime(1); // Run updatePowerState
+
+        // The second time, the animation rate should be slow
+        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE));
+        verify(followerDpc.hbmController).onAmbientLuxChange(ambientLux);
+        verify(followerDpc.animator).animateTo(eq(followerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE));
     }
 
     @Test
@@ -451,6 +518,9 @@
                 FOLLOWER_UNIQUE_ID);
         DisplayPowerControllerHolder secondFollowerDpc = createDisplayPowerController(
                 SECOND_FOLLOWER_DISPLAY_ID, SECOND_FOLLOWER_UNIQUE_DISPLAY_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(secondFollowerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -472,9 +542,11 @@
         when(followerDpc.brightnessSetting.getBrightness()).thenReturn(initialFollowerBrightness);
         followerListener.onBrightnessChanged(initialFollowerBrightness);
         advanceTime(1);
-        verify(followerDpc.animator).animateTo(eq(initialFollowerBrightness),
-                anyFloat(), anyFloat());
+        verify(followerDpc.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
 
+        when(followerDpc.displayPowerState.getScreenBrightness())
+                .thenReturn(initialFollowerBrightness);
 
         mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
         mHolder.dpc.addDisplayBrightnessFollower(secondFollowerDpc.dpc);
@@ -491,17 +563,26 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(secondFollowerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(secondFollowerDpc.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
 
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
+        when(followerDpc.displayPowerState.getScreenBrightness()).thenReturn(brightness);
+        when(secondFollowerDpc.displayPowerState.getScreenBrightness()).thenReturn(brightness);
         clearInvocations(mHolder.animator, followerDpc.animator, secondFollowerDpc.animator);
 
         // Remove the first follower and validate it goes back to its original brightness.
         mHolder.dpc.removeDisplayBrightnessFollower(followerDpc.dpc);
         advanceTime(1);
-        verify(followerDpc.animator).animateTo(eq(initialFollowerBrightness),
-                anyFloat(), anyFloat());
+        verify(followerDpc.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE));
+
+        when(followerDpc.displayPowerState.getScreenBrightness())
+                .thenReturn(initialFollowerBrightness);
         clearInvocations(followerDpc.animator);
 
         // Change the brightness of the lead display and validate only the second follower responds
@@ -515,9 +596,11 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
         verify(followerDpc.animator, never()).animateTo(anyFloat(), anyFloat(), anyFloat());
-        verify(secondFollowerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(secondFollowerDpc.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
     }
 
     @Test
@@ -527,6 +610,9 @@
         DisplayPowerControllerHolder secondFollowerHolder =
                 createDisplayPowerController(SECOND_FOLLOWER_DISPLAY_ID,
                         SECOND_FOLLOWER_UNIQUE_DISPLAY_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(followerHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(secondFollowerHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -556,10 +642,15 @@
         followerListener.onBrightnessChanged(initialFollowerBrightness);
         secondFollowerListener.onBrightnessChanged(initialFollowerBrightness);
         advanceTime(1);
-        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness),
-                anyFloat(), anyFloat());
-        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness),
-                anyFloat(), anyFloat());
+        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+
+        when(followerHolder.displayPowerState.getScreenBrightness())
+                .thenReturn(initialFollowerBrightness);
+        when(secondFollowerHolder.displayPowerState.getScreenBrightness())
+                .thenReturn(initialFollowerBrightness);
 
         mHolder.dpc.addDisplayBrightnessFollower(followerHolder.dpc);
         mHolder.dpc.addDisplayBrightnessFollower(secondFollowerHolder.dpc);
@@ -576,19 +667,25 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(followerHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(secondFollowerHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(followerHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(secondFollowerHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
 
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
+        when(followerHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
+        when(secondFollowerHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
         clearInvocations(mHolder.animator, followerHolder.animator, secondFollowerHolder.animator);
 
         // Stop the lead DPC and validate that the followers go back to their original brightness.
         mHolder.dpc.stop();
         advanceTime(1);
-        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness),
-                anyFloat(), anyFloat());
-        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness),
-                anyFloat(), anyFloat());
+        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE));
+        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE));
         clearInvocations(followerHolder.animator, secondFollowerHolder.animator);
     }
 
@@ -748,6 +845,155 @@
     }
 
     @Test
+    public void testAutoBrightnessEnabled_DisplayIsOn() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController).configure(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED,
+                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                /* userChangedBrightness= */ false, /* adjustment= */ 0,
+                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ false
+        );
+        verify(mHolder.hbmController)
+                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED);
+    }
+
+    @Test
+    public void testAutoBrightnessEnabled_DisplayIsInDoze() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController).configure(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED,
+                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                /* userChangedBrightness= */ false, /* adjustment= */ 0,
+                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_DOZE,
+                /* shouldResetShortTermModel= */ false
+        );
+        verify(mHolder.hbmController)
+                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED);
+    }
+
+    @Test
+    public void testAutoBrightnessDisabled_ManualBrightnessMode() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // One triggered by the test, the other by handleBrightnessModeChange
+        verify(mHolder.automaticBrightnessController, times(2)).configure(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
+                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                /* userChangedBrightness= */ false, /* adjustment= */ 0,
+                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ false
+        );
+        verify(mHolder.hbmController, times(2))
+                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
+    }
+
+    @Test
+    public void testAutoBrightnessDisabled_DisplayIsOff() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_OFF;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF);
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController).configure(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                /* userChangedBrightness= */ false, /* adjustment= */ 0,
+                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_OFF,
+                /* shouldResetShortTermModel= */ false
+        );
+        verify(mHolder.hbmController).setAutoBrightnessEnabled(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE);
+    }
+
+    @Test
+    public void testAutoBrightnessDisabled_DisplayIsInDoze() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, false);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController).configure(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                /* userChangedBrightness= */ false, /* adjustment= */ 0,
+                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_DOZE,
+                /* shouldResetShortTermModel= */ false
+        );
+        verify(mHolder.hbmController).setAutoBrightnessEnabled(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE);
+    }
+
+    @Test
+    public void testAutoBrightnessDisabled_FollowerDisplay() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        mHolder.dpc.setBrightnessToFollow(0.3f, -1, 0, /* slowChange= */ false);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // One triggered by the test, the other by handleBrightnessModeChange
+        verify(mHolder.automaticBrightnessController, times(2)).configure(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
+                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                /* userChangedBrightness= */ false, /* adjustment= */ 0,
+                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ false
+        );
+
+        // HBM should be allowed for the follower display
+        verify(mHolder.hbmController)
+                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED);
+    }
+
+    @Test
     public void testBrightnessNitsPersistWhenDisplayDeviceChanges() {
         float brightness = 0.3f;
         float nits = 500;
@@ -903,6 +1149,14 @@
                 });
         when(displayDeviceConfigMock.getScreenOffBrightnessSensorValueToLux())
                 .thenReturn(new int[0]);
+        when(displayDeviceConfigMock.getBrightnessRampFastDecrease())
+                .thenReturn(BRIGHTNESS_RAMP_RATE_FAST_DECREASE);
+        when(displayDeviceConfigMock.getBrightnessRampFastIncrease())
+                .thenReturn(BRIGHTNESS_RAMP_RATE_FAST_INCREASE);
+        when(displayDeviceConfigMock.getBrightnessRampSlowDecrease())
+                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE);
+        when(displayDeviceConfigMock.getBrightnessRampSlowIncrease())
+                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_INCREASE);
     }
 
     private DisplayPowerControllerHolder createDisplayPowerController(int displayId,
@@ -922,10 +1176,13 @@
         final HysteresisLevels hysteresisLevels = mock(HysteresisLevels.class);
         final ScreenOffBrightnessSensorController screenOffBrightnessSensorController =
                 mock(ScreenOffBrightnessSensorController.class);
+        final HighBrightnessModeController hbmController = mock(HighBrightnessModeController.class);
+
+        when(hbmController.getCurrentBrightnessMax()).thenReturn(PowerManager.BRIGHTNESS_MAX);
 
         TestInjector injector = spy(new TestInjector(displayPowerState, animator,
                 automaticBrightnessController, wakelockController, brightnessMappingStrategy,
-                hysteresisLevels, screenOffBrightnessSensorController));
+                hysteresisLevels, screenOffBrightnessSensorController, hbmController));
 
         final LogicalDisplay display = mock(LogicalDisplay.class);
         final DisplayDevice device = mock(DisplayDevice.class);
@@ -943,8 +1200,8 @@
 
         return new DisplayPowerControllerHolder(dpc, display, displayPowerState, brightnessSetting,
                 animator, automaticBrightnessController, wakelockController,
-                screenOffBrightnessSensorController, hbmMetadata, brightnessMappingStrategy,
-                injector);
+                screenOffBrightnessSensorController, hbmController, hbmMetadata,
+                brightnessMappingStrategy, injector);
     }
 
     /**
@@ -960,6 +1217,7 @@
         public final AutomaticBrightnessController automaticBrightnessController;
         public final WakelockController wakelockController;
         public final ScreenOffBrightnessSensorController screenOffBrightnessSensorController;
+        public final HighBrightnessModeController hbmController;
         public final HighBrightnessModeMetadata hbmMetadata;
         public final BrightnessMappingStrategy brightnessMappingStrategy;
         public final DisplayPowerController2.Injector injector;
@@ -970,6 +1228,7 @@
                 AutomaticBrightnessController automaticBrightnessController,
                 WakelockController wakelockController,
                 ScreenOffBrightnessSensorController screenOffBrightnessSensorController,
+                HighBrightnessModeController hbmController,
                 HighBrightnessModeMetadata hbmMetadata,
                 BrightnessMappingStrategy brightnessMappingStrategy,
                 DisplayPowerController2.Injector injector) {
@@ -981,6 +1240,7 @@
             this.automaticBrightnessController = automaticBrightnessController;
             this.wakelockController = wakelockController;
             this.screenOffBrightnessSensorController = screenOffBrightnessSensorController;
+            this.hbmController = hbmController;
             this.hbmMetadata = hbmMetadata;
             this.brightnessMappingStrategy = brightnessMappingStrategy;
             this.injector = injector;
@@ -995,13 +1255,15 @@
         private final BrightnessMappingStrategy mBrightnessMappingStrategy;
         private final HysteresisLevels mHysteresisLevels;
         private final ScreenOffBrightnessSensorController mScreenOffBrightnessSensorController;
+        private final HighBrightnessModeController mHighBrightnessModeController;
 
         TestInjector(DisplayPowerState dps, DualRampAnimator<DisplayPowerState> animator,
                 AutomaticBrightnessController automaticBrightnessController,
                 WakelockController wakelockController,
                 BrightnessMappingStrategy brightnessMappingStrategy,
                 HysteresisLevels hysteresisLevels,
-                ScreenOffBrightnessSensorController screenOffBrightnessSensorController) {
+                ScreenOffBrightnessSensorController screenOffBrightnessSensorController,
+                HighBrightnessModeController highBrightnessModeController) {
             mDisplayPowerState = dps;
             mAnimator = animator;
             mAutomaticBrightnessController = automaticBrightnessController;
@@ -1009,6 +1271,7 @@
             mBrightnessMappingStrategy = brightnessMappingStrategy;
             mHysteresisLevels = hysteresisLevels;
             mScreenOffBrightnessSensorController = screenOffBrightnessSensorController;
+            mHighBrightnessModeController = highBrightnessModeController;
         }
 
         @Override
@@ -1102,5 +1365,15 @@
                 BrightnessMappingStrategy brightnessMapper) {
             return mScreenOffBrightnessSensorController;
         }
+
+        @Override
+        HighBrightnessModeController getHighBrightnessModeController(Handler handler, int width,
+                int height, IBinder displayToken, String displayUniqueId, float brightnessMin,
+                float brightnessMax, DisplayDeviceConfig.HighBrightnessModeData hbmData,
+                HighBrightnessModeController.HdrBrightnessDeviceConfig hdrBrightnessCfg,
+                Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata,
+                Context context) {
+            return mHighBrightnessModeController;
+        }
     }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
index 95bc26a..db786bd 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -46,6 +46,7 @@
 import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.PowerManager;
 import android.os.SystemProperties;
@@ -95,6 +96,10 @@
     private static final int SECOND_FOLLOWER_DISPLAY_ID = FOLLOWER_DISPLAY_ID + 1;
     private static final String SECOND_FOLLOWER_UNIQUE_DISPLAY_ID = "unique_id_789";
     private static final float PROX_SENSOR_MAX_RANGE = 5;
+    private static final float BRIGHTNESS_RAMP_RATE_FAST_DECREASE = 0.3f;
+    private static final float BRIGHTNESS_RAMP_RATE_FAST_INCREASE = 0.4f;
+    private static final float BRIGHTNESS_RAMP_RATE_SLOW_DECREASE = 0.1f;
+    private static final float BRIGHTNESS_RAMP_RATE_SLOW_INCREASE = 0.2f;
 
     private OffsettableClock mClock;
     private TestLooper mTestLooper;
@@ -299,6 +304,8 @@
     public void testDisplayBrightnessFollowers_BothDpcsSupportNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -322,10 +329,13 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(leadBrightness);
         listener.onBrightnessChanged(leadBrightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
         verify(followerDpc.animator).animateTo(eq(followerBrightness), anyFloat(),
-                anyFloat());
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
 
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(leadBrightness);
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(followerBrightness);
         clearInvocations(mHolder.animator, followerDpc.animator);
 
         // Test the same float scale value
@@ -337,14 +347,18 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
     }
 
     @Test
     public void testDisplayBrightnessFollowers_FollowerDoesNotSupportNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -365,14 +379,18 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
     }
 
     @Test
     public void testDisplayBrightnessFollowers_LeadDpcDoesNotSupportNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -391,14 +409,18 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
     }
 
     @Test
     public void testDisplayBrightnessFollowers_NeitherDpcSupportsNits() {
         DisplayPowerControllerHolder followerDpc =
                 createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -419,8 +441,10 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
     }
 
     @Test
@@ -428,38 +452,86 @@
         Settings.System.putInt(mContext.getContentResolver(),
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
                 Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        final float brightness = 0.4f;
-        final float nits = 300;
-        final float ambientLux = 3000;
-        when(mHolder.automaticBrightnessController.getRawAutomaticScreenBrightness())
-                .thenReturn(brightness);
-        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness())
-                .thenReturn(0.3f);
-        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(mHolder.automaticBrightnessController.getAmbientLux()).thenReturn(ambientLux);
+        DisplayPowerControllerHolder followerDpc =
+                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
         when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
-        DisplayPowerController followerDpc = mock(DisplayPowerController.class);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        float leadBrightness = 0.1f;
+        float rawLeadBrightness = 0.3f;
+        float followerBrightness = 0.4f;
+        float nits = 300;
+        float ambientLux = 3000;
+        when(mHolder.automaticBrightnessController.getRawAutomaticScreenBrightness())
+                .thenReturn(rawLeadBrightness);
+        when(mHolder.automaticBrightnessController
+                .getAutomaticScreenBrightness(any(BrightnessEvent.class)))
+                .thenReturn(leadBrightness);
+        when(mHolder.automaticBrightnessController.convertToNits(rawLeadBrightness))
+                .thenReturn(nits);
+        when(mHolder.automaticBrightnessController.getAmbientLux()).thenReturn(ambientLux);
+        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+                .thenReturn(followerBrightness);
 
-        mHolder.dpc.addDisplayBrightnessFollower(followerDpc);
+        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
+
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        verify(followerDpc).setBrightnessToFollow(brightness, nits, ambientLux);
+        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        // One triggered by handleBrightnessModeChange, another triggered by setBrightnessToFollow
+        verify(followerDpc.hbmController, times(2)).onAmbientLuxChange(ambientLux);
+        verify(followerDpc.animator, times(2)).animateTo(eq(followerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(leadBrightness);
+        when(followerDpc.displayPowerState.getScreenBrightness()).thenReturn(followerBrightness);
+        clearInvocations(mHolder.animator, followerDpc.animator);
+
+        leadBrightness = 0.05f;
+        rawLeadBrightness = 0.2f;
+        followerBrightness = 0.3f;
+        nits = 200;
+        ambientLux = 2000;
+        when(mHolder.automaticBrightnessController.getRawAutomaticScreenBrightness())
+                .thenReturn(rawLeadBrightness);
+        when(mHolder.automaticBrightnessController
+                .getAutomaticScreenBrightness(any(BrightnessEvent.class)))
+                .thenReturn(leadBrightness);
+        when(mHolder.automaticBrightnessController.convertToNits(rawLeadBrightness))
+                .thenReturn(nits);
+        when(mHolder.automaticBrightnessController.getAmbientLux()).thenReturn(ambientLux);
+        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
+                .thenReturn(followerBrightness);
+
+        mHolder.dpc.updateBrightness();
+        advanceTime(1); // Run updatePowerState
+
+        // The second time, the animation rate should be slow
+        verify(mHolder.animator).animateTo(eq(leadBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE));
+        verify(followerDpc.hbmController).onAmbientLuxChange(ambientLux);
+        verify(followerDpc.animator).animateTo(eq(followerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE));
     }
 
     @Test
     public void testDisplayBrightnessFollowersRemoval_RemoveSingleFollower() {
-        DisplayPowerControllerHolder followerHolder =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
-        DisplayPowerControllerHolder secondFollowerHolder =
-                createDisplayPowerController(SECOND_FOLLOWER_DISPLAY_ID,
-                        SECOND_FOLLOWER_UNIQUE_DISPLAY_ID);
+        DisplayPowerControllerHolder followerDpc = createDisplayPowerController(FOLLOWER_DISPLAY_ID,
+                FOLLOWER_UNIQUE_ID);
+        DisplayPowerControllerHolder secondFollowerDpc = createDisplayPowerController(
+                SECOND_FOLLOWER_DISPLAY_ID, SECOND_FOLLOWER_UNIQUE_DISPLAY_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(followerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(secondFollowerDpc.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        followerHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        secondFollowerHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        followerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        secondFollowerDpc.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
         ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
@@ -470,58 +542,71 @@
         // Set the initial brightness on the DPC we're going to remove so we have a fixed value for
         // it to return to.
         listenerCaptor = ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
-        verify(followerHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+        verify(followerDpc.brightnessSetting).registerListener(listenerCaptor.capture());
         BrightnessSetting.BrightnessSettingListener followerListener = listenerCaptor.getValue();
         final float initialFollowerBrightness = 0.3f;
-        when(followerHolder.brightnessSetting.getBrightness()).thenReturn(
-                initialFollowerBrightness);
+        when(followerDpc.brightnessSetting.getBrightness()).thenReturn(initialFollowerBrightness);
         followerListener.onBrightnessChanged(initialFollowerBrightness);
         advanceTime(1);
-        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness),
-                anyFloat(), anyFloat());
+        verify(followerDpc.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
 
-        mHolder.dpc.addDisplayBrightnessFollower(followerHolder.dpc);
-        mHolder.dpc.addDisplayBrightnessFollower(secondFollowerHolder.dpc);
-        clearInvocations(followerHolder.animator);
+        when(followerDpc.displayPowerState.getScreenBrightness())
+                .thenReturn(initialFollowerBrightness);
+
+        mHolder.dpc.addDisplayBrightnessFollower(followerDpc.dpc);
+        mHolder.dpc.addDisplayBrightnessFollower(secondFollowerDpc.dpc);
+        clearInvocations(followerDpc.animator);
 
         // Validate both followers are correctly registered and receiving brightness updates
         float brightness = 0.6f;
         float nits = 600;
         when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerHolder.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
                 .thenReturn(brightness);
-        when(secondFollowerHolder.automaticBrightnessController.convertToFloatScale(nits))
+        when(secondFollowerDpc.automaticBrightnessController.convertToFloatScale(nits))
                 .thenReturn(brightness);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(followerHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(secondFollowerHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(followerDpc.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(secondFollowerDpc.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
 
-        clearInvocations(mHolder.animator, followerHolder.animator, secondFollowerHolder.animator);
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
+        when(followerDpc.displayPowerState.getScreenBrightness()).thenReturn(brightness);
+        when(secondFollowerDpc.displayPowerState.getScreenBrightness()).thenReturn(brightness);
+        clearInvocations(mHolder.animator, followerDpc.animator, secondFollowerDpc.animator);
 
         // Remove the first follower and validate it goes back to its original brightness.
-        mHolder.dpc.removeDisplayBrightnessFollower(followerHolder.dpc);
+        mHolder.dpc.removeDisplayBrightnessFollower(followerDpc.dpc);
         advanceTime(1);
-        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness),
-                anyFloat(), anyFloat());
-        clearInvocations(followerHolder.animator);
+        verify(followerDpc.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE));
+
+        when(followerDpc.displayPowerState.getScreenBrightness())
+                .thenReturn(initialFollowerBrightness);
+        clearInvocations(followerDpc.animator);
 
         // Change the brightness of the lead display and validate only the second follower responds
         brightness = 0.7f;
         nits = 700;
         when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(followerHolder.automaticBrightnessController.convertToFloatScale(nits))
+        when(followerDpc.automaticBrightnessController.convertToFloatScale(nits))
                 .thenReturn(brightness);
-        when(secondFollowerHolder.automaticBrightnessController.convertToFloatScale(nits))
+        when(secondFollowerDpc.automaticBrightnessController.convertToFloatScale(nits))
                 .thenReturn(brightness);
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(followerHolder.animator, never()).animateTo(anyFloat(), anyFloat(), anyFloat());
-        verify(secondFollowerHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(followerDpc.animator, never()).animateTo(anyFloat(), anyFloat(), anyFloat());
+        verify(secondFollowerDpc.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
     }
 
     @Test
@@ -531,6 +616,9 @@
         DisplayPowerControllerHolder secondFollowerHolder =
                 createDisplayPowerController(SECOND_FOLLOWER_DISPLAY_ID,
                         SECOND_FOLLOWER_UNIQUE_DISPLAY_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(followerHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(secondFollowerHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -560,10 +648,15 @@
         followerListener.onBrightnessChanged(initialFollowerBrightness);
         secondFollowerListener.onBrightnessChanged(initialFollowerBrightness);
         advanceTime(1);
-        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness),
-                anyFloat(), anyFloat());
-        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness),
-                anyFloat(), anyFloat());
+        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+
+        when(followerHolder.displayPowerState.getScreenBrightness())
+                .thenReturn(initialFollowerBrightness);
+        when(secondFollowerHolder.displayPowerState.getScreenBrightness())
+                .thenReturn(initialFollowerBrightness);
 
         mHolder.dpc.addDisplayBrightnessFollower(followerHolder.dpc);
         mHolder.dpc.addDisplayBrightnessFollower(secondFollowerHolder.dpc);
@@ -580,19 +673,25 @@
         when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         listener.onBrightnessChanged(brightness);
         advanceTime(1); // Send messages, run updatePowerState
-        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(followerHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
-        verify(secondFollowerHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(followerHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
+        verify(secondFollowerHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE));
 
+        when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
+        when(followerHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
+        when(secondFollowerHolder.displayPowerState.getScreenBrightness()).thenReturn(brightness);
         clearInvocations(mHolder.animator, followerHolder.animator, secondFollowerHolder.animator);
 
         // Stop the lead DPC and validate that the followers go back to their original brightness.
         mHolder.dpc.stop();
         advanceTime(1);
-        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness),
-                anyFloat(), anyFloat());
-        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness),
-                anyFloat(), anyFloat());
+        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE));
+        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_DECREASE));
         clearInvocations(followerHolder.animator, secondFollowerHolder.animator);
     }
 
@@ -751,6 +850,155 @@
     }
 
     @Test
+    public void testAutoBrightnessEnabled_DisplayIsOn() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController).configure(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED,
+                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                /* userChangedBrightness= */ false, /* adjustment= */ 0,
+                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ false
+        );
+        verify(mHolder.hbmController)
+                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED);
+    }
+
+    @Test
+    public void testAutoBrightnessEnabled_DisplayIsInDoze() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController).configure(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED,
+                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                /* userChangedBrightness= */ false, /* adjustment= */ 0,
+                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_DOZE,
+                /* shouldResetShortTermModel= */ false
+        );
+        verify(mHolder.hbmController)
+                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED);
+    }
+
+    @Test
+    public void testAutoBrightnessDisabled_ManualBrightnessMode() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // One triggered by the test, the other by handleBrightnessModeChange
+        verify(mHolder.automaticBrightnessController, times(2)).configure(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
+                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                /* userChangedBrightness= */ false, /* adjustment= */ 0,
+                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ false
+        );
+        verify(mHolder.hbmController, times(2))
+                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
+    }
+
+    @Test
+    public void testAutoBrightnessDisabled_DisplayIsOff() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_OFF;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF);
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController).configure(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                /* userChangedBrightness= */ false, /* adjustment= */ 0,
+                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_OFF,
+                /* shouldResetShortTermModel= */ false
+        );
+        verify(mHolder.hbmController).setAutoBrightnessEnabled(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE);
+    }
+
+    @Test
+    public void testAutoBrightnessDisabled_DisplayIsInDoze() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, false);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController).configure(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                /* userChangedBrightness= */ false, /* adjustment= */ 0,
+                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_DOZE,
+                /* shouldResetShortTermModel= */ false
+        );
+        verify(mHolder.hbmController).setAutoBrightnessEnabled(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE);
+    }
+
+    @Test
+    public void testAutoBrightnessDisabled_FollowerDisplay() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        mHolder.dpc.setBrightnessToFollow(0.3f, -1, 0, /* slowChange= */ false);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // One triggered by the test, the other by handleBrightnessModeChange
+        verify(mHolder.automaticBrightnessController, times(2)).configure(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
+                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                /* userChangedBrightness= */ false, /* adjustment= */ 0,
+                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_BRIGHT,
+                /* shouldResetShortTermModel= */ false
+        );
+
+        // HBM should be allowed for the follower display
+        verify(mHolder.hbmController)
+                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED);
+    }
+
+    @Test
     public void testBrightnessNitsPersistWhenDisplayDeviceChanges() {
         float brightness = 0.3f;
         float nits = 500;
@@ -907,6 +1155,14 @@
                 });
         when(displayDeviceConfigMock.getScreenOffBrightnessSensorValueToLux())
                 .thenReturn(new int[0]);
+        when(displayDeviceConfigMock.getBrightnessRampFastDecrease())
+                .thenReturn(BRIGHTNESS_RAMP_RATE_FAST_DECREASE);
+        when(displayDeviceConfigMock.getBrightnessRampFastIncrease())
+                .thenReturn(BRIGHTNESS_RAMP_RATE_FAST_INCREASE);
+        when(displayDeviceConfigMock.getBrightnessRampSlowDecrease())
+                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_DECREASE);
+        when(displayDeviceConfigMock.getBrightnessRampSlowIncrease())
+                .thenReturn(BRIGHTNESS_RAMP_RATE_SLOW_INCREASE);
     }
 
     private DisplayPowerControllerHolder createDisplayPowerController(int displayId,
@@ -925,10 +1181,13 @@
         final HysteresisLevels hysteresisLevels = mock(HysteresisLevels.class);
         final ScreenOffBrightnessSensorController screenOffBrightnessSensorController =
                 mock(ScreenOffBrightnessSensorController.class);
+        final HighBrightnessModeController hbmController = mock(HighBrightnessModeController.class);
+
+        when(hbmController.getCurrentBrightnessMax()).thenReturn(PowerManager.BRIGHTNESS_MAX);
 
         DisplayPowerController.Injector injector = spy(new TestInjector(displayPowerState, animator,
                 automaticBrightnessController, brightnessMappingStrategy, hysteresisLevels,
-                screenOffBrightnessSensorController));
+                screenOffBrightnessSensorController, hbmController));
 
         final LogicalDisplay display = mock(LogicalDisplay.class);
         final DisplayDevice device = mock(DisplayDevice.class);
@@ -946,7 +1205,7 @@
 
         return new DisplayPowerControllerHolder(dpc, display, displayPowerState, brightnessSetting,
                 animator, automaticBrightnessController, screenOffBrightnessSensorController,
-                hbmMetadata, brightnessMappingStrategy, injector);
+                hbmController, hbmMetadata, brightnessMappingStrategy, injector);
     }
 
     /**
@@ -961,6 +1220,7 @@
         public final DualRampAnimator<DisplayPowerState> animator;
         public final AutomaticBrightnessController automaticBrightnessController;
         public final ScreenOffBrightnessSensorController screenOffBrightnessSensorController;
+        public final HighBrightnessModeController hbmController;
         public final HighBrightnessModeMetadata hbmMetadata;
         public final BrightnessMappingStrategy brightnessMappingStrategy;
         public final DisplayPowerController.Injector injector;
@@ -970,6 +1230,7 @@
                 DualRampAnimator<DisplayPowerState> animator,
                 AutomaticBrightnessController automaticBrightnessController,
                 ScreenOffBrightnessSensorController screenOffBrightnessSensorController,
+                HighBrightnessModeController hbmController,
                 HighBrightnessModeMetadata hbmMetadata,
                 BrightnessMappingStrategy brightnessMappingStrategy,
                 DisplayPowerController.Injector injector) {
@@ -980,6 +1241,7 @@
             this.animator = animator;
             this.automaticBrightnessController = automaticBrightnessController;
             this.screenOffBrightnessSensorController = screenOffBrightnessSensorController;
+            this.hbmController = hbmController;
             this.hbmMetadata = hbmMetadata;
             this.brightnessMappingStrategy = brightnessMappingStrategy;
             this.injector = injector;
@@ -993,18 +1255,21 @@
         private final BrightnessMappingStrategy mBrightnessMappingStrategy;
         private final HysteresisLevels mHysteresisLevels;
         private final ScreenOffBrightnessSensorController mScreenOffBrightnessSensorController;
+        private final HighBrightnessModeController mHighBrightnessModeController;
 
         TestInjector(DisplayPowerState dps, DualRampAnimator<DisplayPowerState> animator,
                 AutomaticBrightnessController automaticBrightnessController,
                 BrightnessMappingStrategy brightnessMappingStrategy,
                 HysteresisLevels hysteresisLevels,
-                ScreenOffBrightnessSensorController screenOffBrightnessSensorController) {
+                ScreenOffBrightnessSensorController screenOffBrightnessSensorController,
+                HighBrightnessModeController highBrightnessModeController) {
             mDisplayPowerState = dps;
             mAnimator = animator;
             mAutomaticBrightnessController = automaticBrightnessController;
             mBrightnessMappingStrategy = brightnessMappingStrategy;
             mHysteresisLevels = hysteresisLevels;
             mScreenOffBrightnessSensorController = screenOffBrightnessSensorController;
+            mHighBrightnessModeController = highBrightnessModeController;
         }
 
         @Override
@@ -1076,5 +1341,15 @@
                 BrightnessMappingStrategy brightnessMapper) {
             return mScreenOffBrightnessSensorController;
         }
+
+        @Override
+        HighBrightnessModeController getHighBrightnessModeController(Handler handler, int width,
+                int height, IBinder displayToken, String displayUniqueId, float brightnessMin,
+                float brightnessMax, DisplayDeviceConfig.HighBrightnessModeData hbmData,
+                HighBrightnessModeController.HdrBrightnessDeviceConfig hdrBrightnessCfg,
+                Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata,
+                Context context) {
+            return mHighBrightnessModeController;
+        }
     }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
index 5b0b989..534a708 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
@@ -34,8 +34,8 @@
 import android.os.test.TestLooper;
 import android.view.Display;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.testutils.OffsettableClock;
 
@@ -92,7 +92,7 @@
                 };
         mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
                 mWakelockController, mDisplayDeviceConfig, mTestLooper.getLooper(),
-                mNudgeUpdatePowerState, 0,
+                mNudgeUpdatePowerState, Display.DEFAULT_DISPLAY,
                 mSensorManager, injector);
         mSensorEventListener = mDisplayPowerProximityStateController.getProximitySensorListener();
     }
@@ -128,7 +128,7 @@
         enableProximitySensor();
         emitAndValidatePositiveProximityEvent();
         mDisplayPowerProximityStateController.ignoreProximitySensorUntilChangedInternal();
-        advanceTime(1);
+        advanceTime();
         assertTrue(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged());
         verify(mNudgeUpdatePowerState, times(2)).run();
 
@@ -170,7 +170,7 @@
     }
 
     @Test
-    public void isProximitySensorAvailableReturnsFalseWhenNotAvailable() {
+    public void isProximitySensorAvailableReturnsFalseWhenNotAvailableAndNoDefault() {
         when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(
                 new DisplayDeviceConfig.SensorData() {
                     {
@@ -180,12 +180,63 @@
                 });
         mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
                 mWakelockController, mDisplayDeviceConfig, mTestLooper.getLooper(),
+                mNudgeUpdatePowerState, Display.DEFAULT_DISPLAY,
+                mSensorManager, null);
+        assertFalse(mDisplayPowerProximityStateController.isProximitySensorAvailable());
+    }
+
+    @Test
+    public void isProximitySensorAvailableReturnsTrueWhenNotAvailableAndHasDefault()
+            throws Exception {
+        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(
+                new DisplayDeviceConfig.SensorData() {
+                    {
+                        type = null;
+                        name = null;
+                    }
+                });
+        when(mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)).thenReturn(
+                TestUtils.createSensor(Sensor.TYPE_PROXIMITY, "proximity"));
+        mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
+                mWakelockController, mDisplayDeviceConfig, mTestLooper.getLooper(),
+                mNudgeUpdatePowerState, Display.DEFAULT_DISPLAY,
+                mSensorManager, null);
+        assertTrue(mDisplayPowerProximityStateController.isProximitySensorAvailable());
+    }
+
+    @Test
+    public void isProximitySensorAvailableReturnsFalseWhenNotAvailableHasDefaultNonDefaultDisplay()
+            throws Exception {
+        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(
+                new DisplayDeviceConfig.SensorData() {
+                    {
+                        type = null;
+                        name = null;
+                    }
+                });
+        when(mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)).thenReturn(
+                TestUtils.createSensor(Sensor.TYPE_PROXIMITY, "proximity"));
+        mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
+                mWakelockController, mDisplayDeviceConfig, mTestLooper.getLooper(),
                 mNudgeUpdatePowerState, 1,
                 mSensorManager, null);
         assertFalse(mDisplayPowerProximityStateController.isProximitySensorAvailable());
     }
 
     @Test
+    public void isProximitySensorAvailableReturnsTrueWhenNoSensorConfigured() throws Exception {
+        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(null);
+        when(mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)).thenReturn(
+                TestUtils.createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_PROXIMITY));
+
+        mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
+                mWakelockController, mDisplayDeviceConfig, mTestLooper.getLooper(),
+                mNudgeUpdatePowerState, Display.DEFAULT_DISPLAY,
+                mSensorManager, null);
+        assertFalse(mDisplayPowerProximityStateController.isProximitySensorAvailable());
+    }
+
+    @Test
     public void notifyDisplayDeviceChangedReloadsTheProximitySensor() throws Exception {
         DisplayDeviceConfig updatedDisplayDeviceConfig = mock(DisplayDeviceConfig.class);
         when(updatedDisplayDeviceConfig.getProximitySensor()).thenReturn(
@@ -326,8 +377,8 @@
         assertEquals(mDisplayPowerProximityStateController.getPendingProximityDebounceTime(), -1);
     }
 
-    private void advanceTime(long timeMs) {
-        mClock.fastForward(timeMs);
+    private void advanceTime() {
+        mClock.fastForward(1);
         mTestLooper.dispatchAll();
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
index f89f73c9..aa0a2fe 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -1257,6 +1257,17 @@
         public LocalDisplayAdapter.SurfaceControlProxy getSurfaceControlProxy() {
             return mSurfaceControlProxy;
         }
+
+        // Instead of using DisplayDeviceConfig.create(context, physicalDisplayId, isFirstDisplay)
+        // we should use DisplayDeviceConfig.create(context, isFirstDisplay) for the test to ensure
+        // that real device DisplayDeviceConfig is not loaded for FakeDisplay and we are getting
+        // consistent behaviour. Please also note that context passed to this method, is
+        // mMockContext and values will be loaded from mMockResources.
+        @Override
+        public DisplayDeviceConfig createDisplayDeviceConfig(Context context,
+                long physicalDisplayId, boolean isFirstDisplay) {
+            return DisplayDeviceConfig.create(context, isFirstDisplay);
+        }
     }
 
     private class TestListener implements DisplayAdapter.Listener {
diff --git a/services/tests/mockingservicestests/src/com/android/server/dreams/DreamManagerServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/dreams/DreamManagerServiceMockingTest.java
new file mode 100644
index 0000000..c02cbd1
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/dreams/DreamManagerServiceMockingTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+
+package com.android.server.dreams;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManagerInternal;
+import android.content.ContextWrapper;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.os.PowerManagerInternal;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+/**
+ * Collection of tests for exercising the {@link DreamManagerService} lifecycle.
+ */
+public class DreamManagerServiceMockingTest {
+    private ContextWrapper mContextSpy;
+    private Resources mResourcesSpy;
+
+    @Mock
+    private ActivityManagerInternal mActivityManagerInternalMock;
+
+    @Mock
+    private PowerManagerInternal mPowerManagerInternalMock;
+
+    @Mock
+    private UserManager mUserManagerMock;
+
+    private MockitoSession mMockitoSession;
+
+    private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
+        LocalServices.removeServiceForTest(clazz);
+        LocalServices.addService(clazz, mock);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
+        mResourcesSpy = spy(mContextSpy.getResources());
+        when(mContextSpy.getResources()).thenReturn(mResourcesSpy);
+
+        addLocalServiceMock(ActivityManagerInternal.class, mActivityManagerInternalMock);
+        addLocalServiceMock(PowerManagerInternal.class, mPowerManagerInternalMock);
+
+        when(mContextSpy.getSystemService(UserManager.class)).thenReturn(mUserManagerMock);
+        mMockitoSession = mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.LENIENT)
+                .mockStatic(Settings.Secure.class)
+                .startMocking();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mMockitoSession.finishMocking();
+        LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+        LocalServices.removeServiceForTest(PowerManagerInternal.class);
+    }
+
+    private DreamManagerService createService() {
+        return new DreamManagerService(mContextSpy);
+    }
+
+    @Test
+    public void testSettingsQueryUserChange() {
+        final DreamManagerService service = createService();
+
+        final SystemService.TargetUser from =
+                new SystemService.TargetUser(mock(UserInfo.class));
+        final SystemService.TargetUser to =
+                new SystemService.TargetUser(mock(UserInfo.class));
+
+        service.onUserSwitching(from, to);
+
+        verify(() -> Settings.Secure.getIntForUser(any(),
+                eq(Settings.Secure.SCREENSAVER_ENABLED),
+                anyInt(),
+                eq(UserHandle.USER_CURRENT)));
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 7cd8819..60c15fe 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -325,6 +325,83 @@
                 () -> mUmi.getBootUser(/* waitUntilSet= */ false));
     }
 
+    @Test
+    public void testGetPreviousFullUserToEnterForeground() throws Exception {
+        addUser(USER_ID);
+        setLastForegroundTime(USER_ID, 1_000_000L);
+        addUser(OTHER_USER_ID);
+        setLastForegroundTime(OTHER_USER_ID, 2_000_000L);
+
+        assertWithMessage("getPreviousFullUserToEnterForeground")
+                .that(mUms.getPreviousFullUserToEnterForeground())
+                .isEqualTo(OTHER_USER_ID);
+    }
+
+    @Test
+    public void testGetPreviousFullUserToEnterForeground_SkipsCurrentUser() throws Exception {
+        addUser(USER_ID);
+        setLastForegroundTime(USER_ID, 1_000_000L);
+        addUser(OTHER_USER_ID);
+        setLastForegroundTime(OTHER_USER_ID, 2_000_000L);
+
+        mockCurrentUser(OTHER_USER_ID);
+        assertWithMessage("getPreviousFullUserToEnterForeground should skip current user")
+                .that(mUms.getPreviousFullUserToEnterForeground())
+                .isEqualTo(USER_ID);
+    }
+
+    @Test
+    public void testGetPreviousFullUserToEnterForeground_SkipsNonFullUsers() throws Exception {
+        addUser(USER_ID);
+        setLastForegroundTime(USER_ID, 1_000_000L);
+        addUser(OTHER_USER_ID);
+        setLastForegroundTime(OTHER_USER_ID, 2_000_000L);
+
+        mUsers.get(OTHER_USER_ID).info.flags &= ~UserInfo.FLAG_FULL;
+        assertWithMessage("getPreviousFullUserToEnterForeground should skip non-full users")
+                .that(mUms.getPreviousFullUserToEnterForeground())
+                .isEqualTo(USER_ID);
+    }
+
+    @Test
+    public void testGetPreviousFullUserToEnterForeground_SkipsPartialUsers() throws Exception {
+        addUser(USER_ID);
+        setLastForegroundTime(USER_ID, 1_000_000L);
+        addUser(OTHER_USER_ID);
+        setLastForegroundTime(OTHER_USER_ID, 2_000_000L);
+
+        mUsers.get(OTHER_USER_ID).info.partial = true;
+        assertWithMessage("getPreviousFullUserToEnterForeground should skip partial users")
+                .that(mUms.getPreviousFullUserToEnterForeground())
+                .isEqualTo(USER_ID);
+    }
+
+    @Test
+    public void testGetPreviousFullUserToEnterForeground_SkipsDisabledUsers() throws Exception {
+        addUser(USER_ID);
+        setLastForegroundTime(USER_ID, 1_000_000L);
+        addUser(OTHER_USER_ID);
+        setLastForegroundTime(OTHER_USER_ID, 2_000_000L);
+
+        mUsers.get(OTHER_USER_ID).info.flags |= UserInfo.FLAG_DISABLED;
+        assertWithMessage("getPreviousFullUserToEnterForeground should skip disabled users")
+                .that(mUms.getPreviousFullUserToEnterForeground())
+                .isEqualTo(USER_ID);
+    }
+
+    @Test
+    public void testGetPreviousFullUserToEnterForeground_SkipsRemovingUsers() throws Exception {
+        addUser(USER_ID);
+        setLastForegroundTime(USER_ID, 1_000_000L);
+        addUser(OTHER_USER_ID);
+        setLastForegroundTime(OTHER_USER_ID, 2_000_000L);
+
+        mUms.addRemovingUserId(OTHER_USER_ID);
+        assertWithMessage("getPreviousFullUserToEnterForeground should skip removing users")
+                .that(mUms.getPreviousFullUserToEnterForeground())
+                .isEqualTo(USER_ID);
+    }
+
     private void mockCurrentUser(@UserIdInt int userId) {
         mockGetLocalService(ActivityManagerInternal.class, mActivityManagerInternal);
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java
index a3a49d70..f3aa427 100644
--- a/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java
@@ -259,6 +259,29 @@
     }
 
     @Test
+    public void testMinTimeBetweenAlarms_freshAlarm() {
+        final AlarmQueue<String> alarmQueue = createAlarmQueue(true, 5 * MINUTE_IN_MILLIS);
+        final long fixedTimeElapsed = mInjector.getElapsedRealtime();
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        final String pkg1 = "com.android.test.1";
+        final String pkg2 = "com.android.test.2";
+        alarmQueue.addAlarm(pkg1, fixedTimeElapsed + MINUTE_IN_MILLIS);
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(fixedTimeElapsed + MINUTE_IN_MILLIS), eq(ALARM_TAG), any(), any());
+
+        advanceElapsedClock(MINUTE_IN_MILLIS);
+
+        alarmQueue.onAlarm();
+        // Minimum of 5 minutes between alarms, so the next alarm should be 5 minutes after the
+        // first.
+        alarmQueue.addAlarm(pkg2, fixedTimeElapsed + 2 * MINUTE_IN_MILLIS);
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(fixedTimeElapsed + 6 * MINUTE_IN_MILLIS), eq(ALARM_TAG), any(), any());
+    }
+
+    @Test
     public void testOnAlarm() {
         final AlarmQueue<String> alarmQueue = createAlarmQueue(true, 0);
         final long nowElapsed = mInjector.getElapsedRealtime();
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index 206af5b..eefe5af 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -31,7 +31,6 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER;
-import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_CROP;
 
 import static org.hamcrest.core.IsNot.not;
 import static org.junit.Assert.assertEquals;
@@ -92,7 +91,6 @@
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
@@ -175,12 +173,12 @@
         sImageWallpaperComponentName = ComponentName.unflattenFromString(
                 sContext.getResources().getString(R.string.image_wallpaper_component));
         // Mock default wallpaper as image wallpaper if there is no pre-defined default wallpaper.
-        sDefaultWallpaperComponent = WallpaperManager.getDefaultWallpaperComponent(sContext);
+        sDefaultWallpaperComponent = WallpaperManager.getCmfDefaultWallpaperComponent(sContext);
 
         if (sDefaultWallpaperComponent == null) {
             sDefaultWallpaperComponent = sImageWallpaperComponentName;
             doReturn(sImageWallpaperComponentName).when(() ->
-                    WallpaperManager.getDefaultWallpaperComponent(any()));
+                    WallpaperManager.getCmfDefaultWallpaperComponent(any()));
         } else {
             sContext.addMockService(sDefaultWallpaperComponent, sWallpaperService);
         }
@@ -276,10 +274,10 @@
             assertEquals(testUserId, newWallpaperData.userId);
 
             WallpaperData wallpaperData = mService.getWallpaperSafeLocked(testUserId, which);
-            assertEquals(wallpaperData.cropFile.getAbsolutePath(),
-                    newWallpaperData.cropFile.getAbsolutePath());
-            assertEquals(wallpaperData.wallpaperFile.getAbsolutePath(),
-                    newWallpaperData.wallpaperFile.getAbsolutePath());
+            assertEquals(wallpaperData.getCropFile().getAbsolutePath(),
+                    newWallpaperData.getCropFile().getAbsolutePath());
+            assertEquals(wallpaperData.getWallpaperFile().getAbsolutePath(),
+                    newWallpaperData.getWallpaperFile().getAbsolutePath());
         }
     }
 
@@ -300,10 +298,8 @@
 
     /**
      * Tests setWallpaperComponent and clearWallpaper should work as expected.
-     * TODO ignored since the assumption never passes. to be investigated.
      */
     @Test
-    @Ignore("b/264533465")
     public void testSetThenClearComponent() {
         // Skip if there is no pre-defined default wallpaper component.
         assumeThat(sDefaultWallpaperComponent,
@@ -528,7 +524,8 @@
     @Test
     public void getWallpaperWithFeature_getCropped_returnsCropFile() throws Exception {
         File cropSystemWallpaperFile =
-                new File(WallpaperUtils.getWallpaperDir(USER_SYSTEM), WALLPAPER_CROP);
+                new WallpaperData(USER_SYSTEM, FLAG_SYSTEM).getCropFile();
+        cropSystemWallpaperFile.getParentFile().mkdirs();
         cropSystemWallpaperFile.createNewFile();
         try (FileOutputStream outputStream = new FileOutputStream(cropSystemWallpaperFile)) {
             outputStream.write("Crop system wallpaper".getBytes());
@@ -550,7 +547,8 @@
     @Test
     public void getWallpaperWithFeature_notGetCropped_returnsOriginalFile() throws Exception {
         File originalSystemWallpaperFile =
-                new File(WallpaperUtils.getWallpaperDir(USER_SYSTEM), WALLPAPER);
+                new WallpaperData(USER_SYSTEM, FLAG_SYSTEM).getWallpaperFile();
+        originalSystemWallpaperFile.getParentFile().mkdirs();
         originalSystemWallpaperFile.createNewFile();
         try (FileOutputStream outputStream = new FileOutputStream(originalSystemWallpaperFile)) {
             outputStream.write("Original system wallpaper".getBytes());
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 16e1b45..ee3a773 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -28,6 +28,7 @@
         "services.accessibility",
         "services.appwidget",
         "services.autofill",
+        "services.contentcapture",
         "services.backup",
         "services.companion",
         "services.core",
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
index e6ef044..5b5c8d4 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
@@ -29,6 +29,7 @@
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -202,6 +203,7 @@
         assertFalse(mFullScreenMagnificationController.isRegistered(DISPLAY_0));
         assertFalse(mFullScreenMagnificationController.isRegistered(DISPLAY_1));
 
+        // Once for each display on unregister
         verify(mMockThumbnail, times(2)).hideThumbnail();
     }
 
@@ -543,7 +545,11 @@
         // The first time is triggered when the thumbnail is just created.
         // The second time is triggered when the magnification region changed.
         verify(mMockThumbnail, times(2)).setThumbnailBounds(
-                any(), anyFloat(), anyFloat(), anyFloat());
+                /* currentBounds= */ any(),
+                /* scale= */ anyFloat(),
+                /* centerX= */ anyFloat(),
+                /* centerY= */ anyFloat()
+        );
     }
 
     @Test
@@ -681,6 +687,9 @@
         checkActivatedAndMagnifying(/* activated= */ true, /* magnifying= */ true, displayId);
         assertTrue(mFullScreenMagnificationController.resetIfNeeded(displayId, SERVICE_ID_2));
         checkActivatedAndMagnifying(/* activated= */ false, /* magnifying= */ false, displayId);
+
+        // Once on init before it's activated and once for reset
+        verify(mMockThumbnail, times(2)).hideThumbnail();
     }
 
     @Test
@@ -783,6 +792,9 @@
         mMessageCapturingHandler.sendAllMessages();
         checkActivatedAndMagnifying(/* activated= */ false, /* magnifying= */ false, DISPLAY_0);
         checkActivatedAndMagnifying(/* activated= */ false, /* magnifying= */ false, DISPLAY_1);
+
+        // Twice for each display: once on init before it's activated and once for screen off
+        verify(mMockThumbnail, times(4)).hideThumbnail();
     }
 
     @Test
@@ -847,6 +859,15 @@
         mMessageCapturingHandler.sendAllMessages();
         checkActivatedAndMagnifying(
                 /* activated= */ expectedActivated, /* magnifying= */ false, displayId);
+
+        if (expectedActivated) {
+            verify(mMockThumbnail, times(2)).setThumbnailBounds(
+                    /* currentBounds= */ any(),
+                    /* scale= */ anyFloat(),
+                    /* centerX= */ anyFloat(),
+                    /* centerY= */ anyFloat()
+            );
+        }
     }
 
     @Test
@@ -950,6 +971,13 @@
                 INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER, scale);
         assertThat(endSpec, closeTo(getMagnificationSpec(scale, expectedOffsets)));
         verify(mMockWindowManager).setMagnificationSpec(eq(displayId), argThat(closeTo(endSpec)));
+
+        verify(mMockThumbnail, atLeastOnce()).setThumbnailBounds(
+                /* currentBounds= */ any(),
+                eq(scale),
+                /* centerX= */ anyFloat(),
+                /* centerY= */ anyFloat()
+        );
     }
 
     @Test
@@ -984,6 +1012,13 @@
                 INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER, scale);
         assertThat(endSpec, closeTo(getMagnificationSpec(scale, expectedOffsets)));
         verify(mMockWindowManager).setMagnificationSpec(eq(displayId), argThat(closeTo(endSpec)));
+
+        verify(mMockThumbnail, atLeastOnce()).setThumbnailBounds(
+                /* currentBounds= */ any(),
+                eq(scale),
+                /* centerX= */ anyFloat(),
+                /* centerY= */ anyFloat()
+        );
     }
 
     @Test
@@ -1246,6 +1281,13 @@
         callbacks.onImeWindowVisibilityChanged(true);
         mMessageCapturingHandler.sendAllMessages();
         verify(mRequestObserver).onImeWindowVisibilityChanged(eq(DISPLAY_0), eq(true));
+
+        verify(mMockThumbnail, atLeastOnce()).setThumbnailBounds(
+                /* currentBounds= */ any(),
+                /* scale= */ anyFloat(),
+                /* centerX= */ anyFloat(),
+                /* centerY= */ anyFloat()
+        );
     }
 
     @Test
@@ -1270,6 +1312,15 @@
         mFullScreenMagnificationController.onUserContextChanged(DISPLAY_0);
 
         verify(mRequestObserver).onFullScreenMagnificationActivationState(eq(DISPLAY_0), eq(false));
+        verify(mMockThumbnail).setThumbnailBounds(
+                /* currentBounds= */ any(),
+                /* scale= */ anyFloat(),
+                /* centerX= */ anyFloat(),
+                /* centerY= */ anyFloat()
+        );
+
+        // Once on init before it's activated and once for reset
+        verify(mMockThumbnail, times(2)).hideThumbnail();
     }
 
     @Test
@@ -1281,6 +1332,12 @@
 
         assertEquals(1.0f, mFullScreenMagnificationController.getScale(DISPLAY_0), 0);
         assertTrue(mFullScreenMagnificationController.isActivated(DISPLAY_0));
+        verify(mMockThumbnail).setThumbnailBounds(
+                /* currentBounds= */ any(),
+                /* scale= */ anyFloat(),
+                /* centerX= */ anyFloat(),
+                /* centerY= */ anyFloat()
+        );
     }
 
     private void setScaleToMagnifying() {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
index e75fc21..81dd961 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
@@ -666,6 +666,21 @@
         assertEquals(config.getCenterX(), actualConfig.getCenterX(), 0);
         assertEquals(config.getCenterY(), actualConfig.getCenterY(), 0);
         assertEquals(config.getScale(), actualConfig.getScale(), 0);
+
+        verify(mWindowMagnificationManager).onUserMagnificationScaleChanged(
+                /* userId= */ anyInt(), eq(TEST_DISPLAY), eq(config.getScale()));
+    }
+
+    @Test
+    public void onSourceBoundChanged_windowEnabled_notifyMagnificationChanged()
+            throws RemoteException {
+        setMagnificationEnabled(MODE_WINDOW);
+        reset(mWindowMagnificationManager);
+
+        mMagnificationController.onSourceBoundsChanged(TEST_DISPLAY, TEST_RECT);
+
+        verify(mWindowMagnificationManager).onUserMagnificationScaleChanged(
+                /* userId= */ anyInt(), eq(TEST_DISPLAY), eq(DEFAULT_SCALE));
     }
 
     @Test
@@ -794,6 +809,7 @@
         verify(mMagnificationController).onWindowMagnificationActivationState(
                 eq(TEST_DISPLAY), eq(true));
     }
+
     @Test
     public void deactivateWindowMagnification_windowActivated_triggerCallbackAndLogUsage()
             throws RemoteException {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java
index 3baa102..8faddf8 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java
@@ -187,6 +187,29 @@
                 .addView(eq(mMagnificationThumbnail.mThumbnailLayout), any());
         verify(mMockWindowManager, never())
                 .removeView(eq(mMagnificationThumbnail.mThumbnailLayout));
+        verify(mMockWindowManager, never())
+                .updateViewLayout(eq(mMagnificationThumbnail.mThumbnailLayout), any());
+    }
+
+    @Test
+    public void whenVisible_setBoundsUpdatesLayout() throws InterruptedException {
+        runOnMainSync(() -> mMagnificationThumbnail.updateThumbnail(
+                /* scale=   */ 2f,
+                /* centerX= */ 5,
+                /* centerY= */ 10
+        ));
+        runOnMainSync(() -> mMagnificationThumbnail.setThumbnailBounds(
+                new Rect(),
+                /* scale=   */ 2f,
+                /* centerX= */ 5,
+                /* centerY= */ 10
+        ));
+        idle();
+
+        verify(mMockWindowManager).updateViewLayout(
+                eq(mMagnificationThumbnail.mThumbnailLayout),
+                /* params= */ any()
+        );
     }
 
     private static void idle() {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java
index 2357e65..8608199 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java
@@ -122,6 +122,15 @@
     }
 
     @Test
+    public void onUserMagnificationScaleChanged() throws RemoteException {
+        final int testUserId = 1;
+        final float testScale = 3f;
+        mConnectionWrapper.onUserMagnificationScaleChanged(testUserId, TEST_DISPLAY, testScale);
+        verify(mConnection).onUserMagnificationScaleChanged(
+                eq(testUserId), eq(TEST_DISPLAY), eq(testScale));
+    }
+
+    @Test
     public void setMirrorWindowCallback() throws RemoteException {
         mConnectionWrapper.setConnectionCallback(mCallback);
         verify(mConnection).setConnectionCallback(mCallback);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
index c98de7c..e8b337a 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
@@ -548,6 +548,18 @@
     }
 
     @Test
+    public void onUserMagnificationScaleChanged_hasConnection_invokeConnectionMethod()
+            throws RemoteException {
+        mWindowMagnificationManager.setConnection(mMockConnection.getConnection());
+
+        final float testScale = 3f;
+        mWindowMagnificationManager.onUserMagnificationScaleChanged(
+                CURRENT_USER_ID, TEST_DISPLAY, testScale);
+        verify(mMockConnection.getConnection()).onUserMagnificationScaleChanged(
+                eq(CURRENT_USER_ID), eq(TEST_DISPLAY), eq(testScale));
+    }
+
+    @Test
     public void pointersInWindow_magnifierEnabled_returnCorrectValue() throws RemoteException {
         mWindowMagnificationManager.setConnection(mMockConnection.getConnection());
         mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3.0f, NaN, NaN);
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index dccacb4..24a628e 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -1342,6 +1342,10 @@
             Message copy = new Message();
             copy.copyFrom(msg);
             mMessages.add(copy);
+            if (msg.getCallback() != null) {
+                msg.getCallback().run();
+                msg.setCallback(null);
+            }
             return super.sendMessageAtTime(msg, uptimeMillis);
         }
     }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java
index 21c9c75..5012335 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java
@@ -61,7 +61,7 @@
 
     @Test
     public void noopWhenBothNull() {
-        final SensorOverlays useless = new SensorOverlays(null, null, null);
+        final SensorOverlays useless = new SensorOverlays(null, null);
         useless.show(SENSOR_ID, 2, null);
         useless.hide(SENSOR_ID);
     }
@@ -69,12 +69,12 @@
     @Test
     public void testProvidesUdfps() {
         final List<IUdfpsOverlayController> udfps = new ArrayList<>();
-        SensorOverlays sensorOverlays = new SensorOverlays(null, mSidefpsController, null);
+        SensorOverlays sensorOverlays = new SensorOverlays(null, mSidefpsController);
 
         sensorOverlays.ifUdfps(udfps::add);
         assertThat(udfps).isEmpty();
 
-        sensorOverlays = new SensorOverlays(mUdfpsOverlayController, mSidefpsController, null);
+        sensorOverlays = new SensorOverlays(mUdfpsOverlayController, mSidefpsController);
         sensorOverlays.ifUdfps(udfps::add);
         assertThat(udfps).containsExactly(mUdfpsOverlayController);
     }
@@ -96,7 +96,7 @@
 
     private void testShow(IUdfpsOverlayController udfps, ISidefpsController sidefps)
             throws Exception {
-        final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps, null);
+        final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps);
         final int reason = BiometricOverlayConstants.REASON_UNKNOWN;
         sensorOverlays.show(SENSOR_ID, reason, mAcquisitionClient);
 
@@ -126,7 +126,7 @@
 
     private void testHide(IUdfpsOverlayController udfps, ISidefpsController sidefps)
             throws Exception {
-        final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps, null);
+        final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps);
         sensorOverlays.hide(SENSOR_ID);
 
         if (udfps != null) {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
index 359d711..046b01c 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
@@ -152,6 +152,28 @@
     }
 
     @Test
+    public void testLockoutEndsOperation() throws RemoteException {
+        final FaceAuthenticationClient client = createClient(2);
+        client.start(mCallback);
+        client.onLockoutPermanent();
+
+        verify(mClientMonitorCallbackConverter).onError(anyInt(), anyInt(),
+                eq(FACE_ERROR_LOCKOUT_PERMANENT), anyInt());
+        verify(mCallback).onClientFinished(client, false);
+    }
+
+    @Test
+    public void testTemporaryLockoutEndsOperation() throws RemoteException {
+        final FaceAuthenticationClient client = createClient(2);
+        client.start(mCallback);
+        client.onLockoutTimed(1000);
+
+        verify(mClientMonitorCallbackConverter).onError(anyInt(), anyInt(),
+                eq(FACE_ERROR_LOCKOUT), anyInt());
+        verify(mCallback).onClientFinished(client, false);
+    }
+
+    @Test
     public void notifyHalWhenContextChanges() throws RemoteException {
         final FaceAuthenticationClient client = createClient();
         client.start(mCallback);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
index 31a58cd..d1d6e9d 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
@@ -36,7 +36,6 @@
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
 
-import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
@@ -73,7 +72,7 @@
 
     private SensorProps[] mSensorProps;
     private LockoutResetDispatcher mLockoutResetDispatcher;
-    private TestableFaceProvider mFaceProvider;
+    private FaceProvider mFaceProvider;
 
     private static void waitForIdle() {
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
@@ -98,8 +97,9 @@
 
         mLockoutResetDispatcher = new LockoutResetDispatcher(mContext);
 
-        mFaceProvider = new TestableFaceProvider(mDaemon, mContext, mBiometricStateCallback,
-                mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext);
+        mFaceProvider = new FaceProvider(mContext, mBiometricStateCallback,
+                mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext,
+                mDaemon);
     }
 
     @Test
@@ -130,6 +130,7 @@
         for (SensorProps prop : mSensorProps) {
             final BiometricScheduler scheduler =
                     mFaceProvider.mFaceSensors.get(prop.commonProps.sensorId).getScheduler();
+            scheduler.reset();
             for (int i = 0; i < numFakeOperations; i++) {
                 final HalClientMonitor testMonitor = mock(HalClientMonitor.class);
                 when(testMonitor.getFreshDaemon()).thenReturn(new Object());
@@ -142,7 +143,7 @@
         for (SensorProps prop : mSensorProps) {
             final BiometricScheduler scheduler =
                     mFaceProvider.mFaceSensors.get(prop.commonProps.sensorId).getScheduler();
-            assertEquals(numFakeOperations, scheduler.getCurrentPendingCount());
+            assertEquals(numFakeOperations - 1, scheduler.getCurrentPendingCount());
             assertNotNull(scheduler.getCurrentClient());
         }
 
@@ -159,25 +160,4 @@
             assertEquals(0, scheduler.getCurrentPendingCount());
         }
     }
-
-    private static class TestableFaceProvider extends FaceProvider {
-        private final IFace mDaemon;
-
-        TestableFaceProvider(@NonNull IFace daemon,
-                @NonNull Context context,
-                @NonNull BiometricStateCallback biometricStateCallback,
-                @NonNull SensorProps[] props,
-                @NonNull String halInstanceName,
-                @NonNull LockoutResetDispatcher lockoutResetDispatcher,
-                @NonNull BiometricContext biometricContext) {
-            super(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher,
-                    biometricContext);
-            mDaemon = daemon;
-        }
-
-        @Override
-        synchronized IFace getHalInstance() {
-            return mDaemon;
-        }
-    }
 }
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 c383a96..46af905 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
@@ -436,7 +436,7 @@
                 mBiometricLogger, mBiometricContext,
                 true /* isStrongBiometric */,
                 null /* taskStackListener */, null /* lockoutCache */,
-                mUdfpsOverlayController, mSideFpsController, null, allowBackgroundAuthentication,
+                mUdfpsOverlayController, mSideFpsController, allowBackgroundAuthentication,
                 mSensorProps,
                 new Handler(mLooper.getLooper()), 0 /* biometricStrength */, mClock) {
             @Override
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java
index 6dfdd87..20d5f93 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java
@@ -163,6 +163,6 @@
                         .setOpPackageName("a-test")
                         .build(),
                 mBiometricLogger, mBiometricContext,
-                mUdfpsOverlayController, null, true /* isStrongBiometric */);
+                mUdfpsOverlayController, true /* isStrongBiometric */);
     }
 }
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 3c89278..ef25380 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
@@ -296,6 +296,6 @@
         mClientMonitorCallbackConverter, 0 /* userId */,
         HAT, "owner", mBiometricUtils, 8 /* sensorId */,
         mBiometricLogger, mBiometricContext, mSensorProps, mUdfpsOverlayController,
-        mSideFpsController, null, 6 /* maxTemplatesPerUser */, FingerprintManager.ENROLL_ENROLL);
+        mSideFpsController, 6 /* maxTemplatesPerUser */, FingerprintManager.ENROLL_ENROLL);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
index 9c01de6..8f6efff 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
@@ -39,7 +39,6 @@
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
 
-import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
@@ -81,7 +80,7 @@
 
     private SensorProps[] mSensorProps;
     private LockoutResetDispatcher mLockoutResetDispatcher;
-    private TestableFingerprintProvider mFingerprintProvider;
+    private FingerprintProvider mFingerprintProvider;
 
     private static void waitForIdle() {
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
@@ -110,17 +109,13 @@
 
         mLockoutResetDispatcher = new LockoutResetDispatcher(mContext);
 
-        mFingerprintProvider = new TestableFingerprintProvider(mDaemon, mContext,
+        mFingerprintProvider = new FingerprintProvider(mContext,
                 mBiometricStateCallback, mSensorProps, TAG, mLockoutResetDispatcher,
-                mGestureAvailabilityDispatcher, mBiometricContext);
+                mGestureAvailabilityDispatcher, mBiometricContext, mDaemon);
     }
 
     @Test
     public void testAddingSensors() {
-        mFingerprintProvider = new TestableFingerprintProvider(mDaemon, mContext,
-                mBiometricStateCallback, mSensorProps, TAG, mLockoutResetDispatcher,
-                mGestureAvailabilityDispatcher, mBiometricContext);
-
         waitForIdle();
 
         for (SensorProps prop : mSensorProps) {
@@ -147,6 +142,7 @@
             final BiometricScheduler scheduler =
                     mFingerprintProvider.mFingerprintSensors.get(prop.commonProps.sensorId)
                             .getScheduler();
+            scheduler.reset();
             for (int i = 0; i < numFakeOperations; i++) {
                 final HalClientMonitor testMonitor = mock(HalClientMonitor.class);
                 when(testMonitor.getFreshDaemon()).thenReturn(new Object());
@@ -160,7 +156,7 @@
             final BiometricScheduler scheduler =
                     mFingerprintProvider.mFingerprintSensors.get(prop.commonProps.sensorId)
                             .getScheduler();
-            assertEquals(numFakeOperations, scheduler.getCurrentPendingCount());
+            assertEquals(numFakeOperations - 1, scheduler.getCurrentPendingCount());
             assertNotNull(scheduler.getCurrentClient());
         }
 
@@ -178,26 +174,4 @@
             assertEquals(0, scheduler.getCurrentPendingCount());
         }
     }
-
-    private static class TestableFingerprintProvider extends FingerprintProvider {
-        private final IFingerprint mDaemon;
-
-        TestableFingerprintProvider(@NonNull IFingerprint daemon,
-                @NonNull Context context,
-                @NonNull BiometricStateCallback biometricStateCallback,
-                @NonNull SensorProps[] props,
-                @NonNull String halInstanceName,
-                @NonNull LockoutResetDispatcher lockoutResetDispatcher,
-                @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
-                @NonNull BiometricContext biometricContext) {
-            super(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher,
-                    gestureAvailabilityDispatcher, biometricContext);
-            mDaemon = daemon;
-        }
-
-        @Override
-        synchronized IFingerprint getHalInstance() {
-            return mDaemon;
-        }
-    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java
new file mode 100644
index 0000000..e457119
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.server.contentcapture;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+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.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.UserInfo;
+import android.service.contentcapture.ContentCaptureServiceInfo;
+import android.view.contentcapture.ContentCaptureEvent;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.LocalServices;
+import com.android.server.contentprotection.ContentProtectionBlocklistManager;
+import com.android.server.contentprotection.RemoteContentProtectionService;
+import com.android.server.pm.UserManagerInternal;
+
+import com.google.common.collect.ImmutableList;
+
+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;
+import org.mockito.junit.MockitoRule;
+
+/**
+ * Test for {@link ContentCaptureManagerService}.
+ *
+ * <p>Run with: {@code atest
+ * FrameworksServicesTests:com.android.server.contentcapture.ContentCaptureManagerServiceTest}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@SuppressWarnings("GuardedBy") // Service not really running, no need to expose locks
+public class ContentCaptureManagerServiceTest {
+
+    private static final int USER_ID = 1234;
+
+    private static final String PACKAGE_NAME = "com.test.package";
+
+    private static final ComponentName COMPONENT_NAME =
+            new ComponentName(PACKAGE_NAME, "TestClass");
+
+    private static final ContentCaptureEvent EVENT =
+            new ContentCaptureEvent(/* sessionId= */ 100, /* type= */ 200);
+
+    private static final ParceledListSlice<ContentCaptureEvent> PARCELED_EVENTS =
+            new ParceledListSlice<>(ImmutableList.of(EVENT));
+
+    private static final Context sContext = ApplicationProvider.getApplicationContext();
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock private UserManagerInternal mMockUserManagerInternal;
+
+    @Mock private ContentProtectionBlocklistManager mMockContentProtectionBlocklistManager;
+
+    @Mock private ContentCaptureServiceInfo mMockContentCaptureServiceInfo;
+
+    @Mock private RemoteContentProtectionService mMockRemoteContentProtectionService;
+
+    private boolean mDevCfgEnableContentProtectionReceiver;
+
+    private int mContentProtectionBlocklistManagersCreated;
+
+    private int mContentProtectionServiceInfosCreated;
+
+    private int mRemoteContentProtectionServicesCreated;
+
+    private String mConfigDefaultContentProtectionService = COMPONENT_NAME.flattenToString();
+
+    private boolean mContentProtectionServiceInfoConstructorShouldThrow;
+
+    private ContentCaptureManagerService mContentCaptureManagerService;
+
+    @Before
+    public void setup() {
+        when(mMockUserManagerInternal.getUserInfos()).thenReturn(new UserInfo[0]);
+        LocalServices.removeServiceForTest(UserManagerInternal.class);
+        LocalServices.addService(UserManagerInternal.class, mMockUserManagerInternal);
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+    }
+
+    @Test
+    public void constructor_contentProtection_flagDisabled_noBlocklistManager() {
+        assertThat(mContentProtectionBlocklistManagersCreated).isEqualTo(0);
+        assertThat(mContentProtectionServiceInfosCreated).isEqualTo(0);
+        verifyZeroInteractions(mMockContentProtectionBlocklistManager);
+    }
+
+    @Test
+    public void constructor_contentProtection_componentNameNull_noBlocklistManager() {
+        mConfigDefaultContentProtectionService = null;
+
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+
+        assertThat(mContentProtectionBlocklistManagersCreated).isEqualTo(0);
+        assertThat(mContentProtectionServiceInfosCreated).isEqualTo(0);
+        verifyZeroInteractions(mMockContentProtectionBlocklistManager);
+    }
+
+    @Test
+    public void constructor_contentProtection_componentNameBlank_noBlocklistManager() {
+        mConfigDefaultContentProtectionService = "   ";
+
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+
+        assertThat(mContentProtectionBlocklistManagersCreated).isEqualTo(0);
+        assertThat(mContentProtectionServiceInfosCreated).isEqualTo(0);
+        verifyZeroInteractions(mMockContentProtectionBlocklistManager);
+    }
+
+    @Test
+    public void constructor_contentProtection_enabled_createsBlocklistManager() {
+        mDevCfgEnableContentProtectionReceiver = true;
+
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+
+        assertThat(mContentProtectionBlocklistManagersCreated).isEqualTo(1);
+        assertThat(mContentProtectionServiceInfosCreated).isEqualTo(0);
+        verify(mMockContentProtectionBlocklistManager).updateBlocklist(anyInt());
+    }
+
+    @Test
+    public void setFineTuneParamsFromDeviceConfig_doesNotUpdateContentProtectionBlocklist() {
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+        mContentCaptureManagerService.mDevCfgContentProtectionAppsBlocklistSize += 100;
+        verify(mMockContentProtectionBlocklistManager).updateBlocklist(anyInt());
+
+        mContentCaptureManagerService.setFineTuneParamsFromDeviceConfig();
+
+        verifyNoMoreInteractions(mMockContentProtectionBlocklistManager);
+    }
+
+    @Test
+    public void getOptions_contentCaptureDisabled_contentProtectionDisabled() {
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+
+        ContentCaptureOptions actual =
+                mContentCaptureManagerService.mGlobalContentCaptureOptions.getOptions(
+                        USER_ID, PACKAGE_NAME);
+
+        assertThat(actual).isNull();
+        verify(mMockContentProtectionBlocklistManager).isAllowed(PACKAGE_NAME);
+    }
+
+    @Test
+    public void getOptions_contentCaptureDisabled_contentProtectionEnabled() {
+        when(mMockContentProtectionBlocklistManager.isAllowed(PACKAGE_NAME)).thenReturn(true);
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+
+        ContentCaptureOptions actual =
+                mContentCaptureManagerService.mGlobalContentCaptureOptions.getOptions(
+                        USER_ID, PACKAGE_NAME);
+
+        assertThat(actual).isNotNull();
+        assertThat(actual.enableReceiver).isFalse();
+        assertThat(actual.contentProtectionOptions).isNotNull();
+        assertThat(actual.contentProtectionOptions.enableReceiver).isTrue();
+        assertThat(actual.whitelistedComponents).isNull();
+    }
+
+    @Test
+    public void getOptions_contentCaptureEnabled_contentProtectionDisabled() {
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+        mContentCaptureManagerService.mGlobalContentCaptureOptions.setWhitelist(
+                USER_ID, ImmutableList.of(PACKAGE_NAME), /* components= */ null);
+
+        ContentCaptureOptions actual =
+                mContentCaptureManagerService.mGlobalContentCaptureOptions.getOptions(
+                        USER_ID, PACKAGE_NAME);
+
+        assertThat(actual).isNotNull();
+        assertThat(actual.enableReceiver).isTrue();
+        assertThat(actual.contentProtectionOptions).isNotNull();
+        assertThat(actual.contentProtectionOptions.enableReceiver).isFalse();
+        assertThat(actual.whitelistedComponents).isNull();
+        verify(mMockContentProtectionBlocklistManager).isAllowed(PACKAGE_NAME);
+    }
+
+    @Test
+    public void getOptions_contentCaptureEnabled_contentProtectionEnabled() {
+        when(mMockContentProtectionBlocklistManager.isAllowed(PACKAGE_NAME)).thenReturn(true);
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+        mContentCaptureManagerService.mGlobalContentCaptureOptions.setWhitelist(
+                USER_ID, ImmutableList.of(PACKAGE_NAME), /* components= */ null);
+
+        ContentCaptureOptions actual =
+                mContentCaptureManagerService.mGlobalContentCaptureOptions.getOptions(
+                        USER_ID, PACKAGE_NAME);
+
+        assertThat(actual).isNotNull();
+        assertThat(actual.enableReceiver).isTrue();
+        assertThat(actual.contentProtectionOptions).isNotNull();
+        assertThat(actual.contentProtectionOptions.enableReceiver).isTrue();
+        assertThat(actual.whitelistedComponents).isNull();
+    }
+
+    @Test
+    public void isWhitelisted_packageName_contentCaptureDisabled_contentProtectionDisabled() {
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+
+        boolean actual =
+                mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted(
+                        USER_ID, PACKAGE_NAME);
+
+        assertThat(actual).isFalse();
+        verify(mMockContentProtectionBlocklistManager).isAllowed(PACKAGE_NAME);
+    }
+
+    @Test
+    public void isWhitelisted_packageName_contentCaptureDisabled_contentProtectionEnabled() {
+        when(mMockContentProtectionBlocklistManager.isAllowed(PACKAGE_NAME)).thenReturn(true);
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+
+        boolean actual =
+                mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted(
+                        USER_ID, PACKAGE_NAME);
+
+        assertThat(actual).isTrue();
+    }
+
+    @Test
+    public void isWhitelisted_packageName_contentCaptureEnabled_contentProtectionNotChecked() {
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+        mContentCaptureManagerService.mGlobalContentCaptureOptions.setWhitelist(
+                USER_ID, ImmutableList.of(PACKAGE_NAME), /* components= */ null);
+
+        boolean actual =
+                mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted(
+                        USER_ID, PACKAGE_NAME);
+
+        assertThat(actual).isTrue();
+        verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString());
+    }
+
+    @Test
+    public void isWhitelisted_componentName_contentCaptureDisabled_contentProtectionDisabled() {
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+
+        boolean actual =
+                mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted(
+                        USER_ID, COMPONENT_NAME);
+
+        assertThat(actual).isFalse();
+        verify(mMockContentProtectionBlocklistManager).isAllowed(PACKAGE_NAME);
+    }
+
+    @Test
+    public void isWhitelisted_componentName_contentCaptureDisabled_contentProtectionEnabled() {
+        when(mMockContentProtectionBlocklistManager.isAllowed(PACKAGE_NAME)).thenReturn(true);
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+
+        boolean actual =
+                mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted(
+                        USER_ID, COMPONENT_NAME);
+
+        assertThat(actual).isTrue();
+    }
+
+    @Test
+    public void isWhitelisted_componentName_contentCaptureEnabled_contentProtectionNotChecked() {
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+        mContentCaptureManagerService.mGlobalContentCaptureOptions.setWhitelist(
+                USER_ID, /* packageNames= */ null, ImmutableList.of(COMPONENT_NAME));
+
+        boolean actual =
+                mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted(
+                        USER_ID, COMPONENT_NAME);
+
+        assertThat(actual).isTrue();
+        verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString());
+    }
+
+    @Test
+    public void isContentProtectionReceiverEnabled_withoutBlocklistManager() {
+        boolean actual =
+                mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted(
+                        USER_ID, PACKAGE_NAME);
+
+        assertThat(actual).isFalse();
+        verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString());
+    }
+
+    @Test
+    public void isContentProtectionReceiverEnabled_disabledWithFlag() {
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+        mContentCaptureManagerService.mDevCfgEnableContentProtectionReceiver = false;
+
+        boolean actual =
+                mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted(
+                        USER_ID, PACKAGE_NAME);
+
+        assertThat(actual).isFalse();
+        verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString());
+    }
+
+    @Test
+    public void onLoginDetected_disabledAfterConstructor() {
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+        mContentCaptureManagerService.mDevCfgEnableContentProtectionReceiver = false;
+
+        mContentCaptureManagerService
+                .getContentCaptureManagerServiceStub()
+                .onLoginDetected(PARCELED_EVENTS);
+
+        assertThat(mContentProtectionServiceInfosCreated).isEqualTo(0);
+        assertThat(mRemoteContentProtectionServicesCreated).isEqualTo(0);
+        verifyZeroInteractions(mMockRemoteContentProtectionService);
+    }
+
+    @Test
+    public void onLoginDetected_invalidPermissions() {
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentProtectionServiceInfoConstructorShouldThrow = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+
+        mContentCaptureManagerService
+                .getContentCaptureManagerServiceStub()
+                .onLoginDetected(PARCELED_EVENTS);
+
+        assertThat(mContentProtectionServiceInfosCreated).isEqualTo(1);
+        assertThat(mRemoteContentProtectionServicesCreated).isEqualTo(0);
+        verifyZeroInteractions(mMockRemoteContentProtectionService);
+    }
+
+    @Test
+    public void onLoginDetected_enabled() {
+        mDevCfgEnableContentProtectionReceiver = true;
+        mContentCaptureManagerService = new TestContentCaptureManagerService();
+
+        mContentCaptureManagerService
+                .getContentCaptureManagerServiceStub()
+                .onLoginDetected(PARCELED_EVENTS);
+
+        assertThat(mContentProtectionServiceInfosCreated).isEqualTo(1);
+        assertThat(mRemoteContentProtectionServicesCreated).isEqualTo(1);
+        verify(mMockRemoteContentProtectionService).onLoginDetected(PARCELED_EVENTS);
+    }
+
+    private class TestContentCaptureManagerService extends ContentCaptureManagerService {
+
+        TestContentCaptureManagerService() {
+            super(sContext);
+            this.mDevCfgEnableContentProtectionReceiver =
+                    ContentCaptureManagerServiceTest.this.mDevCfgEnableContentProtectionReceiver;
+        }
+
+        @Override
+        protected boolean getEnableContentProtectionReceiverLocked() {
+            return ContentCaptureManagerServiceTest.this.mDevCfgEnableContentProtectionReceiver;
+        }
+
+        @Override
+        protected ContentProtectionBlocklistManager createContentProtectionBlocklistManager() {
+            mContentProtectionBlocklistManagersCreated++;
+            return mMockContentProtectionBlocklistManager;
+        }
+
+        @Override
+        protected String getContentProtectionServiceFlatComponentName() {
+            return mConfigDefaultContentProtectionService;
+        }
+
+        @Override
+        protected ContentCaptureServiceInfo createContentProtectionServiceInfo(
+                @NonNull ComponentName componentName) throws PackageManager.NameNotFoundException {
+            mContentProtectionServiceInfosCreated++;
+            if (mContentProtectionServiceInfoConstructorShouldThrow) {
+                throw new RuntimeException("TEST RUNTIME EXCEPTION");
+            }
+            return mMockContentCaptureServiceInfo;
+        }
+
+        @Override
+        protected RemoteContentProtectionService createRemoteContentProtectionService(
+                @NonNull ComponentName componentName) {
+            mRemoteContentProtectionServicesCreated++;
+            return mMockRemoteContentProtectionService;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionBlocklistManagerTest.java b/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionBlocklistManagerTest.java
new file mode 100644
index 0000000..ba9956a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionBlocklistManagerTest.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.contentprotection;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.content.pm.PackageInfo;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableList;
+
+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;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test for {@link ContentProtectionBlocklistManager}.
+ *
+ * <p>Run with: {@code atest
+ * FrameworksServicesTests:
+ * com.android.server.contentprotection.ContentProtectionBlocklistManagerTest}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ContentProtectionBlocklistManagerTest {
+
+    private static final String FIRST_PACKAGE_NAME = "com.test.first.package.name";
+
+    private static final String SECOND_PACKAGE_NAME = "com.test.second.package.name";
+
+    private static final String UNLISTED_PACKAGE_NAME = "com.test.unlisted.package.name";
+
+    private static final String PACKAGE_NAME_BLOCKLIST_FILENAME =
+            "/product/etc/res/raw/content_protection/package_name_blocklist.txt";
+
+    private static final PackageInfo PACKAGE_INFO = new PackageInfo();
+
+    private static final List<String> LINES =
+            ImmutableList.of(FIRST_PACKAGE_NAME, SECOND_PACKAGE_NAME);
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock private ContentProtectionPackageManager mMockContentProtectionPackageManager;
+
+    private final List<String> mReadRawFiles = new ArrayList<>();
+
+    private ContentProtectionBlocklistManager mContentProtectionBlocklistManager;
+
+    @Before
+    public void setup() {
+        mContentProtectionBlocklistManager = new TestContentProtectionBlocklistManager();
+    }
+
+    @Test
+    public void isAllowed_blocklistNotLoaded() {
+        boolean actual = mContentProtectionBlocklistManager.isAllowed(FIRST_PACKAGE_NAME);
+
+        assertThat(actual).isFalse();
+        assertThat(mReadRawFiles).isEmpty();
+        verifyZeroInteractions(mMockContentProtectionPackageManager);
+    }
+
+    @Test
+    public void isAllowed_inBlocklist() {
+        mContentProtectionBlocklistManager.updateBlocklist(LINES.size());
+
+        boolean actual = mContentProtectionBlocklistManager.isAllowed(FIRST_PACKAGE_NAME);
+
+        assertThat(actual).isFalse();
+        verifyZeroInteractions(mMockContentProtectionPackageManager);
+    }
+
+    @Test
+    public void isAllowed_packageInfoNotFound() {
+        mContentProtectionBlocklistManager.updateBlocklist(LINES.size());
+        when(mMockContentProtectionPackageManager.getPackageInfo(UNLISTED_PACKAGE_NAME))
+                .thenReturn(null);
+
+        boolean actual = mContentProtectionBlocklistManager.isAllowed(UNLISTED_PACKAGE_NAME);
+
+        assertThat(actual).isFalse();
+        verify(mMockContentProtectionPackageManager, never())
+                .hasRequestedInternetPermissions(any());
+        verify(mMockContentProtectionPackageManager, never()).isSystemApp(any());
+        verify(mMockContentProtectionPackageManager, never()).isUpdatedSystemApp(any());
+    }
+
+    @Test
+    public void isAllowed_notRequestedInternet() {
+        mContentProtectionBlocklistManager.updateBlocklist(LINES.size());
+        when(mMockContentProtectionPackageManager.getPackageInfo(UNLISTED_PACKAGE_NAME))
+                .thenReturn(PACKAGE_INFO);
+        when(mMockContentProtectionPackageManager.hasRequestedInternetPermissions(PACKAGE_INFO))
+                .thenReturn(false);
+
+        boolean actual = mContentProtectionBlocklistManager.isAllowed(UNLISTED_PACKAGE_NAME);
+
+        assertThat(actual).isFalse();
+        verify(mMockContentProtectionPackageManager, never()).isSystemApp(any());
+        verify(mMockContentProtectionPackageManager, never()).isUpdatedSystemApp(any());
+    }
+
+    @Test
+    public void isAllowed_systemApp() {
+        mContentProtectionBlocklistManager.updateBlocklist(LINES.size());
+        when(mMockContentProtectionPackageManager.getPackageInfo(UNLISTED_PACKAGE_NAME))
+                .thenReturn(PACKAGE_INFO);
+        when(mMockContentProtectionPackageManager.hasRequestedInternetPermissions(PACKAGE_INFO))
+                .thenReturn(true);
+        when(mMockContentProtectionPackageManager.isSystemApp(PACKAGE_INFO)).thenReturn(true);
+
+        boolean actual = mContentProtectionBlocklistManager.isAllowed(UNLISTED_PACKAGE_NAME);
+
+        assertThat(actual).isFalse();
+        verify(mMockContentProtectionPackageManager, never()).isUpdatedSystemApp(any());
+    }
+
+    @Test
+    public void isAllowed_updatedSystemApp() {
+        mContentProtectionBlocklistManager.updateBlocklist(LINES.size());
+        when(mMockContentProtectionPackageManager.getPackageInfo(UNLISTED_PACKAGE_NAME))
+                .thenReturn(PACKAGE_INFO);
+        when(mMockContentProtectionPackageManager.hasRequestedInternetPermissions(PACKAGE_INFO))
+                .thenReturn(true);
+        when(mMockContentProtectionPackageManager.isSystemApp(PACKAGE_INFO)).thenReturn(true);
+        when(mMockContentProtectionPackageManager.isUpdatedSystemApp(PACKAGE_INFO))
+                .thenReturn(true);
+
+        boolean actual = mContentProtectionBlocklistManager.isAllowed(UNLISTED_PACKAGE_NAME);
+
+        assertThat(actual).isFalse();
+    }
+
+    @Test
+    public void isAllowed_allowed() {
+        mContentProtectionBlocklistManager.updateBlocklist(LINES.size());
+        when(mMockContentProtectionPackageManager.getPackageInfo(UNLISTED_PACKAGE_NAME))
+                .thenReturn(PACKAGE_INFO);
+        when(mMockContentProtectionPackageManager.hasRequestedInternetPermissions(PACKAGE_INFO))
+                .thenReturn(true);
+        when(mMockContentProtectionPackageManager.isSystemApp(PACKAGE_INFO)).thenReturn(false);
+        when(mMockContentProtectionPackageManager.isUpdatedSystemApp(PACKAGE_INFO))
+                .thenReturn(false);
+
+        boolean actual = mContentProtectionBlocklistManager.isAllowed(UNLISTED_PACKAGE_NAME);
+
+        assertThat(actual).isTrue();
+    }
+
+    @Test
+    public void updateBlocklist_negativeSize() {
+        mContentProtectionBlocklistManager.updateBlocklist(/* blocklistSize= */ -1);
+        assertThat(mReadRawFiles).isEmpty();
+
+        mContentProtectionBlocklistManager.isAllowed(FIRST_PACKAGE_NAME);
+        verify(mMockContentProtectionPackageManager).getPackageInfo(FIRST_PACKAGE_NAME);
+    }
+
+    @Test
+    public void updateBlocklist_zeroSize() {
+        mContentProtectionBlocklistManager.updateBlocklist(/* blocklistSize= */ 0);
+        assertThat(mReadRawFiles).isEmpty();
+
+        mContentProtectionBlocklistManager.isAllowed(FIRST_PACKAGE_NAME);
+        verify(mMockContentProtectionPackageManager).getPackageInfo(FIRST_PACKAGE_NAME);
+    }
+
+    @Test
+    public void updateBlocklist_positiveSize_belowTotal() {
+        mContentProtectionBlocklistManager.updateBlocklist(/* blocklistSize= */ 1);
+        assertThat(mReadRawFiles).containsExactly(PACKAGE_NAME_BLOCKLIST_FILENAME);
+
+        mContentProtectionBlocklistManager.isAllowed(FIRST_PACKAGE_NAME);
+        mContentProtectionBlocklistManager.isAllowed(SECOND_PACKAGE_NAME);
+
+        verify(mMockContentProtectionPackageManager, never()).getPackageInfo(FIRST_PACKAGE_NAME);
+        verify(mMockContentProtectionPackageManager).getPackageInfo(SECOND_PACKAGE_NAME);
+    }
+
+    @Test
+    public void updateBlocklist_positiveSize_aboveTotal() {
+        mContentProtectionBlocklistManager.updateBlocklist(LINES.size() + 1);
+        assertThat(mReadRawFiles).containsExactly(PACKAGE_NAME_BLOCKLIST_FILENAME);
+
+        mContentProtectionBlocklistManager.isAllowed(FIRST_PACKAGE_NAME);
+        mContentProtectionBlocklistManager.isAllowed(SECOND_PACKAGE_NAME);
+
+        verify(mMockContentProtectionPackageManager, never()).getPackageInfo(FIRST_PACKAGE_NAME);
+        verify(mMockContentProtectionPackageManager, never()).getPackageInfo(SECOND_PACKAGE_NAME);
+    }
+
+    private final class TestContentProtectionBlocklistManager
+            extends ContentProtectionBlocklistManager {
+
+        TestContentProtectionBlocklistManager() {
+            super(mMockContentProtectionPackageManager);
+        }
+
+        @Override
+        protected List<String> readLinesFromRawFile(@NonNull String filename) {
+            mReadRawFiles.add(filename);
+            return LINES;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionPackageManagerTest.java b/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionPackageManagerTest.java
new file mode 100644
index 0000000..7d45ea4
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionPackageManagerTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.contentprotection;
+
+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.when;
+
+import android.Manifest.permission;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManager.PackageInfoFlags;
+import android.testing.TestableContext;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+import org.mockito.junit.MockitoRule;
+
+/**
+ * Test for {@link ContentProtectionPackageManager}.
+ *
+ * <p>Run with: {@code atest
+ * FrameworksServicesTests:com.android.server.contentprotection.ContentProtectionPackageManagerTest}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ContentProtectionPackageManagerTest {
+    private static final String PACKAGE_NAME = "PACKAGE_NAME";
+
+    private static final PackageInfo EMPTY_PACKAGE_INFO = new PackageInfo();
+
+    private static final PackageInfo SYSTEM_APP_PACKAGE_INFO = createSystemAppPackageInfo();
+
+    private static final PackageInfo UPDATED_SYSTEM_APP_PACKAGE_INFO =
+            createUpdatedSystemAppPackageInfo();
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Rule
+    public final TestableContext mContext =
+            new TestableContext(ApplicationProvider.getApplicationContext());
+
+    @Mock private PackageManager mMockPackageManager;
+
+    private ContentProtectionPackageManager mContentProtectionPackageManager;
+
+    @Before
+    public void setup() {
+        mContext.setMockPackageManager(mMockPackageManager);
+        mContentProtectionPackageManager = new ContentProtectionPackageManager(mContext);
+    }
+
+    @Test
+    public void getPackageInfo_found() throws Exception {
+        PackageInfo expected = createPackageInfo(/* flags= */ 0);
+        when(mMockPackageManager.getPackageInfo(eq(PACKAGE_NAME), any(PackageInfoFlags.class)))
+                .thenReturn(expected);
+
+        PackageInfo actual = mContentProtectionPackageManager.getPackageInfo(PACKAGE_NAME);
+
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void getPackageInfo_notFound() throws Exception {
+        when(mMockPackageManager.getPackageInfo(eq(PACKAGE_NAME), any(PackageInfoFlags.class)))
+                .thenThrow(new NameNotFoundException());
+
+        PackageInfo actual = mContentProtectionPackageManager.getPackageInfo(PACKAGE_NAME);
+
+        assertThat(actual).isNull();
+    }
+
+    @Test
+    public void getPackageInfo_null() {
+        PackageInfo actual = mContentProtectionPackageManager.getPackageInfo(PACKAGE_NAME);
+
+        assertThat(actual).isNull();
+    }
+
+    @Test
+    public void isSystemApp_true() {
+        boolean actual = mContentProtectionPackageManager.isSystemApp(SYSTEM_APP_PACKAGE_INFO);
+
+        assertThat(actual).isTrue();
+    }
+
+    @Test
+    public void isSystemApp_false() {
+        boolean actual =
+                mContentProtectionPackageManager.isSystemApp(UPDATED_SYSTEM_APP_PACKAGE_INFO);
+
+        assertThat(actual).isFalse();
+    }
+
+    @Test
+    public void isSystemApp_noApplicationInfo() {
+        boolean actual = mContentProtectionPackageManager.isSystemApp(EMPTY_PACKAGE_INFO);
+
+        assertThat(actual).isFalse();
+    }
+
+    @Test
+    public void isUpdatedSystemApp_true() {
+        boolean actual =
+                mContentProtectionPackageManager.isUpdatedSystemApp(
+                        UPDATED_SYSTEM_APP_PACKAGE_INFO);
+
+        assertThat(actual).isTrue();
+    }
+
+    @Test
+    public void isUpdatedSystemApp_false() {
+        boolean actual =
+                mContentProtectionPackageManager.isUpdatedSystemApp(SYSTEM_APP_PACKAGE_INFO);
+
+        assertThat(actual).isFalse();
+    }
+
+    @Test
+    public void isUpdatedSystemApp_noApplicationInfo() {
+        boolean actual = mContentProtectionPackageManager.isUpdatedSystemApp(EMPTY_PACKAGE_INFO);
+
+        assertThat(actual).isFalse();
+    }
+
+    @Test
+    public void hasRequestedInternetPermissions_true() {
+        PackageInfo packageInfo = createPackageInfo(new String[] {permission.INTERNET});
+
+        boolean actual =
+                mContentProtectionPackageManager.hasRequestedInternetPermissions(packageInfo);
+
+        assertThat(actual).isTrue();
+    }
+
+    @Test
+    public void hasRequestedInternetPermissions_false() {
+        PackageInfo packageInfo = createPackageInfo(new String[] {permission.ACCESS_FINE_LOCATION});
+
+        boolean actual =
+                mContentProtectionPackageManager.hasRequestedInternetPermissions(packageInfo);
+
+        assertThat(actual).isFalse();
+    }
+
+    @Test
+    public void hasRequestedInternetPermissions_noRequestedPermissions() {
+        boolean actual =
+                mContentProtectionPackageManager.hasRequestedInternetPermissions(
+                        EMPTY_PACKAGE_INFO);
+
+        assertThat(actual).isFalse();
+    }
+
+    private static PackageInfo createSystemAppPackageInfo() {
+        return createPackageInfo(ApplicationInfo.FLAG_SYSTEM);
+    }
+
+    private static PackageInfo createUpdatedSystemAppPackageInfo() {
+        return createPackageInfo(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP);
+    }
+
+    private static PackageInfo createPackageInfo(int flags) {
+        return createPackageInfo(flags, /* requestedPermissions= */ new String[0]);
+    }
+
+    private static PackageInfo createPackageInfo(String[] requestedPermissions) {
+        return createPackageInfo(/* flags= */ 0, requestedPermissions);
+    }
+
+    private static PackageInfo createPackageInfo(int flags, String[] requestedPermissions) {
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.packageName = PACKAGE_NAME;
+        packageInfo.applicationInfo = new ApplicationInfo();
+        packageInfo.applicationInfo.packageName = PACKAGE_NAME;
+        packageInfo.applicationInfo.flags = flags;
+        packageInfo.requestedPermissions = requestedPermissions;
+        return packageInfo;
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/contentprotection/RemoteContentProtectionServiceTest.java b/services/tests/servicestests/src/com/android/server/contentprotection/RemoteContentProtectionServiceTest.java
new file mode 100644
index 0000000..9135ef3
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/contentprotection/RemoteContentProtectionServiceTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.contentprotection;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ParceledListSlice;
+import android.os.UserHandle;
+import android.service.contentcapture.IContentProtectionService;
+import android.view.contentcapture.ContentCaptureEvent;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.infra.AndroidFuture;
+import com.android.internal.infra.ServiceConnector;
+
+import com.google.common.collect.ImmutableList;
+
+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;
+import org.mockito.junit.MockitoRule;
+
+/**
+ * Test for {@link RemoteContentProtectionService}.
+ *
+ * <p>Run with: {@code atest
+ * FrameworksServicesTests:com.android.server.contentprotection.RemoteContentProtectionServiceTest}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RemoteContentProtectionServiceTest {
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock private IContentProtectionService mMockContentProtectionService;
+
+    private RemoteContentProtectionService mRemoteContentProtectionService;
+
+    private int mConnectCallCount = 0;
+
+    @Before
+    public void setup() {
+        ComponentName componentName = new ComponentName(mContext.getPackageName(), "TestClass");
+        mRemoteContentProtectionService =
+                new TestRemoteContentProtectionService(mContext, componentName);
+    }
+
+    @Test
+    public void doesNotAutoConnect() {
+        assertThat(mConnectCallCount).isEqualTo(0);
+        verifyZeroInteractions(mMockContentProtectionService);
+    }
+
+    @Test
+    public void getAutoDisconnectTimeoutMs() {
+        long actual = mRemoteContentProtectionService.getAutoDisconnectTimeoutMs();
+
+        assertThat(actual).isEqualTo(3000L);
+    }
+
+    @Test
+    public void onLoginDetected() throws Exception {
+        ContentCaptureEvent event =
+                new ContentCaptureEvent(/* sessionId= */ 1111, /* type= */ 2222);
+        ParceledListSlice<ContentCaptureEvent> events =
+                new ParceledListSlice<>(ImmutableList.of(event));
+
+        mRemoteContentProtectionService.onLoginDetected(events);
+
+        verify(mMockContentProtectionService).onLoginDetected(events);
+    }
+
+    private final class TestRemoteContentProtectionService extends RemoteContentProtectionService {
+
+        TestRemoteContentProtectionService(Context context, ComponentName componentName) {
+            super(context, componentName, UserHandle.myUserId(), /* bindAllowInstant= */ false);
+        }
+
+        @Override // from ServiceConnector
+        public synchronized AndroidFuture<IContentProtectionService> connect() {
+            mConnectCallCount++;
+            return AndroidFuture.completedFuture(mMockContentProtectionService);
+        }
+
+        @Override // from ServiceConnector
+        public boolean run(@NonNull ServiceConnector.VoidJob<IContentProtectionService> job) {
+            try {
+                job.run(mMockContentProtectionService);
+            } catch (Exception ex) {
+                fail("Unexpected exception: " + ex);
+            }
+            return true;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/AmbientBrightnessStatsTrackerTest.java b/services/tests/servicestests/src/com/android/server/display/AmbientBrightnessStatsTrackerTest.java
deleted file mode 100644
index 2c4fe53..0000000
--- a/services/tests/servicestests/src/com/android/server/display/AmbientBrightnessStatsTrackerTest.java
+++ /dev/null
@@ -1,444 +0,0 @@
-/*
- * Copyright 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.server.display;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import android.hardware.display.AmbientBrightnessDayStats;
-import android.os.SystemClock;
-import android.os.UserManager;
-
-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;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-import java.time.LocalDate;
-import java.util.ArrayList;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AmbientBrightnessStatsTrackerTest {
-
-    private TestInjector mTestInjector;
-
-    @Before
-    public void setUp() {
-        mTestInjector = new TestInjector();
-    }
-
-    @Test
-    public void testBrightnessStatsTrackerOverSingleDay() {
-        AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
-        ArrayList<AmbientBrightnessDayStats> userStats;
-        float[] expectedStats;
-        // Test case where no user data
-        userStats = statsTracker.getUserStats(0);
-        assertNull(userStats);
-        // Test after adding some user data
-        statsTracker.start();
-        statsTracker.add(0, 0);
-        mTestInjector.incrementTime(1000);
-        statsTracker.stop();
-        userStats = statsTracker.getUserStats(0);
-        assertEquals(1, userStats.size());
-        assertEquals(mTestInjector.getLocalDate(), userStats.get(0).getLocalDate());
-        expectedStats = getEmptyStatsArray();
-        expectedStats[0] = 1;
-        assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
-        // Test after adding some more user data
-        statsTracker.start();
-        statsTracker.add(0, 0.05f);
-        mTestInjector.incrementTime(1000);
-        statsTracker.add(0, 0.2f);
-        mTestInjector.incrementTime(1500);
-        statsTracker.add(0, 50000);
-        mTestInjector.incrementTime(2500);
-        statsTracker.stop();
-        userStats = statsTracker.getUserStats(0);
-        assertEquals(1, userStats.size());
-        assertEquals(mTestInjector.getLocalDate(), userStats.get(0).getLocalDate());
-        expectedStats = getEmptyStatsArray();
-        expectedStats[0] = 2;
-        expectedStats[1] = 1.5f;
-        expectedStats[11] = 2.5f;
-        assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
-    }
-
-    @Test
-    public void testBrightnessStatsTrackerOverMultipleDays() {
-        AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
-        ArrayList<AmbientBrightnessDayStats> userStats;
-        float[] expectedStats;
-        // Add data for day 1
-        statsTracker.start();
-        statsTracker.add(0, 0.05f);
-        mTestInjector.incrementTime(1000);
-        statsTracker.add(0, 0.2f);
-        mTestInjector.incrementTime(1500);
-        statsTracker.add(0, 1);
-        mTestInjector.incrementTime(2500);
-        statsTracker.stop();
-        // Add data for day 2
-        mTestInjector.incrementDate(1);
-        statsTracker.start();
-        statsTracker.add(0, 0);
-        mTestInjector.incrementTime(3500);
-        statsTracker.add(0, 5);
-        mTestInjector.incrementTime(5000);
-        statsTracker.stop();
-        // Test that the data is tracked as expected
-        userStats = statsTracker.getUserStats(0);
-        assertEquals(2, userStats.size());
-        assertEquals(mTestInjector.getLocalDate().minusDays(1), userStats.get(0).getLocalDate());
-        expectedStats = getEmptyStatsArray();
-        expectedStats[0] = 1;
-        expectedStats[1] = 1.5f;
-        expectedStats[3] = 2.5f;
-        assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
-        assertEquals(mTestInjector.getLocalDate(), userStats.get(1).getLocalDate());
-        expectedStats = getEmptyStatsArray();
-        expectedStats[0] = 3.5f;
-        expectedStats[4] = 5;
-        assertArrayEquals(expectedStats, userStats.get(1).getStats(), 0);
-    }
-
-    @Test
-    public void testBrightnessStatsTrackerOverMultipleUsers() {
-        AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
-        ArrayList<AmbientBrightnessDayStats> userStats;
-        float[] expectedStats;
-        // Add data for user 1
-        statsTracker.start();
-        statsTracker.add(0, 0.05f);
-        mTestInjector.incrementTime(1000);
-        statsTracker.add(0, 0.2f);
-        mTestInjector.incrementTime(1500);
-        statsTracker.add(0, 1);
-        mTestInjector.incrementTime(2500);
-        statsTracker.stop();
-        // Add data for user 2
-        mTestInjector.incrementDate(1);
-        statsTracker.start();
-        statsTracker.add(1, 0);
-        mTestInjector.incrementTime(3500);
-        statsTracker.add(1, 5);
-        mTestInjector.incrementTime(5000);
-        statsTracker.stop();
-        // Test that the data is tracked as expected
-        userStats = statsTracker.getUserStats(0);
-        assertEquals(1, userStats.size());
-        assertEquals(mTestInjector.getLocalDate().minusDays(1), userStats.get(0).getLocalDate());
-        expectedStats = getEmptyStatsArray();
-        expectedStats[0] = 1;
-        expectedStats[1] = 1.5f;
-        expectedStats[3] = 2.5f;
-        assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
-        userStats = statsTracker.getUserStats(1);
-        assertEquals(1, userStats.size());
-        assertEquals(mTestInjector.getLocalDate(), userStats.get(0).getLocalDate());
-        expectedStats = getEmptyStatsArray();
-        expectedStats[0] = 3.5f;
-        expectedStats[4] = 5;
-        assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
-    }
-
-    @Test
-    public void testBrightnessStatsTrackerOverMaxDays() {
-        AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
-        ArrayList<AmbientBrightnessDayStats> userStats;
-        // Add 10 extra days of data over the buffer limit
-        for (int i = 0; i < AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK + 10; i++) {
-            mTestInjector.incrementDate(1);
-            statsTracker.start();
-            statsTracker.add(0, 10);
-            mTestInjector.incrementTime(1000);
-            statsTracker.add(0, 20);
-            mTestInjector.incrementTime(1000);
-            statsTracker.stop();
-        }
-        // Assert that we are only tracking last "MAX_DAYS_TO_TRACK"
-        userStats = statsTracker.getUserStats(0);
-        assertEquals(AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK, userStats.size());
-        LocalDate runningDate = mTestInjector.getLocalDate();
-        for (int i = AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK - 1; i >= 0; i--) {
-            assertEquals(runningDate, userStats.get(i).getLocalDate());
-            runningDate = runningDate.minusDays(1);
-        }
-    }
-
-    @Test
-    public void testReadAmbientBrightnessStats() throws IOException {
-        AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
-        LocalDate date = mTestInjector.getLocalDate();
-        ArrayList<AmbientBrightnessDayStats> userStats;
-        String statsFile =
-                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\r\n"
-                        + "<ambient-brightness-stats>\r\n"
-                        // Old stats that shouldn't be read
-                        + "<ambient-brightness-day-stats user=\"10\" local-date=\""
-                        + date.minusDays(AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK)
-                        + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
-                        + "3000.0,10000.0\" bucket-stats=\"1.088,0.0,0.726,0.0,25.868,0.0,0.0,"
-                        + "0.0,0.0,0.0\" />\r\n"
-                        // Valid stats that should get read
-                        + "<ambient-brightness-day-stats user=\"10\" local-date=\""
-                        + date.minusDays(1)
-                        + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
-                        + "3000.0,10000.0\" bucket-stats=\"1.088,0.0,0.726,0.0,25.868,0.0,0.0,"
-                        + "0.0,0.0,0.0\" />\r\n"
-                        // Valid stats that should get read
-                        + "<ambient-brightness-day-stats user=\"10\" local-date=\"" + date
-                        + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
-                        + "3000.0,10000.0\" bucket-stats=\"0.0,0.0,0.0,0.0,4.482,0.0,0.0,0.0,0.0,"
-                        + "0.0\" />\r\n"
-                        + "</ambient-brightness-stats>";
-        statsTracker.readStats(getInputStream(statsFile));
-        userStats = statsTracker.getUserStats(0);
-        assertEquals(2, userStats.size());
-        assertEquals(new AmbientBrightnessDayStats(date.minusDays(1),
-                new float[]{0, 1, 3, 10, 30, 100, 300, 1000, 3000, 10000},
-                new float[]{1.088f, 0, 0.726f, 0, 25.868f, 0, 0, 0, 0, 0}), userStats.get(0));
-        assertEquals(new AmbientBrightnessDayStats(date,
-                new float[]{0, 1, 3, 10, 30, 100, 300, 1000, 3000, 10000},
-                new float[]{0, 0, 0, 0, 4.482f, 0, 0, 0, 0, 0}), userStats.get(1));
-    }
-
-    @Test
-    public void testFailedReadAmbientBrightnessStatsWithException() {
-        AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
-        LocalDate date = mTestInjector.getLocalDate();
-        String statsFile;
-        // Test with parse error
-        statsFile =
-                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\r\n"
-                        + "<ambient-brightness-stats>\r\n"
-                        // Incorrect since bucket boundaries not parsable
-                        + "<ambient-brightness-day-stats user=\"10\" local-date=\"" + date
-                        + "\" bucket-boundaries=\"asdf,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
-                        + "3000.0,10000.0\" bucket-stats=\"1.088,0.0,0.726,0.0,25.868,0.0,0.0,"
-                        + "0.0,0.0,0.0\" />\r\n"
-                        + "</ambient-brightness-stats>";
-        try {
-            statsTracker.readStats(getInputStream(statsFile));
-        } catch (IOException e) {
-            // Expected
-        }
-        assertNull(statsTracker.getUserStats(0));
-        // Test with incorrect data (bucket boundaries length not equal to stats length)
-        statsFile =
-                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\r\n"
-                        + "<ambient-brightness-stats>\r\n"
-                        // Correct data
-                        + "<ambient-brightness-day-stats user=\"10\" local-date=\""
-                        + date.minusDays(1)
-                        + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
-                        + "3000.0,10000.0\" bucket-stats=\"0.0,0.0,0.0,0.0,4.482,0.0,0.0,0.0,0.0,"
-                        + "0.0\" />\r\n"
-                        // Incorrect data
-                        + "<ambient-brightness-day-stats user=\"10\" local-date=\"" + date
-                        + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,1000.0,"
-                        + "3000.0,10000.0\" bucket-stats=\"1.088,0.0,0.726,0.0,25.868,0.0,0.0,"
-                        + "0.0,0.0,0.0\" />\r\n"
-                        + "</ambient-brightness-stats>";
-        try {
-            statsTracker.readStats(getInputStream(statsFile));
-        } catch (Exception e) {
-            // Expected
-        }
-        assertNull(statsTracker.getUserStats(0));
-        // Test with missing attribute
-        statsFile =
-                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\r\n"
-                        + "<ambient-brightness-stats>\r\n"
-                        + "<ambientBrightnessDayStats user=\"10\" local-date=\"" + date
-                        + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
-                        + "3000.0,10000.0\" />\r\n"
-                        + "</ambient-brightness-stats>";
-        try {
-            statsTracker.readStats(getInputStream(statsFile));
-        } catch (Exception e) {
-            // Expected
-        }
-        assertNull(statsTracker.getUserStats(0));
-    }
-
-    @Test
-    public void testWriteThenReadAmbientBrightnessStats() throws IOException {
-        AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
-        ArrayList<AmbientBrightnessDayStats> userStats;
-        float[] expectedStats;
-        // Generate some placeholder data
-        // Data: very old which should not be read
-        statsTracker.start();
-        statsTracker.add(0, 0.05f);
-        mTestInjector.incrementTime(1000);
-        statsTracker.add(0, 0.2f);
-        mTestInjector.incrementTime(1500);
-        statsTracker.add(0, 1);
-        mTestInjector.incrementTime(2500);
-        statsTracker.stop();
-        // Data: day 1 user 1
-        mTestInjector.incrementDate(AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK - 1);
-        statsTracker.start();
-        statsTracker.add(0, 0.05f);
-        mTestInjector.incrementTime(1000);
-        statsTracker.add(0, 0.2f);
-        mTestInjector.incrementTime(1500);
-        statsTracker.add(0, 1);
-        mTestInjector.incrementTime(2500);
-        statsTracker.stop();
-        // Data: day 1 user 2
-        statsTracker.start();
-        statsTracker.add(1, 0);
-        mTestInjector.incrementTime(3500);
-        statsTracker.add(1, 5);
-        mTestInjector.incrementTime(5000);
-        statsTracker.stop();
-        // Data: day 2 user 1
-        mTestInjector.incrementDate(1);
-        statsTracker.start();
-        statsTracker.add(0, 0);
-        mTestInjector.incrementTime(3500);
-        statsTracker.add(0, 50000);
-        mTestInjector.incrementTime(5000);
-        statsTracker.stop();
-        // Write them
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        statsTracker.writeStats(baos);
-        baos.flush();
-        // Read them back and assert that it's the same
-        ByteArrayInputStream input = new ByteArrayInputStream(baos.toByteArray());
-        AmbientBrightnessStatsTracker newStatsTracker = getTestStatsTracker();
-        newStatsTracker.readStats(input);
-        userStats = newStatsTracker.getUserStats(0);
-        assertEquals(2, userStats.size());
-        // Check day 1 user 1
-        assertEquals(mTestInjector.getLocalDate().minusDays(1), userStats.get(0).getLocalDate());
-        expectedStats = getEmptyStatsArray();
-        expectedStats[0] = 1;
-        expectedStats[1] = 1.5f;
-        expectedStats[3] = 2.5f;
-        assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
-        // Check day 2 user 1
-        assertEquals(mTestInjector.getLocalDate(), userStats.get(1).getLocalDate());
-        expectedStats = getEmptyStatsArray();
-        expectedStats[0] = 3.5f;
-        expectedStats[11] = 5;
-        assertArrayEquals(expectedStats, userStats.get(1).getStats(), 0);
-        userStats = newStatsTracker.getUserStats(1);
-        assertEquals(1, userStats.size());
-        // Check day 1 user 2
-        assertEquals(mTestInjector.getLocalDate().minusDays(1), userStats.get(0).getLocalDate());
-        expectedStats = getEmptyStatsArray();
-        expectedStats[0] = 3.5f;
-        expectedStats[4] = 5;
-        assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
-    }
-
-    @Test
-    public void testTimer() {
-        AmbientBrightnessStatsTracker.Timer timer = new AmbientBrightnessStatsTracker.Timer(
-                () -> mTestInjector.elapsedRealtimeMillis());
-        assertEquals(0, timer.totalDurationSec(), 0);
-        mTestInjector.incrementTime(1000);
-        assertEquals(0, timer.totalDurationSec(), 0);
-        assertFalse(timer.isRunning());
-        // Start timer
-        timer.start();
-        assertTrue(timer.isRunning());
-        assertEquals(0, timer.totalDurationSec(), 0);
-        mTestInjector.incrementTime(1000);
-        assertTrue(timer.isRunning());
-        assertEquals(1, timer.totalDurationSec(), 0);
-        // Reset timer
-        timer.reset();
-        assertEquals(0, timer.totalDurationSec(), 0);
-        assertFalse(timer.isRunning());
-        // Start again
-        timer.start();
-        assertTrue(timer.isRunning());
-        assertEquals(0, timer.totalDurationSec(), 0);
-        mTestInjector.incrementTime(2000);
-        assertTrue(timer.isRunning());
-        assertEquals(2, timer.totalDurationSec(), 0);
-        // Reset again
-        timer.reset();
-        assertEquals(0, timer.totalDurationSec(), 0);
-        assertFalse(timer.isRunning());
-    }
-
-    private class TestInjector extends AmbientBrightnessStatsTracker.Injector {
-
-        private long mElapsedRealtimeMillis = SystemClock.elapsedRealtime();
-        private LocalDate mLocalDate = LocalDate.now();
-
-        public void incrementTime(long timeMillis) {
-            mElapsedRealtimeMillis += timeMillis;
-        }
-
-        public void incrementDate(int numDays) {
-            mLocalDate = mLocalDate.plusDays(numDays);
-        }
-
-        @Override
-        public long elapsedRealtimeMillis() {
-            return mElapsedRealtimeMillis;
-        }
-
-        @Override
-        public int getUserSerialNumber(UserManager userManager, int userId) {
-            return userId + 10;
-        }
-
-        @Override
-        public int getUserId(UserManager userManager, int userSerialNumber) {
-            return userSerialNumber - 10;
-        }
-
-        @Override
-        public LocalDate getLocalDate() {
-            return mLocalDate;
-        }
-    }
-
-    private AmbientBrightnessStatsTracker getTestStatsTracker() {
-        return new AmbientBrightnessStatsTracker(
-                InstrumentationRegistry.getContext().getSystemService(UserManager.class),
-                mTestInjector);
-    }
-
-    private float[] getEmptyStatsArray() {
-        return new float[AmbientBrightnessStatsTracker.BUCKET_BOUNDARIES_FOR_NEW_STATS.length];
-    }
-
-    private InputStream getInputStream(String data) {
-        return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessMappingStrategyTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessMappingStrategyTest.java
deleted file mode 100644
index 5f81869..0000000
--- a/services/tests/servicestests/src/com/android/server/display/BrightnessMappingStrategyTest.java
+++ /dev/null
@@ -1,710 +0,0 @@
-/*
- * 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
- */
-
-package com.android.server.display;
-
-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.anyFloat;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.hardware.display.BrightnessConfiguration;
-import android.os.PowerManager;
-import android.util.MathUtils;
-import android.util.Spline;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-
-import java.util.Arrays;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class BrightnessMappingStrategyTest {
-
-    private static final float[] LUX_LEVELS = {
-        0,
-        5,
-        20,
-        40,
-        100,
-        325,
-        600,
-        1250,
-        2200,
-        4000,
-        5000
-    };
-
-    private static final int[] LUX_LEVELS_IDLE = {
-        0,
-        10,
-        40,
-        80,
-        200,
-        655,
-        1200,
-        2500,
-        4400,
-        8000,
-        10000
-    };
-
-    private static final float[] DISPLAY_LEVELS_NITS = {
-        13.25f,
-        54.0f,
-        78.85f,
-        105.02f,
-        132.7f,
-        170.12f,
-        212.1f,
-        265.2f,
-        335.8f,
-        415.2f,
-        478.5f,
-    };
-
-    private static final float[] DISPLAY_LEVELS_NITS_IDLE = {
-        23.25f,
-        64.0f,
-        88.85f,
-        115.02f,
-        142.7f,
-        180.12f,
-        222.1f,
-        275.2f,
-        345.8f,
-        425.2f,
-        468.5f,
-    };
-
-    private static final int[] DISPLAY_LEVELS_BACKLIGHT = {
-        9,
-        30,
-        45,
-        62,
-        78,
-        96,
-        119,
-        146,
-        178,
-        221,
-        255
-    };
-
-    private static final float[] DISPLAY_RANGE_NITS = { 2.685f, 478.5f };
-    private static final float[] BACKLIGHT_RANGE_ZERO_TO_ONE = { 0.0f, 1.0f };
-    private static final float[] DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT = { 0.03149606299f, 1.0f };
-
-    private static final float[] EMPTY_FLOAT_ARRAY = new float[0];
-    private static final int[] EMPTY_INT_ARRAY = new int[0];
-
-    private static final float MAXIMUM_GAMMA = 3.0f;
-
-    private static final float[] GAMMA_CORRECTION_LUX = {
-        0,
-        100,
-        1000,
-        2500,
-        4000,
-        4900,
-        5000
-    };
-    private static final float[] GAMMA_CORRECTION_NITS = {
-        1.0f,
-        10.55f,
-        96.5f,
-        239.75f,
-        383.0f,
-        468.95f,
-        478.5f,
-    };
-    private static final Spline GAMMA_CORRECTION_SPLINE = Spline.createSpline(
-            new float[] { 0.0f, 100.0f, 1000.0f, 2500.0f, 4000.0f, 4900.0f, 5000.0f },
-            new float[] { 0.0475f, 0.0475f, 0.2225f, 0.5140f, 0.8056f, 0.9805f, 1.0f });
-
-    private static final float TOLERANCE = 0.0001f;
-
-    @Mock
-    DisplayWhiteBalanceController mMockDwbc;
-
-    @Test
-    public void testSimpleStrategyMappingAtControlPoints() {
-        Resources res = createResources(DISPLAY_LEVELS_BACKLIGHT);
-        DisplayDeviceConfig ddc = createDdc();
-        BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNotNull("BrightnessMappingStrategy should not be null", simple);
-        for (int i = 0; i < LUX_LEVELS.length; i++) {
-            final float expectedLevel = MathUtils.map(PowerManager.BRIGHTNESS_OFF + 1,
-                    PowerManager.BRIGHTNESS_ON, PowerManager.BRIGHTNESS_MIN,
-                    PowerManager.BRIGHTNESS_MAX, DISPLAY_LEVELS_BACKLIGHT[i]);
-            assertEquals(expectedLevel,
-                    simple.getBrightness(LUX_LEVELS[i]), 0.0001f /*tolerance*/);
-        }
-    }
-
-    @Test
-    public void testSimpleStrategyMappingBetweenControlPoints() {
-        Resources res = createResources(DISPLAY_LEVELS_BACKLIGHT);
-        DisplayDeviceConfig ddc = createDdc();
-        BrightnessMappingStrategy simple = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNotNull("BrightnessMappingStrategy should not be null", simple);
-        for (int i = 1; i < LUX_LEVELS.length; i++) {
-            final float lux = (LUX_LEVELS[i - 1] + LUX_LEVELS[i]) / 2;
-            final float backlight = simple.getBrightness(lux) * PowerManager.BRIGHTNESS_ON;
-            assertTrue("Desired brightness should be between adjacent control points.",
-                    backlight > DISPLAY_LEVELS_BACKLIGHT[i - 1]
-                            && backlight < DISPLAY_LEVELS_BACKLIGHT[i]);
-        }
-    }
-
-    @Test
-    public void testSimpleStrategyIgnoresNewConfiguration() {
-        Resources res = createResources(DISPLAY_LEVELS_BACKLIGHT);
-        DisplayDeviceConfig ddc = createDdc();
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-
-        final float[] lux = { 0f, 1f };
-        final float[] nits = { 0, PowerManager.BRIGHTNESS_ON };
-
-        BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
-                .build();
-        strategy.setBrightnessConfiguration(config);
-        assertNotEquals(1.0f, strategy.getBrightness(1f), 0.0001f /*tolerance*/);
-    }
-
-    @Test
-    public void testSimpleStrategyIgnoresNullConfiguration() {
-        Resources res = createResources(DISPLAY_LEVELS_BACKLIGHT);
-        DisplayDeviceConfig ddc = createDdc();
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-
-        strategy.setBrightnessConfiguration(null);
-        final int N = DISPLAY_LEVELS_BACKLIGHT.length;
-        final float expectedBrightness =
-                (float) DISPLAY_LEVELS_BACKLIGHT[N - 1] / PowerManager.BRIGHTNESS_ON;
-        assertEquals(expectedBrightness,
-                strategy.getBrightness(LUX_LEVELS[N - 1]), 0.0001f /*tolerance*/);
-    }
-
-    @Test
-    public void testPhysicalStrategyMappingAtControlPoints() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT,
-                LUX_LEVELS, DISPLAY_LEVELS_NITS);
-        BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNotNull("BrightnessMappingStrategy should not be null", physical);
-        for (int i = 0; i < LUX_LEVELS.length; i++) {
-            final float expectedLevel = MathUtils.map(DISPLAY_RANGE_NITS[0], DISPLAY_RANGE_NITS[1],
-                    DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT[0],
-                    DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT[1],
-                    DISPLAY_LEVELS_NITS[i]);
-            assertEquals(expectedLevel,
-                    physical.getBrightness(LUX_LEVELS[i]),
-                    0.0001f /*tolerance*/);
-        }
-    }
-
-    @Test
-    public void testPhysicalStrategyMappingBetweenControlPoints() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE,
-                LUX_LEVELS, DISPLAY_LEVELS_NITS);
-        BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNotNull("BrightnessMappingStrategy should not be null", physical);
-        Spline brightnessToNits =
-                Spline.createSpline(BACKLIGHT_RANGE_ZERO_TO_ONE, DISPLAY_RANGE_NITS);
-        for (int i = 1; i < LUX_LEVELS.length; i++) {
-            final float lux = (LUX_LEVELS[i - 1] + LUX_LEVELS[i]) / 2.0f;
-            final float brightness = physical.getBrightness(lux);
-            final float nits = brightnessToNits.interpolate(brightness);
-            assertTrue("Desired brightness should be between adjacent control points: " + nits,
-                    nits > DISPLAY_LEVELS_NITS[i - 1] && nits < DISPLAY_LEVELS_NITS[i]);
-        }
-    }
-
-    @Test
-    public void testPhysicalStrategyUsesNewConfigurations() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-
-        final float[] lux = {0f, 1f};
-        final float[] nits = {
-                DISPLAY_RANGE_NITS[0],
-                DISPLAY_RANGE_NITS[DISPLAY_RANGE_NITS.length - 1]
-        };
-
-        BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
-                .build();
-        strategy.setBrightnessConfiguration(config);
-        assertEquals(1.0f, strategy.getBrightness(1f), 0.0001f /*tolerance*/);
-
-        // Check that null returns us to the default configuration.
-        strategy.setBrightnessConfiguration(null);
-        final int N = DISPLAY_LEVELS_NITS.length;
-        final float expectedBrightness = DISPLAY_LEVELS_NITS[N - 1] / DISPLAY_RANGE_NITS[1];
-        assertEquals(expectedBrightness,
-                strategy.getBrightness(LUX_LEVELS[N - 1]), 0.0001f /*tolerance*/);
-    }
-
-    @Test
-    public void testPhysicalStrategyRecalculateSplines() {
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        float[] adjustedNits50p = new float[DISPLAY_RANGE_NITS.length];
-        for (int i = 0; i < DISPLAY_RANGE_NITS.length; i++) {
-            adjustedNits50p[i] = DISPLAY_RANGE_NITS[i] * 0.5f;
-        }
-
-        // Default
-        assertEquals(DISPLAY_RANGE_NITS[0], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]),
-                TOLERANCE);
-        assertEquals(DISPLAY_RANGE_NITS[1], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]),
-                TOLERANCE);
-        assertEquals(DISPLAY_RANGE_NITS[0],
-                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]), TOLERANCE);
-        assertEquals(DISPLAY_RANGE_NITS[1],
-                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]), TOLERANCE);
-
-        // Adjustment is turned on
-        strategy.recalculateSplines(true, adjustedNits50p);
-        assertEquals(DISPLAY_RANGE_NITS[0], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]),
-                TOLERANCE);
-        assertEquals(DISPLAY_RANGE_NITS[1], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]),
-                TOLERANCE);
-        assertEquals(DISPLAY_RANGE_NITS[0] / 2,
-                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]), TOLERANCE);
-        assertEquals(DISPLAY_RANGE_NITS[1] / 2,
-                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]), TOLERANCE);
-
-        // Adjustment is turned off
-        strategy.recalculateSplines(false, adjustedNits50p);
-        assertEquals(DISPLAY_RANGE_NITS[0], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]),
-                TOLERANCE);
-        assertEquals(DISPLAY_RANGE_NITS[1], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]),
-                TOLERANCE);
-        assertEquals(DISPLAY_RANGE_NITS[0],
-                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]), TOLERANCE);
-        assertEquals(DISPLAY_RANGE_NITS[1],
-                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]), TOLERANCE);
-    }
-
-    @Test
-    public void testDefaultStrategyIsPhysical() {
-        Resources res = createResources(DISPLAY_LEVELS_BACKLIGHT);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, DISPLAY_LEVELS_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertTrue(strategy instanceof BrightnessMappingStrategy.PhysicalMappingStrategy);
-    }
-
-    @Test
-    public void testNonStrictlyIncreasingLuxLevelsFails() {
-        final float[] lux = Arrays.copyOf(LUX_LEVELS, LUX_LEVELS.length);
-        final int idx = lux.length / 2;
-        float tmp = lux[idx];
-        lux[idx] = lux[idx + 1];
-        lux[idx + 1] = tmp;
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux, DISPLAY_LEVELS_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNull(strategy);
-
-        // And make sure we get the same result even if it's monotone but not increasing.
-        lux[idx] = lux[idx + 1];
-        ddc = createDdc(DISPLAY_RANGE_NITS, DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux,
-                DISPLAY_LEVELS_NITS);
-        strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNull(strategy);
-    }
-
-    @Test
-    public void testDifferentNumberOfControlPointValuesFails() {
-        //Extra lux level
-        final float[] lux = Arrays.copyOf(LUX_LEVELS, LUX_LEVELS.length + 1);
-        // Make sure it's strictly increasing so that the only failure is the differing array
-        // lengths
-        lux[lux.length - 1] = lux[lux.length - 2] + 1;
-        Resources res = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, lux, DISPLAY_LEVELS_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNull(strategy);
-
-        res = createResources(DISPLAY_LEVELS_BACKLIGHT);
-        strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNull(strategy);
-
-        // Extra backlight level
-        final int[] backlight = Arrays.copyOf(
-                DISPLAY_LEVELS_BACKLIGHT, DISPLAY_LEVELS_BACKLIGHT.length + 1);
-        backlight[backlight.length - 1] = backlight[backlight.length - 2] + 1;
-        res = createResources(backlight);
-        ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, EMPTY_FLOAT_ARRAY);
-        strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNull(strategy);
-
-        // Extra nits level
-        final float[] nits = Arrays.copyOf(DISPLAY_RANGE_NITS, DISPLAY_LEVELS_NITS.length + 1);
-        nits[nits.length - 1] = nits[nits.length - 2] + 1;
-        res = createResources(EMPTY_INT_ARRAY);
-        ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, LUX_LEVELS, nits);
-        strategy = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNull(strategy);
-    }
-
-    @Test
-    public void testPhysicalStrategyRequiresNitsMapping() {
-        Resources res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
-        DisplayDeviceConfig ddc = createDdc(EMPTY_FLOAT_ARRAY /*nitsRange*/);
-        BrightnessMappingStrategy physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNull(physical);
-
-        res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
-        physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNull(physical);
-
-        res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
-        physical = BrightnessMappingStrategy.create(res, ddc, mMockDwbc);
-        assertNull(physical);
-    }
-
-    @Test
-    public void testStrategiesAdaptToUserDataPoint() {
-        Resources res = createResources(EMPTY_INT_ARRAY /*brightnessLevelsBacklight*/);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE,
-                LUX_LEVELS, DISPLAY_LEVELS_NITS);
-        assertStrategyAdaptsToUserDataPoints(BrightnessMappingStrategy.create(res, ddc, mMockDwbc));
-        ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE);
-        res = createResources(DISPLAY_LEVELS_BACKLIGHT);
-        assertStrategyAdaptsToUserDataPoints(BrightnessMappingStrategy.create(res, ddc, mMockDwbc));
-    }
-
-    @Test
-    public void testIdleModeConfigLoadsCorrectly() {
-        Resources res = createResourcesIdle(LUX_LEVELS_IDLE, DISPLAY_LEVELS_NITS_IDLE);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS, BACKLIGHT_RANGE_ZERO_TO_ONE);
-
-        // Create an idle mode bms
-        // This will fail if it tries to fetch the wrong configuration.
-        BrightnessMappingStrategy bms = BrightnessMappingStrategy.createForIdleMode(res, ddc,
-                mMockDwbc);
-        assertNotNull("BrightnessMappingStrategy should not be null", bms);
-
-        // Ensure that the config is the one we set
-        // Ensure that the lux -> brightness -> nits path works. ()
-        for (int i = 0; i < DISPLAY_LEVELS_NITS_IDLE.length; i++) {
-            assertEquals(LUX_LEVELS_IDLE[i], bms.getDefaultConfig().getCurve().first[i], TOLERANCE);
-            assertEquals(DISPLAY_LEVELS_NITS_IDLE[i], bms.getDefaultConfig().getCurve().second[i],
-                    TOLERANCE);
-            assertEquals(bms.convertToNits(bms.getBrightness(LUX_LEVELS_IDLE[i])),
-                    DISPLAY_LEVELS_NITS_IDLE[i], TOLERANCE);
-        }
-    }
-
-    private static void assertStrategyAdaptsToUserDataPoints(BrightnessMappingStrategy strategy) {
-        // Save out all of the initial brightness data for comparison after reset.
-        float[] initialBrightnessLevels = new float[LUX_LEVELS.length];
-        for (int i = 0; i < LUX_LEVELS.length; i++) {
-            initialBrightnessLevels[i] = strategy.getBrightness(LUX_LEVELS[i]);
-        }
-
-        // Add a data point in the middle of the curve where the user has set the brightness max
-        final int idx = LUX_LEVELS.length / 2;
-        strategy.addUserDataPoint(LUX_LEVELS[idx], 1.0f);
-
-        // Then make sure that all control points after the middle lux level are also set to max...
-        for (int i = idx; i < LUX_LEVELS.length; i++) {
-            assertEquals(strategy.getBrightness(LUX_LEVELS[idx]), 1.0, 0.0001f /*tolerance*/);
-        }
-
-        // ...and that all control points before the middle lux level are strictly less than the
-        // previous one still.
-        float prevBrightness = strategy.getBrightness(LUX_LEVELS[idx]);
-        for (int i = idx - 1; i >= 0; i--) {
-            float brightness = strategy.getBrightness(LUX_LEVELS[i]);
-            assertTrue("Brightness levels must be monotonic after adapting to user data",
-                    prevBrightness >= brightness);
-            prevBrightness = brightness;
-        }
-
-        // Now reset the curve and make sure we go back to the initial brightness levels recorded
-        // before adding the user data point.
-        strategy.clearUserDataPoints();
-        for (int i = 0; i < LUX_LEVELS.length; i++) {
-            assertEquals(initialBrightnessLevels[i], strategy.getBrightness(LUX_LEVELS[i]),
-                    0.0001f /*tolerance*/);
-        }
-
-        // Now set the middle of the lux range to something just above the minimum.
-        float minBrightness = strategy.getBrightness(LUX_LEVELS[0]);
-        strategy.addUserDataPoint(LUX_LEVELS[idx], minBrightness + 0.0001f);
-
-        // Then make sure the curve is still monotonic.
-        prevBrightness = 0f;
-        for (float lux : LUX_LEVELS) {
-            float brightness = strategy.getBrightness(lux);
-            assertTrue("Brightness levels must be monotonic after adapting to user data",
-                    prevBrightness <= brightness);
-            prevBrightness = brightness;
-        }
-
-        // And that the lowest lux level still gives the absolute minimum brightness. This should
-        // be true assuming that there are more than two lux levels in the curve since we picked a
-        // brightness just barely above the minimum for the middle of the curve.
-        minBrightness = (float) MathUtils.pow(minBrightness, MAXIMUM_GAMMA); // Gamma correction.
-        assertEquals(minBrightness, strategy.getBrightness(LUX_LEVELS[0]), 0.0001f /*tolerance*/);
-    }
-
-    private Resources createResources(int[] brightnessLevelsBacklight) {
-        return createResources(brightnessLevelsBacklight, EMPTY_INT_ARRAY, EMPTY_FLOAT_ARRAY);
-    }
-
-    private Resources createResourcesIdle(int[] luxLevelsIdle, float[] brightnessLevelsNitsIdle) {
-        return createResources(EMPTY_INT_ARRAY,
-                luxLevelsIdle, brightnessLevelsNitsIdle);
-    }
-
-    private Resources createResources(int[] brightnessLevelsBacklight, int[] luxLevelsIdle,
-            float[] brightnessLevelsNitsIdle) {
-
-        Resources mockResources = mock(Resources.class);
-        if (luxLevelsIdle.length > 0) {
-            int[] luxLevelsIdleResource = Arrays.copyOfRange(luxLevelsIdle, 1,
-                    luxLevelsIdle.length);
-            when(mockResources.getIntArray(
-                    com.android.internal.R.array.config_autoBrightnessLevelsIdle))
-                    .thenReturn(luxLevelsIdleResource);
-        }
-
-        when(mockResources.getIntArray(
-                com.android.internal.R.array.config_autoBrightnessLcdBacklightValues))
-                .thenReturn(brightnessLevelsBacklight);
-
-        TypedArray mockBrightnessLevelNitsIdle = createFloatTypedArray(brightnessLevelsNitsIdle);
-        when(mockResources.obtainTypedArray(
-                com.android.internal.R.array.config_autoBrightnessDisplayValuesNitsIdle))
-                .thenReturn(mockBrightnessLevelNitsIdle);
-
-        when(mockResources.getInteger(
-                com.android.internal.R.integer.config_screenBrightnessSettingMinimum))
-                .thenReturn(1);
-        when(mockResources.getInteger(
-                com.android.internal.R.integer.config_screenBrightnessSettingMaximum))
-                .thenReturn(255);
-        when(mockResources.getFraction(
-                com.android.internal.R.fraction.config_autoBrightnessAdjustmentMaxGamma, 1, 1))
-                .thenReturn(MAXIMUM_GAMMA);
-        return mockResources;
-    }
-
-    private DisplayDeviceConfig createDdc() {
-        return createDdc(DISPLAY_RANGE_NITS);
-    }
-
-    private DisplayDeviceConfig createDdc(float[] nitsArray) {
-        return createDdc(nitsArray, DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT);
-    }
-
-    private DisplayDeviceConfig createDdc(float[] nitsArray, float[] backlightArray) {
-        DisplayDeviceConfig mockDdc = mock(DisplayDeviceConfig.class);
-        when(mockDdc.getNits()).thenReturn(nitsArray);
-        when(mockDdc.getBrightness()).thenReturn(backlightArray);
-        when(mockDdc.getAutoBrightnessBrighteningLevelsLux()).thenReturn(LUX_LEVELS);
-        when(mockDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(EMPTY_FLOAT_ARRAY);
-        return mockDdc;
-    }
-
-    private DisplayDeviceConfig createDdc(float[] nitsArray, float[] backlightArray,
-            float[] luxLevelsFloat, float[] brightnessLevelsNits) {
-        DisplayDeviceConfig mockDdc = mock(DisplayDeviceConfig.class);
-        when(mockDdc.getNits()).thenReturn(nitsArray);
-        when(mockDdc.getBrightness()).thenReturn(backlightArray);
-        when(mockDdc.getAutoBrightnessBrighteningLevelsLux()).thenReturn(luxLevelsFloat);
-        when(mockDdc.getAutoBrightnessBrighteningLevelsNits()).thenReturn(brightnessLevelsNits);
-        return mockDdc;
-    }
-
-    private TypedArray createFloatTypedArray(float[] vals) {
-        TypedArray mockArray = mock(TypedArray.class);
-        when(mockArray.length()).thenAnswer(invocation -> {
-            return vals.length;
-        });
-        when(mockArray.getFloat(anyInt(), anyFloat())).thenAnswer(invocation -> {
-            final float def = (float) invocation.getArguments()[1];
-            if (vals == null) {
-                return def;
-            }
-            int idx = (int) invocation.getArguments()[0];
-            if (idx >= 0 && idx < vals.length) {
-                return vals[idx];
-            } else {
-                return def;
-            }
-        });
-        return mockArray;
-    }
-
-    // Gamma correction tests.
-    // x0 = 100   y0 = ~0.01
-    // x1 = 1000  y1 = ~0.20
-    // x2 = 2500  y2 = ~0.50
-    // x3 = 4000  y3 = ~0.80
-    // x4 = 4900  y4 = ~0.99
-
-    @Test
-    public void testGammaCorrectionLowChangeAtCenter() {
-        // If we set a user data point at (x2, y2^0.5), i.e. gamma = 0.5, it should bump the rest
-        // of the spline accordingly.
-        final int x1 = 1000;
-        final int x2 = 2500;
-        final int x3 = 4000;
-        final float y1 = GAMMA_CORRECTION_SPLINE.interpolate(x1);
-        final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2);
-        final float y3 = GAMMA_CORRECTION_SPLINE.interpolate(x3);
-
-        Resources resources = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
-                GAMMA_CORRECTION_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
-                mMockDwbc);
-        // Let's start with a validity check:
-        assertEquals(y1, strategy.getBrightness(x1), 0.0001f /* tolerance */);
-        assertEquals(y2, strategy.getBrightness(x2), 0.0001f /* tolerance */);
-        assertEquals(y3, strategy.getBrightness(x3), 0.0001f /* tolerance */);
-        // OK, let's roll:
-        float gamma = 0.5f;
-        strategy.addUserDataPoint(x2, (float) MathUtils.pow(y2, gamma));
-        assertEquals(MathUtils.pow(y1, gamma), strategy.getBrightness(x1), 0.0001f /* tolerance */);
-        assertEquals(MathUtils.pow(y2, gamma), strategy.getBrightness(x2), 0.0001f /* tolerance */);
-        assertEquals(MathUtils.pow(y3, gamma), strategy.getBrightness(x3), 0.0001f /* tolerance */);
-        // The adjustment should be +0.6308 (manual calculation).
-        assertEquals(+0.6308f, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
-    }
-
-    @Test
-    public void testGammaCorrectionHighChangeAtCenter() {
-        // This time we set a user data point at (x2, y2^0.25), i.e. gamma = 0.3 (the minimum),
-        // which should bump the rest of the spline accordingly, and further correct x2 to hit
-        // y2^0.25 (not y2^0.3).
-        final int x1 = 1000;
-        final int x2 = 2500;
-        final int x3 = 4000;
-        final float y1 = GAMMA_CORRECTION_SPLINE.interpolate(x1);
-        final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2);
-        final float y3 = GAMMA_CORRECTION_SPLINE.interpolate(x3);
-        Resources resources = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
-                GAMMA_CORRECTION_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
-                mMockDwbc);
-        // Validity check:
-        assertEquals(y1, strategy.getBrightness(x1), 0.0001f /* tolerance */);
-        assertEquals(y2, strategy.getBrightness(x2), 0.0001f /* tolerance */);
-        assertEquals(y3, strategy.getBrightness(x3), 0.0001f /* tolerance */);
-        // Let's roll:
-        float gamma = 0.25f;
-        final float minGamma = 1.0f / MAXIMUM_GAMMA;
-        strategy.addUserDataPoint(x2, (float) MathUtils.pow(y2, gamma));
-        assertEquals(MathUtils.pow(y1, minGamma),
-                strategy.getBrightness(x1), 0.0001f /* tolerance */);
-        assertEquals(MathUtils.pow(y2, gamma),
-                strategy.getBrightness(x2), 0.0001f /* tolerance */);
-        assertEquals(MathUtils.pow(y3, minGamma),
-                strategy.getBrightness(x3), 0.0001f /* tolerance */);
-        // The adjustment should be +1.0 (maximum adjustment).
-        assertEquals(+1.0f, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
-    }
-
-    @Test
-    public void testGammaCorrectionExtremeChangeAtCenter() {
-        // Extreme changes (e.g. setting brightness to 0.0 or 1.0) can't be gamma corrected, so we
-        // just make sure the adjustment reflects the change.
-        Resources resources = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
-                GAMMA_CORRECTION_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
-                mMockDwbc);
-        assertEquals(0.0f, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
-        strategy.addUserDataPoint(2500, 1.0f);
-        assertEquals(+1.0f, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
-        strategy.addUserDataPoint(2500, 0.0f);
-        assertEquals(-1.0f, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
-    }
-
-    @Test
-    public void testGammaCorrectionChangeAtEdges() {
-        // The algorithm behaves differently at the edges, because gamma correction there tends to
-        // be extreme. If we add a user data point at (x0, y0+0.3), the adjustment should be 0.3,
-        // resulting in a gamma of 3**-0.6 = ~0.52.
-        final int x0 = 100;
-        final int x2 = 2500;
-        final int x4 = 4900;
-        final float y0 = GAMMA_CORRECTION_SPLINE.interpolate(x0);
-        final float y2 = GAMMA_CORRECTION_SPLINE.interpolate(x2);
-        final float y4 = GAMMA_CORRECTION_SPLINE.interpolate(x4);
-        Resources resources = createResources(EMPTY_INT_ARRAY);
-        DisplayDeviceConfig ddc = createDdc(DISPLAY_RANGE_NITS,
-                DISPLAY_LEVELS_RANGE_BACKLIGHT_FLOAT, GAMMA_CORRECTION_LUX,
-                GAMMA_CORRECTION_NITS);
-        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(resources, ddc,
-                mMockDwbc);
-        // Validity, as per tradition:
-        assertEquals(y0, strategy.getBrightness(x0), 0.0001f /* tolerance */);
-        assertEquals(y2, strategy.getBrightness(x2), 0.0001f /* tolerance */);
-        assertEquals(y4, strategy.getBrightness(x4), 0.0001f /* tolerance */);
-        // Rollin':
-        float adjustment = 0.3f;
-        float gamma = (float) MathUtils.pow(MAXIMUM_GAMMA, -adjustment);
-        strategy.addUserDataPoint(x0, y0 + adjustment);
-        assertEquals(y0 + adjustment, strategy.getBrightness(x0), 0.0001f /* tolerance */);
-        assertEquals(MathUtils.pow(y2, gamma), strategy.getBrightness(x2), 0.0001f /* tolerance */);
-        assertEquals(MathUtils.pow(y4, gamma), strategy.getBrightness(x4), 0.0001f /* tolerance */);
-        assertEquals(adjustment, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
-        // Similarly, if we set a user data point at (x4, 1.0), the adjustment should be 1 - y4.
-        adjustment = 1.0f - y4;
-        gamma = (float) MathUtils.pow(MAXIMUM_GAMMA, -adjustment);
-        strategy.addUserDataPoint(x4, 1.0f);
-        assertEquals(MathUtils.pow(y0, gamma), strategy.getBrightness(x0), 0.0001f /* tolerance */);
-        assertEquals(MathUtils.pow(y2, gamma), strategy.getBrightness(x2), 0.0001f /* tolerance */);
-        assertEquals(1.0f, strategy.getBrightness(x4), 0.0001f /* tolerance */);
-        assertEquals(adjustment, strategy.getAutoBrightnessAdjustment(), 0.0001f /* tolerance */);
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java
deleted file mode 100644
index 46956d7..0000000
--- a/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java
+++ /dev/null
@@ -1,493 +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.server.display;
-
-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.assertTrue;
-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.hardware.display.BrightnessInfo;
-import android.os.Handler;
-import android.os.IThermalEventListener;
-import android.os.IThermalService;
-import android.os.Message;
-import android.os.PowerManager;
-import android.os.Temperature;
-import android.os.Temperature.ThrottlingStatus;
-import android.os.test.TestLooper;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.os.BackgroundThread;
-import com.android.server.display.BrightnessThrottler.Injector;
-import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData;
-import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel;
-import com.android.server.display.mode.DisplayModeDirectorTest;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class BrightnessThrottlerTest {
-    private static final float EPSILON = 0.000001f;
-
-    private Handler mHandler;
-    private TestLooper mTestLooper;
-
-    @Mock IThermalService mThermalServiceMock;
-    @Mock Injector mInjectorMock;
-
-    DisplayModeDirectorTest.FakeDeviceConfig mDeviceConfigFake;
-
-    @Captor ArgumentCaptor<IThermalEventListener> mThermalEventListenerCaptor;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        when(mInjectorMock.getThermalService()).thenReturn(mThermalServiceMock);
-        mTestLooper = new TestLooper();
-        mHandler = new Handler(mTestLooper.getLooper(), new Handler.Callback() {
-            @Override
-            public boolean handleMessage(Message msg) {
-                return true;
-            }
-        });
-        mDeviceConfigFake = new DisplayModeDirectorTest.FakeDeviceConfig();
-        when(mInjectorMock.getDeviceConfig()).thenReturn(mDeviceConfigFake);
-
-    }
-
-    /////////////////
-    // Test Methods
-    /////////////////
-
-    @Test
-    public void testThermalBrightnessThrottlingData() {
-        List<ThrottlingLevel> singleLevel = new ArrayList<>();
-        singleLevel.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.25f));
-
-        List<ThrottlingLevel> validLevels = new ArrayList<>();
-        validLevels.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE, 0.62f));
-        validLevels.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.25f));
-
-        List<ThrottlingLevel> unsortedThermalLevels = new ArrayList<>();
-        unsortedThermalLevels.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.62f));
-        unsortedThermalLevels.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE, 0.25f));
-
-        List<ThrottlingLevel> unsortedBrightnessLevels = new ArrayList<>();
-        unsortedBrightnessLevels.add(
-                new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE, 0.25f));
-        unsortedBrightnessLevels.add(
-                new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.62f));
-
-        List<ThrottlingLevel> unsortedLevels = new ArrayList<>();
-        unsortedLevels.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.25f));
-        unsortedLevels.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE, 0.62f));
-
-        List<ThrottlingLevel> invalidLevel = new ArrayList<>();
-        invalidLevel.add(new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
-                PowerManager.BRIGHTNESS_MAX + EPSILON));
-
-        // Test invalid data
-        ThermalBrightnessThrottlingData data;
-        data = ThermalBrightnessThrottlingData.create((List<ThrottlingLevel>) null);
-        assertEquals(data, null);
-        data = ThermalBrightnessThrottlingData.create(new ArrayList<ThrottlingLevel>());
-        assertEquals(data, null);
-        data = ThermalBrightnessThrottlingData.create(unsortedThermalLevels);
-        assertEquals(data, null);
-        data = ThermalBrightnessThrottlingData.create(unsortedBrightnessLevels);
-        assertEquals(data, null);
-        data = ThermalBrightnessThrottlingData.create(unsortedLevels);
-        assertEquals(data, null);
-        data = ThermalBrightnessThrottlingData.create(invalidLevel);
-        assertEquals(data, null);
-
-        // Test valid data
-        data = ThermalBrightnessThrottlingData.create(singleLevel);
-        assertNotEquals(data, null);
-        assertThrottlingLevelsEquals(singleLevel, data.throttlingLevels);
-
-        data = ThermalBrightnessThrottlingData.create(validLevels);
-        assertNotEquals(data, null);
-        assertThrottlingLevelsEquals(validLevels, data.throttlingLevels);
-    }
-
-    @Test
-    public void testThermalThrottlingUnsupported() {
-        final BrightnessThrottler throttler = createThrottlerUnsupported();
-        assertFalse(throttler.deviceSupportsThrottling());
-
-        // Thermal listener shouldn't be registered if throttling is unsupported
-        verify(mInjectorMock, never()).getThermalService();
-
-        // Ensure that brightness is uncapped when the device doesn't support throttling
-        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
-    }
-
-    @Test
-    public void testThermalThrottlingSingleLevel() throws Exception {
-        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
-            0.25f);
-
-        List<ThrottlingLevel> levels = new ArrayList<>();
-        levels.add(level);
-        final ThermalBrightnessThrottlingData data = ThermalBrightnessThrottlingData.create(levels);
-        final BrightnessThrottler throttler = createThrottlerSupported(data);
-        assertTrue(throttler.deviceSupportsThrottling());
-
-        verify(mThermalServiceMock).registerThermalEventListenerWithType(
-                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
-        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
-
-        // Set status too low to trigger throttling
-        listener.notifyThrottling(getSkinTemp(level.thermalStatus - 1));
-        mTestLooper.dispatchAll();
-        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
-        assertFalse(throttler.isThrottled());
-        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, throttler.getBrightnessMaxReason());
-
-        // Set status just high enough to trigger throttling
-        listener.notifyThrottling(getSkinTemp(level.thermalStatus));
-        mTestLooper.dispatchAll();
-        assertEquals(level.brightness, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
-            throttler.getBrightnessMaxReason());
-
-        // Set status more than high enough to trigger throttling
-        listener.notifyThrottling(getSkinTemp(level.thermalStatus + 1));
-        mTestLooper.dispatchAll();
-        assertEquals(level.brightness, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
-           throttler.getBrightnessMaxReason());
-
-        // Return to the lower throttling level
-        listener.notifyThrottling(getSkinTemp(level.thermalStatus));
-        mTestLooper.dispatchAll();
-        assertEquals(level.brightness, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
-            throttler.getBrightnessMaxReason());
-
-        // Cool down
-        listener.notifyThrottling(getSkinTemp(level.thermalStatus - 1));
-        mTestLooper.dispatchAll();
-        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
-        assertFalse(throttler.isThrottled());
-        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE,
-            throttler.getBrightnessMaxReason());
-    }
-
-    @Test
-    public void testThermalThrottlingMultiLevel() throws Exception {
-        final ThrottlingLevel levelLo = new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE,
-            0.62f);
-        final ThrottlingLevel levelHi = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
-            0.25f);
-
-        List<ThrottlingLevel> levels = new ArrayList<>();
-        levels.add(levelLo);
-        levels.add(levelHi);
-        final ThermalBrightnessThrottlingData data = ThermalBrightnessThrottlingData.create(levels);
-        final BrightnessThrottler throttler = createThrottlerSupported(data);
-        assertTrue(throttler.deviceSupportsThrottling());
-
-        verify(mThermalServiceMock).registerThermalEventListenerWithType(
-                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
-        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
-
-        // Set status too low to trigger throttling
-        listener.notifyThrottling(getSkinTemp(levelLo.thermalStatus - 1));
-        mTestLooper.dispatchAll();
-        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
-        assertFalse(throttler.isThrottled());
-        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, throttler.getBrightnessMaxReason());
-
-        // Set status just high enough to trigger throttling
-        listener.notifyThrottling(getSkinTemp(levelLo.thermalStatus));
-        mTestLooper.dispatchAll();
-        assertEquals(levelLo.brightness, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
-            throttler.getBrightnessMaxReason());
-
-        // Set status to an intermediate throttling level
-        listener.notifyThrottling(getSkinTemp(levelLo.thermalStatus + 1));
-        mTestLooper.dispatchAll();
-        assertEquals(levelLo.brightness, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
-            throttler.getBrightnessMaxReason());
-
-        // Set status to the highest configured throttling level
-        listener.notifyThrottling(getSkinTemp(levelHi.thermalStatus));
-        mTestLooper.dispatchAll();
-        assertEquals(levelHi.brightness, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
-            throttler.getBrightnessMaxReason());
-
-        // Set status to exceed the highest configured throttling level
-        listener.notifyThrottling(getSkinTemp(levelHi.thermalStatus + 1));
-        mTestLooper.dispatchAll();
-        assertEquals(levelHi.brightness, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
-            throttler.getBrightnessMaxReason());
-
-        // Return to an intermediate throttling level
-        listener.notifyThrottling(getSkinTemp(levelLo.thermalStatus + 1));
-        mTestLooper.dispatchAll();
-        assertEquals(levelLo.brightness, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
-            throttler.getBrightnessMaxReason());
-
-        // Return to the lowest configured throttling level
-        listener.notifyThrottling(getSkinTemp(levelLo.thermalStatus));
-        mTestLooper.dispatchAll();
-        assertEquals(levelLo.brightness, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL,
-            throttler.getBrightnessMaxReason());
-
-        // Cool down
-        listener.notifyThrottling(getSkinTemp(levelLo.thermalStatus - 1));
-        mTestLooper.dispatchAll();
-        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
-        assertFalse(throttler.isThrottled());
-        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, throttler.getBrightnessMaxReason());
-    }
-
-    @Test public void testUpdateThermalThrottlingData() throws Exception {
-        // Initialise brightness throttling levels
-        // Ensure that they are overridden by setting the data through device config.
-        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
-                0.25f);
-        List<ThrottlingLevel> levels = new ArrayList<>();
-        levels.add(level);
-        final ThermalBrightnessThrottlingData data = ThermalBrightnessThrottlingData.create(levels);
-        mDeviceConfigFake.setThermalBrightnessThrottlingData("123,1,critical,0.4");
-        final BrightnessThrottler throttler = createThrottlerSupported(data);
-
-        verify(mThermalServiceMock).registerThermalEventListenerWithType(
-                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
-        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.4f);
-
-        // Set new (valid) data from device config
-        mDeviceConfigFake.setThermalBrightnessThrottlingData("123,1,critical,0.8");
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.8f);
-
-        mDeviceConfigFake.setThermalBrightnessThrottlingData(
-                "123,1,critical,0.75;123,1,critical,0.99,id_2");
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.75f);
-        mDeviceConfigFake.setThermalBrightnessThrottlingData(
-                "123,1,critical,0.8,default;123,1,critical,0.99,id_2");
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.8f);
-    }
-
-    @Test public void testInvalidThrottlingStrings() throws Exception {
-        // Initialise brightness throttling levels
-        // Ensure that they are not overridden by invalid data through device config.
-        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
-                0.25f);
-        List<ThrottlingLevel> levels = new ArrayList<>();
-        levels.add(level);
-        final ThermalBrightnessThrottlingData data = ThermalBrightnessThrottlingData.create(levels);
-        final BrightnessThrottler throttler = createThrottlerSupported(data);
-        verify(mThermalServiceMock).registerThermalEventListenerWithType(
-                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
-        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
-
-        // None of these are valid so shouldn't override the original data
-
-        // Not the current id
-        mDeviceConfigFake.setThermalBrightnessThrottlingData("321,1,critical,0.4");
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
-        // Incorrect number
-        mDeviceConfigFake.setThermalBrightnessThrottlingData("123,0,critical,0.4");
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
-        // Incorrect number
-        mDeviceConfigFake.setThermalBrightnessThrottlingData("123,2,critical,0.4");
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
-        // Invalid level
-        mDeviceConfigFake.setThermalBrightnessThrottlingData("123,1,invalid,0.4");
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
-        // Invalid brightness
-        mDeviceConfigFake.setThermalBrightnessThrottlingData("123,1,critical,none");
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
-        // Invalid brightness
-        mDeviceConfigFake.setThermalBrightnessThrottlingData("123,1,critical,-3");
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
-        // Invalid format
-        mDeviceConfigFake.setThermalBrightnessThrottlingData("invalid string");
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
-        // Invalid format
-        mDeviceConfigFake.setThermalBrightnessThrottlingData("");
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
-        // Invalid string format
-        mDeviceConfigFake.setThermalBrightnessThrottlingData(
-                "123,default,1,critical,0.75,1,critical,0.99");
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
-        // Invalid level string and number string
-        mDeviceConfigFake.setThermalBrightnessThrottlingData(
-                "123,1,1,critical,0.75,id_2,1,critical,0.99");
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
-        // Invalid format - (two default ids for same display)
-        mDeviceConfigFake.setThermalBrightnessThrottlingData(
-                "123,1,critical,0.75,default;123,1,critical,0.99");
-        testThermalThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
-    }
-
-    private void testThermalThrottling(BrightnessThrottler throttler,
-            IThermalEventListener listener, float tooLowCap, float tooHighCap) throws Exception {
-        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
-                tooHighCap);
-
-        // Set status too low to trigger throttling
-        listener.notifyThrottling(getSkinTemp(level.thermalStatus - 1));
-        mTestLooper.dispatchAll();
-        assertEquals(tooLowCap, throttler.getBrightnessCap(), 0f);
-        assertFalse(throttler.isThrottled());
-
-        // Set status high enough to trigger throttling
-        listener.notifyThrottling(getSkinTemp(level.thermalStatus));
-        mTestLooper.dispatchAll();
-        assertEquals(tooHighCap, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-    }
-
-    @Test public void testMultipleConfigPoints() throws Exception {
-        // Initialise brightness throttling levels
-        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
-                0.25f);
-        List<ThrottlingLevel> levels = new ArrayList<>();
-        levels.add(level);
-        final ThermalBrightnessThrottlingData data = ThermalBrightnessThrottlingData.create(levels);
-
-        // These are identical to the string set below
-        final ThrottlingLevel levelSevere = new ThrottlingLevel(PowerManager.THERMAL_STATUS_SEVERE,
-                0.9f);
-        final ThrottlingLevel levelCritical = new ThrottlingLevel(
-                PowerManager.THERMAL_STATUS_CRITICAL, 0.5f);
-        final ThrottlingLevel levelEmergency = new ThrottlingLevel(
-                PowerManager.THERMAL_STATUS_EMERGENCY, 0.1f);
-
-        mDeviceConfigFake.setThermalBrightnessThrottlingData(
-                "123,3,severe,0.9,critical,0.5,emergency,0.1");
-        final BrightnessThrottler throttler = createThrottlerSupported(data);
-
-        verify(mThermalServiceMock).registerThermalEventListenerWithType(
-                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
-        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
-
-        // Ensure that the multiple levels set via the string through the device config correctly
-        // override the original display device config ones.
-
-        // levelSevere
-        // Set status too low to trigger throttling
-        listener.notifyThrottling(getSkinTemp(levelSevere.thermalStatus - 1));
-        mTestLooper.dispatchAll();
-        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
-        assertFalse(throttler.isThrottled());
-
-        // Set status high enough to trigger throttling
-        listener.notifyThrottling(getSkinTemp(levelSevere.thermalStatus));
-        mTestLooper.dispatchAll();
-        assertEquals(0.9f, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-
-        // levelCritical
-        // Set status too low to trigger throttling
-        listener.notifyThrottling(getSkinTemp(levelCritical.thermalStatus - 1));
-        mTestLooper.dispatchAll();
-        assertEquals(0.9f, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-
-        // Set status high enough to trigger throttling
-        listener.notifyThrottling(getSkinTemp(levelCritical.thermalStatus));
-        mTestLooper.dispatchAll();
-        assertEquals(0.5f, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-
-        //levelEmergency
-        // Set status too low to trigger throttling
-        listener.notifyThrottling(getSkinTemp(levelEmergency.thermalStatus - 1));
-        mTestLooper.dispatchAll();
-        assertEquals(0.5f, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-
-        // Set status high enough to trigger throttling
-        listener.notifyThrottling(getSkinTemp(levelEmergency.thermalStatus));
-        mTestLooper.dispatchAll();
-        assertEquals(0.1f, throttler.getBrightnessCap(), 0f);
-        assertTrue(throttler.isThrottled());
-    }
-
-    private void assertThrottlingLevelsEquals(
-            List<ThrottlingLevel> expected,
-            List<ThrottlingLevel> actual) {
-        assertEquals(expected.size(), actual.size());
-
-        for (int i = 0; i < expected.size(); i++) {
-            ThrottlingLevel expectedLevel = expected.get(i);
-            ThrottlingLevel actualLevel = actual.get(i);
-
-            assertEquals(expectedLevel.thermalStatus, actualLevel.thermalStatus);
-            assertEquals(expectedLevel.brightness, actualLevel.brightness, 0.0f);
-        }
-    }
-
-    private BrightnessThrottler createThrottlerUnsupported() {
-        return new BrightnessThrottler(mInjectorMock, mHandler, mHandler,
-                /* throttlingChangeCallback= */ () -> {}, /* uniqueDisplayId= */ null,
-                /* thermalThrottlingDataId= */ null,
-                /* thermalThrottlingDataMap= */ new HashMap<>(1));
-    }
-
-    private BrightnessThrottler createThrottlerSupported(ThermalBrightnessThrottlingData data) {
-        assertNotNull(data);
-        HashMap<String, ThermalBrightnessThrottlingData> throttlingDataMap = new HashMap<>(1);
-        throttlingDataMap.put("default", data);
-        return new BrightnessThrottler(mInjectorMock, mHandler, BackgroundThread.getHandler(),
-                () -> {}, "123", "default", throttlingDataMap);
-    }
-
-    private Temperature getSkinTemp(@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/BrightnessTrackerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
deleted file mode 100644
index 021f2d1..0000000
--- a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
+++ /dev/null
@@ -1,1247 +0,0 @@
-/*
- * 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
- */
-
-package com.android.server.display;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.app.ActivityTaskManager.RootTaskInfo;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ParceledListSlice;
-import android.database.ContentObserver;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.display.AmbientBrightnessDayStats;
-import android.hardware.display.BrightnessChangeEvent;
-import android.hardware.display.BrightnessConfiguration;
-import android.hardware.display.ColorDisplayManager;
-import android.hardware.display.DisplayManager;
-import android.hardware.display.DisplayedContentSample;
-import android.hardware.display.DisplayedContentSamplingAttributes;
-import android.hardware.input.InputSensorInfo;
-import android.os.BatteryManager;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.MessageQueue;
-import android.os.Parcel;
-import android.os.RemoteException;
-import android.os.SystemClock;
-import android.os.UserManager;
-import android.provider.Settings;
-import android.util.AtomicFile;
-import android.view.Display;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.R;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.reflect.Constructor;
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class BrightnessTrackerTest {
-    private static final float DEFAULT_INITIAL_BRIGHTNESS = 2.5f;
-    private static final boolean DEFAULT_COLOR_SAMPLING_ENABLED = true;
-    private static final String DEFAULT_DISPLAY_ID = "123";
-    private static final float FLOAT_DELTA = 0.01f;
-
-    @Mock private InputSensorInfo mInputSensorInfoMock;
-
-    private BrightnessTracker mTracker;
-    private TestInjector mInjector;
-    private Sensor mLightSensorFake;
-
-    private static Object sHandlerLock = new Object();
-    private static Handler sHandler;
-    private static HandlerThread sThread =
-            new HandlerThread("brightness.test", android.os.Process.THREAD_PRIORITY_BACKGROUND);
-
-    private int mDefaultNightModeColorTemperature;
-    private float mRbcOffsetFactor;
-
-    private static Handler ensureHandler() {
-        synchronized (sHandlerLock) {
-            if (sHandler == null) {
-                sThread.start();
-                sHandler = new Handler(sThread.getLooper());
-            }
-            return sHandler;
-        }
-    }
-
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        mInjector = new TestInjector(ensureHandler());
-        mLightSensorFake = new Sensor(mInputSensorInfoMock);
-
-        mTracker = new BrightnessTracker(InstrumentationRegistry.getContext(), mInjector);
-        mTracker.setLightSensor(mLightSensorFake);
-        mDefaultNightModeColorTemperature =
-                InstrumentationRegistry.getContext().getResources().getInteger(
-                R.integer.config_nightDisplayColorTemperatureDefault);
-        mRbcOffsetFactor = InstrumentationRegistry.getContext()
-                .getSystemService(ColorDisplayManager.class).getReduceBrightColorsOffsetFactor();
-    }
-
-    @Test
-    public void testStartStopTrackerScreenOnOff() {
-        mInjector.mInteractive = false;
-        startTracker(mTracker);
-        assertNull(mInjector.mSensorListener);
-        assertNotNull(mInjector.mBroadcastReceiver);
-        assertTrue(mInjector.mIdleScheduled);
-        mInjector.sendScreenChange(/* screenOn= */ true);
-        assertNotNull(mInjector.mSensorListener);
-        assertTrue(mInjector.mColorSamplingEnabled);
-
-        mInjector.sendScreenChange(/* screenOn= */ false);
-        assertNull(mInjector.mSensorListener);
-        assertFalse(mInjector.mColorSamplingEnabled);
-
-        // Turn screen on while brightness mode is manual
-        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ false);
-        mInjector.sendScreenChange(/* screenOn= */ true);
-        assertNull(mInjector.mSensorListener);
-        assertFalse(mInjector.mColorSamplingEnabled);
-
-        // Set brightness mode to automatic while screen is off.
-        mInjector.sendScreenChange(/* screenOn= */ false);
-        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ true);
-        assertNull(mInjector.mSensorListener);
-        assertFalse(mInjector.mColorSamplingEnabled);
-
-        // Turn on screen while brightness mode is automatic.
-        mInjector.sendScreenChange(/* screenOn= */ true);
-        assertNotNull(mInjector.mSensorListener);
-        assertTrue(mInjector.mColorSamplingEnabled);
-
-        mTracker.stop();
-        assertNull(mInjector.mSensorListener);
-        assertNull(mInjector.mBroadcastReceiver);
-        assertFalse(mInjector.mIdleScheduled);
-        assertFalse(mInjector.mColorSamplingEnabled);
-    }
-
-    @Test
-    public void testModifyBrightnessConfiguration() {
-        mInjector.mInteractive = true;
-        // Start with tracker not listening for color samples.
-        startTracker(mTracker, DEFAULT_INITIAL_BRIGHTNESS, /* collectColorSamples= */ false);
-        assertFalse(mInjector.mColorSamplingEnabled);
-
-        // Update brightness config to enabled color sampling.
-        mTracker.setShouldCollectColorSample(/* collectColorSamples= */ true);
-        mInjector.waitForHandler();
-        assertTrue(mInjector.mColorSamplingEnabled);
-
-        // Update brightness config to disable color sampling.
-        mTracker.setShouldCollectColorSample(/* collectColorSamples= */ false);
-        mInjector.waitForHandler();
-        assertFalse(mInjector.mColorSamplingEnabled);
-
-        // Pretend screen is off, update config to turn on color sampling.
-        mInjector.sendScreenChange(/* screenOn= */ false);
-        mTracker.setShouldCollectColorSample(/* collectColorSamples= */ true);
-        mInjector.waitForHandler();
-        assertFalse(mInjector.mColorSamplingEnabled);
-
-        // Pretend screen is on.
-        mInjector.sendScreenChange(/* screenOn= */ true);
-        assertTrue(mInjector.mColorSamplingEnabled);
-
-        mTracker.stop();
-        assertFalse(mInjector.mColorSamplingEnabled);
-    }
-
-    @Test
-    public void testNoColorSampling_WrongPixelFormat() {
-        mInjector.mDefaultSamplingAttributes =
-                new DisplayedContentSamplingAttributes(
-                        0x23,
-                        mInjector.mDefaultSamplingAttributes.getDataspace(),
-                        mInjector.mDefaultSamplingAttributes.getComponentMask());
-        startTracker(mTracker);
-        assertFalse(mInjector.mColorSamplingEnabled);
-        assertNull(mInjector.mDisplayListener);
-    }
-
-    @Test
-    public void testNoColorSampling_MissingComponent() {
-        mInjector.mDefaultSamplingAttributes =
-                new DisplayedContentSamplingAttributes(
-                        mInjector.mDefaultSamplingAttributes.getPixelFormat(),
-                        mInjector.mDefaultSamplingAttributes.getDataspace(),
-                        0x2);
-        startTracker(mTracker);
-        assertFalse(mInjector.mColorSamplingEnabled);
-        assertNull(mInjector.mDisplayListener);
-    }
-
-    @Test
-    public void testNoColorSampling_NoSupport() {
-        mInjector.mDefaultSamplingAttributes = null;
-        startTracker(mTracker);
-        assertFalse(mInjector.mColorSamplingEnabled);
-        assertNull(mInjector.mDisplayListener);
-    }
-
-    @Test
-    public void testColorSampling_FrameRateChange() {
-        startTracker(mTracker);
-        assertTrue(mInjector.mColorSamplingEnabled);
-        assertNotNull(mInjector.mDisplayListener);
-        int noFramesSampled = mInjector.mNoColorSamplingFrames;
-        mInjector.mFrameRate = 120.0f;
-        // Wrong display
-        mInjector.mDisplayListener.onDisplayChanged(Display.DEFAULT_DISPLAY + 10);
-        assertEquals(noFramesSampled, mInjector.mNoColorSamplingFrames);
-        // Correct display
-        mInjector.mDisplayListener.onDisplayChanged(Display.DEFAULT_DISPLAY);
-        assertEquals(noFramesSampled * 2, mInjector.mNoColorSamplingFrames);
-    }
-
-    @Test
-    public void testAdaptiveOnOff() {
-        mInjector.mInteractive = true;
-        mInjector.mIsBrightnessModeAutomatic = false;
-        startTracker(mTracker);
-        assertNull(mInjector.mSensorListener);
-        assertNotNull(mInjector.mBroadcastReceiver);
-        assertNotNull(mInjector.mContentObserver);
-        assertTrue(mInjector.mIdleScheduled);
-        assertFalse(mInjector.mColorSamplingEnabled);
-        assertNull(mInjector.mDisplayListener);
-
-        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ true);
-        assertNotNull(mInjector.mSensorListener);
-        assertTrue(mInjector.mColorSamplingEnabled);
-        assertNotNull(mInjector.mDisplayListener);
-
-        SensorEventListener listener = mInjector.mSensorListener;
-        DisplayManager.DisplayListener displayListener = mInjector.mDisplayListener;
-        mInjector.mSensorListener = null;
-        mInjector.mColorSamplingEnabled = false;
-        mInjector.mDisplayListener = null;
-        // Duplicate notification
-        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ true);
-        // Sensor shouldn't have been registered as it was already registered.
-        assertNull(mInjector.mSensorListener);
-        assertFalse(mInjector.mColorSamplingEnabled);
-        assertNull(mInjector.mDisplayListener);
-        mInjector.mDisplayListener = displayListener;
-        mInjector.mColorSamplingEnabled = true;
-
-        mInjector.setBrightnessMode(/* isBrightnessModeAutomatic= */ false);
-        assertNull(mInjector.mSensorListener);
-        assertFalse(mInjector.mColorSamplingEnabled);
-        assertNull(mInjector.mDisplayListener);
-
-        mTracker.stop();
-        assertNull(mInjector.mSensorListener);
-        assertNull(mInjector.mBroadcastReceiver);
-        assertNull(mInjector.mContentObserver);
-        assertFalse(mInjector.mIdleScheduled);
-        assertFalse(mInjector.mColorSamplingEnabled);
-        assertNull(mInjector.mDisplayListener);
-    }
-
-    @Test
-    public void testBrightnessEvent() {
-        final float brightness = 0.5f;
-        final String displayId = "1234";
-
-        startTracker(mTracker);
-        final long sensorTime = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos());
-        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
-        final long currentTime = mInjector.currentTimeMillis();
-        notifyBrightnessChanged(mTracker, brightness, displayId, new float[] {1.0f},
-                new long[] {sensorTime});
-        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
-        mTracker.stop();
-
-        assertEquals(1, events.size());
-        BrightnessChangeEvent event = events.get(0);
-        assertEquals(currentTime, event.timeStamp);
-        assertEquals(displayId, event.uniqueDisplayId);
-        assertEquals(1, event.luxValues.length);
-        assertEquals(1.0f, event.luxValues[0], FLOAT_DELTA);
-        assertEquals(currentTime - TimeUnit.SECONDS.toMillis(2),
-                event.luxTimestamps[0]);
-        assertEquals(brightness, event.brightness, FLOAT_DELTA);
-        assertEquals(DEFAULT_INITIAL_BRIGHTNESS, event.lastBrightness, FLOAT_DELTA);
-
-        // System had no data so these should all be at defaults.
-        assertEquals(Float.NaN, event.batteryLevel, 0.0);
-        assertFalse(event.nightMode);
-        assertEquals(mDefaultNightModeColorTemperature, event.colorTemperature);
-    }
-
-    @Test
-    public void testMultipleBrightnessEvents() {
-        final float brightnessOne = 0.2f;
-        final float brightnessTwo = 0.4f;
-        final float brightnessThree = 0.6f;
-        final float brightnessFour = 0.3f;
-        final String displayId = "1234";
-        final float[] luxValues = new float[]{1.0f};
-
-        startTracker(mTracker);
-        final long sensorTime = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos());
-        final long sensorTime2 = sensorTime + TimeUnit.SECONDS.toMillis(20);
-        final long sensorTime3 = sensorTime2 + TimeUnit.SECONDS.toMillis(30);
-        final long sensorTime4 = sensorTime3 + TimeUnit.SECONDS.toMillis(40);
-        final long originalTime = mInjector.currentTimeMillis();
-
-        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
-        notifyBrightnessChanged(mTracker, brightnessOne, displayId, luxValues,
-                new long[] {sensorTime});
-
-        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(20));
-        notifyBrightnessChanged(mTracker, brightnessTwo, displayId, luxValues,
-                new long[] {sensorTime2});
-
-        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(30));
-        notifyBrightnessChanged(mTracker, brightnessThree, displayId, luxValues,
-                new long[] {sensorTime3});
-
-        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(40));
-        notifyBrightnessChanged(mTracker, brightnessFour, displayId, luxValues,
-                new long[] {sensorTime4});
-        mTracker.stop();
-        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
-        assertEquals(4, events.size());
-        BrightnessChangeEvent eventOne = events.get(0);
-        assertEquals(brightnessOne, eventOne.brightness, FLOAT_DELTA);
-        assertEquals(originalTime,
-                eventOne.luxTimestamps[0]);
-
-        BrightnessChangeEvent eventTwo = events.get(1);
-        assertEquals(brightnessTwo, eventTwo.brightness, FLOAT_DELTA);
-        assertEquals(originalTime + TimeUnit.SECONDS.toMillis(20),
-                eventTwo.luxTimestamps[0]);
-
-        BrightnessChangeEvent eventThree = events.get(2);
-        assertEquals(brightnessThree, eventThree.brightness, FLOAT_DELTA);
-        assertEquals(originalTime + TimeUnit.SECONDS.toMillis(50),
-                eventThree.luxTimestamps[0]);
-
-        BrightnessChangeEvent eventFour = events.get(3);
-        assertEquals(brightnessFour, eventFour.brightness, FLOAT_DELTA);
-        assertEquals(originalTime + TimeUnit.SECONDS.toMillis(90),
-                eventFour.luxTimestamps[0]);
-    }
-
-    @Test
-    public void testBrightnessFullPopulatedEvent() {
-        final int initialBrightness = 230;
-        final int brightness = 130;
-        final String displayId = "1234";
-
-        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_ACTIVATED, 1);
-        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE, 3333);
-
-        mInjector.mSecureIntSettings.put(Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, 1);
-        mInjector.mSecureIntSettings.put(Settings.Secure.REDUCE_BRIGHT_COLORS_LEVEL, 40);
-
-        startTracker(mTracker, initialBrightness, DEFAULT_COLOR_SAMPLING_ENABLED);
-        mInjector.mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(),
-                batteryChangeEvent(30, 60));
-        final long currentTime = mInjector.currentTimeMillis();
-        notifyBrightnessChanged(mTracker, brightness, displayId, new float[] {1000.0f},
-                new long[] {TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos())});
-        List<BrightnessChangeEvent> eventsNoPackage
-                = mTracker.getEvents(0, false).getList();
-        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
-        mTracker.stop();
-
-        assertEquals(1, events.size());
-        BrightnessChangeEvent event = events.get(0);
-        assertEquals(event.timeStamp, currentTime);
-        assertEquals(displayId, event.uniqueDisplayId);
-        assertArrayEquals(new float[] {1000.0f}, event.luxValues, FLOAT_DELTA);
-        assertArrayEquals(new long[] {currentTime}, event.luxTimestamps);
-        assertEquals(brightness, event.brightness, FLOAT_DELTA);
-        assertEquals(initialBrightness, event.lastBrightness, FLOAT_DELTA);
-        assertEquals(0.5, event.batteryLevel, FLOAT_DELTA);
-        assertTrue(event.nightMode);
-        assertEquals(3333, event.colorTemperature);
-        assertTrue(event.reduceBrightColors);
-        assertEquals(40, event.reduceBrightColorsStrength);
-        assertEquals(brightness * mRbcOffsetFactor, event.reduceBrightColorsOffset, FLOAT_DELTA);
-        assertEquals("a.package", event.packageName);
-        assertEquals(0, event.userId);
-        assertArrayEquals(new long[] {1, 10, 100, 1000, 300, 30, 10, 1}, event.colorValueBuckets);
-        assertEquals(10000, event.colorSampleDuration);
-
-        assertEquals(1, eventsNoPackage.size());
-        assertNull(eventsNoPackage.get(0).packageName);
-    }
-
-    @Test
-    public void testIgnoreAutomaticBrightnessChange() {
-        final int initialBrightness = 30;
-        startTracker(mTracker, initialBrightness, DEFAULT_COLOR_SAMPLING_ENABLED);
-        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(1));
-
-        final int systemUpdatedBrightness = 20;
-        notifyBrightnessChanged(mTracker, systemUpdatedBrightness, /* userInitiated= */ false,
-                /* powerBrightnessFactor= */ 0.5f, /* isUserSetBrightness= */ false,
-                /* isDefaultBrightnessConfig= */ false, DEFAULT_DISPLAY_ID);
-        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
-        // No events because we filtered out our change.
-        assertEquals(0, events.size());
-
-        final int firstUserUpdateBrightness = 20;
-        // Then change comes from somewhere else so we shouldn't filter.
-        notifyBrightnessChanged(mTracker, firstUserUpdateBrightness);
-
-        // and with a different brightness value.
-        final int secondUserUpdateBrightness = 34;
-        notifyBrightnessChanged(mTracker, secondUserUpdateBrightness);
-        events = mTracker.getEvents(0, true).getList();
-
-        assertEquals(2, events.size());
-        // First event is change from system update (20) to first user update (20)
-        assertEquals(systemUpdatedBrightness, events.get(0).lastBrightness, FLOAT_DELTA);
-        assertEquals(firstUserUpdateBrightness, events.get(0).brightness, FLOAT_DELTA);
-        // Second event is from first to second user update.
-        assertEquals(firstUserUpdateBrightness, events.get(1).lastBrightness, FLOAT_DELTA);
-        assertEquals(secondUserUpdateBrightness, events.get(1).brightness, FLOAT_DELTA);
-
-        mTracker.stop();
-    }
-
-    @Test
-    public void testLimitedBufferSize() {
-        startTracker(mTracker);
-
-        for (int brightness = 0; brightness <= 255; ++brightness) {
-            mInjector.incrementTime(TimeUnit.SECONDS.toNanos(1));
-            notifyBrightnessChanged(mTracker, brightness);
-        }
-        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
-        mTracker.stop();
-
-        // Should be capped at 100 events, and they should be the most recent 100.
-        assertEquals(100, events.size());
-        for (int i = 0; i < events.size(); i++) {
-            BrightnessChangeEvent event = events.get(i);
-            assertEquals(156 + i, event.brightness, FLOAT_DELTA);
-        }
-    }
-
-    @Test
-    public void testReadEvents() throws Exception {
-        BrightnessTracker tracker = new BrightnessTracker(InstrumentationRegistry.getContext(),
-                mInjector);
-        mInjector.mCurrentTimeMillis = System.currentTimeMillis();
-        long someTimeAgo = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(12);
-        long twoMonthsAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60);
-        // 3 Events in the file but one too old to read.
-        String eventFile =
-                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
-                + "<events>\n"
-                + "<event nits=\"194.2\" timestamp=\""
-                + Long.toString(someTimeAgo) + "\" packageName=\""
-                + "com.example.app\" user=\"10\" "
-                + "lastNits=\"32.333\" "
-                + "batteryLevel=\"1.0\" nightMode=\"false\" colorTemperature=\"0\" "
-                + "reduceBrightColors=\"false\" reduceBrightColorsStrength=\"40\" "
-                + "reduceBrightColorsOffset=\"0\"\n"
-                + "uniqueDisplayId=\"123\""
-                + "lux=\"32.2,31.1\" luxTimestamps=\""
-                + Long.toString(someTimeAgo) + "," + Long.toString(someTimeAgo) + "\""
-                + "defaultConfig=\"true\" powerSaveFactor=\"0.5\" userPoint=\"true\" />"
-                + "<event nits=\"71\" timestamp=\""
-                + Long.toString(someTimeAgo) + "\" packageName=\""
-                + "com.android.anapp\" user=\"11\" "
-                + "lastNits=\"32\" "
-                + "batteryLevel=\"0.5\" nightMode=\"true\" colorTemperature=\"3235\" "
-                + "reduceBrightColors=\"true\" reduceBrightColorsStrength=\"40\" "
-                + "reduceBrightColorsOffset=\"0\"\n"
-                + "uniqueDisplayId=\"456\""
-                + "lux=\"132.2,131.1\" luxTimestamps=\""
-                + Long.toString(someTimeAgo) + "," + Long.toString(someTimeAgo) + "\""
-                + "colorSampleDuration=\"3456\" colorValueBuckets=\"123,598,23,19\"/>"
-                // Event that is too old so shouldn't show up.
-                + "<event nits=\"142\" timestamp=\""
-                + Long.toString(twoMonthsAgo) + "\" packageName=\""
-                + "com.example.app\" user=\"10\" "
-                + "lastNits=\"32\" "
-                + "batteryLevel=\"1.0\" nightMode=\"false\" colorTemperature=\"0\" "
-                + "reduceBrightColors=\"false\" reduceBrightColorsStrength=\"40\" "
-                + "reduceBrightColorsOffset=\"0\"\n"
-                + "uniqueDisplayId=\"789\""
-                + "lux=\"32.2,31.1\" luxTimestamps=\""
-                + Long.toString(twoMonthsAgo) + "," + Long.toString(twoMonthsAgo) + "\"/>"
-                + "</events>";
-        tracker.readEventsLocked(getInputStream(eventFile));
-        List<BrightnessChangeEvent> events = tracker.getEvents(0, true).getList();
-        assertEquals(1, events.size());
-        BrightnessChangeEvent event = events.get(0);
-        assertEquals(someTimeAgo, event.timeStamp);
-        assertEquals(194.2, event.brightness, FLOAT_DELTA);
-        assertEquals("123", event.uniqueDisplayId);
-        assertArrayEquals(new float[] {32.2f, 31.1f}, event.luxValues, FLOAT_DELTA);
-        assertArrayEquals(new long[] {someTimeAgo, someTimeAgo}, event.luxTimestamps);
-        assertEquals(32.333, event.lastBrightness, FLOAT_DELTA);
-        assertEquals(0, event.userId);
-        assertFalse(event.nightMode);
-        assertFalse(event.reduceBrightColors);
-        assertEquals(1.0f, event.batteryLevel, FLOAT_DELTA);
-        assertEquals("com.example.app", event.packageName);
-        assertTrue(event.isDefaultBrightnessConfig);
-        assertEquals(0.5f, event.powerBrightnessFactor, FLOAT_DELTA);
-        assertTrue(event.isUserSetBrightness);
-        assertNull(event.colorValueBuckets);
-
-        events = tracker.getEvents(1, true).getList();
-        assertEquals(1, events.size());
-        event = events.get(0);
-        assertEquals(someTimeAgo, event.timeStamp);
-        assertEquals(71, event.brightness, FLOAT_DELTA);
-        assertEquals("456", event.uniqueDisplayId);
-        assertArrayEquals(new float[] {132.2f, 131.1f}, event.luxValues, FLOAT_DELTA);
-        assertArrayEquals(new long[] {someTimeAgo, someTimeAgo}, event.luxTimestamps);
-        assertEquals(32, event.lastBrightness, FLOAT_DELTA);
-        assertEquals(1, event.userId);
-        assertTrue(event.nightMode);
-        assertEquals(3235, event.colorTemperature);
-        assertTrue(event.reduceBrightColors);
-        assertEquals(0.5f, event.batteryLevel, FLOAT_DELTA);
-        assertEquals("com.android.anapp", event.packageName);
-        // Not present in the event so default to false.
-        assertFalse(event.isDefaultBrightnessConfig);
-        assertEquals(1.0, event.powerBrightnessFactor, FLOAT_DELTA);
-        assertFalse(event.isUserSetBrightness);
-        assertEquals(3456L, event.colorSampleDuration);
-        assertArrayEquals(new long[] {123L, 598L, 23L, 19L}, event.colorValueBuckets);
-
-        // Pretend user 1 is a profile of user 0.
-        mInjector.mProfiles = new int[]{0, 1};
-        events = tracker.getEvents(0, true).getList();
-        // Both events should now be returned.
-        assertEquals(2, events.size());
-        BrightnessChangeEvent userZeroEvent;
-        BrightnessChangeEvent userOneEvent;
-        if (events.get(0).userId == 0) {
-            userZeroEvent = events.get(0);
-            userOneEvent = events.get(1);
-        } else {
-            userZeroEvent = events.get(1);
-            userOneEvent = events.get(0);
-        }
-        assertEquals(0, userZeroEvent.userId);
-        assertEquals("com.example.app", userZeroEvent.packageName);
-        assertEquals(1, userOneEvent.userId);
-        // Events from user 1 should have the package name redacted
-        assertNull(userOneEvent.packageName);
-    }
-
-    @Test
-    public void testFailedRead() {
-        String someTimeAgo =
-                Long.toString(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(12));
-        mInjector.mCurrentTimeMillis = System.currentTimeMillis();
-
-        BrightnessTracker tracker = new BrightnessTracker(InstrumentationRegistry.getContext(),
-                mInjector);
-        String eventFile = "junk in the file";
-        try {
-            tracker.readEventsLocked(getInputStream(eventFile));
-        } catch (IOException e) {
-            // Expected;
-        }
-        assertEquals(0, tracker.getEvents(0, true).getList().size());
-
-        // Missing lux value.
-        eventFile =
-                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
-                        + "<events>\n"
-                        + "<event nits=\"194\" timestamp=\"" + someTimeAgo + "\" packageName=\""
-                        + "com.example.app\" user=\"10\" "
-                        + "batteryLevel=\"0.7\" nightMode=\"false\" colorTemperature=\"0\" />\n"
-                        + "</events>";
-        try {
-            tracker.readEventsLocked(getInputStream(eventFile));
-        } catch (IOException e) {
-            // Expected;
-        }
-        assertEquals(0, tracker.getEvents(0, true).getList().size());
-    }
-
-    @Test
-    public void testWriteThenRead() throws Exception {
-        final int brightness = 20;
-        final String displayId = "1234";
-
-        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_ACTIVATED, 1);
-        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE, 3339);
-
-        mInjector.mSecureIntSettings.put(Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, 1);
-        mInjector.mSecureIntSettings.put(Settings.Secure.REDUCE_BRIGHT_COLORS_LEVEL, 40);
-
-        startTracker(mTracker);
-        mInjector.mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(),
-                batteryChangeEvent(30, 100));
-        final long elapsedTime1 = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos());
-        final long currentTime1 = mInjector.currentTimeMillis();
-        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
-        final long elapsedTime2 = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos());
-        final long currentTime2 = mInjector.currentTimeMillis();
-        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(3));
-        notifyBrightnessChanged(mTracker, brightness, /* userInitiated= */ true,
-                /* powerBrightnessFactor= */ 0.5f, /* isUserSetBrightness= */ true,
-                /* isDefaultBrightnessConfig= */ false, displayId, new float[] {2000.0f, 3000.0f},
-                new long[] {elapsedTime1, elapsedTime2});
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        mTracker.writeEventsLocked(baos);
-        mTracker.stop();
-
-        baos.flush();
-        ByteArrayInputStream input = new ByteArrayInputStream(baos.toByteArray());
-        BrightnessTracker tracker = new BrightnessTracker(InstrumentationRegistry.getContext(),
-                mInjector);
-        tracker.readEventsLocked(input);
-        List<BrightnessChangeEvent> events = tracker.getEvents(0, true).getList();
-
-        assertEquals(1, events.size());
-        BrightnessChangeEvent event = events.get(0);
-        assertEquals(displayId, event.uniqueDisplayId);
-        assertArrayEquals(new float[] {2000.0f, 3000.0f}, event.luxValues, FLOAT_DELTA);
-        assertArrayEquals(new long[] {currentTime1, currentTime2}, event.luxTimestamps);
-        assertEquals(brightness, event.brightness, FLOAT_DELTA);
-        assertEquals(0.3, event.batteryLevel, FLOAT_DELTA);
-        assertTrue(event.nightMode);
-        assertEquals(3339, event.colorTemperature);
-        assertTrue(event.reduceBrightColors);
-        assertEquals(40, event.reduceBrightColorsStrength);
-        assertEquals(brightness * mRbcOffsetFactor, event.reduceBrightColorsOffset, FLOAT_DELTA);
-        assertEquals(0.5f, event.powerBrightnessFactor, FLOAT_DELTA);
-        assertTrue(event.isUserSetBrightness);
-        assertFalse(event.isDefaultBrightnessConfig);
-        assertArrayEquals(new long[] {1, 10, 100, 1000, 300, 30, 10, 1}, event.colorValueBuckets);
-        assertEquals(10000, event.colorSampleDuration);
-    }
-
-    @Test
-    public void testParcelUnParcel() {
-        Parcel parcel = Parcel.obtain();
-        BrightnessChangeEvent.Builder builder = new BrightnessChangeEvent.Builder();
-        builder.setBrightness(23f);
-        builder.setTimeStamp(345L);
-        builder.setPackageName("com.example");
-        builder.setUserId(12);
-        builder.setUniqueDisplayId("9876");
-        float[] luxValues = new float[2];
-        luxValues[0] = 3000.0f;
-        luxValues[1] = 4000.0f;
-        builder.setLuxValues(luxValues);
-        long[] luxTimestamps = new long[2];
-        luxTimestamps[0] = 325L;
-        luxTimestamps[1] = 315L;
-        builder.setLuxTimestamps(luxTimestamps);
-        builder.setBatteryLevel(0.7f);
-        builder.setNightMode(false);
-        builder.setColorTemperature(345);
-        builder.setReduceBrightColors(false);
-        builder.setReduceBrightColorsStrength(40);
-        builder.setReduceBrightColorsOffset(20f);
-        builder.setLastBrightness(50f);
-        builder.setColorValues(new long[] {23, 34, 45}, 1000L);
-        BrightnessChangeEvent event = builder.build();
-
-        event.writeToParcel(parcel, 0);
-        byte[] parceled = parcel.marshall();
-        parcel.recycle();
-
-        parcel = Parcel.obtain();
-        parcel.unmarshall(parceled, 0, parceled.length);
-        parcel.setDataPosition(0);
-
-        BrightnessChangeEvent event2 = BrightnessChangeEvent.CREATOR.createFromParcel(parcel);
-        parcel.recycle();
-        assertEquals(event.brightness, event2.brightness, FLOAT_DELTA);
-        assertEquals(event.timeStamp, event2.timeStamp);
-        assertEquals(event.packageName, event2.packageName);
-        assertEquals(event.userId, event2.userId);
-        assertEquals(event.uniqueDisplayId, event2.uniqueDisplayId);
-        assertArrayEquals(event.luxValues, event2.luxValues, FLOAT_DELTA);
-        assertArrayEquals(event.luxTimestamps, event2.luxTimestamps);
-        assertEquals(event.batteryLevel, event2.batteryLevel, FLOAT_DELTA);
-        assertEquals(event.nightMode, event2.nightMode);
-        assertEquals(event.colorTemperature, event2.colorTemperature);
-        assertEquals(event.reduceBrightColors, event2.reduceBrightColors);
-        assertEquals(event.reduceBrightColorsStrength, event2.reduceBrightColorsStrength);
-        assertEquals(event.reduceBrightColorsOffset, event2.reduceBrightColorsOffset, FLOAT_DELTA);
-        assertEquals(event.lastBrightness, event2.lastBrightness, FLOAT_DELTA);
-        assertArrayEquals(event.colorValueBuckets, event2.colorValueBuckets);
-        assertEquals(event.colorSampleDuration, event2.colorSampleDuration);
-
-        parcel = Parcel.obtain();
-        builder.setBatteryLevel(Float.NaN);
-        event = builder.build();
-        event.writeToParcel(parcel, 0);
-        parceled = parcel.marshall();
-        parcel.recycle();
-
-        parcel = Parcel.obtain();
-        parcel.unmarshall(parceled, 0, parceled.length);
-        parcel.setDataPosition(0);
-        event2 = BrightnessChangeEvent.CREATOR.createFromParcel(parcel);
-        assertEquals(event.batteryLevel, event2.batteryLevel, FLOAT_DELTA);
-    }
-
-    @Test
-    public void testNonNullAmbientStats() {
-        // getAmbientBrightnessStats should return an empty list rather than null when
-        // tracker isn't started or hasn't collected any data.
-        ParceledListSlice<AmbientBrightnessDayStats> slice = mTracker.getAmbientBrightnessStats(0);
-        assertNotNull(slice);
-        assertTrue(slice.getList().isEmpty());
-        startTracker(mTracker);
-        slice = mTracker.getAmbientBrightnessStats(0);
-        assertNotNull(slice);
-        assertTrue(slice.getList().isEmpty());
-    }
-
-    @Test
-    public void testBackgroundHandlerDelay() {
-        final int brightness = 20;
-
-        // Setup tracker.
-        startTracker(mTracker);
-        mInjector.mSensorListener.onSensorChanged(createSensorEvent(1.0f));
-        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
-
-        // Block handler from running.
-        final CountDownLatch latch = new CountDownLatch(1);
-        mInjector.mHandler.post(
-                () -> {
-                    try {
-                        latch.await();
-                    } catch (InterruptedException e) {
-                        fail(e.getMessage());
-                    }
-                });
-
-        // Send an event.
-        long eventTime = mInjector.currentTimeMillis();
-        mTracker.notifyBrightnessChanged(brightness, /* userInitiated= */ true,
-                /* powerBrightnessFactor= */ 1.0f, /* isUserSetBrightness= */ false,
-                /* isDefaultBrightnessConfig= */ false, DEFAULT_DISPLAY_ID, new float[10],
-                new long[10]);
-
-        // Time passes before handler can run.
-        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
-
-        // Let the handler run.
-        latch.countDown();
-        mInjector.waitForHandler();
-
-        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
-        mTracker.stop();
-
-        // Check event was recorded with time it was sent rather than handler ran.
-        assertEquals(1, events.size());
-        BrightnessChangeEvent event = events.get(0);
-        assertEquals(eventTime, event.timeStamp);
-    }
-
-    @Test
-    public void testDisplayIdChange() {
-        float firstBrightness = 0.5f;
-        float secondBrightness = 0.75f;
-        String firstDisplayId = "123";
-        String secondDisplayId = "456";
-
-        startTracker(mTracker);
-        mInjector.mSensorListener.onSensorChanged(createSensorEvent(1000.0f));
-
-        notifyBrightnessChanged(mTracker, firstBrightness, firstDisplayId);
-        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
-        List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList();
-        assertEquals(1, events.size());
-        BrightnessChangeEvent firstEvent = events.get(0);
-        assertEquals(firstDisplayId, firstEvent.uniqueDisplayId);
-        assertEquals(firstBrightness, firstEvent.brightness, 0.001f);
-
-        notifyBrightnessChanged(mTracker, secondBrightness, secondDisplayId);
-        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
-        events = mTracker.getEvents(0, true).getList();
-        assertEquals(2, events.size());
-        BrightnessChangeEvent secondEvent = events.get(1);
-        assertEquals(secondDisplayId, secondEvent.uniqueDisplayId);
-        assertEquals(secondBrightness, secondEvent.brightness, 0.001f);
-
-        mTracker.stop();
-    }
-
-    @Test
-    public void testLightSensorChange() {
-        // verify the tracker started correctly and a listener registered
-        startTracker(mTracker);
-        assertNotNull(mInjector.mSensorListener);
-        assertEquals(mInjector.mLightSensor, mLightSensorFake);
-
-        // Setting the sensor to null should stop the registered listener.
-        mTracker.setLightSensor(null);
-        mInjector.waitForHandler();
-        assertNull(mInjector.mSensorListener);
-        assertNull(mInjector.mLightSensor);
-
-        // Resetting sensor should start listener again
-        mTracker.setLightSensor(mLightSensorFake);
-        mInjector.waitForHandler();
-        assertNotNull(mInjector.mSensorListener);
-        assertEquals(mInjector.mLightSensor, mLightSensorFake);
-
-        Sensor secondSensor = new Sensor(mInputSensorInfoMock);
-        // Setting a different listener should keep things working
-        mTracker.setLightSensor(secondSensor);
-        mInjector.waitForHandler();
-        assertNotNull(mInjector.mSensorListener);
-        assertEquals(mInjector.mLightSensor, secondSensor);
-    }
-
-    @Test
-    public void testSetLightSensorDoesntStartListener() {
-        mTracker.setLightSensor(mLightSensorFake);
-        assertNull(mInjector.mSensorListener);
-    }
-
-    @Test
-    public void testNullLightSensorWontRegister() {
-        mTracker.setLightSensor(null);
-        startTracker(mTracker);
-        assertNull(mInjector.mSensorListener);
-        assertNull(mInjector.mLightSensor);
-    }
-
-    @Test
-    public void testOnlyOneReceiverRegistered() {
-        assertNull(mInjector.mLightSensor);
-        assertNull(mInjector.mSensorListener);
-        assertNull(mInjector.mContentObserver);
-        assertNull(mInjector.mBroadcastReceiver);
-        assertFalse(mInjector.mIdleScheduled);
-        startTracker(mTracker, 0.3f, false);
-
-        assertNotNull(mInjector.mLightSensor);
-        assertNotNull(mInjector.mSensorListener);
-        assertNotNull(mInjector.mContentObserver);
-        assertNotNull(mInjector.mBroadcastReceiver);
-        assertTrue(mInjector.mIdleScheduled);
-        Sensor registeredLightSensor = mInjector.mLightSensor;
-        SensorEventListener registeredSensorListener = mInjector.mSensorListener;
-        ContentObserver registeredContentObserver = mInjector.mContentObserver;
-        BroadcastReceiver registeredBroadcastReceiver = mInjector.mBroadcastReceiver;
-
-        mTracker.start(0.3f);
-        assertSame(registeredLightSensor, mInjector.mLightSensor);
-        assertSame(registeredSensorListener, mInjector.mSensorListener);
-        assertSame(registeredContentObserver, mInjector.mContentObserver);
-        assertSame(registeredBroadcastReceiver, mInjector.mBroadcastReceiver);
-
-        mTracker.stop();
-        assertNull(mInjector.mLightSensor);
-        assertNull(mInjector.mSensorListener);
-        assertNull(mInjector.mContentObserver);
-        assertNull(mInjector.mBroadcastReceiver);
-        assertFalse(mInjector.mIdleScheduled);
-
-        // mInjector asserts that we aren't removing a null receiver
-        mTracker.stop();
-    }
-
-    private InputStream getInputStream(String data) {
-        return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
-    }
-
-    private Intent batteryChangeEvent(int level, int scale) {
-        Intent intent = new Intent();
-        intent.setAction(Intent.ACTION_BATTERY_CHANGED);
-        intent.putExtra(BatteryManager.EXTRA_LEVEL, level);
-        intent.putExtra(BatteryManager.EXTRA_SCALE, scale);
-        return intent;
-    }
-
-    private SensorEvent createSensorEvent(float lux) {
-        SensorEvent event;
-        try {
-            Constructor<SensorEvent> constr =
-                    SensorEvent.class.getDeclaredConstructor(Integer.TYPE);
-            constr.setAccessible(true);
-            event = constr.newInstance(1);
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-        event.values[0] = lux;
-        event.timestamp = mInjector.mElapsedRealtimeNanos;
-
-        return event;
-    }
-
-    private void startTracker(BrightnessTracker tracker) {
-        startTracker(tracker, DEFAULT_INITIAL_BRIGHTNESS,  DEFAULT_COLOR_SAMPLING_ENABLED);
-    }
-
-    private void startTracker(BrightnessTracker tracker, float initialBrightness,
-            boolean collectColorSamples) {
-        tracker.start(initialBrightness);
-        tracker.setShouldCollectColorSample(collectColorSamples);
-        mInjector.waitForHandler();
-    }
-
-    private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness) {
-        notifyBrightnessChanged(tracker, brightness, DEFAULT_DISPLAY_ID);
-    }
-
-    private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness,
-            String displayId) {
-        notifyBrightnessChanged(tracker, brightness, /* userInitiated= */ true,
-                /* powerBrightnessFactor= */ 1.0f, /* isUserSetBrightness= */ false,
-                /* isDefaultBrightnessConfig= */ false, displayId, new float[10], new long[10]);
-    }
-
-    private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness,
-            String displayId, float[] luxValues, long[] luxTimestamps) {
-        notifyBrightnessChanged(tracker, brightness, /* userInitiated= */ true,
-                /* powerBrightnessFactor= */ 1.0f, /* isUserSetBrightness= */ false,
-                /* isDefaultBrightnessConfig= */ false, displayId, luxValues, luxTimestamps);
-    }
-
-    private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness,
-            boolean userInitiated, float powerBrightnessFactor, boolean isUserSetBrightness,
-            boolean isDefaultBrightnessConfig, String displayId) {
-        tracker.notifyBrightnessChanged(brightness, userInitiated, powerBrightnessFactor,
-                isUserSetBrightness, isDefaultBrightnessConfig, displayId, new float[10],
-                new long[10]);
-        mInjector.waitForHandler();
-    }
-
-    private void notifyBrightnessChanged(BrightnessTracker tracker, float brightness,
-            boolean userInitiated, float powerBrightnessFactor, boolean isUserSetBrightness,
-            boolean isDefaultBrightnessConfig, String displayId, float[] luxValues,
-            long[] luxTimestamps) {
-        tracker.notifyBrightnessChanged(brightness, userInitiated, powerBrightnessFactor,
-                isUserSetBrightness, isDefaultBrightnessConfig, displayId, luxValues,
-                luxTimestamps);
-        mInjector.waitForHandler();
-    }
-
-    private BrightnessConfiguration buildBrightnessConfiguration(boolean collectColorSamples) {
-        BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder(
-                /* lux= */ new float[] {0f, 10f, 100f},
-                /* nits= */ new float[] {1f, 90f, 100f});
-        builder.setShouldCollectColorSamples(collectColorSamples);
-        return builder.build();
-    }
-
-    private static final class Idle implements MessageQueue.IdleHandler {
-        private boolean mIdle;
-
-        @Override
-        public boolean queueIdle() {
-            synchronized (this) {
-                mIdle = true;
-                notifyAll();
-            }
-            return false;
-        }
-
-        public synchronized void waitForIdle() {
-            while (!mIdle) {
-                try {
-                    wait();
-                } catch (InterruptedException e) {
-                }
-            }
-        }
-    }
-
-    private class TestInjector extends BrightnessTracker.Injector {
-        SensorEventListener mSensorListener;
-        Sensor mLightSensor;
-        BroadcastReceiver mBroadcastReceiver;
-        DisplayManager.DisplayListener mDisplayListener;
-        Map<String, Integer> mSecureIntSettings = new HashMap<>();
-        long mCurrentTimeMillis = System.currentTimeMillis();
-        long mElapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos();
-        Handler mHandler;
-        boolean mIdleScheduled;
-        boolean mInteractive = true;
-        int[] mProfiles;
-        ContentObserver mContentObserver;
-        boolean mIsBrightnessModeAutomatic = true;
-        boolean mColorSamplingEnabled = false;
-        DisplayedContentSamplingAttributes mDefaultSamplingAttributes =
-                new DisplayedContentSamplingAttributes(0x37, 0, 0x4);
-        float mFrameRate = 60.0f;
-        int mNoColorSamplingFrames;
-
-
-        public TestInjector(Handler handler) {
-            mHandler = handler;
-        }
-
-        void incrementTime(long timeMillis) {
-            mCurrentTimeMillis += timeMillis;
-            mElapsedRealtimeNanos += TimeUnit.MILLISECONDS.toNanos(timeMillis);
-        }
-
-        void setBrightnessMode(boolean isBrightnessModeAutomatic) {
-          mIsBrightnessModeAutomatic = isBrightnessModeAutomatic;
-          mContentObserver.dispatchChange(false, null);
-          waitForHandler();
-        }
-
-        void sendScreenChange(boolean screenOn) {
-            mInteractive = screenOn;
-            Intent intent = new Intent();
-            intent.setAction(screenOn ? Intent.ACTION_SCREEN_ON : Intent.ACTION_SCREEN_OFF);
-            mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(), intent);
-            waitForHandler();
-        }
-
-        void waitForHandler() {
-            Idle idle = new Idle();
-            mHandler.getLooper().getQueue().addIdleHandler(idle);
-            mHandler.post(() -> {});
-            idle.waitForIdle();
-        }
-
-        @Override
-        public void registerSensorListener(Context context,
-                SensorEventListener sensorListener, Sensor lightSensor, Handler handler) {
-            mSensorListener = sensorListener;
-            mLightSensor = lightSensor;
-        }
-
-        @Override
-        public void unregisterSensorListener(Context context,
-                SensorEventListener sensorListener) {
-            mSensorListener = null;
-            mLightSensor = null;
-        }
-
-        @Override
-        public void registerBrightnessModeObserver(ContentResolver resolver,
-                ContentObserver settingsObserver) {
-            mContentObserver = settingsObserver;
-        }
-
-        @Override
-        public void unregisterBrightnessModeObserver(Context context,
-                ContentObserver settingsObserver) {
-            mContentObserver = null;
-        }
-
-        @Override
-        public void registerReceiver(Context context,
-                BroadcastReceiver shutdownReceiver, IntentFilter shutdownFilter) {
-            mBroadcastReceiver = shutdownReceiver;
-        }
-
-        @Override
-        public void unregisterReceiver(Context context,
-                BroadcastReceiver broadcastReceiver) {
-            assertEquals(mBroadcastReceiver, broadcastReceiver);
-            mBroadcastReceiver = null;
-        }
-
-        @Override
-        public Handler getBackgroundHandler() {
-            return mHandler;
-        }
-
-        @Override
-        public boolean isBrightnessModeAutomatic(ContentResolver resolver) {
-            return mIsBrightnessModeAutomatic;
-        }
-
-        @Override
-        public int getSecureIntForUser(ContentResolver resolver, String setting, int defaultValue,
-                int userId) {
-            Integer value = mSecureIntSettings.get(setting);
-            if (value == null) {
-                return defaultValue;
-            } else {
-                return value;
-            }
-        }
-
-        @Override
-        public AtomicFile getFile(String filename) {
-            // Don't have the test write / read from anywhere.
-            return null;
-        }
-
-        @Override
-        public AtomicFile getLegacyFile(String filename) {
-            // Don't have the test write / read from anywhere.
-            return null;
-        }
-
-        @Override
-        public long currentTimeMillis() {
-            return mCurrentTimeMillis;
-        }
-
-        @Override
-        public long elapsedRealtimeNanos() {
-            return mElapsedRealtimeNanos;
-        }
-
-        @Override
-        public int getUserSerialNumber(UserManager userManager, int userId) {
-            return userId + 10;
-        }
-
-        @Override
-        public int getUserId(UserManager userManager, int userSerialNumber) {
-            return userSerialNumber - 10;
-        }
-
-        @Override
-        public int[] getProfileIds(UserManager userManager, int userId) {
-            if (mProfiles != null) {
-                return mProfiles;
-            } else {
-                return new int[]{userId};
-            }
-        }
-
-        @Override
-        public RootTaskInfo getFocusedStack() throws RemoteException {
-            RootTaskInfo focusedStack = new RootTaskInfo();
-            focusedStack.userId = 0;
-            focusedStack.topActivity = new ComponentName("a.package", "a.class");
-            return focusedStack;
-        }
-
-        @Override
-        public void scheduleIdleJob(Context context) {
-            // Don't actually schedule jobs during unit tests.
-            mIdleScheduled = true;
-        }
-
-        @Override
-        public void cancelIdleJob(Context context) {
-            mIdleScheduled = false;
-        }
-
-        @Override
-        public boolean isInteractive(Context context) {
-            return mInteractive;
-        }
-
-        @Override
-        public int getNightDisplayColorTemperature(Context context) {
-          return mSecureIntSettings.getOrDefault(Settings.Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE,
-                  mDefaultNightModeColorTemperature);
-        }
-
-        @Override
-        public boolean isNightDisplayActivated(Context context) {
-            return mSecureIntSettings.getOrDefault(Settings.Secure.NIGHT_DISPLAY_ACTIVATED,
-                    0) == 1;
-        }
-
-        @Override
-        public int getReduceBrightColorsStrength(Context context) {
-            return mSecureIntSettings.getOrDefault(Settings.Secure.REDUCE_BRIGHT_COLORS_LEVEL,
-                    0);
-        }
-
-        @Override
-        public boolean isReduceBrightColorsActivated(Context context) {
-            return mSecureIntSettings.getOrDefault(Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
-                    0) == 1;
-        }
-
-        @Override
-        public DisplayedContentSample sampleColor(int noFramesToSample) {
-            return new DisplayedContentSample(600L,
-                    null,
-                    null,
-                     new long[] {1, 10, 100, 1000, 300, 30, 10, 1},
-                    null);
-        }
-
-        @Override
-        public float getFrameRate(Context context) {
-            return mFrameRate;
-        }
-
-        @Override
-        public DisplayedContentSamplingAttributes getSamplingAttributes() {
-            return mDefaultSamplingAttributes;
-        }
-
-        @Override
-        public boolean enableColorSampling(boolean enable, int noFrames) {
-            mColorSamplingEnabled = enable;
-            mNoColorSamplingFrames = noFrames;
-            return true;
-        }
-
-        @Override
-        public void registerDisplayListener(Context context,
-                DisplayManager.DisplayListener listener, Handler handler) {
-            mDisplayListener = listener;
-        }
-
-        @Override
-        public void unRegisterDisplayListener(Context context,
-                DisplayManager.DisplayListener listener) {
-            mDisplayListener = null;
-        }
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
deleted file mode 100644
index 708421d..0000000
--- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ /dev/null
@@ -1,953 +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.server.display;
-
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.mockito.ArgumentMatchers.anyFloat;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.os.Temperature;
-import android.util.SparseArray;
-import android.view.SurfaceControl;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.R;
-import com.android.server.display.config.ThermalStatus;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-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};
-    private static final int[] HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{30000};
-
-    private DisplayDeviceConfig mDisplayDeviceConfig;
-    private static final float ZERO_DELTA = 0.0f;
-    private static final float SMALL_DELTA = 0.0001f;
-    @Mock
-    private Context mContext;
-
-    @Mock
-    private Resources mResources;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        when(mContext.getResources()).thenReturn(mResources);
-        mockDeviceConfigs();
-    }
-
-    @Test
-    public void testConfigValuesFromDisplayConfig() throws IOException {
-        setupDisplayDeviceConfigFromDisplayConfigFile();
-
-        assertEquals(mDisplayDeviceConfig.getName(), "Example Display");
-        assertEquals(mDisplayDeviceConfig.getAmbientHorizonLong(), 5000);
-        assertEquals(mDisplayDeviceConfig.getAmbientHorizonShort(), 50);
-        assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000);
-        assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000);
-        assertEquals(mDisplayDeviceConfig.getBrightnessRampFastDecrease(), 0.01f, ZERO_DELTA);
-        assertEquals(mDisplayDeviceConfig.getBrightnessRampFastIncrease(), 0.02f, ZERO_DELTA);
-        assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowIncrease(), 0.04f, ZERO_DELTA);
-        assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowDecrease(), 0.03f, ZERO_DELTA);
-        assertEquals(mDisplayDeviceConfig.getBrightnessDefault(), 0.5f, ZERO_DELTA);
-        assertArrayEquals(mDisplayDeviceConfig.getBrightness(), new float[]{0.0f, 0.62f, 1.0f},
-                ZERO_DELTA);
-        assertArrayEquals(mDisplayDeviceConfig.getNits(), new float[]{2.0f, 500.0f, 800.0f},
-                ZERO_DELTA);
-        assertArrayEquals(mDisplayDeviceConfig.getBacklight(), new float[]{0.0f, 0.62f, 1.0f},
-                ZERO_DELTA);
-        assertEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLightDebounce(), 2000);
-        assertEquals(mDisplayDeviceConfig.getAutoBrightnessDarkeningLightDebounce(), 1000);
-        assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new
-                float[]{0.0f, 50.0f, 80.0f}, ZERO_DELTA);
-        assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new
-                float[]{45.32f, 75.43f}, ZERO_DELTA);
-
-        // Test thresholds
-        assertEquals(10, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(),
-                ZERO_DELTA);
-        assertEquals(20, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(),
-                ZERO_DELTA);
-        assertEquals(30, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), ZERO_DELTA);
-        assertEquals(40, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(), ZERO_DELTA);
-
-        assertEquals(0.1f, mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), ZERO_DELTA);
-        assertEquals(0.2f, mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle(), ZERO_DELTA);
-        assertEquals(0.3f, mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), ZERO_DELTA);
-        assertEquals(0.4f, mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle(), ZERO_DELTA);
-
-        assertArrayEquals(new float[]{0, 0.10f, 0.20f},
-                mDisplayDeviceConfig.getScreenBrighteningLevels(), ZERO_DELTA);
-        assertArrayEquals(new float[]{9, 10, 11},
-                mDisplayDeviceConfig.getScreenBrighteningPercentages(), ZERO_DELTA);
-        assertArrayEquals(new float[]{0, 0.11f, 0.21f},
-                mDisplayDeviceConfig.getScreenDarkeningLevels(), ZERO_DELTA);
-        assertArrayEquals(new float[]{11, 12, 13},
-                mDisplayDeviceConfig.getScreenDarkeningPercentages(), ZERO_DELTA);
-
-        assertArrayEquals(new float[]{0, 100, 200},
-                mDisplayDeviceConfig.getAmbientBrighteningLevels(), ZERO_DELTA);
-        assertArrayEquals(new float[]{13, 14, 15},
-                mDisplayDeviceConfig.getAmbientBrighteningPercentages(), ZERO_DELTA);
-        assertArrayEquals(new float[]{0, 300, 400},
-                mDisplayDeviceConfig.getAmbientDarkeningLevels(), ZERO_DELTA);
-        assertArrayEquals(new float[]{15, 16, 17},
-                mDisplayDeviceConfig.getAmbientDarkeningPercentages(), ZERO_DELTA);
-
-        assertArrayEquals(new float[]{0, 0.12f, 0.22f},
-                mDisplayDeviceConfig.getScreenBrighteningLevelsIdle(), ZERO_DELTA);
-        assertArrayEquals(new float[]{17, 18, 19},
-                mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle(), ZERO_DELTA);
-        assertArrayEquals(new float[]{0, 0.13f, 0.23f},
-                mDisplayDeviceConfig.getScreenDarkeningLevelsIdle(), ZERO_DELTA);
-        assertArrayEquals(new float[]{19, 20, 21},
-                mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle(), ZERO_DELTA);
-
-        assertArrayEquals(new float[]{0, 500, 600},
-                mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle(), ZERO_DELTA);
-        assertArrayEquals(new float[]{21, 22, 23},
-                mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle(), ZERO_DELTA);
-        assertArrayEquals(new float[]{0, 700, 800},
-                mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA);
-        assertArrayEquals(new float[]{23, 24, 25},
-                mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
-
-        assertEquals(75, mDisplayDeviceConfig.getDefaultLowBlockingZoneRefreshRate());
-        assertEquals(90, mDisplayDeviceConfig.getDefaultHighBlockingZoneRefreshRate());
-        assertEquals(85, mDisplayDeviceConfig.getDefaultPeakRefreshRate());
-        assertEquals(45, mDisplayDeviceConfig.getDefaultRefreshRate());
-        assertEquals(2, mDisplayDeviceConfig.getRefreshRangeProfiles().size());
-        assertEquals(60, mDisplayDeviceConfig.getRefreshRange("test1").min, SMALL_DELTA);
-        assertEquals(60, mDisplayDeviceConfig.getRefreshRange("test1").max, SMALL_DELTA);
-        assertEquals(80, mDisplayDeviceConfig.getRefreshRange("test2").min, SMALL_DELTA);
-        assertEquals(90, mDisplayDeviceConfig.getRefreshRange("test2").max, SMALL_DELTA);
-        assertEquals(82, mDisplayDeviceConfig.getDefaultRefreshRateInHbmHdr());
-        assertEquals(83, mDisplayDeviceConfig.getDefaultRefreshRateInHbmSunlight());
-        assertArrayEquals(new int[]{45, 55},
-                mDisplayDeviceConfig.getLowDisplayBrightnessThresholds());
-        assertArrayEquals(new int[]{50, 60},
-                mDisplayDeviceConfig.getLowAmbientBrightnessThresholds());
-        assertArrayEquals(new int[]{65, 75},
-                mDisplayDeviceConfig.getHighDisplayBrightnessThresholds());
-        assertArrayEquals(new int[]{70, 80},
-                mDisplayDeviceConfig.getHighAmbientBrightnessThresholds());
-
-        assertEquals("sensor_12345",
-                mDisplayDeviceConfig.getScreenOffBrightnessSensor().type);
-        assertEquals("Sensor 12345",
-                mDisplayDeviceConfig.getScreenOffBrightnessSensor().name);
-
-        assertArrayEquals(new int[]{-1, 10, 20, 30, 40},
-                mDisplayDeviceConfig.getScreenOffBrightnessSensorValueToLux());
-
-        List<DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel>
-                defaultThrottlingLevels = new ArrayList<>();
-        defaultThrottlingLevels.add(
-                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
-                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.light), 0.4f
-        ));
-        defaultThrottlingLevels.add(
-                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
-                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.moderate), 0.3f
-        ));
-        defaultThrottlingLevels.add(
-                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
-                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.severe), 0.2f
-        ));
-        defaultThrottlingLevels.add(
-                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
-                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.critical), 0.1f
-        ));
-        defaultThrottlingLevels.add(
-                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
-                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.emergency), 0.05f
-        ));
-        defaultThrottlingLevels.add(
-                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
-                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.shutdown), 0.025f
-        ));
-
-        DisplayDeviceConfig.ThermalBrightnessThrottlingData defaultThrottlingData =
-                new DisplayDeviceConfig.ThermalBrightnessThrottlingData(defaultThrottlingLevels);
-
-        List<DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel>
-                concurrentThrottlingLevels = new ArrayList<>();
-        concurrentThrottlingLevels.add(
-                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
-                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.light), 0.2f
-        ));
-        concurrentThrottlingLevels.add(
-                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
-                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.moderate), 0.15f
-        ));
-        concurrentThrottlingLevels.add(
-                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
-                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.severe), 0.1f
-        ));
-        concurrentThrottlingLevels.add(
-                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
-                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.critical), 0.05f
-        ));
-        concurrentThrottlingLevels.add(
-                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
-                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.emergency), 0.025f
-        ));
-        concurrentThrottlingLevels.add(
-                new DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel(
-                DisplayDeviceConfig.convertThermalStatus(ThermalStatus.shutdown), 0.0125f
-        ));
-        DisplayDeviceConfig.ThermalBrightnessThrottlingData concurrentThrottlingData =
-                new DisplayDeviceConfig.ThermalBrightnessThrottlingData(concurrentThrottlingLevels);
-
-        HashMap<String, DisplayDeviceConfig.ThermalBrightnessThrottlingData> throttlingDataMap =
-                new HashMap<>(2);
-        throttlingDataMap.put("default", defaultThrottlingData);
-        throttlingDataMap.put("concurrent", concurrentThrottlingData);
-
-        assertEquals(throttlingDataMap,
-                mDisplayDeviceConfig.getThermalBrightnessThrottlingDataMapByThrottlingId());
-
-        assertNotNull(mDisplayDeviceConfig.getHostUsiVersion());
-        assertEquals(mDisplayDeviceConfig.getHostUsiVersion().getMajorVersion(), 2);
-        assertEquals(mDisplayDeviceConfig.getHostUsiVersion().getMinorVersion(), 0);
-
-        // Max desired Hdr/SDR ratio upper-bounds the HDR brightness.
-        assertEquals(1.0f,
-                mDisplayDeviceConfig.getHdrBrightnessFromSdr(0.62f, Float.POSITIVE_INFINITY),
-                ZERO_DELTA);
-        assertEquals(0.62f,
-                mDisplayDeviceConfig.getHdrBrightnessFromSdr(0.62f, 1.0f),
-                ZERO_DELTA);
-        assertEquals(0.77787f,
-                mDisplayDeviceConfig.getHdrBrightnessFromSdr(0.62f, 1.25f),
-                SMALL_DELTA);
-
-        // Todo: Add asserts for DensityMapping,
-        // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
-    }
-
-    @Test
-    public void testConfigValuesFromConfigResource() {
-        setupDisplayDeviceConfigFromConfigResourceFile();
-        assertNull(mDisplayDeviceConfig.getName());
-        assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new
-                float[]{2.0f, 200.0f, 600.0f}, ZERO_DELTA);
-        assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new
-                float[]{0.0f, 0.0f, 110.0f, 500.0f}, ZERO_DELTA);
-
-        // Test thresholds
-        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(), ZERO_DELTA);
-        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(),
-                ZERO_DELTA);
-        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), ZERO_DELTA);
-        assertEquals(0, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(), ZERO_DELTA);
-
-        assertEquals(0, mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), ZERO_DELTA);
-        assertEquals(0, mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle(), ZERO_DELTA);
-        assertEquals(0, mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), ZERO_DELTA);
-        assertEquals(0, mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle(), ZERO_DELTA);
-
-        // screen levels will be considered "old screen brightness scale"
-        // and therefore will divide by 255
-        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
-                mDisplayDeviceConfig.getScreenBrighteningLevels(), SMALL_DELTA);
-        assertArrayEquals(new float[]{35, 36, 37},
-                mDisplayDeviceConfig.getScreenBrighteningPercentages(), ZERO_DELTA);
-        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
-                mDisplayDeviceConfig.getScreenDarkeningLevels(), SMALL_DELTA);
-        assertArrayEquals(new float[]{37, 38, 39},
-                mDisplayDeviceConfig.getScreenDarkeningPercentages(), ZERO_DELTA);
-
-        assertArrayEquals(new float[]{0, 30, 31},
-                mDisplayDeviceConfig.getAmbientBrighteningLevels(), ZERO_DELTA);
-        assertArrayEquals(new float[]{27, 28, 29},
-                mDisplayDeviceConfig.getAmbientBrighteningPercentages(), ZERO_DELTA);
-        assertArrayEquals(new float[]{0, 30, 31},
-                mDisplayDeviceConfig.getAmbientDarkeningLevels(), ZERO_DELTA);
-        assertArrayEquals(new float[]{29, 30, 31},
-                mDisplayDeviceConfig.getAmbientDarkeningPercentages(), ZERO_DELTA);
-
-        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
-                mDisplayDeviceConfig.getScreenBrighteningLevelsIdle(), SMALL_DELTA);
-        assertArrayEquals(new float[]{35, 36, 37},
-                mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle(), ZERO_DELTA);
-        assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f},
-                mDisplayDeviceConfig.getScreenDarkeningLevelsIdle(), SMALL_DELTA);
-        assertArrayEquals(new float[]{37, 38, 39},
-                mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle(), ZERO_DELTA);
-
-        assertArrayEquals(new float[]{0, 30, 31},
-                mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle(), ZERO_DELTA);
-        assertArrayEquals(new float[]{27, 28, 29},
-                mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle(), ZERO_DELTA);
-        assertArrayEquals(new float[]{0, 30, 31},
-                mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA);
-        assertArrayEquals(new float[]{29, 30, 31},
-                mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
-        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(0, mDisplayDeviceConfig.getRefreshRangeProfiles().size());
-        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(),
-                LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
-        assertArrayEquals(mDisplayDeviceConfig.getHighDisplayBrightnessThresholds(),
-                HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
-        assertArrayEquals(mDisplayDeviceConfig.getHighAmbientBrightnessThresholds(),
-                HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
-
-        // Todo: Add asserts for ThermalBrightnessThrottlingData, DensityMapping,
-        // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
-    }
-
-    @Test
-    public void testThermalRefreshRateThrottlingFromDisplayConfig() throws IOException {
-        setupDisplayDeviceConfigFromDisplayConfigFile();
-
-        SparseArray<SurfaceControl.RefreshRateRange> defaultMap =
-                mDisplayDeviceConfig.getThermalRefreshRateThrottlingData(null);
-        assertNotNull(defaultMap);
-        assertEquals(2, defaultMap.size());
-        assertEquals(30, defaultMap.get(Temperature.THROTTLING_CRITICAL).min, SMALL_DELTA);
-        assertEquals(60, defaultMap.get(Temperature.THROTTLING_CRITICAL).max, SMALL_DELTA);
-        assertEquals(0, defaultMap.get(Temperature.THROTTLING_SHUTDOWN).min, SMALL_DELTA);
-        assertEquals(30, defaultMap.get(Temperature.THROTTLING_SHUTDOWN).max, SMALL_DELTA);
-
-        SparseArray<SurfaceControl.RefreshRateRange> testMap =
-                mDisplayDeviceConfig.getThermalRefreshRateThrottlingData("test");
-        assertNotNull(testMap);
-        assertEquals(1, testMap.size());
-        assertEquals(60, testMap.get(Temperature.THROTTLING_EMERGENCY).min, SMALL_DELTA);
-        assertEquals(90, testMap.get(Temperature.THROTTLING_EMERGENCY).max, SMALL_DELTA);
-    }
-
-    @Test
-    public void testValidLuxThrottling() throws Exception {
-        setupDisplayDeviceConfigFromDisplayConfigFile();
-
-        Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData =
-                mDisplayDeviceConfig.getLuxThrottlingData();
-        assertEquals(2, luxThrottlingData.size());
-
-        Map<Float, Float> adaptiveOnBrightnessPoints = luxThrottlingData.get(
-                DisplayDeviceConfig.BrightnessLimitMapType.ADAPTIVE);
-        assertEquals(2, adaptiveOnBrightnessPoints.size());
-        assertEquals(0.3f, adaptiveOnBrightnessPoints.get(1000f), SMALL_DELTA);
-        assertEquals(0.5f, adaptiveOnBrightnessPoints.get(5000f), SMALL_DELTA);
-
-        Map<Float, Float> adaptiveOffBrightnessPoints = luxThrottlingData.get(
-                DisplayDeviceConfig.BrightnessLimitMapType.DEFAULT);
-        assertEquals(2, adaptiveOffBrightnessPoints.size());
-        assertEquals(0.35f, adaptiveOffBrightnessPoints.get(1500f), SMALL_DELTA);
-        assertEquals(0.55f, adaptiveOffBrightnessPoints.get(5500f), SMALL_DELTA);
-    }
-
-    @Test
-    public void testInvalidLuxThrottling() throws Exception {
-        setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getInvalidLuxThrottling()));
-
-        Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData =
-                mDisplayDeviceConfig.getLuxThrottlingData();
-        assertEquals(1, luxThrottlingData.size());
-
-        Map<Float, Float> adaptiveOnBrightnessPoints = luxThrottlingData.get(
-                DisplayDeviceConfig.BrightnessLimitMapType.ADAPTIVE);
-        assertEquals(1, adaptiveOnBrightnessPoints.size());
-        assertEquals(0.3f, adaptiveOnBrightnessPoints.get(1000f), SMALL_DELTA);
-    }
-
-    private String getValidLuxThrottling() {
-        return "<luxThrottling>\n"
-                + "    <brightnessLimitMap>\n"
-                + "        <type>adaptive</type>\n"
-                + "        <map>\n"
-                + "            <point>"
-                + "                <first>1000</first>\n"
-                + "                <second>0.3</second>\n"
-                + "            </point>"
-                + "            <point>"
-                + "                <first>5000</first>\n"
-                + "                <second>0.5</second>\n"
-                + "            </point>"
-                + "        </map>\n"
-                + "    </brightnessLimitMap>\n"
-                + "    <brightnessLimitMap>\n"
-                + "        <type>default</type>\n"
-                + "        <map>\n"
-                + "            <point>"
-                + "                <first>1500</first>\n"
-                + "                <second>0.35</second>\n"
-                + "            </point>"
-                + "            <point>"
-                + "                <first>5500</first>\n"
-                + "                <second>0.55</second>\n"
-                + "            </point>"
-                + "        </map>\n"
-                + "    </brightnessLimitMap>\n"
-                + "</luxThrottling>";
-    }
-
-    private String getInvalidLuxThrottling() {
-        return "<luxThrottling>\n"
-                + "    <brightnessLimitMap>\n"
-                + "        <type>adaptive</type>\n"
-                + "        <map>\n"
-                + "            <point>"
-                + "                <first>1000</first>\n"
-                + "                <second>0.3</second>\n"
-                + "            </point>"
-                + "            <point>" // second > hbm.transitionPoint, skipped
-                + "                <first>1500</first>\n"
-                + "                <second>0.9</second>\n"
-                + "            </point>"
-                + "            <point>" // same lux value, skipped
-                + "                <first>1000</first>\n"
-                + "                <second>0.5</second>\n"
-                + "            </point>"
-                + "        </map>\n"
-                + "    </brightnessLimitMap>\n"
-                + "    <brightnessLimitMap>\n" // Same type, skipped
-                + "        <type>adaptive</type>\n"
-                + "        <map>\n"
-                + "            <point>"
-                + "                <first>2000</first>\n"
-                + "                <second>0.35</second>\n"
-                + "            </point>"
-                + "            <point>"
-                + "                <first>6000</first>\n"
-                + "                <second>0.55</second>\n"
-                + "            </point>"
-                + "        </map>\n"
-                + "    </brightnessLimitMap>\n"
-                + "    <brightnessLimitMap>\n" // Invalid points only, skipped
-                + "        <type>default</type>\n"
-                + "        <map>\n"
-                + "            <point>"
-                + "                <first>2500</first>\n"
-                + "                <second>0.99</second>\n"
-                + "            </point>"
-                + "        </map>\n"
-                + "    </brightnessLimitMap>\n"
-                + "</luxThrottling>";
-    }
-
-    private String getRefreshThermalThrottlingMaps() {
-        return "<refreshRateThrottlingMap>\n"
-               + "    <refreshRateThrottlingPoint>\n"
-               + "        <thermalStatus>critical</thermalStatus>\n"
-               + "        <refreshRateRange>\n"
-               + "            <minimum>30</minimum>\n"
-               + "            <maximum>60</maximum>\n"
-               + "        </refreshRateRange>\n"
-               + "    </refreshRateThrottlingPoint>\n"
-               + "    <refreshRateThrottlingPoint>\n"
-               + "        <thermalStatus>shutdown</thermalStatus>\n"
-               + "        <refreshRateRange>\n"
-               + "            <minimum>0</minimum>\n"
-               + "            <maximum>30</maximum>\n"
-               + "        </refreshRateRange>\n"
-               + "    </refreshRateThrottlingPoint>\n"
-               + "</refreshRateThrottlingMap>\n"
-               + "<refreshRateThrottlingMap id=\"test\">\n"
-               + "    <refreshRateThrottlingPoint>\n"
-               + "        <thermalStatus>emergency</thermalStatus>\n"
-               + "        <refreshRateRange>\n"
-               + "            <minimum>60</minimum>\n"
-               + "            <maximum>90</maximum>\n"
-               + "        </refreshRateRange>\n"
-               + "    </refreshRateThrottlingPoint>\n"
-               + "</refreshRateThrottlingMap>\n";
-    }
-
-    private String getContent() {
-        return getContent(getValidLuxThrottling());
-    }
-
-    private String getContent(String brightnessCapConfig) {
-        return "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
-                + "<displayConfiguration>\n"
-                +   "<name>Example Display</name>"
-                +   "<screenBrightnessMap>\n"
-                +       "<point>\n"
-                +           "<value>0.0</value>\n"
-                +           "<nits>2.0</nits>\n"
-                +       "</point>\n"
-                +       "<point>\n"
-                +           "<value>0.62</value>\n"
-                +           "<nits>500.0</nits>\n"
-                +       "</point>\n"
-                +       "<point>\n"
-                +           "<value>1.0</value>\n"
-                +           "<nits>800.0</nits>\n"
-                +       "</point>\n"
-                +   "</screenBrightnessMap>\n"
-                +   "<autoBrightness>\n"
-                +       "<brighteningLightDebounceMillis>2000</brighteningLightDebounceMillis>\n"
-                +       "<darkeningLightDebounceMillis>1000</darkeningLightDebounceMillis>\n"
-                +       "<displayBrightnessMapping>\n"
-                +            "<displayBrightnessPoint>\n"
-                +                "<lux>50</lux>\n"
-                +                "<nits>45.32</nits>\n"
-                +            "</displayBrightnessPoint>\n"
-                +            "<displayBrightnessPoint>\n"
-                +                "<lux>80</lux>\n"
-                +                "<nits>75.43</nits>\n"
-                +            "</displayBrightnessPoint>\n"
-                +       "</displayBrightnessMapping>\n"
-                +   "</autoBrightness>\n"
-                +   "<highBrightnessMode enabled=\"true\">\n"
-                +       "<transitionPoint>0.62</transitionPoint>\n"
-                +       "<minimumLux>10000</minimumLux>\n"
-                +       "<timing>\n"
-                +           "<!-- allow for 5 minutes out of every 30 minutes -->\n"
-                +           "<timeWindowSecs>1800</timeWindowSecs>\n"
-                +           "<timeMaxSecs>300</timeMaxSecs>\n"
-                +           "<timeMinSecs>60</timeMinSecs>\n"
-                +       "</timing>\n"
-                +       "<refreshRate>\n"
-                +           "<minimum>120</minimum>\n"
-                +           "<maximum>120</maximum>\n"
-                +       "</refreshRate>\n"
-                +       "<thermalStatusLimit>light</thermalStatusLimit>\n"
-                +       "<allowInLowPowerMode>false</allowInLowPowerMode>\n"
-                +       "<sdrHdrRatioMap>\n"
-                +            "<point>\n"
-                +                "<sdrNits>2.000</sdrNits>\n"
-                +                "<hdrRatio>4.000</hdrRatio>\n"
-                +            "</point>\n"
-                +            "<point>\n"
-                +                "<sdrNits>500.0</sdrNits>\n"
-                +                "<hdrRatio>1.6</hdrRatio>\n"
-                +            "</point>\n"
-                +       "</sdrHdrRatioMap>\n"
-                +   "</highBrightnessMode>\n"
-                + brightnessCapConfig
-                +   "<screenOffBrightnessSensor>\n"
-                +       "<type>sensor_12345</type>\n"
-                +       "<name>Sensor 12345</name>\n"
-                +   "</screenOffBrightnessSensor>\n"
-                +   "<ambientBrightnessChangeThresholds>\n"
-                +       "<brighteningThresholds>\n"
-                +           "<minimum>10</minimum>\n"
-                +           "<brightnessThresholdPoints>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0</threshold><percentage>13</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>100</threshold><percentage>14</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>200</threshold><percentage>15</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +           "</brightnessThresholdPoints>\n"
-                +       "</brighteningThresholds>\n"
-                +       "<darkeningThresholds>\n"
-                +           "<minimum>30</minimum>\n"
-                +           "<brightnessThresholdPoints>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0</threshold><percentage>15</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>300</threshold><percentage>16</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>400</threshold><percentage>17</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +           "</brightnessThresholdPoints>\n"
-                +       "</darkeningThresholds>\n"
-                +   "</ambientBrightnessChangeThresholds>\n"
-                +   "<displayBrightnessChangeThresholds>\n"
-                +       "<brighteningThresholds>\n"
-                +           "<minimum>0.1</minimum>\n"
-                +           "<brightnessThresholdPoints>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0</threshold>\n"
-                +                   "<percentage>9</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0.10</threshold>\n"
-                +                   "<percentage>10</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0.20</threshold>\n"
-                +                   "<percentage>11</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +           "</brightnessThresholdPoints>\n"
-                +       "</brighteningThresholds>\n"
-                +       "<darkeningThresholds>\n"
-                +           "<minimum>0.3</minimum>\n"
-                +           "<brightnessThresholdPoints>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0</threshold><percentage>11</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0.11</threshold><percentage>12</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0.21</threshold><percentage>13</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +           "</brightnessThresholdPoints>\n"
-                +       "</darkeningThresholds>\n"
-                +   "</displayBrightnessChangeThresholds>\n"
-                +   "<ambientBrightnessChangeThresholdsIdle>\n"
-                +       "<brighteningThresholds>\n"
-                +           "<minimum>20</minimum>\n"
-                +           "<brightnessThresholdPoints>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0</threshold><percentage>21</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>500</threshold><percentage>22</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>600</threshold><percentage>23</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +           "</brightnessThresholdPoints>\n"
-                +       "</brighteningThresholds>\n"
-                +       "<darkeningThresholds>\n"
-                +           "<minimum>40</minimum>\n"
-                +           "<brightnessThresholdPoints>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0</threshold><percentage>23</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>700</threshold><percentage>24</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>800</threshold><percentage>25</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +           "</brightnessThresholdPoints>\n"
-                +       "</darkeningThresholds>\n"
-                +   "</ambientBrightnessChangeThresholdsIdle>\n"
-                +   "<displayBrightnessChangeThresholdsIdle>\n"
-                +       "<brighteningThresholds>\n"
-                +           "<minimum>0.2</minimum>\n"
-                +           "<brightnessThresholdPoints>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0</threshold><percentage>17</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0.12</threshold><percentage>18</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0.22</threshold><percentage>19</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +           "</brightnessThresholdPoints>\n"
-                +       "</brighteningThresholds>\n"
-                +       "<darkeningThresholds>\n"
-                +           "<minimum>0.4</minimum>\n"
-                +           "<brightnessThresholdPoints>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0</threshold><percentage>19</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0.13</threshold><percentage>20</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +               "<brightnessThresholdPoint>\n"
-                +                   "<threshold>0.23</threshold><percentage>21</percentage>\n"
-                +               "</brightnessThresholdPoint>\n"
-                +           "</brightnessThresholdPoints>\n"
-                +       "</darkeningThresholds>\n"
-                +   "</displayBrightnessChangeThresholdsIdle>\n"
-                +   "<screenBrightnessRampFastDecrease>0.01</screenBrightnessRampFastDecrease> "
-                +   "<screenBrightnessRampFastIncrease>0.02</screenBrightnessRampFastIncrease>  "
-                +   "<screenBrightnessRampSlowDecrease>0.03</screenBrightnessRampSlowDecrease>"
-                +   "<screenBrightnessRampSlowIncrease>0.04</screenBrightnessRampSlowIncrease>"
-                +   "<screenBrightnessRampIncreaseMaxMillis>"
-                +       "2000"
-                +   "</screenBrightnessRampIncreaseMaxMillis>"
-                +   "<screenBrightnessRampDecreaseMaxMillis>"
-                +       "3000"
-                +   "</screenBrightnessRampDecreaseMaxMillis>"
-                +   "<ambientLightHorizonLong>5000</ambientLightHorizonLong>\n"
-                +   "<ambientLightHorizonShort>50</ambientLightHorizonShort>\n"
-                +   "<screenBrightnessRampIncreaseMaxMillis>"
-                +       "2000"
-                +   "</screenBrightnessRampIncreaseMaxMillis>\n"
-                +   "<thermalThrottling>\n"
-                +       "<brightnessThrottlingMap>\n"
-                +           "<brightnessThrottlingPoint>\n"
-                +               "<thermalStatus>light</thermalStatus>\n"
-                +               "<brightness>0.4</brightness>\n"
-                +           "</brightnessThrottlingPoint>\n"
-                +           "<brightnessThrottlingPoint>\n"
-                +               "<thermalStatus>moderate</thermalStatus>\n"
-                +               "<brightness>0.3</brightness>\n"
-                +           "</brightnessThrottlingPoint>\n"
-                +           "<brightnessThrottlingPoint>\n"
-                +               "<thermalStatus>severe</thermalStatus>\n"
-                +               "<brightness>0.2</brightness>\n"
-                +           "</brightnessThrottlingPoint>\n"
-                +           "<brightnessThrottlingPoint>\n"
-                +               "<thermalStatus>critical</thermalStatus>\n"
-                +               "<brightness>0.1</brightness>\n"
-                +           "</brightnessThrottlingPoint>\n"
-                +           "<brightnessThrottlingPoint>\n"
-                +               "<thermalStatus>emergency</thermalStatus>\n"
-                +               "<brightness>0.05</brightness>\n"
-                +           "</brightnessThrottlingPoint>\n"
-                +           "<brightnessThrottlingPoint>\n"
-                +               "<thermalStatus>shutdown</thermalStatus>\n"
-                +               "<brightness>0.025</brightness>\n"
-                +           "</brightnessThrottlingPoint>\n"
-                +       "</brightnessThrottlingMap>\n"
-                +       "<brightnessThrottlingMap id=\"concurrent\">\n"
-                +           "<brightnessThrottlingPoint>\n"
-                +               "<thermalStatus>light</thermalStatus>\n"
-                +               "<brightness>0.2</brightness>\n"
-                +           "</brightnessThrottlingPoint>\n"
-                +           "<brightnessThrottlingPoint>\n"
-                +               "<thermalStatus>moderate</thermalStatus>\n"
-                +               "<brightness>0.15</brightness>\n"
-                +           "</brightnessThrottlingPoint>\n"
-                +           "<brightnessThrottlingPoint>\n"
-                +               "<thermalStatus>severe</thermalStatus>\n"
-                +               "<brightness>0.1</brightness>\n"
-                +           "</brightnessThrottlingPoint>\n"
-                +           "<brightnessThrottlingPoint>\n"
-                +               "<thermalStatus>critical</thermalStatus>\n"
-                +               "<brightness>0.05</brightness>\n"
-                +           "</brightnessThrottlingPoint>\n"
-                +           "<brightnessThrottlingPoint>\n"
-                +               "<thermalStatus>emergency</thermalStatus>\n"
-                +               "<brightness>0.025</brightness>\n"
-                +           "</brightnessThrottlingPoint>\n"
-                +           "<brightnessThrottlingPoint>\n"
-                +               "<thermalStatus>shutdown</thermalStatus>\n"
-                +               "<brightness>0.0125</brightness>\n"
-                +           "</brightnessThrottlingPoint>\n"
-                +       "</brightnessThrottlingMap>\n"
-                +  getRefreshThermalThrottlingMaps()
-                +   "</thermalThrottling>\n"
-                +   "<refreshRate>\n"
-                +       "<defaultRefreshRate>45</defaultRefreshRate>\n"
-                +       "<defaultPeakRefreshRate>85</defaultPeakRefreshRate>\n"
-                +       "<refreshRateZoneProfiles>"
-                +           "<refreshRateZoneProfile id=\"test1\">"
-                +               "<refreshRateRange>\n"
-                +                   "<minimum>60</minimum>\n"
-                +                   "<maximum>60</maximum>\n"
-                +               "</refreshRateRange>\n"
-                +           "</refreshRateZoneProfile>\n"
-                +           "<refreshRateZoneProfile id=\"test2\">"
-                +               "<refreshRateRange>\n"
-                +                   "<minimum>80</minimum>\n"
-                +                   "<maximum>90</maximum>\n"
-                +               "</refreshRateRange>\n"
-                +           "</refreshRateZoneProfile>\n"
-                +       "</refreshRateZoneProfiles>"
-                +       "<defaultRefreshRateInHbmHdr>82</defaultRefreshRateInHbmHdr>\n"
-                +       "<defaultRefreshRateInHbmSunlight>83</defaultRefreshRateInHbmSunlight>\n"
-                +       "<lowerBlockingZoneConfigs>\n"
-                +           "<defaultRefreshRate>75</defaultRefreshRate>\n"
-                +           "<blockingZoneThreshold>\n"
-                +               "<displayBrightnessPoint>\n"
-                +                   "<lux>50</lux>\n"
-                // This number will be rounded to integer when read by the system
-                +                   "<nits>45.3</nits>\n"
-                +               "</displayBrightnessPoint>\n"
-                +               "<displayBrightnessPoint>\n"
-                +                   "<lux>60</lux>\n"
-                // This number will be rounded to integer when read by the system
-                +                   "<nits>55.2</nits>\n"
-                +               "</displayBrightnessPoint>\n"
-                +           "</blockingZoneThreshold>\n"
-                +       "</lowerBlockingZoneConfigs>\n"
-                +       "<higherBlockingZoneConfigs>\n"
-                +           "<defaultRefreshRate>90</defaultRefreshRate>\n"
-                +           "<blockingZoneThreshold>\n"
-                +               "<displayBrightnessPoint>\n"
-                +                   "<lux>70</lux>\n"
-                // This number will be rounded to integer when read by the system
-                +                   "<nits>65.6</nits>\n"
-                +               "</displayBrightnessPoint>\n"
-                +               "<displayBrightnessPoint>\n"
-                +                   "<lux>80</lux>\n"
-                // This number will be rounded to integer when read by the system
-                +                   "<nits>75</nits>\n"
-                +               "</displayBrightnessPoint>\n"
-                +           "</blockingZoneThreshold>\n"
-                +       "</higherBlockingZoneConfigs>\n"
-                +   "</refreshRate>\n"
-                +   "<screenOffBrightnessSensorValueToLux>\n"
-                +       "<item>-1</item>\n"
-                +       "<item>10</item>\n"
-                +       "<item>20</item>\n"
-                +       "<item>30</item>\n"
-                +       "<item>40</item>\n"
-                +   "</screenOffBrightnessSensorValueToLux>\n"
-                +   "<usiVersion>\n"
-                +       "<majorVersion>2</majorVersion>\n"
-                +       "<minorVersion>0</minorVersion>\n"
-                +   "</usiVersion>\n"
-                + "</displayConfiguration>\n";
-    }
-
-    private void mockDeviceConfigs() {
-        when(mResources.getFloat(com.android.internal.R.dimen
-                .config_screenBrightnessSettingDefaultFloat)).thenReturn(0.5f);
-        when(mResources.getFloat(com.android.internal.R.dimen
-                .config_screenBrightnessSettingMaximumFloat)).thenReturn(1.0f);
-    }
-
-    private void setupDisplayDeviceConfigFromDisplayConfigFile() throws IOException {
-        setupDisplayDeviceConfigFromDisplayConfigFile(getContent());
-    }
-
-    private void setupDisplayDeviceConfigFromDisplayConfigFile(String content) throws IOException {
-        Path tempFile = Files.createTempFile("display_config", ".tmp");
-        Files.write(tempFile, content.getBytes(StandardCharsets.UTF_8));
-        mDisplayDeviceConfig = new DisplayDeviceConfig(mContext);
-        mDisplayDeviceConfig.initFromFile(tempFile.toFile());
-    }
-
-    private void setupDisplayDeviceConfigFromConfigResourceFile() {
-        TypedArray screenBrightnessNits = createFloatTypedArray(new float[]{2.0f, 250.0f, 650.0f});
-        when(mResources.obtainTypedArray(
-                com.android.internal.R.array.config_screenBrightnessNits))
-                .thenReturn(screenBrightnessNits);
-        TypedArray screenBrightnessBacklight = createFloatTypedArray(new
-                float[]{0.0f, 120.0f, 255.0f});
-        when(mResources.obtainTypedArray(
-                com.android.internal.R.array.config_screenBrightnessBacklight))
-                .thenReturn(screenBrightnessBacklight);
-        when(mResources.getIntArray(com.android.internal.R.array
-                .config_screenBrightnessBacklight)).thenReturn(new int[]{0, 120, 255});
-
-        when(mResources.getIntArray(com.android.internal.R.array
-                .config_autoBrightnessLevels)).thenReturn(new int[]{30, 80});
-        when(mResources.getIntArray(com.android.internal.R.array
-                .config_autoBrightnessDisplayValuesNits)).thenReturn(new int[]{25, 55});
-
-        TypedArray screenBrightnessLevelNits = createFloatTypedArray(new
-                float[]{2.0f, 200.0f, 600.0f});
-        when(mResources.obtainTypedArray(
-                com.android.internal.R.array.config_autoBrightnessDisplayValuesNits))
-                .thenReturn(screenBrightnessLevelNits);
-        int[] screenBrightnessLevelLux = new int[]{0, 110, 500};
-        when(mResources.getIntArray(
-                com.android.internal.R.array.config_autoBrightnessLevels))
-                .thenReturn(screenBrightnessLevelLux);
-
-        // Thresholds
-        // Config.xml requires the levels arrays to be of length N and the thresholds arrays to be
-        // of length N+1
-        when(mResources.getIntArray(com.android.internal.R.array.config_ambientThresholdLevels))
-                .thenReturn(new int[]{30, 31});
-        when(mResources.getIntArray(com.android.internal.R.array.config_screenThresholdLevels))
-                .thenReturn(new int[]{42, 43});
-        when(mResources.getIntArray(
-                com.android.internal.R.array.config_ambientBrighteningThresholds))
-                .thenReturn(new int[]{270, 280, 290});
-        when(mResources.getIntArray(com.android.internal.R.array.config_ambientDarkeningThresholds))
-                .thenReturn(new int[]{290, 300, 310});
-        when(mResources.getIntArray(R.array.config_screenBrighteningThresholds))
-                .thenReturn(new int[]{350, 360, 370});
-        when(mResources.getIntArray(R.array.config_screenDarkeningThresholds))
-                .thenReturn(new int[]{370, 380, 390});
-
-        // Configs related to refresh rates and blocking zones
-        when(mResources.getInteger(R.integer.config_defaultPeakRefreshRate))
-                .thenReturn(DEFAULT_PEAK_REFRESH_RATE);
-        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))
-                .thenReturn(LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
-        when(mResources.getIntArray(
-                R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate))
-                .thenReturn(HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
-        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);
-    }
-
-    private TypedArray createFloatTypedArray(float[] vals) {
-        TypedArray mockArray = mock(TypedArray.class);
-        when(mockArray.length()).thenAnswer(invocation -> {
-            return vals.length;
-        });
-        when(mockArray.getFloat(anyInt(), anyFloat())).thenAnswer(invocation -> {
-            final float def = (float) invocation.getArguments()[1];
-            if (vals == null) {
-                return def;
-            }
-            int idx = (int) invocation.getArguments()[0];
-            if (idx >= 0 && idx < vals.length) {
-                return vals[idx];
-            } else {
-                return def;
-            }
-        });
-        return mockArray;
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
deleted file mode 100644
index 0e775d5..0000000
--- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ /dev/null
@@ -1,2242 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display;
-
-import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY;
-import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
-import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
-import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
-import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
-import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
-import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
-
-import static com.android.server.display.VirtualDisplayAdapter.UNIQUE_ID_PREFIX;
-
-import static com.google.common.truth.Truth.assertThat;
-
-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.assertThrows;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.PropertyInvalidatedCache;
-import android.companion.virtual.IVirtualDevice;
-import android.companion.virtual.IVirtualDeviceManager;
-import android.companion.virtual.VirtualDeviceManager;
-import android.compat.testing.PlatformCompatChangeRule;
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.graphics.Insets;
-import android.graphics.Rect;
-import android.hardware.display.BrightnessConfiguration;
-import android.hardware.display.Curve;
-import android.hardware.display.DisplayManager;
-import android.hardware.display.DisplayManagerGlobal;
-import android.hardware.display.DisplayManagerInternal;
-import android.hardware.display.DisplayViewport;
-import android.hardware.display.DisplayedContentSample;
-import android.hardware.display.DisplayedContentSamplingAttributes;
-import android.hardware.display.HdrConversionMode;
-import android.hardware.display.IDisplayManagerCallback;
-import android.hardware.display.IVirtualDisplayCallback;
-import android.hardware.display.VirtualDisplayConfig;
-import android.media.projection.IMediaProjection;
-import android.media.projection.IMediaProjectionManager;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.MessageQueue;
-import android.os.Process;
-import android.os.RemoteException;
-import android.view.ContentRecordingSession;
-import android.view.Display;
-import android.view.DisplayCutout;
-import android.view.DisplayEventReceiver;
-import android.view.DisplayInfo;
-import android.view.Surface;
-import android.view.SurfaceControl;
-import android.window.DisplayWindowPolicyController;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.FlakyTest;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.R;
-import com.android.server.LocalServices;
-import com.android.server.SystemService;
-import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
-import com.android.server.display.DisplayManagerService.DeviceStateListener;
-import com.android.server.display.DisplayManagerService.SyncRoot;
-import com.android.server.input.InputManagerInternal;
-import com.android.server.lights.LightsManager;
-import com.android.server.sensors.SensorManagerInternal;
-import com.android.server.wm.WindowManagerInternal;
-
-import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
-import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
-
-import com.google.common.collect.ImmutableMap;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-import java.time.Duration;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.LongStream;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class DisplayManagerServiceTest {
-    private static final int MSG_REGISTER_DEFAULT_DISPLAY_ADAPTERS = 1;
-    private static final long SHORT_DEFAULT_DISPLAY_TIMEOUT_MILLIS = 10;
-    private static final String VIRTUAL_DISPLAY_NAME = "Test Virtual Display";
-    private static final String PACKAGE_NAME = "com.android.frameworks.servicestests";
-    private static final long STANDARD_DISPLAY_EVENTS = DisplayManager.EVENT_FLAG_DISPLAY_ADDED
-                    | DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
-                    | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED;
-
-    @Rule
-    public TestRule compatChangeRule = new PlatformCompatChangeRule();
-
-    private Context mContext;
-
-    private int mHdrConversionMode;
-
-    private int mPreferredHdrOutputType;
-
-    private final DisplayManagerService.Injector mShortMockedInjector =
-            new DisplayManagerService.Injector() {
-                @Override
-                VirtualDisplayAdapter getVirtualDisplayAdapter(SyncRoot syncRoot,
-                        Context context, Handler handler, DisplayAdapter.Listener listener) {
-                    return mMockVirtualDisplayAdapter;
-                }
-
-                @Override
-                LocalDisplayAdapter getLocalDisplayAdapter(SyncRoot syncRoot, Context context,
-                        Handler handler, DisplayAdapter.Listener displayAdapterListener) {
-                    return new LocalDisplayAdapter(syncRoot, context, handler,
-                            displayAdapterListener, new LocalDisplayAdapter.Injector() {
-                        @Override
-                        public LocalDisplayAdapter.SurfaceControlProxy getSurfaceControlProxy() {
-                            return mSurfaceControlProxy;
-                        }
-                    });
-                }
-
-                @Override
-                long getDefaultDisplayDelayTimeout() {
-                    return SHORT_DEFAULT_DISPLAY_TIMEOUT_MILLIS;
-                }
-            };
-
-    class BasicInjector extends DisplayManagerService.Injector {
-        @Override
-        IMediaProjectionManager getProjectionService() {
-            return mMockProjectionService;
-        }
-
-        @Override
-        VirtualDisplayAdapter getVirtualDisplayAdapter(SyncRoot syncRoot, Context context,
-                Handler handler, DisplayAdapter.Listener displayAdapterListener) {
-            return new VirtualDisplayAdapter(syncRoot, context, handler, displayAdapterListener,
-                    new VirtualDisplayAdapter.SurfaceControlDisplayFactory() {
-                        @Override
-                        public IBinder createDisplay(String name, boolean secure,
-                                float requestedRefreshRate) {
-                            return mMockDisplayToken;
-                        }
-
-                        @Override
-                        public void destroyDisplay(IBinder displayToken) {
-                        }
-                    });
-        }
-
-        @Override
-        LocalDisplayAdapter getLocalDisplayAdapter(SyncRoot syncRoot, Context context,
-                Handler handler, DisplayAdapter.Listener displayAdapterListener) {
-            return new LocalDisplayAdapter(syncRoot, context, handler,
-                    displayAdapterListener, new LocalDisplayAdapter.Injector() {
-                        @Override
-                        public LocalDisplayAdapter.SurfaceControlProxy getSurfaceControlProxy() {
-                            return mSurfaceControlProxy;
-                        }
-                    });
-        }
-
-        @Override
-        int setHdrConversionMode(int conversionMode, int preferredHdrOutputType,
-                int[] autoHdrTypes) {
-            mHdrConversionMode = conversionMode;
-            mPreferredHdrOutputType = preferredHdrOutputType;
-            return Display.HdrCapabilities.HDR_TYPE_INVALID;
-        }
-
-        @Override
-        int[] getSupportedHdrOutputTypes() {
-            return new int[]{};
-        }
-
-        @Override
-        int[] getHdrOutputTypesWithLatency() {
-            return new int[]{Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION};
-        }
-
-        boolean getHdrOutputConversionSupport() {
-            return true;
-        }
-   }
-
-    private final DisplayManagerService.Injector mBasicInjector = new BasicInjector();
-
-    @Mock IMediaProjectionManager mMockProjectionService;
-    @Mock IVirtualDeviceManager mIVirtualDeviceManager;
-    @Mock InputManagerInternal mMockInputManagerInternal;
-    @Mock VirtualDeviceManagerInternal mMockVirtualDeviceManagerInternal;
-    @Mock IVirtualDisplayCallback.Stub mMockAppToken;
-    @Mock IVirtualDisplayCallback.Stub mMockAppToken2;
-    @Mock WindowManagerInternal mMockWindowManagerInternal;
-    @Mock LightsManager mMockLightsManager;
-    @Mock VirtualDisplayAdapter mMockVirtualDisplayAdapter;
-    @Mock LocalDisplayAdapter.SurfaceControlProxy mSurfaceControlProxy;
-    @Mock IBinder mMockDisplayToken;
-    @Mock SensorManagerInternal mMockSensorManagerInternal;
-
-    @Captor ArgumentCaptor<ContentRecordingSession> mContentRecordingSessionCaptor;
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        LocalServices.removeServiceForTest(InputManagerInternal.class);
-        LocalServices.addService(InputManagerInternal.class, mMockInputManagerInternal);
-        LocalServices.removeServiceForTest(WindowManagerInternal.class);
-        LocalServices.addService(WindowManagerInternal.class, mMockWindowManagerInternal);
-        LocalServices.removeServiceForTest(LightsManager.class);
-        LocalServices.addService(LightsManager.class, mMockLightsManager);
-        LocalServices.removeServiceForTest(SensorManagerInternal.class);
-        LocalServices.addService(SensorManagerInternal.class, mMockSensorManagerInternal);
-        LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class);
-        LocalServices.addService(
-                VirtualDeviceManagerInternal.class, mMockVirtualDeviceManagerInternal);
-
-        mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
-
-        VirtualDeviceManager vdm = new VirtualDeviceManager(mIVirtualDeviceManager, mContext);
-        when(mContext.getSystemService(VirtualDeviceManager.class)).thenReturn(vdm);
-        // Disable binder caches in this process.
-        PropertyInvalidatedCache.disableForTestMode();
-        setUpDisplay();
-    }
-
-    private void setUpDisplay() {
-        long[] ids = new long[] {100};
-        when(mSurfaceControlProxy.getPhysicalDisplayIds()).thenReturn(ids);
-        when(mSurfaceControlProxy.getPhysicalDisplayToken(anyLong()))
-                .thenReturn(mMockDisplayToken);
-        SurfaceControl.StaticDisplayInfo staticDisplayInfo = new SurfaceControl.StaticDisplayInfo();
-        staticDisplayInfo.isInternal = true;
-        when(mSurfaceControlProxy.getStaticDisplayInfo(anyLong()))
-                .thenReturn(staticDisplayInfo);
-        SurfaceControl.DynamicDisplayInfo dynamicDisplayMode =
-                new SurfaceControl.DynamicDisplayInfo();
-        SurfaceControl.DisplayMode displayMode = new SurfaceControl.DisplayMode();
-        displayMode.width = 100;
-        displayMode.height = 200;
-        displayMode.supportedHdrTypes = new int[]{1, 2};
-        dynamicDisplayMode.supportedDisplayModes = new SurfaceControl.DisplayMode[] {displayMode};
-        when(mSurfaceControlProxy.getDynamicDisplayInfo(anyLong()))
-                .thenReturn(dynamicDisplayMode);
-        when(mSurfaceControlProxy.getDesiredDisplayModeSpecs(mMockDisplayToken))
-                .thenReturn(new SurfaceControl.DesiredDisplayModeSpecs());
-    }
-
-    @Test
-    public void testCreateVirtualDisplay_sentToInputManager() throws RemoteException {
-        // This is to update the display device config such that DisplayManagerService can ignore
-        // the usage of SensorManager, which is available only after the PowerManagerService
-        // is ready.
-        resetConfigToIgnoreSensorManager(mContext);
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-        displayManager.systemReady(false /* safeMode */);
-        displayManager.windowManagerAndInputReady();
-
-        // This is effectively the DisplayManager service published to ServiceManager.
-        DisplayManagerService.BinderService bs = displayManager.new BinderService();
-
-        String uniqueId = "uniqueId --- Test";
-        String uniqueIdPrefix = UNIQUE_ID_PREFIX + mContext.getPackageName() + ":";
-        int width = 600;
-        int height = 800;
-        int dpi = 320;
-        int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH;
-
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, width, height, dpi);
-        builder.setUniqueId(uniqueId);
-        builder.setFlags(flags);
-        int displayId = bs.createVirtualDisplay(builder.build(), mMockAppToken /* callback */,
-                null /* projection */, PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
-
-        // flush the handler
-        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
-
-        ArgumentCaptor<List<DisplayViewport>> viewportCaptor = ArgumentCaptor.forClass(List.class);
-        verify(mMockInputManagerInternal).setDisplayViewports(viewportCaptor.capture());
-        List<DisplayViewport> viewports = viewportCaptor.getValue();
-
-        // Expect to receive at least 2 viewports: at least 1 internal, and 1 virtual
-        assertTrue(viewports.size() >= 2);
-
-        DisplayViewport virtualViewport = null;
-        DisplayViewport internalViewport = null;
-        for (int i = 0; i < viewports.size(); i++) {
-            DisplayViewport v = viewports.get(i);
-            switch (v.type) {
-                case DisplayViewport.VIEWPORT_INTERNAL: {
-                    // If more than one internal viewport, this will get overwritten several times,
-                    // which for the purposes of this test is fine.
-                    internalViewport = v;
-                    assertTrue(internalViewport.valid);
-                    break;
-                }
-                case DisplayViewport.VIEWPORT_EXTERNAL: {
-                    // External view port is present for auto devices in the form of instrument
-                    // cluster.
-                    break;
-                }
-                case DisplayViewport.VIEWPORT_VIRTUAL: {
-                    virtualViewport = v;
-                    break;
-                }
-            }
-        }
-        // INTERNAL viewport gets created upon access.
-        assertNotNull(internalViewport);
-        assertNotNull(virtualViewport);
-
-        // VIRTUAL
-        assertEquals(height, virtualViewport.deviceHeight);
-        assertEquals(width, virtualViewport.deviceWidth);
-        assertEquals(uniqueIdPrefix + uniqueId, virtualViewport.uniqueId);
-        assertEquals(displayId, virtualViewport.displayId);
-    }
-
-    @Test
-    public void testPhysicalViewports() {
-        // This is to update the display device config such that DisplayManagerService can ignore
-        // the usage of SensorManager, which is available only after the PowerManagerService
-        // is ready.
-        resetConfigToIgnoreSensorManager(mContext);
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-        displayManager.systemReady(false /* safeMode */);
-        displayManager.windowManagerAndInputReady();
-
-        // This is effectively the DisplayManager service published to ServiceManager.
-        DisplayManagerService.BinderService bs = displayManager.new BinderService();
-
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-
-        final int[] displayIds = bs.getDisplayIds(/* includeDisabled= */ true);
-        final int size = displayIds.length;
-        assertTrue(size > 0);
-
-        Map<Integer, Integer> expectedDisplayTypeToViewPortTypeMapping = ImmutableMap.of(
-                Display.TYPE_INTERNAL, DisplayViewport.VIEWPORT_INTERNAL,
-                Display.TYPE_EXTERNAL, DisplayViewport.VIEWPORT_EXTERNAL
-        );
-        for (int i = 0; i < size; i++) {
-            DisplayInfo info = bs.getDisplayInfo(displayIds[i]);
-            assertTrue(expectedDisplayTypeToViewPortTypeMapping.keySet().contains(info.type));
-        }
-
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
-
-        // flush the handler
-        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
-
-        ArgumentCaptor<List<DisplayViewport>> viewportCaptor = ArgumentCaptor.forClass(List.class);
-        verify(mMockInputManagerInternal).setDisplayViewports(viewportCaptor.capture());
-        List<DisplayViewport> viewports = viewportCaptor.getValue();
-
-        // Due to the nature of foldables, we may have a different number of viewports than
-        // displays, just verify there's at least one.
-        final int viewportSize = viewports.size();
-        assertTrue(viewportSize > 0);
-
-        // Now verify that each viewport's displayId is valid.
-        Arrays.sort(displayIds);
-        for (int i = 0; i < viewportSize; i++) {
-            DisplayViewport viewport = viewports.get(i);
-            assertNotNull(viewport);
-            DisplayInfo displayInfo = bs.getDisplayInfo(viewport.displayId);
-            assertTrue(expectedDisplayTypeToViewPortTypeMapping.get(displayInfo.type)
-                    == viewport.type);
-            assertTrue(viewport.valid);
-            assertTrue(Arrays.binarySearch(displayIds, viewport.displayId) >= 0);
-        }
-    }
-
-    @Test
-    public void testCreateVirtualDisplayRotatesWithContent() throws Exception {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-
-        // This is effectively the DisplayManager service published to ServiceManager.
-        DisplayManagerService.BinderService bs = displayManager.new BinderService();
-
-        String uniqueId = "uniqueId --- Rotates With Content Test";
-        int width = 600;
-        int height = 800;
-        int dpi = 320;
-        int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT;
-
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, width, height, dpi);
-        builder.setFlags(flags);
-        builder.setUniqueId(uniqueId);
-        int displayId = bs.createVirtualDisplay(builder.build(), mMockAppToken /* callback */,
-                null /* projection */, PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
-
-        // flush the handler
-        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
-
-        DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
-        assertNotNull(ddi);
-        assertTrue((ddi.flags & DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT) != 0);
-    }
-
-    @Test
-    public void testCreateVirtualDisplayOwnFocus() throws RemoteException {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-
-        // This is effectively the DisplayManager service published to ServiceManager.
-        DisplayManagerService.BinderService bs = displayManager.new BinderService();
-
-        String uniqueId = "uniqueId --- Own Focus Test";
-        int width = 600;
-        int height = 800;
-        int dpi = 320;
-        int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_FOCUS
-                | DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
-
-        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)).thenReturn(
-                PackageManager.PERMISSION_GRANTED);
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, width, height, dpi);
-        builder.setFlags(flags);
-        builder.setUniqueId(uniqueId);
-        int displayId = bs.createVirtualDisplay(builder.build(), /* callback= */ mMockAppToken,
-                /* projection= */ null, PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
-
-        // flush the handler
-        displayManager.getDisplayHandler().runWithScissors(() -> {}, /* now= */ 0);
-
-        DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
-        assertNotNull(ddi);
-        assertTrue((ddi.flags & DisplayDeviceInfo.FLAG_OWN_FOCUS) != 0);
-    }
-
-    @Test
-    public void testCreateVirtualDisplayOwnFocus_nonTrustedDisplay() throws RemoteException {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-
-        // This is effectively the DisplayManager service published to ServiceManager.
-        DisplayManagerService.BinderService bs = displayManager.new BinderService();
-
-        String uniqueId = "uniqueId --- Own Focus Test -- nonTrustedDisplay";
-        int width = 600;
-        int height = 800;
-        int dpi = 320;
-        int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_FOCUS;
-
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, width, height, dpi);
-        builder.setFlags(flags);
-        builder.setUniqueId(uniqueId);
-        int displayId = bs.createVirtualDisplay(builder.build(), /* callback= */ mMockAppToken,
-                /* projection= */ null, PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
-
-        // flush the handler
-        displayManager.getDisplayHandler().runWithScissors(() -> {}, /* now= */ 0);
-
-        DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
-        assertNotNull(ddi);
-        assertTrue((ddi.flags & DisplayDeviceInfo.FLAG_OWN_FOCUS) == 0);
-    }
-
-    /**
-     * Tests that the virtual display is created along-side the default display.
-     */
-    @Test
-    public void testStartVirtualDisplayWithDefaultDisplay_Succeeds() throws Exception {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        registerDefaultDisplays(displayManager);
-        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
-    }
-
-    /**
-     * Tests that we send the device state to window manager
-     */
-    @Test
-    public void testOnStateChanged_sendsStateChangedEventToWm() throws Exception {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        registerDefaultDisplays(displayManager);
-        displayManager.windowManagerAndInputReady();
-        displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
-        DeviceStateListener listener = displayManager.new DeviceStateListener();
-        Handler handler = displayManager.getDisplayHandler();
-        IDisplayManagerCallback displayChangesCallback = registerDisplayChangeCallback(
-                displayManager);
-
-        listener.onStateChanged(123);
-        waitForIdleHandler(handler);
-
-        InOrder inOrder = inOrder(mMockWindowManagerInternal, displayChangesCallback);
-        // Verify there are no display events before WM call
-        inOrder.verify(displayChangesCallback, never()).onDisplayEvent(anyInt(), anyInt());
-        inOrder.verify(mMockWindowManagerInternal).onDisplayManagerReceivedDeviceState(123);
-    }
-
-    /**
-     * Tests that there should be a display change notification to WindowManager to update its own
-     * internal state for things like display cutout when nonOverrideDisplayInfo is changed.
-     */
-    @Test
-    public void testShouldNotifyChangeWhenNonOverrideDisplayInfoChanged() 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();
-        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
-        displayDeviceInfo.width = 100;
-        displayDeviceInfo.height = 200;
-        displayDeviceInfo.supportedModes = new Display.Mode[1];
-        displayDeviceInfo.supportedModes[0] = new Display.Mode(1, 100, 200, 60f);
-        displayDeviceInfo.modeId = 1;
-        final Rect zeroRect = new Rect();
-        displayDeviceInfo.displayCutout = new DisplayCutout(
-                Insets.of(0, 10, 0, 0),
-                zeroRect, new Rect(0, 0, 10, 10), zeroRect, zeroRect);
-        displayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY;
-        displayDeviceInfo.address = new TestUtils.TestDisplayAddress();
-        displayDevice.setDisplayDeviceInfo(displayDeviceInfo);
-        displayManager.getDisplayDeviceRepository()
-                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
-
-        // Find the display id of the added FakeDisplayDevice
-        DisplayManagerService.BinderService bs = displayManager.new BinderService();
-        int displayId = getDisplayIdForDisplayDevice(displayManager, bs, displayDevice);
-        // Setup override DisplayInfo
-        DisplayInfo overrideInfo = bs.getDisplayInfo(displayId);
-        displayManager.setDisplayInfoOverrideFromWindowManagerInternal(displayId, overrideInfo);
-
-        FakeDisplayManagerCallback callback = registerDisplayListenerCallback(
-                displayManager, bs, displayDevice);
-
-        // Simulate DisplayDevice change
-        DisplayDeviceInfo displayDeviceInfo2 = new DisplayDeviceInfo();
-        displayDeviceInfo2.copyFrom(displayDeviceInfo);
-        displayDeviceInfo2.displayCutout = null;
-        displayDevice.setDisplayDeviceInfo(displayDeviceInfo2);
-        displayManager.getDisplayDeviceRepository()
-                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_CHANGED);
-
-        Handler handler = displayManager.getDisplayHandler();
-        waitForIdleHandler(handler);
-        assertTrue(callback.mDisplayChangedCalled);
-    }
-
-    /**
-     * Tests that we get a Runtime exception when we cannot initialize the default display.
-     */
-    @Test
-    public void testStartVirtualDisplayWithDefDisplay_NoDefaultDisplay() throws Exception {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        Handler handler = displayManager.getDisplayHandler();
-        waitForIdleHandler(handler);
-
-        try {
-            displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
-        } catch (RuntimeException e) {
-            return;
-        }
-        fail("Expected DisplayManager to throw RuntimeException when it cannot initialize the"
-                + " default display");
-    }
-
-    /**
-     * Tests that we get a Runtime exception when we cannot initialize the virtual display.
-     */
-    @Test
-    public void testStartVirtualDisplayWithDefDisplay_NoVirtualDisplayAdapter() throws Exception {
-        DisplayManagerService displayManager = new DisplayManagerService(mContext,
-                new DisplayManagerService.Injector() {
-                    @Override
-                    VirtualDisplayAdapter getVirtualDisplayAdapter(SyncRoot syncRoot,
-                            Context context, Handler handler, DisplayAdapter.Listener listener) {
-                        return null;  // return null for the adapter.  This should cause a failure.
-                    }
-
-                    @Override
-                    long getDefaultDisplayDelayTimeout() {
-                        return SHORT_DEFAULT_DISPLAY_TIMEOUT_MILLIS;
-                    }
-                });
-        try {
-            displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
-        } catch (RuntimeException e) {
-            return;
-        }
-        fail("Expected DisplayManager to throw RuntimeException when it cannot initialize the"
-                + " virtual display adapter");
-    }
-
-    /**
-     * Tests that an exception is raised for too dark a brightness configuration.
-     */
-    @Test
-    public void testTooDarkBrightnessConfigurationThrowException() {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        Curve minimumBrightnessCurve = displayManager.getMinimumBrightnessCurveInternal();
-        float[] lux = minimumBrightnessCurve.getX();
-        float[] minimumNits = minimumBrightnessCurve.getY();
-        float[] nits = new float[minimumNits.length];
-        // For every control point, assert that making it slightly lower than the minimum throws an
-        // exception.
-        for (int i = 0; i < nits.length; i++) {
-            for (int j = 0; j < nits.length; j++) {
-                nits[j] = minimumNits[j];
-                if (j == i) {
-                    nits[j] -= 0.1f;
-                }
-                if (nits[j] < 0) {
-                    nits[j] = 0;
-                }
-            }
-            BrightnessConfiguration config =
-                    new BrightnessConfiguration.Builder(lux, nits).build();
-            Exception thrown = null;
-            try {
-                displayManager.validateBrightnessConfiguration(config);
-            } catch (IllegalArgumentException e) {
-                thrown = e;
-            }
-            assertNotNull("Building too dark a brightness configuration must throw an exception");
-        }
-    }
-
-    /**
-     * Tests that no exception is raised for not too dark a brightness configuration.
-     */
-    @Test
-    public void testBrightEnoughBrightnessConfigurationDoesNotThrowException() {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        Curve minimumBrightnessCurve = displayManager.getMinimumBrightnessCurveInternal();
-        float[] lux = minimumBrightnessCurve.getX();
-        float[] nits = minimumBrightnessCurve.getY();
-        BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits).build();
-        displayManager.validateBrightnessConfiguration(config);
-    }
-
-    /**
-     * Tests that null brightness configurations are alright.
-     */
-    @Test
-    public void testNullBrightnessConfiguration() {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        displayManager.validateBrightnessConfiguration(null);
-    }
-
-    /**
-     * Tests that collection of display color sampling results are sensible.
-     */
-    @Test
-    public void testDisplayedContentSampling() {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        registerDefaultDisplays(displayManager);
-
-        DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(0);
-        assertNotNull(ddi);
-
-        DisplayedContentSamplingAttributes attr =
-                displayManager.getDisplayedContentSamplingAttributesInternal(0);
-        if (attr == null) return; //sampling not supported on device, skip remainder of test.
-
-        boolean enabled = displayManager.setDisplayedContentSamplingEnabledInternal(0, true, 0, 0);
-        assertTrue(enabled);
-
-        displayManager.setDisplayedContentSamplingEnabledInternal(0, false, 0, 0);
-        DisplayedContentSample sample = displayManager.getDisplayedContentSampleInternal(0, 0, 0);
-        assertNotNull(sample);
-
-        long numPixels = ddi.width * ddi.height * sample.getNumFrames();
-        long[] samples = sample.getSampleComponent(DisplayedContentSample.ColorComponent.CHANNEL0);
-        assertTrue(samples.length == 0 || LongStream.of(samples).sum() == numPixels);
-
-        samples = sample.getSampleComponent(DisplayedContentSample.ColorComponent.CHANNEL1);
-        assertTrue(samples.length == 0 || LongStream.of(samples).sum() == numPixels);
-
-        samples = sample.getSampleComponent(DisplayedContentSample.ColorComponent.CHANNEL2);
-        assertTrue(samples.length == 0 || LongStream.of(samples).sum() == numPixels);
-
-        samples = sample.getSampleComponent(DisplayedContentSample.ColorComponent.CHANNEL3);
-        assertTrue(samples.length == 0 || LongStream.of(samples).sum() == numPixels);
-    }
-
-    /**
-     * Tests that the virtual display is created with
-     * {@link VirtualDisplayConfig.Builder#setDisplayIdToMirror(int)}
-     */
-    @Test
-    @FlakyTest(bugId = 127687569)
-    public void testCreateVirtualDisplay_displayIdToMirror() throws Exception {
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-
-        // This is effectively the DisplayManager service published to ServiceManager.
-        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
-        DisplayManagerService.LocalService localDisplayManager = displayManager.new LocalService();
-
-        final String uniqueId = "uniqueId --- displayIdToMirrorTest";
-        final int width = 600;
-        final int height = 800;
-        final int dpi = 320;
-
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, width, height, dpi);
-        builder.setUniqueId(uniqueId);
-        final int firstDisplayId = binderService.createVirtualDisplay(builder.build(),
-                mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-
-        // The second virtual display requests to mirror the first virtual display.
-        final String uniqueId2 = "uniqueId --- displayIdToMirrorTest #2";
-        when(mMockAppToken2.asBinder()).thenReturn(mMockAppToken2);
-        final VirtualDisplayConfig.Builder builder2 = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, width, height, dpi).setUniqueId(uniqueId2);
-        builder2.setUniqueId(uniqueId2);
-        builder2.setDisplayIdToMirror(firstDisplayId);
-        final int secondDisplayId = binderService.createVirtualDisplay(builder2.build(),
-                mMockAppToken2 /* callback */, null /* projection */,
-                PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
-
-        // flush the handler
-        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
-
-        // The displayId to mirror should be a default display if there is none initially.
-        assertEquals(localDisplayManager.getDisplayIdToMirror(firstDisplayId),
-                Display.DEFAULT_DISPLAY);
-        assertEquals(localDisplayManager.getDisplayIdToMirror(secondDisplayId),
-                firstDisplayId);
-    }
-
-    /** Tests that the virtual device is created in a device display group. */
-    @Test
-    public void createVirtualDisplay_addsDisplaysToDeviceDisplayGroups() throws Exception {
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        DisplayManagerInternal localService = displayManager.new LocalService();
-
-        registerDefaultDisplays(displayManager);
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-
-        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
-        when(virtualDevice.getDeviceId()).thenReturn(1);
-        when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
-        // Create a first virtual display. A display group should be created for this display on the
-        // virtual device.
-        final VirtualDisplayConfig.Builder builder1 =
-                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
-                        .setUniqueId("uniqueId --- device display group 1");
-
-        int displayId1 =
-                localService.createVirtualDisplay(
-                        builder1.build(),
-                        mMockAppToken /* callback */,
-                        virtualDevice /* virtualDeviceToken */,
-                        mock(DisplayWindowPolicyController.class),
-                        PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-        int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
-
-        // Create a second virtual display. This should be added to the previously created display
-        // group.
-        final VirtualDisplayConfig.Builder builder2 =
-                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
-                        .setUniqueId("uniqueId --- device display group 1");
-
-        int displayId2 =
-                localService.createVirtualDisplay(
-                        builder2.build(),
-                        mMockAppToken /* callback */,
-                        virtualDevice /* virtualDeviceToken */,
-                        mock(DisplayWindowPolicyController.class),
-                        PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-        int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
-
-        assertEquals(
-                "Both displays should be added to the same displayGroup.",
-                displayGroupId1,
-                displayGroupId2);
-    }
-
-    /**
-     * Tests that the virtual display is not added to the device display group when
-     * VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is set.
-     */
-    @Test
-    public void createVirtualDisplay_addsDisplaysToOwnDisplayGroups() throws Exception {
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        DisplayManagerInternal localService = displayManager.new LocalService();
-
-        registerDefaultDisplays(displayManager);
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-
-        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
-        when(virtualDevice.getDeviceId()).thenReturn(1);
-        when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
-
-        // Create a first virtual display. A display group should be created for this display on the
-        // virtual device.
-        final VirtualDisplayConfig.Builder builder1 =
-                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
-                        .setUniqueId("uniqueId --- device display group");
-
-        int displayId1 =
-                localService.createVirtualDisplay(
-                        builder1.build(),
-                        mMockAppToken /* callback */,
-                        virtualDevice /* virtualDeviceToken */,
-                        mock(DisplayWindowPolicyController.class),
-                        PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-        int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
-
-        // Create a second virtual display. With the flag VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP,
-        // the display should not be added to the previously created display group.
-        final VirtualDisplayConfig.Builder builder2 =
-                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
-                        .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
-                        .setUniqueId("uniqueId --- own display group");
-
-        when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
-
-        int displayId2 =
-                localService.createVirtualDisplay(
-                        builder2.build(),
-                        mMockAppToken /* callback */,
-                        virtualDevice /* virtualDeviceToken */,
-                        mock(DisplayWindowPolicyController.class),
-                        PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-        int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
-
-        assertNotEquals(
-                "Display 1 should be in the device display group and display 2 in its own display"
-                        + " group.",
-                displayGroupId1,
-                displayGroupId2);
-    }
-
-    @Test
-    public void displaysInDeviceOrOwnDisplayGroupShouldPreserveAlwaysUnlockedFlag()
-            throws Exception {
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        DisplayManagerInternal localService = displayManager.new LocalService();
-
-        registerDefaultDisplays(displayManager);
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-
-        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
-        when(virtualDevice.getDeviceId()).thenReturn(1);
-        when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
-
-        // Allow an ALWAYS_UNLOCKED display to be created.
-        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
-                .thenReturn(PackageManager.PERMISSION_GRANTED);
-
-        when(mContext.checkCallingPermission(ADD_ALWAYS_UNLOCKED_DISPLAY))
-                .thenReturn(PackageManager.PERMISSION_GRANTED);
-
-        // Create a virtual display in a device display group.
-        final VirtualDisplayConfig deviceDisplayGroupDisplayConfig =
-                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
-                        .setUniqueId("uniqueId --- device display group 1")
-                        .setFlags(VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED)
-                        .build();
-
-        int deviceDisplayGroupDisplayId =
-                localService.createVirtualDisplay(
-                        deviceDisplayGroupDisplayConfig,
-                        mMockAppToken /* callback */,
-                        virtualDevice /* virtualDeviceToken */,
-                        mock(DisplayWindowPolicyController.class),
-                        PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-
-        // Check that FLAG_ALWAYS_UNLOCKED is set.
-        assertNotEquals(
-                "FLAG_ALWAYS_UNLOCKED should be set for displays created in a device display"
-                        + " group.",
-                (displayManager.getDisplayDeviceInfoInternal(deviceDisplayGroupDisplayId).flags
-                        & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED),
-                0);
-
-        // Create a virtual display in its own display group.
-        final VirtualDisplayConfig ownDisplayGroupConfig =
-                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
-                        .setUniqueId("uniqueId --- own display group 1")
-                        .setFlags(
-                                VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED
-                                        | VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
-                        .build();
-
-        int ownDisplayGroupDisplayId =
-                localService.createVirtualDisplay(
-                        ownDisplayGroupConfig,
-                        mMockAppToken /* callback */,
-                        virtualDevice /* virtualDeviceToken */,
-                        mock(DisplayWindowPolicyController.class),
-                        PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-
-        // Check that FLAG_ALWAYS_UNLOCKED is set.
-        assertNotEquals(
-                "FLAG_ALWAYS_UNLOCKED should be set for displays created in their own display"
-                        + " group.",
-                (displayManager.getDisplayDeviceInfoInternal(ownDisplayGroupDisplayId).flags
-                        & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED),
-                0);
-
-        // Create a virtual display in a device display group.
-        final VirtualDisplayConfig defaultDisplayGroupConfig =
-                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
-                        .setUniqueId("uniqueId --- default display group 1")
-                        .setFlags(VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED)
-                        .build();
-
-        int defaultDisplayGroupDisplayId =
-                localService.createVirtualDisplay(
-                        defaultDisplayGroupConfig,
-                        mMockAppToken /* callback */,
-                        null /* virtualDeviceToken */,
-                        mock(DisplayWindowPolicyController.class),
-                        PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-
-        // Check that FLAG_ALWAYS_UNLOCKED is not set.
-        assertEquals(
-                "FLAG_ALWAYS_UNLOCKED should not be set for displays created in the default"
-                        + " display group.",
-                (displayManager.getDisplayDeviceInfoInternal(defaultDisplayGroupDisplayId).flags
-                        & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED),
-                0);
-    }
-
-    @Test
-    public void testGetDisplayIdToMirror() throws Exception {
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-
-        // This is effectively the DisplayManager service published to ServiceManager.
-        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
-        DisplayManagerService.LocalService localDisplayManager = displayManager.new LocalService();
-
-        final String uniqueId = "uniqueId --- displayIdToMirrorTest";
-        final int width = 600;
-        final int height = 800;
-        final int dpi = 320;
-
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, width, height, dpi)
-                .setUniqueId(uniqueId)
-                .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
-        final int firstDisplayId = binderService.createVirtualDisplay(builder.build(),
-                mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-
-        // The second virtual display requests to mirror the first virtual display.
-        final String uniqueId2 = "uniqueId --- displayIdToMirrorTest #2";
-        when(mMockAppToken2.asBinder()).thenReturn(mMockAppToken2);
-        final VirtualDisplayConfig.Builder builder2 = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, width, height, dpi)
-                .setUniqueId(uniqueId2)
-                .setWindowManagerMirroringEnabled(true);
-        final int secondDisplayId = binderService.createVirtualDisplay(builder2.build(),
-                mMockAppToken2 /* callback */, null /* projection */,
-                PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
-
-        // flush the handler
-        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
-
-        // The displayId to mirror should be a invalid since the display had flag OWN_CONTENT_ONLY
-        assertEquals(localDisplayManager.getDisplayIdToMirror(firstDisplayId),
-                Display.INVALID_DISPLAY);
-        // The second display has mirroring managed by WindowManager so the mirror displayId should
-        // be invalid.
-        assertEquals(localDisplayManager.getDisplayIdToMirror(secondDisplayId),
-                Display.INVALID_DISPLAY);
-    }
-
-    @Test
-    public void testCreateVirtualDisplay_isValidProjection_notValid()
-            throws RemoteException {
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-        IMediaProjection projection = mock(IMediaProjection.class);
-        doReturn(false).when(projection).isValid();
-        when(mMockProjectionService
-                .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
-                .thenReturn(true);
-        doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
-
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
-        builder.setUniqueId("uniqueId --- isValid false");
-
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-        displayManager.windowManagerAndInputReady();
-
-        // Pass in a non-null projection.
-        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
-        final int displayId = binderService.createVirtualDisplay(builder.build(),
-                mMockAppToken /* callback */, projection, PACKAGE_NAME);
-
-        // VirtualDisplay is created for mirroring.
-        assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
-        verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
-                any(ContentRecordingSession.class), nullable(IMediaProjection.class));
-        // But mirroring doesn't begin.
-        verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
-                mContentRecordingSessionCaptor.capture(), nullable(IMediaProjection.class));
-        ContentRecordingSession session = mContentRecordingSessionCaptor.getValue();
-        assertThat(session.isWaitingForConsent()).isTrue();
-    }
-
-    @Test
-    public void testCreateVirtualDisplay_setContentRecordingSessionSuccess()
-            throws RemoteException {
-        final int displayToRecord = 50;
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-        IMediaProjection projection = mock(IMediaProjection.class);
-        doReturn(true).when(projection).isValid();
-        when(mMockProjectionService
-                .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
-                .thenReturn(true);
-        doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
-
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
-        builder.setUniqueId("uniqueId --- setContentRecordingSession true");
-        builder.setDisplayIdToMirror(displayToRecord);
-
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-        displayManager.windowManagerAndInputReady();
-
-        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
-        final int displayId = binderService.createVirtualDisplay(builder.build(),
-                mMockAppToken /* callback */, projection, PACKAGE_NAME);
-
-        assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
-        verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
-                mContentRecordingSessionCaptor.capture(), nullable(IMediaProjection.class));
-        ContentRecordingSession session = mContentRecordingSessionCaptor.getValue();
-        assertThat(session.getContentToRecord()).isEqualTo(RECORD_CONTENT_DISPLAY);
-        assertThat(session.getVirtualDisplayId()).isEqualTo(displayId);
-        assertThat(session.getDisplayToRecord()).isEqualTo(displayToRecord);
-        assertThat(session.isWaitingForConsent()).isFalse();
-    }
-
-    @Test
-    public void testCreateVirtualDisplay_setContentRecordingSessionFail() throws RemoteException {
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-        IMediaProjection projection = mock(IMediaProjection.class);
-        doReturn(true).when(projection).isValid();
-        when(mMockProjectionService
-                .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
-                .thenReturn(false);
-        doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
-
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
-        builder.setUniqueId("uniqueId --- setContentRecordingSession false");
-
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-        displayManager.windowManagerAndInputReady();
-
-        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
-        final int displayId = binderService.createVirtualDisplay(builder.build(),
-                mMockAppToken /* callback */, projection, PACKAGE_NAME);
-
-        assertThat(displayId).isEqualTo(Display.INVALID_DISPLAY);
-    }
-
-    @Test
-    public void testCreateVirtualDisplay_setContentRecordingSession_taskSession()
-            throws RemoteException {
-        final int displayToRecord = 50;
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-        IMediaProjection projection = mock(IMediaProjection.class);
-        doReturn(true).when(projection).isValid();
-        when(mMockProjectionService
-                .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
-                .thenReturn(true);
-        doReturn(mock(IBinder.class)).when(projection).getLaunchCookie();
-        doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
-
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
-        builder.setUniqueId("uniqueId --- setContentRecordingSession false");
-        builder.setDisplayIdToMirror(displayToRecord);
-
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-        displayManager.windowManagerAndInputReady();
-
-        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
-        final int displayId = binderService.createVirtualDisplay(builder.build(),
-                mMockAppToken /* callback */, projection, PACKAGE_NAME);
-
-        assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
-        verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
-                mContentRecordingSessionCaptor.capture(), nullable(IMediaProjection.class));
-        ContentRecordingSession session = mContentRecordingSessionCaptor.getValue();
-        assertThat(session.getContentToRecord()).isEqualTo(RECORD_CONTENT_TASK);
-        assertThat(session.getVirtualDisplayId()).isEqualTo(displayId);
-        assertThat(session.getTokenToRecord()).isNotNull();
-    }
-
-    @Test
-    public void testCreateVirtualDisplay_setContentRecordingSession_noProjection_noFlags()
-            throws RemoteException {
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-
-        // Set no flags for the VirtualDisplay.
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
-        builder.setUniqueId("uniqueId --- setContentRecordingSession false");
-
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-        displayManager.windowManagerAndInputReady();
-
-        // Pass in a null projection.
-        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
-        final int displayId = binderService.createVirtualDisplay(builder.build(),
-                mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME);
-
-        // VirtualDisplay is created but not for mirroring.
-        assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
-        verify(mMockProjectionService, never()).setContentRecordingSession(
-                any(ContentRecordingSession.class), nullable(IMediaProjection.class));
-    }
-
-    @Test
-    public void testCreateVirtualDisplay_setContentRecordingSession_noProjection_noMirroringFlag()
-            throws RemoteException {
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-
-        // Set a non-mirroring flag for the VirtualDisplay.
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
-        builder.setUniqueId("uniqueId --- setContentRecordingSession false");
-        builder.setFlags(VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
-
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-        displayManager.windowManagerAndInputReady();
-
-        // Pass in a null projection.
-        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
-        final int displayId = binderService.createVirtualDisplay(builder.build(),
-                mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME);
-
-        // VirtualDisplay is created but not for mirroring.
-        assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
-        verify(mMockProjectionService, never()).setContentRecordingSession(
-                any(ContentRecordingSession.class), nullable(IMediaProjection.class));
-    }
-
-    @Test
-    public void testCreateVirtualDisplay_setContentRecordingSession_projection_noMirroringFlag()
-            throws RemoteException {
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-        IMediaProjection projection = mock(IMediaProjection.class);
-        doReturn(true).when(projection).isValid();
-        when(mMockProjectionService
-                .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
-                .thenReturn(true);
-        doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
-
-        // Set no flags for the VirtualDisplay.
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
-        builder.setUniqueId("uniqueId --- setContentRecordingSession false");
-
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-        displayManager.windowManagerAndInputReady();
-
-        // Pass in a non-null projection.
-        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
-        final int displayId = binderService.createVirtualDisplay(builder.build(),
-                mMockAppToken /* callback */, projection, PACKAGE_NAME);
-
-        // VirtualDisplay is created for mirroring.
-        assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
-        verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
-                any(ContentRecordingSession.class), nullable(IMediaProjection.class));
-    }
-
-    /**
-     * Tests that the virtual display is created with
-     * {@link VirtualDisplayConfig.Builder#setSurface(Surface)}
-     */
-    @Test
-    @FlakyTest(bugId = 127687569)
-    public void testCreateVirtualDisplay_setSurface() throws Exception {
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-
-        // This is effectively the DisplayManager service published to ServiceManager.
-        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
-
-        final String uniqueId = "uniqueId --- setSurface";
-        final int width = 600;
-        final int height = 800;
-        final int dpi = 320;
-        final Surface surface = new Surface();
-
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, width, height, dpi);
-        builder.setSurface(surface);
-        builder.setUniqueId(uniqueId);
-        final int displayId = binderService.createVirtualDisplay(builder.build(),
-                mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
-
-        // flush the handler
-        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
-
-        assertEquals(displayManager.getVirtualDisplaySurfaceInternal(mMockAppToken), surface);
-    }
-
-    /**
-     * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is allowed when the permission
-     * ADD_TRUSTED_DISPLAY is granted.
-     */
-    @Test
-    public void testOwnDisplayGroup_allowCreationWithAddTrustedDisplayPermission()
-            throws RemoteException {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-
-        DisplayManagerService.BinderService bs = displayManager.new BinderService();
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-
-        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)).thenReturn(
-                PackageManager.PERMISSION_GRANTED);
-
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
-        builder.setFlags(DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP);
-        builder.setUniqueId("uniqueId --- OWN_DISPLAY_GROUP");
-
-        int displayId = bs.createVirtualDisplay(builder.build(), mMockAppToken /* callback */,
-                null /* projection */, PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
-        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
-        DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
-        assertNotNull(ddi);
-        assertNotEquals(0, ddi.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
-    }
-
-    /**
-     * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is blocked when the permission
-     * ADD_TRUSTED_DISPLAY is denied.
-     */
-    @Test
-    public void testOwnDisplayGroup_withoutAddTrustedDisplayPermission_throwsSecurityException() {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mBasicInjector);
-        registerDefaultDisplays(displayManager);
-
-        DisplayManagerService.BinderService bs = displayManager.new BinderService();
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-
-        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)).thenReturn(
-                PackageManager.PERMISSION_DENIED);
-
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
-        builder.setFlags(DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP);
-        builder.setUniqueId("uniqueId --- OWN_DISPLAY_GROUP");
-
-        try {
-            bs.createVirtualDisplay(builder.build(), mMockAppToken /* callback */,
-                    null /* projection */, PACKAGE_NAME);
-            fail("Creating virtual display with VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP without "
-                    + "ADD_TRUSTED_DISPLAY permission should throw SecurityException.");
-        } catch (SecurityException e) {
-            // SecurityException is expected
-        }
-    }
-
-    /**
-     * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is allowed when called with
-     * a virtual device, even if ADD_TRUSTED_DISPLAY is not granted.
-     */
-    @Test
-    public void testOwnDisplayGroup_allowCreationWithVirtualDevice()  throws Exception {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mBasicInjector);
-        DisplayManagerInternal localService = displayManager.new LocalService();
-
-        registerDefaultDisplays(displayManager);
-
-        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
-
-        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)).thenReturn(
-                PackageManager.PERMISSION_DENIED);
-
-        final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
-                VIRTUAL_DISPLAY_NAME, 600, 800, 320);
-        builder.setFlags(DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP);
-        builder.setUniqueId("uniqueId --- OWN_DISPLAY_GROUP");
-
-        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
-        when(virtualDevice.getDeviceId()).thenReturn(1);
-        when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
-
-        int displayId = localService.createVirtualDisplay(builder.build(),
-                mMockAppToken /* callback */, virtualDevice /* virtualDeviceToken */,
-                mock(DisplayWindowPolicyController.class), PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-        displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
-        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
-        DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
-        assertNotNull(ddi);
-        assertNotEquals(0, ddi.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
-    }
-
-    /**
-     * Tests that there is a display change notification if the frame rate override
-     * list is updated.
-     */
-    @Test
-    public void testShouldNotifyChangeWhenDisplayInfoFrameRateOverrideChanged() throws Exception {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        DisplayManagerService.BinderService displayManagerBinderService =
-                displayManager.new BinderService();
-        registerDefaultDisplays(displayManager);
-        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
-
-        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager, new float[]{60f});
-        FakeDisplayManagerCallback callback = registerDisplayListenerCallback(displayManager,
-                displayManagerBinderService, displayDevice);
-
-        int myUid = Process.myUid();
-        updateFrameRateOverride(displayManager, displayDevice,
-                new DisplayEventReceiver.FrameRateOverride[]{
-                        new DisplayEventReceiver.FrameRateOverride(myUid, 30f),
-                });
-        assertTrue(callback.mDisplayChangedCalled);
-        callback.clear();
-
-        updateFrameRateOverride(displayManager, displayDevice,
-                new DisplayEventReceiver.FrameRateOverride[]{
-                        new DisplayEventReceiver.FrameRateOverride(myUid, 30f),
-                        new DisplayEventReceiver.FrameRateOverride(1234, 30f),
-                });
-        assertFalse(callback.mDisplayChangedCalled);
-
-        updateFrameRateOverride(displayManager, displayDevice,
-                new DisplayEventReceiver.FrameRateOverride[]{
-                        new DisplayEventReceiver.FrameRateOverride(myUid, 20f),
-                        new DisplayEventReceiver.FrameRateOverride(1234, 30f),
-                        new DisplayEventReceiver.FrameRateOverride(5678, 30f),
-                });
-        assertTrue(callback.mDisplayChangedCalled);
-        callback.clear();
-
-        updateFrameRateOverride(displayManager, displayDevice,
-                new DisplayEventReceiver.FrameRateOverride[]{
-                        new DisplayEventReceiver.FrameRateOverride(1234, 30f),
-                        new DisplayEventReceiver.FrameRateOverride(5678, 30f),
-                });
-        assertTrue(callback.mDisplayChangedCalled);
-        callback.clear();
-
-        updateFrameRateOverride(displayManager, displayDevice,
-                new DisplayEventReceiver.FrameRateOverride[]{
-                        new DisplayEventReceiver.FrameRateOverride(5678, 30f),
-                });
-        assertFalse(callback.mDisplayChangedCalled);
-    }
-
-    /**
-     * Tests that the DisplayInfo is updated correctly with a frame rate override
-     */
-    @Test
-    public void testDisplayInfoFrameRateOverride() throws Exception {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        DisplayManagerService.BinderService displayManagerBinderService =
-                displayManager.new BinderService();
-        registerDefaultDisplays(displayManager);
-        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
-
-        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
-                new float[]{60f, 30f, 20f});
-        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
-                displayDevice);
-        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
-        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
-
-        updateFrameRateOverride(displayManager, displayDevice,
-                new DisplayEventReceiver.FrameRateOverride[]{
-                        new DisplayEventReceiver.FrameRateOverride(
-                                Process.myUid(), 20f),
-                        new DisplayEventReceiver.FrameRateOverride(
-                                Process.myUid() + 1, 30f)
-                });
-        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
-        assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
-
-        // Changing the mode to 30Hz should not override the refresh rate to 20Hz anymore
-        // as 20 is not a divider of 30.
-        updateModeId(displayManager, displayDevice, 2);
-        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
-        assertEquals(30f, displayInfo.getRefreshRate(), 0.01f);
-    }
-
-    /**
-     * Tests that the frame rate override is returning the correct value from
-     * DisplayInfo#getRefreshRate
-     */
-    @Test
-    public void testDisplayInfoNonNativeFrameRateOverride() throws Exception {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mBasicInjector);
-        DisplayManagerService.BinderService displayManagerBinderService =
-                displayManager.new BinderService();
-        registerDefaultDisplays(displayManager);
-        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
-
-        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
-                new float[]{60f});
-        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
-                displayDevice);
-        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
-        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
-
-        updateFrameRateOverride(displayManager, displayDevice,
-                new DisplayEventReceiver.FrameRateOverride[]{
-                        new DisplayEventReceiver.FrameRateOverride(
-                                Process.myUid(), 20f)
-                });
-        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
-        assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
-    }
-
-    /**
-     * Tests that the mode reflects the frame rate override is in compat mode
-     */
-    @Test
-    @DisableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
-    public  void testDisplayInfoFrameRateOverrideModeCompat() throws Exception {
-        testDisplayInfoFrameRateOverrideModeCompat(/*compatChangeEnabled*/ false);
-    }
-
-    /**
-     * Tests that the mode reflects the physical display refresh rate when not in compat mode.
-     */
-    @Test
-    @EnableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
-    public  void testDisplayInfoFrameRateOverrideMode() throws Exception {
-        testDisplayInfoFrameRateOverrideModeCompat(/*compatChangeEnabled*/ true);
-    }
-
-    /**
-     * Tests that the mode reflects the frame rate override is in compat mode and accordingly to the
-     * allowNonNativeRefreshRateOverride policy.
-     */
-    @Test
-    @DisableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
-    public void testDisplayInfoNonNativeFrameRateOverrideModeCompat() throws Exception {
-        testDisplayInfoNonNativeFrameRateOverrideMode(/*compatChangeEnabled*/ false);
-    }
-
-    /**
-     * Tests that the mode reflects the physical display refresh rate when not in compat mode.
-     */
-    @Test
-    @EnableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
-    public void testDisplayInfoNonNativeFrameRateOverrideMode() throws Exception {
-        testDisplayInfoNonNativeFrameRateOverrideMode(/*compatChangeEnabled*/ true);
-    }
-
-    /**
-     * Tests that there is a display change notification if the render frame rate is updated
-     */
-    @Test
-    public void testShouldNotifyChangeWhenDisplayInfoRenderFrameRateChanged() throws Exception {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        DisplayManagerService.BinderService displayManagerBinderService =
-                displayManager.new BinderService();
-        registerDefaultDisplays(displayManager);
-        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
-
-        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager, new float[]{60f});
-        FakeDisplayManagerCallback callback = registerDisplayListenerCallback(displayManager,
-                displayManagerBinderService, displayDevice);
-
-        updateRenderFrameRate(displayManager, displayDevice, 30f);
-        assertTrue(callback.mDisplayChangedCalled);
-        callback.clear();
-
-        updateRenderFrameRate(displayManager, displayDevice, 30f);
-        assertFalse(callback.mDisplayChangedCalled);
-
-        updateRenderFrameRate(displayManager, displayDevice, 20f);
-        assertTrue(callback.mDisplayChangedCalled);
-        callback.clear();
-    }
-
-    /**
-     * Tests that the DisplayInfo is updated correctly with a render frame rate
-     */
-    @Test
-    public void testDisplayInfoRenderFrameRate() throws Exception {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        DisplayManagerService.BinderService displayManagerBinderService =
-                displayManager.new BinderService();
-        registerDefaultDisplays(displayManager);
-        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
-
-        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
-                new float[]{60f, 30f, 20f});
-        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
-                displayDevice);
-        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
-        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
-
-        updateRenderFrameRate(displayManager, displayDevice, 20f);
-        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
-        assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
-    }
-
-    /**
-     * Tests that the mode reflects the render frame rate is in compat mode
-     */
-    @Test
-    @DisableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
-    public  void testDisplayInfoRenderFrameRateModeCompat() throws Exception {
-        testDisplayInfoRenderFrameRateModeCompat(/*compatChangeEnabled*/ false);
-    }
-
-    /**
-     * Tests that the mode reflects the physical display refresh rate when not in compat mode.
-     */
-    @Test
-    @EnableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
-    public  void testDisplayInfoRenderFrameRateMode() throws Exception {
-        testDisplayInfoRenderFrameRateModeCompat(/*compatChangeEnabled*/ true);
-    }
-
-    /**
-     * Tests that EVENT_DISPLAY_ADDED is sent when a display is added.
-     */
-    @Test
-    public void testShouldNotifyDisplayAdded_WhenNewDisplayDeviceIsAdded() {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        DisplayManagerService.BinderService displayManagerBinderService =
-                displayManager.new BinderService();
-
-        Handler handler = displayManager.getDisplayHandler();
-        waitForIdleHandler(handler);
-
-        // register display listener callback
-        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
-        displayManagerBinderService.registerCallbackWithEventMask(
-                callback, STANDARD_DISPLAY_EVENTS);
-
-        waitForIdleHandler(handler);
-
-        createFakeDisplayDevice(displayManager, new float[]{60f});
-
-        waitForIdleHandler(handler);
-
-        assertFalse(callback.mDisplayChangedCalled);
-        assertFalse(callback.mDisplayRemovedCalled);
-        assertTrue(callback.mDisplayAddedCalled);
-    }
-
-    /**
-     * Tests that EVENT_DISPLAY_ADDED is not sent when a display is added and the
-     * client has a callback which is not subscribed to this event type.
-     */
-    @Test
-    public void testShouldNotNotifyDisplayAdded_WhenClientIsNotSubscribed() {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        DisplayManagerService.BinderService displayManagerBinderService =
-                displayManager.new BinderService();
-
-        Handler handler = displayManager.getDisplayHandler();
-        waitForIdleHandler(handler);
-
-        // register display listener callback
-        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
-        long allEventsExceptDisplayAdded = STANDARD_DISPLAY_EVENTS
-                & ~DisplayManager.EVENT_FLAG_DISPLAY_ADDED;
-        displayManagerBinderService.registerCallbackWithEventMask(callback,
-                allEventsExceptDisplayAdded);
-
-        waitForIdleHandler(handler);
-
-        createFakeDisplayDevice(displayManager, new float[]{60f});
-
-        waitForIdleHandler(handler);
-
-        assertFalse(callback.mDisplayChangedCalled);
-        assertFalse(callback.mDisplayRemovedCalled);
-        assertFalse(callback.mDisplayAddedCalled);
-    }
-
-    /**
-     * Tests that EVENT_DISPLAY_REMOVED is sent when a display is removed.
-     */
-    @Test
-    public void testShouldNotifyDisplayRemoved_WhenDisplayDeviceIsRemoved() {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        DisplayManagerService.BinderService displayManagerBinderService =
-                displayManager.new BinderService();
-
-        Handler handler = displayManager.getDisplayHandler();
-        waitForIdleHandler(handler);
-
-        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
-                new float[]{60f});
-
-        waitForIdleHandler(handler);
-
-        FakeDisplayManagerCallback callback = registerDisplayListenerCallback(
-                displayManager, displayManagerBinderService, displayDevice);
-
-        waitForIdleHandler(handler);
-
-        displayManager.getDisplayDeviceRepository()
-                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED);
-
-        waitForIdleHandler(handler);
-
-        assertFalse(callback.mDisplayChangedCalled);
-        assertTrue(callback.mDisplayRemovedCalled);
-        assertFalse(callback.mDisplayAddedCalled);
-    }
-
-    /**
-     * Tests that EVENT_DISPLAY_REMOVED is not sent when a display is added and the
-     * client has a callback which is not subscribed to this event type.
-     */
-    @Test
-    public void testShouldNotNotifyDisplayRemoved_WhenClientIsNotSubscribed() {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        DisplayManagerService.BinderService displayManagerBinderService =
-                displayManager.new BinderService();
-
-        Handler handler = displayManager.getDisplayHandler();
-        waitForIdleHandler(handler);
-
-        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
-                new float[]{60f});
-
-        waitForIdleHandler(handler);
-
-        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
-        long allEventsExceptDisplayRemoved = STANDARD_DISPLAY_EVENTS
-                & ~DisplayManager.EVENT_FLAG_DISPLAY_REMOVED;
-        displayManagerBinderService.registerCallbackWithEventMask(callback,
-                allEventsExceptDisplayRemoved);
-
-        waitForIdleHandler(handler);
-
-        displayManager.getDisplayDeviceRepository()
-                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED);
-
-        waitForIdleHandler(handler);
-
-        assertFalse(callback.mDisplayChangedCalled);
-        assertFalse(callback.mDisplayRemovedCalled);
-        assertFalse(callback.mDisplayAddedCalled);
-    }
-
-
-
-    @Test
-    public void testSettingTwoBrightnessConfigurationsOnMultiDisplay() {
-        Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
-        DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
-
-        // get the first two internal displays
-        Display[] displays = displayManager.getDisplays();
-        Display internalDisplayOne = null;
-        Display internalDisplayTwo = null;
-        for (Display display : displays) {
-            if (display.getType() == Display.TYPE_INTERNAL) {
-                if (internalDisplayOne == null) {
-                    internalDisplayOne = display;
-                } else {
-                    internalDisplayTwo = display;
-                    break;
-                }
-            }
-        }
-
-        // return if there are fewer than 2 displays on this device
-        if (internalDisplayOne == null || internalDisplayTwo == null) {
-            return;
-        }
-
-        final String uniqueDisplayIdOne = internalDisplayOne.getUniqueId();
-        final String uniqueDisplayIdTwo = internalDisplayTwo.getUniqueId();
-
-        BrightnessConfiguration configOne =
-                new BrightnessConfiguration.Builder(
-                        new float[]{0.0f, 12345.0f}, new float[]{15.0f, 400.0f})
-                        .setDescription("model:1").build();
-        BrightnessConfiguration configTwo =
-                new BrightnessConfiguration.Builder(
-                        new float[]{0.0f, 6789.0f}, new float[]{12.0f, 300.0f})
-                        .setDescription("model:2").build();
-
-        displayManager.setBrightnessConfigurationForDisplay(configOne,
-                uniqueDisplayIdOne);
-        displayManager.setBrightnessConfigurationForDisplay(configTwo,
-                uniqueDisplayIdTwo);
-
-        BrightnessConfiguration configFromOne =
-                displayManager.getBrightnessConfigurationForDisplay(uniqueDisplayIdOne);
-        BrightnessConfiguration configFromTwo =
-                displayManager.getBrightnessConfigurationForDisplay(uniqueDisplayIdTwo);
-
-        assertNotNull(configFromOne);
-        assertEquals(configOne, configFromOne);
-        assertEquals(configTwo, configFromTwo);
-
-    }
-
-    @Test
-    public void testHdrConversionModeEquals() {
-        assertEquals(
-                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 2),
-                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 2));
-        assertNotEquals(
-                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 2),
-                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 3));
-        assertEquals(
-                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM),
-                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM));
-        assertNotEquals(
-                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, 2),
-                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM));
-    }
-
-    @Test
-    public void testCreateHdrConversionMode_withInvalidArguments_throwsException() {
-        assertThrows(
-                "preferredHdrOutputType must not be set if the conversion mode is "
-                        + "HDR_CONVERSION_PASSTHROUGH",
-                IllegalArgumentException.class,
-                () -> new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_PASSTHROUGH,
-                        Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION));
-    }
-
-    @Test
-    public void testSetHdrConversionModeInternal_withInvalidArguments_throwsException() {
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        assertThrows("Expected DisplayManager to throw IllegalArgumentException when "
-                        + "preferredHdrOutputType is set and the conversion mode is "
-                        + "HDR_CONVERSION_SYSTEM",
-                IllegalArgumentException.class,
-                () -> displayManager.setHdrConversionModeInternal(new HdrConversionMode(
-                        HdrConversionMode.HDR_CONVERSION_SYSTEM,
-                        Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION)));
-    }
-
-    @Test
-    public void testSetAndGetHdrConversionModeInternal() {
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        final HdrConversionMode mode = new HdrConversionMode(
-                HdrConversionMode.HDR_CONVERSION_FORCE,
-                Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION);
-        displayManager.setHdrConversionModeInternal(mode);
-        assertEquals(mode, displayManager.getHdrConversionModeSettingInternal());
-        assertEquals(mode.getConversionMode(), mHdrConversionMode);
-        assertEquals(mode.getPreferredHdrOutputType(), mPreferredHdrOutputType);
-    }
-
-    @Test
-    public void testHdrConversionMode_withMinimalPostProcessing() {
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        DisplayManagerService.BinderService displayManagerBinderService =
-                displayManager.new BinderService();
-        registerDefaultDisplays(displayManager);
-        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
-        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
-                new float[]{60f, 30f, 20f});
-        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
-                displayDevice);
-
-        final HdrConversionMode mode = new HdrConversionMode(
-                HdrConversionMode.HDR_CONVERSION_FORCE,
-                Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION);
-        displayManager.setHdrConversionModeInternal(mode);
-        assertEquals(mode, displayManager.getHdrConversionModeSettingInternal());
-
-        displayManager.setMinimalPostProcessingAllowed(true);
-        displayManager.setDisplayPropertiesInternal(displayId, false /* hasContent */,
-                30.0f /* requestedRefreshRate */,
-                displayDevice.getDisplayDeviceInfoLocked().modeId /* requestedModeId */,
-                30.0f /* requestedMinRefreshRate */, 120.0f /* requestedMaxRefreshRate */,
-                true /* preferMinimalPostProcessing */, false /* disableHdrConversion */,
-                true /* inTraversal */);
-
-        assertEquals(new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_PASSTHROUGH),
-                displayManager.getHdrConversionModeInternal());
-
-        displayManager.setDisplayPropertiesInternal(displayId, false /* hasContent */,
-                30.0f /* requestedRefreshRate */,
-                displayDevice.getDisplayDeviceInfoLocked().modeId /* requestedModeId */,
-                30.0f /* requestedMinRefreshRate */, 120.0f /* requestedMaxRefreshRate */,
-                false /* preferMinimalPostProcessing */, false /* disableHdrConversion */,
-                true /* inTraversal */);
-        assertEquals(mode, displayManager.getHdrConversionModeInternal());
-    }
-
-    private void testDisplayInfoFrameRateOverrideModeCompat(boolean compatChangeEnabled)
-            throws Exception {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        DisplayManagerService.BinderService displayManagerBinderService =
-                displayManager.new BinderService();
-        registerDefaultDisplays(displayManager);
-        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
-
-        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
-                new float[]{60f, 30f, 20f});
-        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
-                displayDevice);
-        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
-        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
-
-        updateFrameRateOverride(displayManager, displayDevice,
-                new DisplayEventReceiver.FrameRateOverride[]{
-                        new DisplayEventReceiver.FrameRateOverride(
-                                Process.myUid(), 20f),
-                        new DisplayEventReceiver.FrameRateOverride(
-                                Process.myUid() + 1, 30f)
-                });
-        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
-        assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
-        Display.Mode expectedMode;
-        if (compatChangeEnabled) {
-            expectedMode = new Display.Mode(1, 100, 200, 60f);
-        } else {
-            expectedMode = new Display.Mode(3, 100, 200, 20f);
-        }
-        assertEquals(expectedMode, displayInfo.getMode());
-    }
-
-    private void testDisplayInfoRenderFrameRateModeCompat(boolean compatChangeEnabled)
-            throws Exception {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mShortMockedInjector);
-        DisplayManagerService.BinderService displayManagerBinderService =
-                displayManager.new BinderService();
-        registerDefaultDisplays(displayManager);
-        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
-
-        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
-                new float[]{60f, 30f, 20f});
-        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
-                displayDevice);
-        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
-        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
-
-        updateRenderFrameRate(displayManager, displayDevice, 20f);
-        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
-        assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
-        Display.Mode expectedMode;
-        if (compatChangeEnabled) {
-            expectedMode = new Display.Mode(1, 100, 200, 60f);
-        } else {
-            expectedMode = new Display.Mode(3, 100, 200, 20f);
-        }
-        assertEquals(expectedMode, displayInfo.getMode());
-    }
-
-    private void testDisplayInfoNonNativeFrameRateOverrideMode(boolean compatChangeEnabled) {
-        DisplayManagerService displayManager =
-                new DisplayManagerService(mContext, mBasicInjector);
-        DisplayManagerService.BinderService displayManagerBinderService =
-                displayManager.new BinderService();
-        registerDefaultDisplays(displayManager);
-        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
-
-        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
-                new float[]{60f});
-        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
-                displayDevice);
-        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
-        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
-
-        updateFrameRateOverride(displayManager, displayDevice,
-                new DisplayEventReceiver.FrameRateOverride[]{
-                        new DisplayEventReceiver.FrameRateOverride(
-                                Process.myUid(), 20f)
-                });
-        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
-        Display.Mode expectedMode;
-        if (compatChangeEnabled) {
-            expectedMode = new Display.Mode(1, 100, 200, 60f);
-        } else {
-            expectedMode = new Display.Mode(255, 100, 200, 20f);
-        }
-        assertEquals(expectedMode, displayInfo.getMode());
-    }
-
-    private int getDisplayIdForDisplayDevice(
-            DisplayManagerService displayManager,
-            DisplayManagerService.BinderService displayManagerBinderService,
-            FakeDisplayDevice displayDevice) {
-
-        final int[] displayIds = displayManagerBinderService.getDisplayIds(
-                /* includeDisabled= */ true);
-        assertTrue(displayIds.length > 0);
-        int displayId = Display.INVALID_DISPLAY;
-        for (int i = 0; i < displayIds.length; i++) {
-            DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayIds[i]);
-            if (displayDevice.getDisplayDeviceInfoLocked().equals(ddi)) {
-                displayId = displayIds[i];
-                break;
-            }
-        }
-        assertFalse(displayId == Display.INVALID_DISPLAY);
-        return displayId;
-    }
-
-    private void updateDisplayDeviceInfo(DisplayManagerService displayManager,
-            FakeDisplayDevice displayDevice,
-            DisplayDeviceInfo displayDeviceInfo) {
-        displayDevice.setDisplayDeviceInfo(displayDeviceInfo);
-        displayManager.getDisplayDeviceRepository()
-                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_CHANGED);
-        Handler handler = displayManager.getDisplayHandler();
-        waitForIdleHandler(handler);
-    }
-
-    private void updateFrameRateOverride(DisplayManagerService displayManager,
-            FakeDisplayDevice displayDevice,
-            DisplayEventReceiver.FrameRateOverride[] frameRateOverrides) {
-        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
-        displayDeviceInfo.copyFrom(displayDevice.getDisplayDeviceInfoLocked());
-        displayDeviceInfo.frameRateOverrides = frameRateOverrides;
-        updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo);
-    }
-
-    private void updateRenderFrameRate(DisplayManagerService displayManager,
-            FakeDisplayDevice displayDevice,
-            float renderFrameRate) {
-        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
-        displayDeviceInfo.copyFrom(displayDevice.getDisplayDeviceInfoLocked());
-        displayDeviceInfo.renderFrameRate = renderFrameRate;
-        updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo);
-    }
-
-    private void updateModeId(DisplayManagerService displayManager,
-            FakeDisplayDevice displayDevice,
-            int modeId) {
-        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
-        displayDeviceInfo.copyFrom(displayDevice.getDisplayDeviceInfoLocked());
-        displayDeviceInfo.modeId = modeId;
-        updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo);
-    }
-
-    private IDisplayManagerCallback registerDisplayChangeCallback(
-            DisplayManagerService displayManager) {
-        IDisplayManagerCallback displayChangesCallback = mock(IDisplayManagerCallback.class);
-        when(displayChangesCallback.asBinder()).thenReturn(new Binder());
-        DisplayManagerService.BinderService binderService = displayManager.new BinderService();
-        binderService.registerCallback(displayChangesCallback);
-        return displayChangesCallback;
-    }
-
-    private FakeDisplayManagerCallback registerDisplayListenerCallback(
-            DisplayManagerService displayManager,
-            DisplayManagerService.BinderService displayManagerBinderService,
-            FakeDisplayDevice displayDevice) {
-        // Find the display id of the added FakeDisplayDevice
-        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
-                displayDevice);
-
-        Handler handler = displayManager.getDisplayHandler();
-        waitForIdleHandler(handler);
-
-        // register display listener callback
-        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(displayId);
-        displayManagerBinderService.registerCallbackWithEventMask(
-                callback, STANDARD_DISPLAY_EVENTS);
-        return callback;
-    }
-
-    private FakeDisplayDevice createFakeDisplayDevice(DisplayManagerService displayManager,
-            float[] refreshRates) {
-        FakeDisplayDevice displayDevice = new FakeDisplayDevice();
-        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
-        int width = 100;
-        int height = 200;
-        displayDeviceInfo.supportedModes = new Display.Mode[refreshRates.length];
-        for (int i = 0; i < refreshRates.length; i++) {
-            displayDeviceInfo.supportedModes[i] =
-                    new Display.Mode(i + 1, width, height, refreshRates[i]);
-        }
-        displayDeviceInfo.modeId = 1;
-        displayDeviceInfo.renderFrameRate = displayDeviceInfo.supportedModes[0].getRefreshRate();
-        displayDeviceInfo.width = width;
-        displayDeviceInfo.height = height;
-        final Rect zeroRect = new Rect();
-        displayDeviceInfo.displayCutout = new DisplayCutout(
-                Insets.of(0, 10, 0, 0),
-                zeroRect, new Rect(0, 0, 10, 10), zeroRect, zeroRect);
-        displayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY;
-        displayDeviceInfo.address = new TestUtils.TestDisplayAddress();
-        displayDevice.setDisplayDeviceInfo(displayDeviceInfo);
-        displayManager.getDisplayDeviceRepository()
-                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
-        return displayDevice;
-    }
-
-    private void registerDefaultDisplays(DisplayManagerService displayManager) {
-        Handler handler = displayManager.getDisplayHandler();
-        // Would prefer to call displayManager.onStart() directly here but it performs binderService
-        // registration which triggers security exceptions when running from a test.
-        handler.sendEmptyMessage(MSG_REGISTER_DEFAULT_DISPLAY_ADAPTERS);
-        waitForIdleHandler(handler);
-    }
-
-    private void waitForIdleHandler(Handler handler) {
-        waitForIdleHandler(handler, Duration.ofSeconds(1));
-    }
-
-    private void waitForIdleHandler(Handler handler, Duration timeout) {
-        final MessageQueue queue = handler.getLooper().getQueue();
-        final CountDownLatch latch = new CountDownLatch(1);
-        queue.addIdleHandler(() -> {
-            latch.countDown();
-            // Remove idle handler
-            return false;
-        });
-        try {
-            latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            fail("Interrupted unexpectedly: " + e);
-        }
-    }
-
-    private void resetConfigToIgnoreSensorManager(Context context) {
-        final Resources res = Mockito.spy(context.getResources());
-        doReturn(new int[]{-1}).when(res).getIntArray(R.array
-                .config_ambientThresholdsOfPeakRefreshRate);
-        doReturn(new int[]{-1}).when(res).getIntArray(R.array
-                .config_brightnessThresholdsOfPeakRefreshRate);
-        doReturn(new int[]{-1}).when(res).getIntArray(R.array
-                .config_highDisplayBrightnessThresholdsOfFixedRefreshRate);
-        doReturn(new int[]{-1}).when(res).getIntArray(R.array
-                .config_highAmbientBrightnessThresholdsOfFixedRefreshRate);
-
-        when(context.getResources()).thenReturn(res);
-    }
-
-    private class FakeDisplayManagerCallback extends IDisplayManagerCallback.Stub {
-        int mDisplayId;
-        boolean mDisplayAddedCalled = false;
-        boolean mDisplayChangedCalled = false;
-        boolean mDisplayRemovedCalled = false;
-
-        FakeDisplayManagerCallback(int displayId) {
-            mDisplayId = displayId;
-        }
-
-        FakeDisplayManagerCallback() {
-            mDisplayId = -1;
-        }
-
-        @Override
-        public void onDisplayEvent(int displayId, int event) {
-            if (mDisplayId != -1 && displayId != mDisplayId) {
-                return;
-            }
-
-            if (event == DisplayManagerGlobal.EVENT_DISPLAY_ADDED) {
-                mDisplayAddedCalled = true;
-            }
-
-            if (event == DisplayManagerGlobal.EVENT_DISPLAY_CHANGED) {
-                mDisplayChangedCalled = true;
-            }
-
-            if (event == DisplayManagerGlobal.EVENT_DISPLAY_REMOVED) {
-                mDisplayRemovedCalled = true;
-            }
-        }
-
-        public void clear() {
-            mDisplayAddedCalled = false;
-            mDisplayChangedCalled = false;
-            mDisplayRemovedCalled = false;
-        }
-    }
-
-    private class FakeDisplayDevice extends DisplayDevice {
-        private DisplayDeviceInfo mDisplayDeviceInfo;
-
-        FakeDisplayDevice() {
-            super(null, null, "", mContext);
-        }
-
-        public void setDisplayDeviceInfo(DisplayDeviceInfo displayDeviceInfo) {
-            mDisplayDeviceInfo = displayDeviceInfo;
-        }
-
-        @Override
-        public boolean hasStableUniqueId() {
-            return false;
-        }
-
-        @Override
-        public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
-            return mDisplayDeviceInfo;
-        }
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java
deleted file mode 100644
index e2a66f0..0000000
--- a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java
+++ /dev/null
@@ -1,736 +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.server.display;
-
-import static android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR;
-import static android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF;
-import static android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT;
-
-import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED;
-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;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.anyFloat;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.hardware.display.BrightnessInfo;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.Message;
-import android.os.test.TestLooper;
-import android.test.mock.MockContentResolver;
-import android.util.MathUtils;
-
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.util.FrameworkStatsLog;
-import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.internal.util.test.FakeSettingsProviderRule;
-import com.android.server.display.DisplayDeviceConfig.HighBrightnessModeData;
-import com.android.server.display.HighBrightnessModeController.Injector;
-import com.android.server.testutils.OffsettableClock;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class HighBrightnessModeControllerTest {
-
-    private static final int MINIMUM_LUX = 100;
-    private static final float TRANSITION_POINT = 0.763f;
-    private static final long TIME_WINDOW_MILLIS = 55 * 1000;
-    private static final long TIME_ALLOWED_IN_WINDOW_MILLIS = 12 * 1000;
-    private static final long TIME_MINIMUM_AVAILABLE_TO_ENABLE_MILLIS = 5 * 1000;
-    private static final boolean ALLOW_IN_LOW_POWER_MODE = false;
-
-    private static final float DEFAULT_MIN = 0.01f;
-    private static final float DEFAULT_MAX = 0.80f;
-
-    private static final int DISPLAY_WIDTH = 900;
-    private static final int DISPLAY_HEIGHT = 1600;
-
-    private static final float EPSILON = 0.000001f;
-
-    private OffsettableClock mClock;
-    private TestLooper mTestLooper;
-    private Handler mHandler;
-    private Binder mDisplayToken;
-    private String mDisplayUniqueId;
-    private Context mContextSpy;
-    private HighBrightnessModeMetadata mHighBrightnessModeMetadata;
-
-    @Rule
-    public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
-
-    @Mock Injector mInjectorMock;
-    @Mock HighBrightnessModeController.HdrBrightnessDeviceConfig mHdrBrightnessDeviceConfigMock;
-
-    private static final HighBrightnessModeData DEFAULT_HBM_DATA =
-            new HighBrightnessModeData(MINIMUM_LUX, TRANSITION_POINT, TIME_WINDOW_MILLIS,
-                    TIME_ALLOWED_IN_WINDOW_MILLIS, TIME_MINIMUM_AVAILABLE_TO_ENABLE_MILLIS,
-                    ALLOW_IN_LOW_POWER_MODE, HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT);
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mClock = new OffsettableClock.Stopped();
-        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);
-    }
-
-    /////////////////
-    // Test Methods
-    /////////////////
-
-    @Test
-    public void testNoHbmData() {
-        initHandler(null);
-        final HighBrightnessModeController hbmc = new HighBrightnessModeController(
-                mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken,
-                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);
-    }
-
-    @Test
-    public void testNoHbmData_Enabled() {
-        initHandler(null);
-        final HighBrightnessModeController hbmc = new HighBrightnessModeController(
-                mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken,
-                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);
-        assertEquals(hbmc.getTransitionPoint(), HBM_TRANSITION_POINT_INVALID, 0.0f);
-    }
-
-    @Test
-    public void testOffByDefault() {
-        final HighBrightnessModeController hbmc = createDefaultHbm();
-
-        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
-    }
-
-    @Test
-    public void testAutoBrightnessEnabled_NoLux() {
-        final HighBrightnessModeController hbmc = createDefaultHbm();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
-    }
-
-    @Test
-    public void testAutoBrightnessEnabled_LowLux() {
-        final HighBrightnessModeController hbmc = createDefaultHbm();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX - 1); // below allowed range
-        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
-    }
-
-    @Test
-    public void testAutoBrightnessEnabled_HighLux() {
-        final HighBrightnessModeController hbmc = createDefaultHbm();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
-    }
-
-    @Test
-    public void testAutoBrightnessEnabled_HighLux_ThenDisable() {
-        final HighBrightnessModeController hbmc = createDefaultHbm();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_DISABLED);
-
-        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
-    }
-
-    @Test
-    public void testWithinHighRange_thenOverTime_thenEarnBackTime() {
-        final HighBrightnessModeController hbmc = createDefaultHbm();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
-
-        // Verify we are in HBM
-        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
-
-        // Use up all the time in the window.
-        advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS);
-
-        // Verify we are not out of HBM
-        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
-
-        // Shift time so that the HBM event is at the beginning of the current window
-        advanceTime(TIME_WINDOW_MILLIS - TIME_ALLOWED_IN_WINDOW_MILLIS);
-        // Shift time again so that we are just below the minimum allowable
-        advanceTime(TIME_MINIMUM_AVAILABLE_TO_ENABLE_MILLIS - 1);
-
-        // Verify we are not out of HBM
-        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
-
-        // Advance the necessary millisecond
-        advanceTime(1);
-
-        // Verify we are allowed HBM again.
-        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
-    }
-
-    @Test
-    public void testInHBM_ThenLowLux() {
-        final HighBrightnessModeController hbmc = createDefaultHbm();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
-
-        // Verify we are in HBM
-        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
-
-        advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2);
-
-        // Verify we are in HBM
-        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
-
-        hbmc.onAmbientLuxChange(1);
-        advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2 + 1);
-
-        // Verify we are out of HBM
-        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
-
-    }
-
-    @Test
-    public void testInHBM_TestMultipleEvents_DueToAutoBrightness() {
-        final HighBrightnessModeController hbmc = createDefaultHbm();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-
-        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
-        advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2);
-
-        // Verify we are in HBM
-        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
-
-        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT - 0.01f);
-        advanceTime(1);
-
-        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
-
-        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
-        advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2);
-
-        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
-
-        advanceTime(2);
-
-        // Now we should be out again.
-        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
-    }
-
-    @Test
-    public void testInHBM_TestMultipleEvents_DueToLux() {
-        final HighBrightnessModeController hbmc = createDefaultHbm();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-
-        // Go into HBM for half the allowed window
-        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
-        advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2);
-        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
-
-        // Move lux below threshold (ending first event);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX - 1);
-        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT);
-        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
-
-        // Move up some amount of time so that there's still time in the window even after a
-        // second event.
-        advanceTime((TIME_WINDOW_MILLIS - TIME_ALLOWED_IN_WINDOW_MILLIS) / 2);
-        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
-
-        // Go into HBM for just under the second half of allowed window
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 1);
-        advanceTime((TIME_ALLOWED_IN_WINDOW_MILLIS / 2) - 1);
-
-        assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
-
-        // Now exhaust the time
-        advanceTime(2);
-        assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
-    }
-
-    @Test
-    public void testHbmIsNotTurnedOffInHighThermalState() throws Exception {
-        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
-
-        // Disabled thermal throttling
-        hbmc.onBrightnessChanged(/*brightness=*/ 1f, /*unthrottledBrightness*/ 1f,
-                BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE);
-
-        assertFalse(hbmc.isThermalThrottlingActive());
-
-        // Try to go into HBM mode
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-        advanceTime(1);
-
-        assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
-
-        // Enable thermal throttling
-        hbmc.onBrightnessChanged(/*brightness=*/ TRANSITION_POINT - 0.01f,
-                /*unthrottledBrightness*/ 1f, BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL);
-        advanceTime(10);
-        assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
-        assertTrue(hbmc.isThermalThrottlingActive());
-
-        // Disabled thermal throttling
-        hbmc.onBrightnessChanged(/*brightness=*/ 1f, /*unthrottledBrightness*/ 1f,
-                BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE);
-        advanceTime(1);
-        assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
-        assertFalse(hbmc.isThermalThrottlingActive());
-    }
-
-    @Test
-    public void testHdrRequires50PercentOfScreen() {
-        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
-
-        final int layerWidth = DISPLAY_WIDTH;
-        final int smallLayerHeight = DISPLAY_HEIGHT / 2 - 1; // height to use for <50%
-        final int largeLayerHeight = DISPLAY_HEIGHT / 2 + 1; // height to use for >50%
-
-        // ensure hdr doesn't turn on if layer is too small
-        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
-                layerWidth, smallLayerHeight, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
-        advanceTime(0);
-        assertEquals(HIGH_BRIGHTNESS_MODE_OFF, hbmc.getHighBrightnessMode());
-
-        // Now check with layer larger than 50%
-        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
-                layerWidth, largeLayerHeight, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
-        advanceTime(0);
-        assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
-    }
-
-    @Test
-    public void testHdrRespectsMaxDesiredHdrSdrRatio() {
-        final HighBrightnessModeController hbmc = new TestHbmBuilder()
-                .setClock(new OffsettableClock())
-                .setHdrBrightnessConfig(mHdrBrightnessDeviceConfigMock)
-                .build();
-
-        // Passthrough return the max desired hdr/sdr ratio
-        when(mHdrBrightnessDeviceConfigMock.getHdrBrightnessFromSdr(anyFloat(), anyFloat()))
-                .thenAnswer(i -> i.getArgument(1));
-
-        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
-                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 2.0f /*maxDesiredHdrSdrRatio*/);
-        advanceTime(0);
-        assertEquals(2.0f, hbmc.getHdrBrightnessValue(), EPSILON);
-
-        // The hdr ratio cannot be less than 1.
-        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
-                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 0.5f /*maxDesiredHdrSdrRatio*/);
-        advanceTime(0);
-        assertEquals(1.0f, hbmc.getHdrBrightnessValue(), EPSILON);
-
-        // The hdr ratio can be as much as positive infinity
-        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
-                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/,
-                Float.POSITIVE_INFINITY /*maxDesiredHdrSdrRatio*/);
-        advanceTime(0);
-        assertEquals(Float.POSITIVE_INFINITY, hbmc.getHdrBrightnessValue(), 0.0);
-    }
-
-
-    @Test
-    public void testHdrTrumpsSunlight() {
-        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
-
-        // Turn on sunlight
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-        advanceTime(0);
-        assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
-        assertEquals(DEFAULT_MAX, hbmc.getCurrentBrightnessMax(), EPSILON);
-
-        // turn on hdr
-        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
-                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
-        advanceTime(0);
-        assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
-        assertEquals(TRANSITION_POINT, hbmc.getCurrentBrightnessMax(), EPSILON);
-    }
-
-    @Test
-    public void testHdrBrightnessLimitSameAsNormalLimit() {
-        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
-
-        // Check limit when HBM is off
-        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
-                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
-        advanceTime(0);
-        assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
-        assertEquals(TRANSITION_POINT, hbmc.getCurrentBrightnessMax(), EPSILON);
-
-        // Check limit with HBM is set to HDR
-        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 0 /*numberOfHdrLayers*/,
-                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
-        advanceTime(0);
-        assertEquals(HIGH_BRIGHTNESS_MODE_OFF, hbmc.getHighBrightnessMode());
-        assertEquals(TRANSITION_POINT, hbmc.getCurrentBrightnessMax(), EPSILON);
-    }
-
-    @Test
-    public void testHdrBrightnessScaledNormalBrightness() {
-        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
-
-        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
-                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
-        advanceTime(0);
-        assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
-
-        // verify things are scaled for 0.5f
-        float brightness = 0.5f;
-        float expectedHdrBrightness = MathUtils.map(DEFAULT_MIN, TRANSITION_POINT,
-                DEFAULT_MIN, DEFAULT_MAX, brightness); // map value from normal range to hdr range
-        hbmcOnBrightnessChanged(hbmc, brightness);
-        advanceTime(0);
-        assertEquals(expectedHdrBrightness, hbmc.getHdrBrightnessValue(), EPSILON);
-
-        // Try another value
-        brightness = 0.33f;
-        expectedHdrBrightness = MathUtils.map(DEFAULT_MIN, TRANSITION_POINT,
-                DEFAULT_MIN, DEFAULT_MAX, brightness); // map value from normal range to hdr range
-        hbmcOnBrightnessChanged(hbmc, brightness);
-        advanceTime(0);
-        assertEquals(expectedHdrBrightness, hbmc.getHdrBrightnessValue(), EPSILON);
-
-        // Try the min value
-        brightness = DEFAULT_MIN;
-        expectedHdrBrightness = DEFAULT_MIN;
-        hbmcOnBrightnessChanged(hbmc, brightness);
-        advanceTime(0);
-        assertEquals(expectedHdrBrightness, hbmc.getHdrBrightnessValue(), EPSILON);
-
-        // Try the max value
-        brightness = TRANSITION_POINT;
-        expectedHdrBrightness = DEFAULT_MAX;
-        hbmcOnBrightnessChanged(hbmc, brightness);
-        advanceTime(0);
-        assertEquals(expectedHdrBrightness, hbmc.getHdrBrightnessValue(), EPSILON);
-    }
-
-    @Test
-    public void testHbmStats_StateChange() {
-        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
-        final int displayStatsId = mDisplayUniqueId.hashCode();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT);
-        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
-                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
-        advanceTime(0);
-        assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
-
-        // Verify Stats HBM_ON_HDR
-        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_HDR),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
-
-        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 0 /*numberOfHdrLayers*/,
-                0, 0, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
-        advanceTime(0);
-
-        // Verify Stats HBM_OFF
-        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
-
-        // Verify Stats HBM_ON_SUNLIGHT
-        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_SUNLIGHT),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
-
-        hbmc.onAmbientLuxChange(1);
-        advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2 + 1);
-
-        // Verify Stats HBM_OFF
-        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_SV_OFF_LUX_DROP));
-    }
-
-    @Test
-    public void testHbmStats_NbmHdrNoReport() {
-        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
-        final int displayStatsId = mDisplayUniqueId.hashCode();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmcOnBrightnessChanged(hbmc, DEFAULT_MIN);
-        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
-                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
-        advanceTime(0);
-        assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
-
-        // Verify Stats HBM_ON_HDR not report
-        verify(mInjectorMock, never()).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_HDR),
-            anyInt());
-    }
-
-    @Test
-    public void testHbmStats_HighLuxLowBrightnessNoReport() {
-        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
-        final int displayStatsId = mDisplayUniqueId.hashCode();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-        hbmcOnBrightnessChanged(hbmc, DEFAULT_MIN);
-        advanceTime(0);
-        // verify in HBM sunlight mode
-        assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
-
-        // Verify Stats HBM_ON_SUNLIGHT not report
-        verify(mInjectorMock, never()).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_SUNLIGHT),
-            anyInt());
-    }
-
-    // Test reporting of thermal throttling when triggered externally through
-    // HighBrightnessModeController.onBrightnessChanged()
-    @Test
-    public void testHbmStats_ExternalThermalOff() throws Exception {
-        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
-        final int displayStatsId = mDisplayUniqueId.hashCode();
-        final float hbmBrightness = TRANSITION_POINT + 0.01f;
-        final float nbmBrightness = TRANSITION_POINT - 0.01f;
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-        // Brightness is unthrottled, HBM brightness granted
-        hbmc.onBrightnessChanged(hbmBrightness, hbmBrightness,
-                BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE);
-        advanceTime(1);
-        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_SUNLIGHT),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
-
-        // Brightness is thermally throttled, HBM brightness denied (NBM brightness granted)
-        hbmc.onBrightnessChanged(nbmBrightness, hbmBrightness,
-                BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL);
-        advanceTime(1);
-        // We expect HBM mode to remain set to sunlight, indicating that HBMC *allows* this mode.
-        // However, we expect the HBM state reported by HBMC to be off, since external thermal
-        // throttling (reported to HBMC through onBrightnessChanged()) lowers brightness to below
-        // the HBM transition point.
-        assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
-        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_SV_OFF_THERMAL_LIMIT));
-    }
-
-    @Test
-    public void testHbmStats_TimeOut() {
-        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
-        final int displayStatsId = mDisplayUniqueId.hashCode();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
-        advanceTime(0);
-        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_SUNLIGHT),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
-
-        // Use up all the time in the window.
-        advanceTime(TIME_WINDOW_MILLIS + 1);
-
-        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_SV_OFF_TIME_LIMIT));
-    }
-
-    @Test
-    public void testHbmStats_DisplayOff() {
-        final HighBrightnessModeController hbmc = createDefaultHbm();
-        final int displayStatsId = mDisplayUniqueId.hashCode();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
-        advanceTime(0);
-        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_SUNLIGHT),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE);
-        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_SV_OFF_DISPLAY_OFF));
-    }
-
-    @Test
-    public void testHbmStats_HdrPlaying() {
-        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
-        final int displayStatsId = mDisplayUniqueId.hashCode();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
-        advanceTime(0);
-        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_SUNLIGHT),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
-
-        hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
-                DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/, 1.0f /*maxDesiredHdrSdrRatio*/);
-        advanceTime(0);
-
-        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_HDR),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_SV_OFF_HDR_PLAYING));
-    }
-
-    @Test
-    public void tetHbmStats_LowRequestedBrightness() {
-        final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
-        final int displayStatsId = mDisplayUniqueId.hashCode();
-
-        hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
-        hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
-        hbmcOnBrightnessChanged(hbmc, TRANSITION_POINT + 0.01f);
-        advanceTime(0);
-        // verify in HBM sunlight mode
-        assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
-        // verify HBM_ON_SUNLIGHT
-        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_ON_SUNLIGHT),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__REASON__HBM_TRANSITION_REASON_UNKNOWN));
-
-        hbmcOnBrightnessChanged(hbmc, DEFAULT_MIN);
-        // verify HBM_SV_OFF due to LOW_REQUESTED_BRIGHTNESS
-        verify(mInjectorMock).reportHbmStateChange(eq(displayStatsId),
-            eq(FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF),
-            eq(FrameworkStatsLog
-                      .DISPLAY_HBM_STATE_CHANGED__REASON__HBM_SV_OFF_LOW_REQUESTED_BRIGHTNESS));
-    }
-
-    private void assertState(HighBrightnessModeController hbmc,
-            float brightnessMin, float brightnessMax, int hbmMode) {
-        assertEquals(brightnessMin, hbmc.getCurrentBrightnessMin(), EPSILON);
-        assertEquals(brightnessMax, hbmc.getCurrentBrightnessMax(), EPSILON);
-        assertEquals(hbmMode, hbmc.getHighBrightnessMode());
-    }
-
-    private class TestHbmBuilder {
-        OffsettableClock mClock;
-        HighBrightnessModeController.HdrBrightnessDeviceConfig mHdrBrightnessCfg;
-
-        TestHbmBuilder setClock(OffsettableClock clock) {
-            mClock = clock;
-            return this;
-        }
-
-        TestHbmBuilder setHdrBrightnessConfig(
-                HighBrightnessModeController.HdrBrightnessDeviceConfig hdrBrightnessCfg
-        ) {
-            mHdrBrightnessCfg = hdrBrightnessCfg;
-            return this;
-        }
-
-        HighBrightnessModeController build() {
-            initHandler(mClock);
-            if (mHighBrightnessModeMetadata == null) {
-                mHighBrightnessModeMetadata = new HighBrightnessModeMetadata();
-            }
-            return new HighBrightnessModeController(mInjectorMock, mHandler, DISPLAY_WIDTH,
-                    DISPLAY_HEIGHT, mDisplayToken, mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX,
-                    DEFAULT_HBM_DATA, mHdrBrightnessCfg, () -> {}, mHighBrightnessModeMetadata,
-                    mContextSpy);
-        }
-
-    }
-
-    private HighBrightnessModeController createDefaultHbm() {
-        return new TestHbmBuilder().build();
-    }
-
-    // Creates instance with standard initialization values.
-    private HighBrightnessModeController createDefaultHbm(OffsettableClock clock) {
-        return new TestHbmBuilder().setClock(clock).build();
-    }
-
-    private void initHandler(OffsettableClock clock) {
-        mClock = clock != null ? clock : new OffsettableClock.Stopped();
-        when(mInjectorMock.getClock()).thenReturn(mClock::now);
-        mTestLooper = new TestLooper(mClock::now);
-        mHandler = new Handler(mTestLooper.getLooper(), new Handler.Callback() {
-            @Override
-            public boolean handleMessage(Message msg) {
-                return true;
-            }
-        });
-    }
-
-    private void advanceTime(long timeMs) {
-        mClock.fastForward(timeMs);
-        mTestLooper.dispatchAll();
-    }
-
-    private void hbmcOnBrightnessChanged(HighBrightnessModeController hbmc, float brightness) {
-        hbmc.onBrightnessChanged(brightness, brightness, BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE);
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java
deleted file mode 100644
index 30ff8ba..0000000
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java
+++ /dev/null
@@ -1,287 +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.server.display;
-
-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.Mockito.any;
-import static org.mockito.Mockito.eq;
-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.app.PropertyInvalidatedCache;
-import android.graphics.Point;
-import android.util.SparseArray;
-import android.view.Display;
-import android.view.DisplayInfo;
-import android.view.Surface;
-import android.view.SurfaceControl;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.server.display.layout.Layout;
-
-import org.junit.Before;
-import org.junit.Test;
-
-import java.io.InputStream;
-import java.io.OutputStream;
-
-@SmallTest
-public class LogicalDisplayTest {
-    private static final int DISPLAY_ID = 0;
-    private static final int LAYER_STACK = 0;
-    private static final int DISPLAY_WIDTH = 100;
-    private static final int DISPLAY_HEIGHT = 200;
-    private static final int MODE_ID = 1;
-
-    private LogicalDisplay mLogicalDisplay;
-    private DisplayDevice mDisplayDevice;
-    private DisplayDeviceRepository mDeviceRepo;
-    private final DisplayDeviceInfo mDisplayDeviceInfo = new DisplayDeviceInfo();
-
-    @Before
-    public void setUp() {
-        // Share classloader to allow package private access.
-        System.setProperty("dexmaker.share_classloader", "true");
-        mDisplayDevice = mock(DisplayDevice.class);
-        mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice);
-
-        mDisplayDeviceInfo.copyFrom(new DisplayDeviceInfo());
-        mDisplayDeviceInfo.width = DISPLAY_WIDTH;
-        mDisplayDeviceInfo.height = DISPLAY_HEIGHT;
-        mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT;
-        mDisplayDeviceInfo.touch = DisplayDeviceInfo.TOUCH_INTERNAL;
-        mDisplayDeviceInfo.modeId = MODE_ID;
-        mDisplayDeviceInfo.supportedModes = new Display.Mode[] {new Display.Mode(MODE_ID,
-                DISPLAY_WIDTH, DISPLAY_HEIGHT, /* refreshRate= */ 60)};
-        when(mDisplayDevice.getDisplayDeviceInfoLocked()).thenReturn(mDisplayDeviceInfo);
-
-        // Disable binder caches in this process.
-        PropertyInvalidatedCache.disableForTestMode();
-
-        mDeviceRepo = new DisplayDeviceRepository(
-                new DisplayManagerService.SyncRoot(),
-                new PersistentDataStore(new PersistentDataStore.Injector() {
-                    @Override
-                    public InputStream openRead() {
-                        return null;
-                    }
-
-                    @Override
-                    public OutputStream startWrite() {
-                        return null;
-                    }
-
-                    @Override
-                    public void finishWrite(OutputStream os, boolean success) {}
-                }));
-        mDeviceRepo.onDisplayDeviceEvent(mDisplayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
-    }
-
-    @Test
-    public void testGetDisplayPosition() {
-        Point expectedPosition = new Point();
-
-        SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
-        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
-        assertEquals(expectedPosition, mLogicalDisplay.getDisplayPosition());
-
-        expectedPosition.set(20, 40);
-        mLogicalDisplay.setDisplayOffsetsLocked(20, 40);
-        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
-        assertEquals(expectedPosition, mLogicalDisplay.getDisplayPosition());
-
-        expectedPosition.set(40, -20);
-        DisplayInfo displayInfo = new DisplayInfo();
-        displayInfo.logicalWidth = DISPLAY_HEIGHT;
-        displayInfo.logicalHeight = DISPLAY_WIDTH;
-        displayInfo.rotation = Surface.ROTATION_90;
-        mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo);
-        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
-        assertEquals(expectedPosition, mLogicalDisplay.getDisplayPosition());
-    }
-
-    @Test
-    public void testDisplayInputFlags() {
-        SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
-        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
-        verify(t).setDisplayFlags(any(), eq(SurfaceControl.DISPLAY_RECEIVES_INPUT));
-        reset(t);
-
-        mDisplayDeviceInfo.touch = DisplayDeviceInfo.TOUCH_NONE;
-        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
-        verify(t).setDisplayFlags(any(), eq(0));
-        reset(t);
-
-        mDisplayDeviceInfo.touch = DisplayDeviceInfo.TOUCH_VIRTUAL;
-        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
-        verify(t).setDisplayFlags(any(), eq(SurfaceControl.DISPLAY_RECEIVES_INPUT));
-        reset(t);
-
-        mLogicalDisplay.setEnabledLocked(false);
-        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
-        verify(t).setDisplayFlags(any(), eq(0));
-        reset(t);
-
-        mLogicalDisplay.setEnabledLocked(true);
-        mDisplayDeviceInfo.touch = DisplayDeviceInfo.TOUCH_EXTERNAL;
-        mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
-        verify(t).setDisplayFlags(any(), eq(SurfaceControl.DISPLAY_RECEIVES_INPUT));
-        reset(t);
-    }
-
-    @Test
-    public void testRearDisplaysArePresentationDisplaysThatDestroyContentOnRemoval() {
-        // Assert that the display isn't a presentation display by default, with a default remove
-        // mode
-        assertEquals(0, mLogicalDisplay.getDisplayInfoLocked().flags);
-        assertEquals(Display.REMOVE_MODE_MOVE_CONTENT_TO_PRIMARY,
-                mLogicalDisplay.getDisplayInfoLocked().removeMode);
-
-        // Update position and test to see that it's been updated to a rear, presentation display
-        // that destroys content on removal
-        mLogicalDisplay.setDevicePositionLocked(Layout.Display.POSITION_REAR);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
-        assertEquals(Display.FLAG_REAR | Display.FLAG_PRESENTATION,
-                mLogicalDisplay.getDisplayInfoLocked().flags);
-        assertEquals(Display.REMOVE_MODE_DESTROY_CONTENT,
-                mLogicalDisplay.getDisplayInfoLocked().removeMode);
-
-        // And then check the unsetting the position resets both
-        mLogicalDisplay.setDevicePositionLocked(Layout.Display.POSITION_UNKNOWN);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
-        assertEquals(0, mLogicalDisplay.getDisplayInfoLocked().flags);
-        assertEquals(Display.REMOVE_MODE_MOVE_CONTENT_TO_PRIMARY,
-                mLogicalDisplay.getDisplayInfoLocked().removeMode);
-    }
-
-    @Test
-    public void testUpdateLayoutLimitedRefreshRate() {
-        SurfaceControl.RefreshRateRange layoutLimitedRefreshRate =
-                new SurfaceControl.RefreshRateRange(0, 120);
-        DisplayInfo info1 = mLogicalDisplay.getDisplayInfoLocked();
-        mLogicalDisplay.updateLayoutLimitedRefreshRateLocked(layoutLimitedRefreshRate);
-        DisplayInfo info2 = mLogicalDisplay.getDisplayInfoLocked();
-        // Display info should only be updated when updateLocked is called
-        assertEquals(info2, info1);
-
-        mLogicalDisplay.updateLocked(mDeviceRepo);
-        DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
-        assertNotEquals(info3, info2);
-        assertEquals(layoutLimitedRefreshRate, info3.layoutLimitedRefreshRate);
-    }
-
-    @Test
-    public void testUpdateLayoutLimitedRefreshRate_setsDirtyFlag() {
-        SurfaceControl.RefreshRateRange layoutLimitedRefreshRate =
-                new SurfaceControl.RefreshRateRange(0, 120);
-        assertFalse(mLogicalDisplay.isDirtyLocked());
-
-        mLogicalDisplay.updateLayoutLimitedRefreshRateLocked(layoutLimitedRefreshRate);
-        assertTrue(mLogicalDisplay.isDirtyLocked());
-
-        mLogicalDisplay.updateLocked(mDeviceRepo);
-        assertFalse(mLogicalDisplay.isDirtyLocked());
-    }
-
-    @Test
-    public void testUpdateRefreshRateThermalThrottling() {
-        SparseArray<SurfaceControl.RefreshRateRange> refreshRanges = new SparseArray<>();
-        refreshRanges.put(0, new SurfaceControl.RefreshRateRange(0, 120));
-        DisplayInfo info1 = mLogicalDisplay.getDisplayInfoLocked();
-        mLogicalDisplay.updateThermalRefreshRateThrottling(refreshRanges);
-        DisplayInfo info2 = mLogicalDisplay.getDisplayInfoLocked();
-        // Display info should only be updated when updateLocked is called
-        assertEquals(info2, info1);
-
-        mLogicalDisplay.updateLocked(mDeviceRepo);
-        DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
-        assertNotEquals(info3, info2);
-        assertTrue(refreshRanges.contentEquals(info3.thermalRefreshRateThrottling));
-    }
-
-    @Test
-    public void testUpdateRefreshRateThermalThrottling_setsDirtyFlag() {
-        SparseArray<SurfaceControl.RefreshRateRange> refreshRanges = new SparseArray<>();
-        refreshRanges.put(0, new SurfaceControl.RefreshRateRange(0, 120));
-        assertFalse(mLogicalDisplay.isDirtyLocked());
-
-        mLogicalDisplay.updateThermalRefreshRateThrottling(refreshRanges);
-        assertTrue(mLogicalDisplay.isDirtyLocked());
-
-        mLogicalDisplay.updateLocked(mDeviceRepo);
-        assertFalse(mLogicalDisplay.isDirtyLocked());
-    }
-
-    @Test
-    public void testUpdateDisplayGroupIdLocked() {
-        int newId = 999;
-        DisplayInfo info1 = mLogicalDisplay.getDisplayInfoLocked();
-        mLogicalDisplay.updateDisplayGroupIdLocked(newId);
-        DisplayInfo info2 = mLogicalDisplay.getDisplayInfoLocked();
-        // Display info should only be updated when updateLocked is called
-        assertEquals(info2, info1);
-
-        mLogicalDisplay.updateLocked(mDeviceRepo);
-        DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
-        assertNotEquals(info3, info2);
-        assertEquals(newId, info3.displayGroupId);
-    }
-
-    @Test
-    public void testUpdateDisplayGroupIdLocked_setsDirtyFlag() {
-        assertFalse(mLogicalDisplay.isDirtyLocked());
-
-        mLogicalDisplay.updateDisplayGroupIdLocked(99);
-        assertTrue(mLogicalDisplay.isDirtyLocked());
-
-        mLogicalDisplay.updateLocked(mDeviceRepo);
-        assertFalse(mLogicalDisplay.isDirtyLocked());
-    }
-
-    @Test
-    public void testSetThermalBrightnessThrottlingDataId() {
-        String brightnessThrottlingDataId = "throttling_data_id";
-        DisplayInfo info1 = mLogicalDisplay.getDisplayInfoLocked();
-        mLogicalDisplay.setThermalBrightnessThrottlingDataIdLocked(brightnessThrottlingDataId);
-        DisplayInfo info2 = mLogicalDisplay.getDisplayInfoLocked();
-        // Display info should only be updated when updateLocked is called
-        assertEquals(info2, info1);
-
-        mLogicalDisplay.updateLocked(mDeviceRepo);
-        DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
-        assertNotEquals(info3, info2);
-        assertEquals(brightnessThrottlingDataId, info3.thermalBrightnessThrottlingDataId);
-    }
-
-    @Test
-    public void testSetThermalBrightnessThrottlingDataId_setsDirtyFlag() {
-        assertFalse(mLogicalDisplay.isDirtyLocked());
-
-        mLogicalDisplay.setThermalBrightnessThrottlingDataIdLocked("99");
-        assertTrue(mLogicalDisplay.isDirtyLocked());
-
-        mLogicalDisplay.updateLocked(mDeviceRepo);
-        assertFalse(mLogicalDisplay.isDirtyLocked());
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
deleted file mode 100644
index 817b245..0000000
--- a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
+++ /dev/null
@@ -1,462 +0,0 @@
-/*
- * 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
- */
-
-package com.android.server.display;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import android.content.Context;
-import android.hardware.display.BrightnessConfiguration;
-import android.os.Handler;
-import android.os.test.TestLooper;
-import android.util.Pair;
-
-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;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class PersistentDataStoreTest {
-    private PersistentDataStore mDataStore;
-    private TestInjector mInjector;
-    private TestLooper mTestLooper;
-
-    @Before
-    public void setUp() {
-        mInjector = new TestInjector();
-        mTestLooper = new TestLooper();
-        Handler handler = new Handler(mTestLooper.getLooper());
-        mDataStore = new PersistentDataStore(mInjector, handler);
-    }
-
-    @Test
-    public void testLoadingBrightnessConfigurations() {
-        String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
-                + "<display-manager-state>\n"
-                + "  <brightness-configurations>\n"
-                + "    <brightness-configuration"
-                + "         user-serial=\"1\""
-                + "         package-name=\"example.com\""
-                + "         timestamp=\"123456\">\n"
-                + "      <brightness-curve description=\"something\">\n"
-                + "        <brightness-point lux=\"0\" nits=\"13.25\"/>\n"
-                + "        <brightness-point lux=\"25\" nits=\"35.94\"/>\n"
-                + "      </brightness-curve>\n"
-                + "    </brightness-configuration>\n"
-                + "    <brightness-configuration user-serial=\"3\">\n"
-                + "      <brightness-curve>\n"
-                + "        <brightness-point lux=\"0\" nits=\"13.25\"/>\n"
-                + "        <brightness-point lux=\"10.2\" nits=\"15\"/>\n"
-                + "      </brightness-curve>\n"
-                + "    </brightness-configuration>\n"
-                + "  </brightness-configurations>\n"
-                + "</display-manager-state>\n";
-        InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8));
-        mInjector.setReadStream(is);
-        mDataStore.loadIfNeeded();
-        BrightnessConfiguration config = mDataStore.getBrightnessConfiguration(1 /*userSerial*/);
-        Pair<float[], float[]> curve = config.getCurve();
-        float[] expectedLux = { 0f, 25f };
-        float[] expectedNits = { 13.25f, 35.94f };
-        assertArrayEquals(expectedLux, curve.first, "lux");
-        assertArrayEquals(expectedNits, curve.second, "nits");
-        assertEquals("something", config.getDescription());
-
-        config = mDataStore.getBrightnessConfiguration(3 /*userSerial*/);
-        curve = config.getCurve();
-        expectedLux = new float[] { 0f, 10.2f };
-        expectedNits = new float[] { 13.25f, 15f };
-        assertArrayEquals(expectedLux, curve.first, "lux");
-        assertArrayEquals(expectedNits, curve.second, "nits");
-        assertNull(config.getDescription());
-    }
-
-    @Test
-    public void testBrightnessConfigWithInvalidCurveIsIgnored() {
-        String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
-                + "<display-manager-state>\n"
-                + "  <brightness-configurations>\n"
-                + "    <brightness-configuration user-serial=\"0\">\n"
-                + "      <brightness-curve>\n"
-                + "        <brightness-point lux=\"1\" nits=\"13.25\"/>\n"
-                + "        <brightness-point lux=\"25\" nits=\"35.94\"/>\n"
-                + "      </brightness-curve>\n"
-                + "    </brightness-configuration>\n"
-                + "  </brightness-configurations>\n"
-                + "</display-manager-state>\n";
-        InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8));
-        mInjector.setReadStream(is);
-        mDataStore.loadIfNeeded();
-        assertNull(mDataStore.getBrightnessConfiguration(0 /*userSerial*/));
-    }
-
-    @Test
-    public void testBrightnessConfigWithInvalidFloatsIsIgnored() {
-        String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
-                + "<display-manager-state>\n"
-                + "  <brightness-configurations>\n"
-                + "    <brightness-configuration user-serial=\"0\">\n"
-                + "      <brightness-curve>\n"
-                + "        <brightness-point lux=\"0\" nits=\"13.25\"/>\n"
-                + "        <brightness-point lux=\"0xFF\" nits=\"foo\"/>\n"
-                + "      </brightness-curve>\n"
-                + "    </brightness-configuration>\n"
-                + "  </brightness-configurations>\n"
-                + "</display-manager-state>\n";
-        InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8));
-        mInjector.setReadStream(is);
-        mDataStore.loadIfNeeded();
-        assertNull(mDataStore.getBrightnessConfiguration(0 /*userSerial*/));
-    }
-
-    @Test
-    public void testEmptyBrightnessConfigurationsDoesntCrash() {
-        String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
-                + "<display-manager-state>\n"
-                + "  <brightness-configurations />\n"
-                + "</display-manager-state>\n";
-        InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8));
-        mInjector.setReadStream(is);
-        mDataStore.loadIfNeeded();
-        assertNull(mDataStore.getBrightnessConfiguration(0 /*userSerial*/));
-    }
-
-    @Test
-    public void testStoreAndReloadOfDisplayBrightnessConfigurations() throws InterruptedException {
-        final String uniqueDisplayId = "test:123";
-        int userSerial = 0;
-        String packageName = "pdsTestPackage";
-        final float[] lux = { 0f, 10f };
-        final float[] nits = {1f, 100f };
-        final BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
-                .setDescription("a description")
-                .build();
-        mDataStore.loadIfNeeded();
-        assertNull(mDataStore.getBrightnessConfigurationForDisplayLocked(uniqueDisplayId,
-                userSerial));
-
-        DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
-            @Override
-            public boolean hasStableUniqueId() {
-                return true;
-            }
-
-            @Override
-            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
-                return null;
-            }
-        };
-
-        mDataStore.setBrightnessConfigurationForDisplayLocked(config, testDisplayDevice, userSerial,
-                packageName);
-
-        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        mInjector.setWriteStream(baos);
-        mDataStore.saveIfNeeded();
-        mTestLooper.dispatchAll();
-        assertTrue(mInjector.wasWriteSuccessful());
-        TestInjector newInjector = new TestInjector();
-        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
-        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
-        newInjector.setReadStream(bais);
-        newDataStore.loadIfNeeded();
-        assertNotNull(newDataStore.getBrightnessConfigurationForDisplayLocked(uniqueDisplayId,
-                userSerial));
-        assertEquals(mDataStore.getBrightnessConfigurationForDisplayLocked(uniqueDisplayId,
-                userSerial), newDataStore.getBrightnessConfigurationForDisplayLocked(
-                        uniqueDisplayId, userSerial));
-    }
-
-    @Test
-    public void testSetBrightnessConfigurationFailsWithUnstableId() {
-        final String uniqueDisplayId = "test:123";
-        int userSerial = 0;
-        String packageName = "pdsTestPackage";
-        final float[] lux = { 0f, 10f };
-        final float[] nits = {1f, 100f };
-        final BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
-                .setDescription("a description")
-                .build();
-        mDataStore.loadIfNeeded();
-        assertNull(mDataStore.getBrightnessConfigurationForDisplayLocked(uniqueDisplayId,
-                userSerial));
-
-        DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
-            @Override
-            public boolean hasStableUniqueId() {
-                return false;
-            }
-
-            @Override
-            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
-                return null;
-            }
-        };
-
-        assertFalse(mDataStore.setBrightnessConfigurationForDisplayLocked(
-                config, testDisplayDevice, userSerial, packageName));
-    }
-
-    @Test
-    public void testStoreAndReloadOfBrightnessConfigurations() throws InterruptedException {
-        final float[] lux = { 0f, 10f };
-        final float[] nits = {1f, 100f };
-        final BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
-                .setDescription("a description")
-                .build();
-        Context context = InstrumentationRegistry.getInstrumentation().getContext();
-        String packageName = context.getPackageName();
-
-        mDataStore.loadIfNeeded();
-        assertNull(mDataStore.getBrightnessConfiguration(0 /*userSerial*/));
-        mDataStore.setBrightnessConfigurationForUser(config, 0, packageName);
-
-        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        mInjector.setWriteStream(baos);
-        mDataStore.saveIfNeeded();
-        mTestLooper.dispatchAll();
-        assertTrue(mInjector.wasWriteSuccessful());
-
-        TestInjector newInjector = new TestInjector();
-        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
-        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
-        newInjector.setReadStream(bais);
-        newDataStore.loadIfNeeded();
-        assertNotNull(newDataStore.getBrightnessConfiguration(0 /*userSerial*/));
-        assertEquals(mDataStore.getBrightnessConfiguration(0 /*userSerial*/),
-                newDataStore.getBrightnessConfiguration(0 /*userSerial*/));
-    }
-
-    @Test
-    public void testNullBrightnessConfiguration() {
-        final float[] lux = { 0f, 10f };
-        final float[] nits = {1f, 100f };
-        int userSerial = 0;
-        final BrightnessConfiguration config = new BrightnessConfiguration.Builder(lux, nits)
-                .setDescription("a description")
-                .build();
-        mDataStore.loadIfNeeded();
-        assertNull(mDataStore.getBrightnessConfiguration(userSerial));
-
-        mDataStore.setBrightnessConfigurationForUser(config, userSerial, "packagename");
-        assertNotNull(mDataStore.getBrightnessConfiguration(userSerial));
-
-        mDataStore.setBrightnessConfigurationForUser(null, userSerial, "packagename");
-        assertNull(mDataStore.getBrightnessConfiguration(userSerial));
-    }
-
-    @Test
-    public void testStoreAndRestoreResolution() {
-        final String uniqueDisplayId = "test:123";
-        DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
-            @Override
-            public boolean hasStableUniqueId() {
-                return true;
-            }
-
-            @Override
-            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
-                return null;
-            }
-        };
-        int width = 35;
-        int height = 45;
-        mDataStore.loadIfNeeded();
-        mDataStore.setUserPreferredResolution(testDisplayDevice, width, height);
-
-        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        mInjector.setWriteStream(baos);
-        mDataStore.saveIfNeeded();
-        mTestLooper.dispatchAll();
-        assertTrue(mInjector.wasWriteSuccessful());
-        TestInjector newInjector = new TestInjector();
-        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
-        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
-        newInjector.setReadStream(bais);
-        newDataStore.loadIfNeeded();
-        assertNotNull(newDataStore.getUserPreferredResolution(testDisplayDevice));
-        assertEquals(35, newDataStore.getUserPreferredResolution(testDisplayDevice).x);
-        assertEquals(35, mDataStore.getUserPreferredResolution(testDisplayDevice).x);
-        assertEquals(45, newDataStore.getUserPreferredResolution(testDisplayDevice).y);
-        assertEquals(45, mDataStore.getUserPreferredResolution(testDisplayDevice).y);
-    }
-
-    @Test
-    public void testStoreAndRestoreRefreshRate() {
-        final String uniqueDisplayId = "test:123";
-        DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
-            @Override
-            public boolean hasStableUniqueId() {
-                return true;
-            }
-
-            @Override
-            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
-                return null;
-            }
-        };
-        float refreshRate = 85.3f;
-        mDataStore.loadIfNeeded();
-        mDataStore.setUserPreferredRefreshRate(testDisplayDevice, refreshRate);
-
-        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        mInjector.setWriteStream(baos);
-        mDataStore.saveIfNeeded();
-        mTestLooper.dispatchAll();
-        assertTrue(mInjector.wasWriteSuccessful());
-        TestInjector newInjector = new TestInjector();
-        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
-        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
-        newInjector.setReadStream(bais);
-        newDataStore.loadIfNeeded();
-        assertNotNull(newDataStore.getUserPreferredRefreshRate(testDisplayDevice));
-        assertEquals(85.3f, mDataStore.getUserPreferredRefreshRate(testDisplayDevice), 01.f);
-        assertEquals(85.3f, newDataStore.getUserPreferredRefreshRate(testDisplayDevice), 0.1f);
-    }
-
-    @Test
-    public void testBrightnessInitialisesWithInvalidFloat() {
-        final String uniqueDisplayId = "test:123";
-        DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) {
-            @Override
-            public boolean hasStableUniqueId() {
-                return true;
-            }
-
-            @Override
-            public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
-                return null;
-            }
-        };
-
-        // Set any value which initialises Display state
-        float refreshRate = 85.3f;
-        mDataStore.loadIfNeeded();
-        mDataStore.setUserPreferredRefreshRate(testDisplayDevice, refreshRate);
-
-        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        mInjector.setWriteStream(baos);
-        mDataStore.saveIfNeeded();
-        mTestLooper.dispatchAll();
-        assertTrue(mInjector.wasWriteSuccessful());
-        TestInjector newInjector = new TestInjector();
-        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
-        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
-        newInjector.setReadStream(bais);
-        newDataStore.loadIfNeeded();
-        assertTrue(Float.isNaN(mDataStore.getBrightness(testDisplayDevice)));
-    }
-
-    @Test
-    public void testStoreAndRestoreBrightnessNitsForDefaultDisplay() {
-        float brightnessNitsForDefaultDisplay = 190;
-        mDataStore.loadIfNeeded();
-        mDataStore.setBrightnessNitsForDefaultDisplay(brightnessNitsForDefaultDisplay);
-
-        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        mInjector.setWriteStream(baos);
-        mDataStore.saveIfNeeded();
-        mTestLooper.dispatchAll();
-        assertTrue(mInjector.wasWriteSuccessful());
-        TestInjector newInjector = new TestInjector();
-        PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
-        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
-        newInjector.setReadStream(bais);
-        newDataStore.loadIfNeeded();
-        assertEquals(brightnessNitsForDefaultDisplay,
-                mDataStore.getBrightnessNitsForDefaultDisplay(), 0);
-        assertEquals(brightnessNitsForDefaultDisplay,
-                newDataStore.getBrightnessNitsForDefaultDisplay(), 0);
-    }
-
-    @Test
-    public void testInitialBrightnessNitsForDefaultDisplay() {
-        mDataStore.loadIfNeeded();
-        assertEquals(-1, mDataStore.getBrightnessNitsForDefaultDisplay(), 0);
-    }
-
-    public class TestInjector extends PersistentDataStore.Injector {
-        private InputStream mReadStream;
-        private OutputStream mWriteStream;
-
-        private boolean mWasSuccessful;
-
-        @Override
-        public InputStream openRead() throws FileNotFoundException {
-            if (mReadStream != null) {
-                return mReadStream;
-            } else {
-                throw new FileNotFoundException();
-            }
-        }
-
-        @Override
-        public OutputStream startWrite() {
-            return mWriteStream;
-        }
-
-        @Override
-        public void finishWrite(OutputStream os, boolean success) {
-            mWasSuccessful = success;
-            try {
-                os.close();
-            } catch (IOException e) {
-                // This method can't throw IOException since the super implementation doesn't, so
-                // we just wrap it in a RuntimeException so we end up crashing the test all the
-                // same.
-                throw new RuntimeException(e);
-            }
-        }
-
-        public void setReadStream(InputStream is) {
-            mReadStream = is;
-        }
-
-        public void setWriteStream(OutputStream os) {
-            mWriteStream = os;
-        }
-
-        public boolean wasWriteSuccessful() {
-            return mWasSuccessful;
-        }
-    }
-
-    private static void assertArrayEquals(float[] expected, float[] actual, String name) {
-        assertEquals("Expected " + name + " arrays to be the same length!",
-                expected.length, actual.length);
-        for (int i = 0; i < expected.length; i++) {
-            assertEquals("Expected " + name + " arrays to be equivalent when value " + i
-                    + "differs", expected[i], actual[i], 0.01 /*tolerance*/);
-        }
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/display/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/display/TEST_MAPPING
deleted file mode 100644
index 92d8abd..0000000
--- a/services/tests/servicestests/src/com/android/server/display/TEST_MAPPING
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "presubmit": [
-    {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {"include-filter": "com.android.server.display"},
-        {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
-        {"exclude-annotation": "androidx.test.filters.FlakyTest"},
-        {"exclude-annotation": "org.junit.Ignore"}
-      ]
-    }
-  ]
-}
diff --git a/services/tests/servicestests/src/com/android/server/display/TestUtils.java b/services/tests/servicestests/src/com/android/server/display/TestUtils.java
index 90d9bae..8b45145 100644
--- a/services/tests/servicestests/src/com/android/server/display/TestUtils.java
+++ b/services/tests/servicestests/src/com/android/server/display/TestUtils.java
@@ -18,6 +18,7 @@
 
 import android.hardware.Sensor;
 import android.hardware.SensorEvent;
+import android.hardware.input.InputSensorInfo;
 import android.os.Parcel;
 import android.os.SystemClock;
 import android.view.DisplayAddress;
@@ -75,6 +76,12 @@
         return sensor;
     }
 
+    public static Sensor createSensor(String type, String name) {
+        return new Sensor(new InputSensorInfo(
+                name, "vendor", 0, 0, 0, 1f, 1f, 1, 1, 1, 1,
+                type, "", 0, 0, 0));
+    }
+
     /**
      * Create a custom {@link DisplayAddress} to ensure we're not relying on any specific
      * display-address implementation in our code. Intentionally uses default object (reference)
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java b/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
deleted file mode 100644
index e6d3bbc..0000000
--- a/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
+++ /dev/null
@@ -1,361 +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.server.display.brightness;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
-import android.os.HandlerExecutor;
-import android.os.PowerManager;
-import android.view.Display;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.server.display.AutomaticBrightnessController;
-import com.android.server.display.BrightnessSetting;
-import com.android.server.display.brightness.strategy.DisplayBrightnessStrategy;
-import com.android.server.display.brightness.strategy.TemporaryBrightnessStrategy;
-
-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)
-public final class DisplayBrightnessControllerTest {
-    private static final int DISPLAY_ID = Display.DEFAULT_DISPLAY;
-    private static final float DEFAULT_BRIGHTNESS = 0.15f;
-
-    @Mock
-    private DisplayBrightnessStrategySelector mDisplayBrightnessStrategySelector;
-    @Mock
-    private Context mContext;
-    @Mock
-    private Resources mResources;
-    @Mock
-    private BrightnessSetting mBrightnessSetting;
-    @Mock
-    private Runnable mOnBrightnessChangeRunnable;
-
-    @Mock
-    private HandlerExecutor mBrightnessChangeExecutor;
-
-    private final DisplayBrightnessController.Injector mInjector = new
-            DisplayBrightnessController.Injector() {
-        @Override
-        DisplayBrightnessStrategySelector getDisplayBrightnessStrategySelector(
-                Context context, int displayId) {
-            return mDisplayBrightnessStrategySelector;
-        }
-    };
-
-    private DisplayBrightnessController mDisplayBrightnessController;
-
-    @Before
-    public void before() {
-        MockitoAnnotations.initMocks(this);
-        when(mContext.getResources()).thenReturn(mResources);
-        when(mBrightnessSetting.getBrightness()).thenReturn(Float.NaN);
-        when(mBrightnessSetting.getBrightnessNitsForDefaultDisplay()).thenReturn(-1f);
-        when(mResources.getBoolean(
-                com.android.internal.R.bool.config_persistBrightnessNitsForDefaultDisplay))
-                .thenReturn(true);
-        mDisplayBrightnessController = new DisplayBrightnessController(mContext, mInjector,
-                DISPLAY_ID, DEFAULT_BRIGHTNESS, mBrightnessSetting, mOnBrightnessChangeRunnable,
-                mBrightnessChangeExecutor);
-    }
-
-    @Test
-    public void testIfFirstScreenBrightnessIsDefault() {
-        assertEquals(DEFAULT_BRIGHTNESS, mDisplayBrightnessController.getCurrentBrightness(),
-                /* delta= */ 0.0f);
-    }
-
-    @Test
-    public void testUpdateBrightness() {
-        DisplayPowerRequest displayPowerRequest = mock(DisplayPowerRequest.class);
-        DisplayBrightnessStrategy displayBrightnessStrategy = mock(DisplayBrightnessStrategy.class);
-        int targetDisplayState = Display.STATE_DOZE;
-        when(mDisplayBrightnessStrategySelector.selectStrategy(displayPowerRequest,
-                targetDisplayState)).thenReturn(displayBrightnessStrategy);
-        mDisplayBrightnessController.updateBrightness(displayPowerRequest, targetDisplayState);
-        verify(displayBrightnessStrategy).updateBrightness(displayPowerRequest);
-        assertEquals(mDisplayBrightnessController.getCurrentDisplayBrightnessStrategy(),
-                displayBrightnessStrategy);
-    }
-
-    @Test
-    public void isAllowAutoBrightnessWhileDozingConfigDelegatesToDozeBrightnessStrategy() {
-        mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig();
-        verify(mDisplayBrightnessStrategySelector).isAllowAutoBrightnessWhileDozingConfig();
-    }
-
-    @Test
-    public void setTemporaryBrightness() {
-        float temporaryBrightness = 0.4f;
-        TemporaryBrightnessStrategy temporaryBrightnessStrategy = mock(
-                TemporaryBrightnessStrategy.class);
-        when(mDisplayBrightnessStrategySelector.getTemporaryDisplayBrightnessStrategy()).thenReturn(
-                temporaryBrightnessStrategy);
-        mDisplayBrightnessController.setTemporaryBrightness(temporaryBrightness);
-        verify(temporaryBrightnessStrategy).setTemporaryScreenBrightness(temporaryBrightness);
-    }
-
-    @Test
-    public void setCurrentScreenBrightness() {
-        // Current Screen brightness is set as expected when a different value than the current
-        // is set
-        float currentScreenBrightness = 0.4f;
-        mDisplayBrightnessController.setAndNotifyCurrentScreenBrightness(currentScreenBrightness);
-        assertEquals(mDisplayBrightnessController.getCurrentBrightness(),
-                currentScreenBrightness, /* delta= */ 0.0f);
-        verify(mBrightnessChangeExecutor).execute(mOnBrightnessChangeRunnable);
-
-        // No change to the current screen brightness is same as the existing one
-        mDisplayBrightnessController.setAndNotifyCurrentScreenBrightness(currentScreenBrightness);
-        verifyNoMoreInteractions(mBrightnessChangeExecutor);
-    }
-
-    @Test
-    public void setPendingScreenBrightnessSetting() {
-        float pendingScreenBrightness = 0.4f;
-        mDisplayBrightnessController.setPendingScreenBrightness(pendingScreenBrightness);
-        assertEquals(mDisplayBrightnessController.getPendingScreenBrightness(),
-                pendingScreenBrightness, /* delta= */ 0.0f);
-    }
-
-    @Test
-    public void updateUserSetScreenBrightness() {
-        // No brightness is set if the pending brightness is invalid
-        mDisplayBrightnessController.setPendingScreenBrightness(Float.NaN);
-        assertFalse(mDisplayBrightnessController.updateUserSetScreenBrightness());
-
-        // user set brightness is not set if the current and the pending brightness are same.
-        float currentBrightness = 0.4f;
-        TemporaryBrightnessStrategy temporaryBrightnessStrategy = mock(
-                TemporaryBrightnessStrategy.class);
-        when(mDisplayBrightnessStrategySelector.getTemporaryDisplayBrightnessStrategy()).thenReturn(
-                temporaryBrightnessStrategy);
-        mDisplayBrightnessController.setAndNotifyCurrentScreenBrightness(currentBrightness);
-        mDisplayBrightnessController.setPendingScreenBrightness(currentBrightness);
-        mDisplayBrightnessController.setTemporaryBrightness(currentBrightness);
-        assertFalse(mDisplayBrightnessController.updateUserSetScreenBrightness());
-        verify(temporaryBrightnessStrategy).setTemporaryScreenBrightness(
-                PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        assertEquals(mDisplayBrightnessController.getPendingScreenBrightness(),
-                PowerManager.BRIGHTNESS_INVALID_FLOAT, /* delta= */ 0.0f);
-
-        // user set brightness is set as expected
-        currentBrightness = 0.4f;
-        float pendingScreenBrightness = 0.3f;
-        float temporaryScreenBrightness = 0.2f;
-        mDisplayBrightnessController.setAndNotifyCurrentScreenBrightness(currentBrightness);
-        mDisplayBrightnessController.setPendingScreenBrightness(pendingScreenBrightness);
-        mDisplayBrightnessController.setTemporaryBrightness(temporaryScreenBrightness);
-        assertTrue(mDisplayBrightnessController.updateUserSetScreenBrightness());
-        assertEquals(mDisplayBrightnessController.getCurrentBrightness(),
-                pendingScreenBrightness, /* delta= */ 0.0f);
-        assertEquals(mDisplayBrightnessController.getLastUserSetScreenBrightness(),
-                pendingScreenBrightness, /* delta= */ 0.0f);
-        verify(mBrightnessChangeExecutor, times(2))
-                .execute(mOnBrightnessChangeRunnable);
-        verify(temporaryBrightnessStrategy, times(2))
-                .setTemporaryScreenBrightness(PowerManager.BRIGHTNESS_INVALID_FLOAT);
-        assertEquals(mDisplayBrightnessController.getPendingScreenBrightness(),
-                PowerManager.BRIGHTNESS_INVALID_FLOAT, /* delta= */ 0.0f);
-    }
-
-    @Test
-    public void registerBrightnessSettingChangeListener() {
-        BrightnessSetting.BrightnessSettingListener brightnessSettingListener = mock(
-                BrightnessSetting.BrightnessSettingListener.class);
-        mDisplayBrightnessController.registerBrightnessSettingChangeListener(
-                brightnessSettingListener);
-        verify(mBrightnessSetting).registerListener(brightnessSettingListener);
-        assertEquals(mDisplayBrightnessController.getBrightnessSettingListener(),
-                brightnessSettingListener);
-    }
-
-    @Test
-    public void getScreenBrightnessSetting() {
-        // getScreenBrightnessSetting returns the value relayed by BrightnessSetting, if the
-        // valid is valid and in range
-        float brightnessSetting = 0.2f;
-        when(mBrightnessSetting.getBrightness()).thenReturn(brightnessSetting);
-        assertEquals(mDisplayBrightnessController.getScreenBrightnessSetting(), brightnessSetting,
-                /* delta= */ 0.0f);
-
-        // getScreenBrightnessSetting value is clamped if BrightnessSetting returns value beyond max
-        brightnessSetting = 1.1f;
-        when(mBrightnessSetting.getBrightness()).thenReturn(brightnessSetting);
-        assertEquals(mDisplayBrightnessController.getScreenBrightnessSetting(), 1.0f,
-                /* delta= */ 0.0f);
-
-        // getScreenBrightnessSetting returns default value is BrightnessSetting returns invalid
-        // value.
-        brightnessSetting = Float.NaN;
-        when(mBrightnessSetting.getBrightness()).thenReturn(brightnessSetting);
-        assertEquals(mDisplayBrightnessController.getScreenBrightnessSetting(), DEFAULT_BRIGHTNESS,
-                /* delta= */ 0.0f);
-    }
-
-    @Test
-    public void setBrightnessSetsInBrightnessSetting() {
-        float brightnessValue = 0.3f;
-        mDisplayBrightnessController.setBrightness(brightnessValue);
-        verify(mBrightnessSetting).setBrightness(brightnessValue);
-    }
-
-    @Test
-    public void updateScreenBrightnessSetting() {
-        // This interaction happens in the constructor itself
-        verify(mBrightnessSetting).getBrightness();
-
-        // Sets the appropriate value when valid, and not equal to the current brightness
-        float brightnessValue = 0.3f;
-        mDisplayBrightnessController.updateScreenBrightnessSetting(brightnessValue);
-        assertEquals(mDisplayBrightnessController.getCurrentBrightness(), brightnessValue,
-                0.0f);
-        verify(mBrightnessChangeExecutor).execute(mOnBrightnessChangeRunnable);
-        verify(mBrightnessSetting).setBrightness(brightnessValue);
-
-        // Does nothing if the value is invalid
-        mDisplayBrightnessController.updateScreenBrightnessSetting(Float.NaN);
-        verifyNoMoreInteractions(mBrightnessChangeExecutor, mBrightnessSetting);
-
-        // Does nothing if the value is same as the current brightness
-        brightnessValue = 0.2f;
-        mDisplayBrightnessController.setAndNotifyCurrentScreenBrightness(brightnessValue);
-        verify(mBrightnessChangeExecutor, times(2))
-                .execute(mOnBrightnessChangeRunnable);
-        mDisplayBrightnessController.updateScreenBrightnessSetting(brightnessValue);
-        verifyNoMoreInteractions(mBrightnessChangeExecutor, mBrightnessSetting);
-    }
-
-    @Test
-    public void testConvertToNits() {
-        final float brightness = 0.5f;
-        final float nits = 300;
-        final float adjustedNits = 200;
-
-        // ABC is null
-        assertEquals(-1f, mDisplayBrightnessController.convertToNits(brightness),
-                /* delta= */ 0);
-        assertEquals(-1f, mDisplayBrightnessController.convertToAdjustedNits(brightness),
-                /* delta= */ 0);
-
-        AutomaticBrightnessController automaticBrightnessController =
-                mock(AutomaticBrightnessController.class);
-        when(automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
-        when(automaticBrightnessController.convertToAdjustedNits(brightness))
-                .thenReturn(adjustedNits);
-        mDisplayBrightnessController.setAutomaticBrightnessController(
-                automaticBrightnessController);
-
-        assertEquals(nits, mDisplayBrightnessController.convertToNits(brightness), /* delta= */ 0);
-        assertEquals(adjustedNits, mDisplayBrightnessController.convertToAdjustedNits(brightness),
-                /* delta= */ 0);
-    }
-
-    @Test
-    public void testConvertToFloatScale() {
-        float brightness = 0.5f;
-        float nits = 300;
-
-        // ABC is null
-        assertEquals(PowerManager.BRIGHTNESS_INVALID_FLOAT,
-                mDisplayBrightnessController.convertToFloatScale(nits), /* delta= */ 0);
-
-        AutomaticBrightnessController automaticBrightnessController =
-                mock(AutomaticBrightnessController.class);
-        when(automaticBrightnessController.convertToFloatScale(nits)).thenReturn(brightness);
-        mDisplayBrightnessController.setAutomaticBrightnessController(
-                automaticBrightnessController);
-
-        assertEquals(brightness, mDisplayBrightnessController.convertToFloatScale(nits),
-                /* delta= */ 0);
-    }
-
-    @Test
-    public void stop() {
-        BrightnessSetting.BrightnessSettingListener brightnessSettingListener = mock(
-                BrightnessSetting.BrightnessSettingListener.class);
-        mDisplayBrightnessController.registerBrightnessSettingChangeListener(
-                brightnessSettingListener);
-        mDisplayBrightnessController.stop();
-        verify(mBrightnessSetting).unregisterListener(brightnessSettingListener);
-    }
-
-    @Test
-    public void testLoadNitBasedBrightnessSetting() {
-        // When the nits value is valid, the brightness is set from the old default display nits
-        // value
-        float nits = 200f;
-        float brightness = 0.3f;
-        AutomaticBrightnessController automaticBrightnessController =
-                mock(AutomaticBrightnessController.class);
-        when(automaticBrightnessController.convertToFloatScale(nits)).thenReturn(brightness);
-        when(mBrightnessSetting.getBrightnessNitsForDefaultDisplay()).thenReturn(nits);
-        mDisplayBrightnessController.setAutomaticBrightnessController(
-                automaticBrightnessController);
-        verify(mBrightnessSetting).setBrightness(brightness);
-        assertEquals(brightness, mDisplayBrightnessController.getCurrentBrightness(), 0.01f);
-        clearInvocations(automaticBrightnessController, mBrightnessSetting);
-
-        // When the nits value is invalid, the brightness is resumed from where it was last set
-        nits = -1;
-        brightness = 0.4f;
-        when(automaticBrightnessController.convertToFloatScale(nits)).thenReturn(brightness);
-        when(mBrightnessSetting.getBrightnessNitsForDefaultDisplay()).thenReturn(nits);
-        when(mBrightnessSetting.getBrightness()).thenReturn(brightness);
-        mDisplayBrightnessController.setAutomaticBrightnessController(
-                automaticBrightnessController);
-        verify(mBrightnessSetting, never()).setBrightness(brightness);
-        assertEquals(brightness, mDisplayBrightnessController.getCurrentBrightness(), 0.01f);
-        clearInvocations(automaticBrightnessController, mBrightnessSetting);
-
-        // When the display is a non-default display, the brightness is resumed from where it was
-        // last set
-        int nonDefaultDisplayId = 1;
-        mDisplayBrightnessController = new DisplayBrightnessController(mContext, mInjector,
-                nonDefaultDisplayId, DEFAULT_BRIGHTNESS, mBrightnessSetting,
-                mOnBrightnessChangeRunnable, mBrightnessChangeExecutor);
-        brightness = 0.5f;
-        when(mBrightnessSetting.getBrightness()).thenReturn(brightness);
-        mDisplayBrightnessController.setAutomaticBrightnessController(
-                automaticBrightnessController);
-        assertEquals(brightness, mDisplayBrightnessController.getCurrentBrightness(), 0.01f);
-        verifyZeroInteractions(automaticBrightnessController);
-        verify(mBrightnessSetting, never()).getBrightnessNitsForDefaultDisplay();
-        verify(mBrightnessSetting, never()).setBrightness(brightness);
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java b/services/tests/servicestests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
deleted file mode 100644
index d9cf15b..0000000
--- a/services/tests/servicestests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
+++ /dev/null
@@ -1,332 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.brightness.strategy;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.hardware.display.BrightnessConfiguration;
-import android.hardware.display.DisplayManagerInternal;
-import android.os.PowerManager;
-import android.os.UserHandle;
-import android.provider.Settings;
-import android.test.mock.MockContentResolver;
-import android.view.Display;
-
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.internal.util.test.FakeSettingsProviderRule;
-import com.android.server.display.AutomaticBrightnessController;
-import com.android.server.display.brightness.BrightnessEvent;
-import com.android.server.display.brightness.BrightnessReason;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AutomaticBrightnessStrategyTest {
-    private static final int DISPLAY_ID = 0;
-    @Rule
-    public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
-
-    @Mock
-    private AutomaticBrightnessController mAutomaticBrightnessController;
-
-    private BrightnessConfiguration mBrightnessConfiguration;
-    private float mDefaultScreenAutoBrightnessAdjustment;
-    private Context mContext;
-    private AutomaticBrightnessStrategy mAutomaticBrightnessStrategy;
-
-    @Before
-    public void before() {
-        MockitoAnnotations.initMocks(this);
-        mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
-        final MockContentResolver resolver = mSettingsProviderRule.mockContentResolver(mContext);
-        when(mContext.getContentResolver()).thenReturn(resolver);
-        mDefaultScreenAutoBrightnessAdjustment = Settings.System.getFloat(
-                mContext.getContentResolver(),
-                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, Float.NaN);
-        Settings.System.putFloat(mContext.getContentResolver(),
-                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.5f);
-        mAutomaticBrightnessStrategy = new AutomaticBrightnessStrategy(mContext, DISPLAY_ID);
-
-        mBrightnessConfiguration = new BrightnessConfiguration.Builder(
-                new float[]{0f, 1f}, new float[]{0, PowerManager.BRIGHTNESS_ON}).build();
-        when(mAutomaticBrightnessController.hasUserDataPoints()).thenReturn(true);
-        mAutomaticBrightnessStrategy.setAutomaticBrightnessController(
-                mAutomaticBrightnessController);
-        mAutomaticBrightnessStrategy.setBrightnessConfiguration(mBrightnessConfiguration,
-                true);
-    }
-
-    @After
-    public void after() {
-        Settings.System.putFloat(mContext.getContentResolver(),
-                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, mDefaultScreenAutoBrightnessAdjustment);
-    }
-
-    @Test
-    public void setAutoBrightnessWhenDisabled() {
-        mAutomaticBrightnessStrategy.setUseAutoBrightness(false);
-        int targetDisplayState = Display.STATE_ON;
-        boolean allowAutoBrightnessWhileDozing = false;
-        float brightnessState = Float.NaN;
-        int brightnessReason = BrightnessReason.REASON_OVERRIDE;
-        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
-        float lastUserSetBrightness = 0.2f;
-        boolean userSetBrightnessChanged = true;
-        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(true);
-        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
-                allowAutoBrightnessWhileDozing, brightnessState, brightnessReason, policy,
-                lastUserSetBrightness, userSetBrightnessChanged);
-        verify(mAutomaticBrightnessController)
-                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
-                        mBrightnessConfiguration,
-                        lastUserSetBrightness,
-                        userSetBrightnessChanged, 0.5f,
-                        false, policy, true);
-    }
-
-    @Test
-    public void setAutoBrightnessWhenEnabledAndDisplayIsDozing() {
-        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
-        int targetDisplayState = Display.STATE_DOZE;
-        float brightnessState = Float.NaN;
-        boolean allowAutoBrightnessWhileDozing = true;
-        int brightnessReason = BrightnessReason.REASON_DOZE;
-        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
-        float lastUserSetBrightness = 0.2f;
-        boolean userSetBrightnessChanged = true;
-        Settings.System.putFloat(mContext.getContentResolver(),
-                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.4f);
-        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(false);
-        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
-                allowAutoBrightnessWhileDozing, brightnessState, brightnessReason, policy,
-                lastUserSetBrightness, userSetBrightnessChanged);
-        verify(mAutomaticBrightnessController)
-                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED,
-                        mBrightnessConfiguration,
-                        lastUserSetBrightness,
-                        userSetBrightnessChanged, 0.4f,
-                        true, policy, true);
-    }
-
-    @Test
-    public void setAutoBrightnessWhenEnabledAndDisplayIsOn() {
-        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
-        int targetDisplayState = Display.STATE_ON;
-        float brightnessState = Float.NaN;
-        boolean allowAutoBrightnessWhileDozing = false;
-        int brightnessReason = BrightnessReason.REASON_OVERRIDE;
-        float lastUserSetBrightness = 0.2f;
-        boolean userSetBrightnessChanged = true;
-        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
-        float pendingBrightnessAdjustment = 0.1f;
-        Settings.System.putFloat(mContext.getContentResolver(),
-                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingBrightnessAdjustment);
-        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(false);
-        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
-                allowAutoBrightnessWhileDozing, brightnessState, brightnessReason, policy,
-                lastUserSetBrightness, userSetBrightnessChanged);
-        verify(mAutomaticBrightnessController)
-                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED,
-                        mBrightnessConfiguration,
-                        lastUserSetBrightness,
-                        userSetBrightnessChanged, pendingBrightnessAdjustment,
-                        true, policy, true);
-    }
-
-    @Test
-    public void accommodateUserBrightnessChangesWorksAsExpected() {
-        // Verify the state if automaticBrightnessController is configured.
-        assertFalse(mAutomaticBrightnessStrategy.isShortTermModelActive());
-        boolean userSetBrightnessChanged = true;
-        float lastUserSetScreenBrightness = 0.2f;
-        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
-        BrightnessConfiguration brightnessConfiguration = new BrightnessConfiguration.Builder(
-                new float[]{0f, 1f}, new float[]{0, PowerManager.BRIGHTNESS_ON}).build();
-        int autoBrightnessState = AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED;
-        float temporaryAutoBrightnessAdjustments = 0.4f;
-        mAutomaticBrightnessStrategy.setShouldResetShortTermModel(true);
-        setTemporaryAutoBrightnessAdjustment(temporaryAutoBrightnessAdjustments);
-        mAutomaticBrightnessStrategy.accommodateUserBrightnessChanges(userSetBrightnessChanged,
-                lastUserSetScreenBrightness, policy, brightnessConfiguration,
-                autoBrightnessState);
-        verify(mAutomaticBrightnessController).configure(autoBrightnessState,
-                brightnessConfiguration,
-                lastUserSetScreenBrightness,
-                userSetBrightnessChanged, temporaryAutoBrightnessAdjustments,
-                /* userChangedAutoBrightnessAdjustment= */ false, policy,
-                /* shouldResetShortTermModel= */ true);
-        assertTrue(mAutomaticBrightnessStrategy.isTemporaryAutoBrightnessAdjustmentApplied());
-        assertFalse(mAutomaticBrightnessStrategy.shouldResetShortTermModel());
-        assertTrue(mAutomaticBrightnessStrategy.isShortTermModelActive());
-        // Verify the state when automaticBrightnessController is not configured
-        setTemporaryAutoBrightnessAdjustment(Float.NaN);
-        mAutomaticBrightnessStrategy.setAutomaticBrightnessController(null);
-        mAutomaticBrightnessStrategy.setShouldResetShortTermModel(true);
-        mAutomaticBrightnessStrategy.accommodateUserBrightnessChanges(userSetBrightnessChanged,
-                lastUserSetScreenBrightness, policy, brightnessConfiguration,
-                autoBrightnessState);
-        assertFalse(mAutomaticBrightnessStrategy.isTemporaryAutoBrightnessAdjustmentApplied());
-        assertTrue(mAutomaticBrightnessStrategy.shouldResetShortTermModel());
-        assertFalse(mAutomaticBrightnessStrategy.isShortTermModelActive());
-    }
-
-    @Test
-    public void adjustAutomaticBrightnessStateIfValid() throws Settings.SettingNotFoundException {
-        float brightnessState = 0.3f;
-        float autoBrightnessAdjustment = 0.2f;
-        when(mAutomaticBrightnessController.getAutomaticScreenBrightnessAdjustment()).thenReturn(
-                autoBrightnessAdjustment);
-        mAutomaticBrightnessStrategy.adjustAutomaticBrightnessStateIfValid(brightnessState);
-        assertTrue(mAutomaticBrightnessStrategy.hasAppliedAutoBrightness());
-        assertEquals(autoBrightnessAdjustment,
-                mAutomaticBrightnessStrategy.getAutoBrightnessAdjustment(), 0.0f);
-        assertEquals(autoBrightnessAdjustment, Settings.System.getFloatForUser(
-                mContext.getContentResolver(),
-                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ,
-                UserHandle.USER_CURRENT), 0.0f);
-        assertEquals(BrightnessReason.ADJUSTMENT_AUTO,
-                mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags());
-        float invalidBrightness = -0.5f;
-        mAutomaticBrightnessStrategy
-                .adjustAutomaticBrightnessStateIfValid(invalidBrightness);
-        assertFalse(mAutomaticBrightnessStrategy.hasAppliedAutoBrightness());
-        assertEquals(autoBrightnessAdjustment,
-                mAutomaticBrightnessStrategy.getAutoBrightnessAdjustment(), 0.0f);
-        assertEquals(0,
-                mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags());
-    }
-
-    @Test
-    public void updatePendingAutoBrightnessAdjustments() {
-        // Verify the state when the pendingAutoBrightnessAdjustments are not present
-        setPendingAutoBrightnessAdjustment(Float.NaN);
-        assertFalse(mAutomaticBrightnessStrategy.processPendingAutoBrightnessAdjustments());
-        assertFalse(mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged());
-        // Verify the state when the pendingAutoBrightnessAdjustments are present, but
-        // pendingAutoBrightnessAdjustments and autoBrightnessAdjustments are the same
-        float autoBrightnessAdjustment = 0.3f;
-        setPendingAutoBrightnessAdjustment(autoBrightnessAdjustment);
-        setAutoBrightnessAdjustment(autoBrightnessAdjustment);
-        assertFalse(mAutomaticBrightnessStrategy.processPendingAutoBrightnessAdjustments());
-        assertFalse(mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged());
-        assertEquals(Float.NaN, mAutomaticBrightnessStrategy.getPendingAutoBrightnessAdjustment(),
-                0.0f);
-        // Verify the state when the pendingAutoBrightnessAdjustments are present, and
-        // pendingAutoBrightnessAdjustments and autoBrightnessAdjustments are not the same
-        float pendingAutoBrightnessAdjustment = 0.2f;
-        setPendingAutoBrightnessAdjustment(pendingAutoBrightnessAdjustment);
-        setTemporaryAutoBrightnessAdjustment(0.1f);
-        assertTrue(mAutomaticBrightnessStrategy.processPendingAutoBrightnessAdjustments());
-        assertTrue(mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged());
-        assertEquals(pendingAutoBrightnessAdjustment,
-                mAutomaticBrightnessStrategy.getAutoBrightnessAdjustment(), 0.0f);
-        assertEquals(Float.NaN, mAutomaticBrightnessStrategy.getPendingAutoBrightnessAdjustment(),
-                0.0f);
-        assertEquals(Float.NaN, mAutomaticBrightnessStrategy.getTemporaryAutoBrightnessAdjustment(),
-                0.0f);
-    }
-
-    @Test
-    public void setAutomaticBrightnessWorksAsExpected() {
-        float automaticScreenBrightness = 0.3f;
-        AutomaticBrightnessController automaticBrightnessController = mock(
-                AutomaticBrightnessController.class);
-        when(automaticBrightnessController.getAutomaticScreenBrightness(any(BrightnessEvent.class)))
-                .thenReturn(automaticScreenBrightness);
-        mAutomaticBrightnessStrategy.setAutomaticBrightnessController(
-                automaticBrightnessController);
-        assertEquals(automaticScreenBrightness,
-                mAutomaticBrightnessStrategy.getAutomaticScreenBrightness(
-                        new BrightnessEvent(DISPLAY_ID)), 0.0f);
-    }
-
-    @Test
-    public void shouldUseAutoBrightness() {
-        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
-        assertTrue(mAutomaticBrightnessStrategy.shouldUseAutoBrightness());
-    }
-
-    @Test
-    public void setPendingAutoBrightnessAdjustments() throws Settings.SettingNotFoundException {
-        float pendingAutoBrightnessAdjustments = 0.3f;
-        setPendingAutoBrightnessAdjustment(pendingAutoBrightnessAdjustments);
-        assertEquals(pendingAutoBrightnessAdjustments,
-                mAutomaticBrightnessStrategy.getPendingAutoBrightnessAdjustment(), 0.0f);
-        assertEquals(pendingAutoBrightnessAdjustments, Settings.System.getFloatForUser(
-                mContext.getContentResolver(),
-                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ,
-                UserHandle.USER_CURRENT), 0.0f);
-    }
-
-    @Test
-    public void setTemporaryAutoBrightnessAdjustment() {
-        float temporaryAutoBrightnessAdjustment = 0.3f;
-        mAutomaticBrightnessStrategy.setTemporaryAutoBrightnessAdjustment(
-                temporaryAutoBrightnessAdjustment);
-        assertEquals(temporaryAutoBrightnessAdjustment,
-                mAutomaticBrightnessStrategy.getTemporaryAutoBrightnessAdjustment(), 0.0f);
-    }
-
-    @Test
-    public void setAutoBrightnessApplied() {
-        mAutomaticBrightnessStrategy.setAutoBrightnessApplied(true);
-        assertTrue(mAutomaticBrightnessStrategy.hasAppliedAutoBrightness());
-    }
-
-    @Test
-    public void testVerifyNoAutoBrightnessAdjustmentsArePopulatedForNonDefaultDisplay() {
-        int newDisplayId = 1;
-        mAutomaticBrightnessStrategy = new AutomaticBrightnessStrategy(mContext, newDisplayId);
-        mAutomaticBrightnessStrategy.putAutoBrightnessAdjustmentSetting(0.3f);
-        assertEquals(0.5f, mAutomaticBrightnessStrategy.getAutoBrightnessAdjustment(),
-                0.0f);
-    }
-
-    private void setPendingAutoBrightnessAdjustment(float pendingAutoBrightnessAdjustment) {
-        Settings.System.putFloat(mContext.getContentResolver(),
-                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingAutoBrightnessAdjustment);
-        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(false);
-    }
-
-    private void setTemporaryAutoBrightnessAdjustment(float temporaryAutoBrightnessAdjustment) {
-        mAutomaticBrightnessStrategy.setTemporaryAutoBrightnessAdjustment(
-                temporaryAutoBrightnessAdjustment);
-    }
-
-    private void setAutoBrightnessAdjustment(float autoBrightnessAdjustment) {
-        mAutomaticBrightnessStrategy.putAutoBrightnessAdjustmentSetting(autoBrightnessAdjustment);
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java b/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java
deleted file mode 100644
index 618ab1b..0000000
--- a/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java
+++ /dev/null
@@ -1,1406 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display.color;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.annotation.NonNull;
-import android.app.ActivityManager;
-import android.app.AlarmManager;
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.content.res.Resources;
-import android.hardware.display.ColorDisplayManager;
-import android.hardware.display.DisplayManagerInternal;
-import android.hardware.display.Time;
-import android.os.Handler;
-import android.os.UserHandle;
-import android.provider.Settings;
-import android.provider.Settings.Secure;
-import android.provider.Settings.System;
-import android.test.mock.MockContentResolver;
-import android.view.Display;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.R;
-import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.server.LocalServices;
-import com.android.server.SystemService;
-import com.android.server.twilight.TwilightListener;
-import com.android.server.twilight.TwilightManager;
-import com.android.server.twilight.TwilightState;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mockito;
-
-import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.time.ZoneId;
-import java.util.Calendar;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public class ColorDisplayServiceTest {
-
-    private Context mContext;
-    private int mUserId;
-
-    private MockTwilightManager mTwilightManager;
-    private DisplayTransformManager mDisplayTransformManager;
-    private DisplayManagerInternal mDisplayManagerInternal;
-
-    private ColorDisplayService mCds;
-    private ColorDisplayService.BinderService mBinderService;
-
-    private Resources mResourcesSpy;
-
-    private static final int[] MINIMAL_COLOR_MODES = new int[] {
-        ColorDisplayManager.COLOR_MODE_NATURAL,
-        ColorDisplayManager.COLOR_MODE_BOOSTED,
-    };
-
-    @Before
-    public void setUp() {
-        mContext = Mockito.spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
-        doReturn(mContext).when(mContext).getApplicationContext();
-
-        final Resources res = Mockito.spy(mContext.getResources());
-        doReturn(MINIMAL_COLOR_MODES).when(res).getIntArray(R.array.config_availableColorModes);
-        doReturn(true).when(res).getBoolean(R.bool.config_nightDisplayAvailable);
-        doReturn(true).when(res).getBoolean(R.bool.config_displayWhiteBalanceAvailable);
-        when(mContext.getResources()).thenReturn(res);
-        mResourcesSpy = res;
-
-        mUserId = ActivityManager.getCurrentUser();
-
-        final MockContentResolver cr = new MockContentResolver(mContext);
-        cr.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
-        doReturn(cr).when(mContext).getContentResolver();
-
-        final AlarmManager am = Mockito.mock(AlarmManager.class);
-        doReturn(am).when(mContext).getSystemService(Context.ALARM_SERVICE);
-
-        mTwilightManager = new MockTwilightManager();
-        LocalServices.addService(TwilightManager.class, mTwilightManager);
-
-        mDisplayTransformManager = Mockito.mock(DisplayTransformManager.class);
-        doReturn(true).when(mDisplayTransformManager).needsLinearColorMatrix();
-        LocalServices.addService(DisplayTransformManager.class, mDisplayTransformManager);
-
-        mDisplayManagerInternal = Mockito.mock(DisplayManagerInternal.class);
-        LocalServices.removeServiceForTest(DisplayManagerInternal.class);
-        LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternal);
-
-        mCds = new ColorDisplayService(mContext);
-        mBinderService = mCds.new BinderService();
-        LocalServices.addService(ColorDisplayService.ColorDisplayServiceInternal.class,
-                mCds.new ColorDisplayServiceInternal());
-    }
-
-    @After
-    public void tearDown() {
-        /*
-         * Wait for internal {@link Handler} to finish processing pending messages, so that test
-         * code can safelyremove {@link DisplayTransformManager} mock from {@link LocalServices}.
-         */
-        mCds.mHandler.runWithScissors(() -> { /* nop */ }, /* timeout */ 1000);
-        mCds = null;
-
-        LocalServices.removeServiceForTest(TwilightManager.class);
-        mTwilightManager = null;
-
-        LocalServices.removeServiceForTest(DisplayTransformManager.class);
-
-        mUserId = UserHandle.USER_NULL;
-        mContext = null;
-
-        FakeSettingsProvider.clearSettingsProvider();
-
-        LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class);
-        LocalServices.removeServiceForTest(DisplayManagerInternal.class);
-    }
-
-    @Test
-    public void customSchedule_whenStartedAfterNight_ifOffAfterNight_turnsOff() {
-        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
-        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedAfterNight_ifOffBeforeNight_turnsOff() {
-        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
-        setNightDisplayActivated(false /* activated */, -180 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedAfterNight_ifOffDuringNight_turnsOff() {
-        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
-        setNightDisplayActivated(false /* activated */, -90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedAfterNight_ifOffInFuture_turnsOff() {
-        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
-        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedAfterNight_ifOnAfterNight_turnsOn() {
-        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
-        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedAfterNight_ifOnBeforeNight_turnsOff() {
-        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
-        setNightDisplayActivated(true /* activated */, -180 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedAfterNight_ifOnDuringNight_turnsOff() {
-        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
-        setNightDisplayActivated(true /* activated */, -90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedAfterNight_ifOnInFuture_turnsOff() {
-        setAutoModeCustom(-120 /* startTimeOffset */, -60 /* endTimeOffset */);
-        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedBeforeNight_ifOffAfterNight_turnsOff() {
-        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
-        setNightDisplayActivated(false /* activated */, 180 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedBeforeNight_ifOffBeforeNight_turnsOff() {
-        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
-        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedBeforeNight_ifOffDuringNight_turnsOff() {
-        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
-        setNightDisplayActivated(false /* activated */, 90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedBeforeNight_ifOffInPast_turnsOff() {
-        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
-        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedBeforeNight_ifOnAfterNight_turnsOff() {
-        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
-        setNightDisplayActivated(true /* activated */, 180 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedBeforeNight_ifOnBeforeNight_turnsOff() {
-        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
-        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedBeforeNight_ifOnDuringNight_turnsOff() {
-        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
-        setNightDisplayActivated(true /* activated */, 90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedBeforeNight_ifOnInPast_turnsOn() {
-        setAutoModeCustom(60 /* startTimeOffset */, 120 /* endTimeOffset */);
-        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedDuringNight_ifOffAfterNight_turnsOn() {
-        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
-        setNightDisplayActivated(false /* activated */, 90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedDuringNight_ifOffBeforeNight_turnsOn() {
-        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
-        setNightDisplayActivated(false /* activated */, -90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedDuringNight_ifOffDuringNightInFuture_turnsOn() {
-        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
-        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedDuringNight_ifOffDuringNightInPast_turnsOff() {
-        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
-        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedDuringNight_ifOnAfterNight_turnsOn() {
-        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
-        setNightDisplayActivated(true /* activated */, 90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedDuringNight_ifOnBeforeNight_turnsOn() {
-        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
-        setNightDisplayActivated(true /* activated */, -90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedDuringNight_ifOnDuringNightInFuture_turnsOn() {
-        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
-        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void customSchedule_whenStartedDuringNight_ifOnDuringNightInPast_turnsOn() {
-        setAutoModeCustom(-60 /* startTimeOffset */, 60 /* endTimeOffset */);
-        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedAfterNight_ifOffAfterNight_turnsOff() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedAfterNight_ifOffBeforeNight_turnsOff() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, -180 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedAfterNight_ifOffDuringNight_turnsOff() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, -90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedAfterNight_ifOffInFuture_turnsOff() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedAfterNight_ifOnAfterNight_turnsOn() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedAfterNight_ifOnBeforeNight_turnsOff() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, -180 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedAfterNight_ifOnDuringNight_turnsOff() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, -90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedAfterNight_ifOnInFuture_turnsOff() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedBeforeNight_ifOffAfterNight_turnsOff() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, 180 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedBeforeNight_ifOffBeforeNight_turnsOff() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedBeforeNight_ifOffDuringNight_turnsOff() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, 90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedBeforeNight_ifOffInPast_turnsOff() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedBeforeNight_ifOnAfterNight_turnsOff() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, 180 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedBeforeNight_ifOnBeforeNight_turnsOff() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedBeforeNight_ifOnDuringNight_turnsOff() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, 90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedBeforeNight_ifOnInPast_turnsOn() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedDuringNight_ifOffAfterNight_turnsOn() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, 90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedDuringNight_ifOffBeforeNight_turnsOn() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, -90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedDuringNight_ifOffDuringNightInFuture_turnsOn() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedDuringNight_ifOffDuringNightInPast_turnsOff() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedDuringNight_ifOnAfterNight_turnsOn() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, 90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedDuringNight_ifOnBeforeNight_turnsOn() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, -90 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedDuringNight_ifOnDuringNightInFuture_turnsOn() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenStartedDuringNight_ifOnDuringNightInPast_turnsOn() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        startService();
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedAfterNight_ifOffAfterNight_turnsOff() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(false /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedAfterNight_ifOffBeforeNight_turnsOff() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, -180 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(false /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedAfterNight_ifOffDuringNight_turnsOff() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, -90 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(false /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedAfterNight_ifOffInFuture_turnsOff() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(false /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedAfterNight_ifOnAfterNight_turnsOn() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(true /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedAfterNight_ifOnBeforeNight_turnsOff() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, -180 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(true /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedAfterNight_ifOnDuringNight_turnsOff() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, -90 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(true /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedAfterNight_ifOnInFuture_turnsOff() {
-        setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(true /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedBeforeNight_ifOffAfterNight_turnsOff() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, 180 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(false /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedBeforeNight_ifOffBeforeNight_turnsOff() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(false /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedBeforeNight_ifOffDuringNight_turnsOff() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, 90 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(false /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedBeforeNight_ifOffInPast_turnsOff() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(false /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedBeforeNight_ifOnAfterNight_turnsOff() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, 180 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(true /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedBeforeNight_ifOnBeforeNight_turnsOff() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(true /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedBeforeNight_ifOnDuringNight_turnsOff() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, 90 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(true /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedBeforeNight_ifOnInPast_turnsOn() {
-        setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(true /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedDuringNight_ifOffAfterNight_turnsOn() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, 90 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(false /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedDuringNight_ifOffBeforeNight_turnsOn() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, -90 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(false /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedDuringNight_ifOffDuringNightInFuture_turnsOn() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(false /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedDuringNight_ifOffDuringNightInPast_turnsOff() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(false /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(false /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedDuringNight_ifOnAfterNight_turnsOn() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, 90 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(true /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedDuringNight_ifOnBeforeNight_turnsOn() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, -90 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(true /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedDuringNight_ifOnDuringNightInFuture_turnsOn() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, 30 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(true /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void twilightSchedule_whenRebootedDuringNight_ifOnDuringNightInPast_turnsOn() {
-        setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */);
-        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        final TwilightState state = mTwilightManager.getLastTwilightState();
-        mTwilightManager.setTwilightState(null);
-
-        startService();
-        assertActivated(true /* activated */);
-
-        mTwilightManager.setTwilightState(state);
-        assertActivated(true /* activated */);
-    }
-
-    @Test
-    public void accessibility_colorInversion_transformActivated() {
-        if (!mContext.getResources().getConfiguration().isScreenWideColorGamut()) {
-            return;
-        }
-
-        setAccessibilityColorInversion(true);
-        setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
-
-        startService();
-        assertUserColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
-        assertActiveColorMode(mContext.getResources().getInteger(
-                R.integer.config_accessibilityColorMode));
-    }
-
-    @Test
-    public void accessibility_colorCorrection_transformActivated() {
-        if (!mContext.getResources().getConfiguration().isScreenWideColorGamut()) {
-            return;
-        }
-
-        setAccessibilityColorCorrection(true);
-        setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
-
-        startService();
-        assertUserColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
-        assertActiveColorMode(mContext.getResources().getInteger(
-                R.integer.config_accessibilityColorMode));
-    }
-
-    @Test
-    public void accessibility_all_transformActivated() {
-        if (!mContext.getResources().getConfiguration().isScreenWideColorGamut()) {
-            return;
-        }
-
-        setAccessibilityColorCorrection(true);
-        setAccessibilityColorInversion(true);
-        setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
-
-        startService();
-        assertUserColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
-        assertActiveColorMode(mContext.getResources().getInteger(
-                R.integer.config_accessibilityColorMode));
-    }
-
-    @Test
-    public void accessibility_none_transformActivated() {
-        if (!mContext.getResources().getConfiguration().isScreenWideColorGamut()) {
-            return;
-        }
-
-        setAccessibilityColorCorrection(false);
-        setAccessibilityColorInversion(false);
-        setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
-
-        startService();
-        assertUserColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
-        assertActiveColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
-    }
-
-    @Test
-    public void displayWhiteBalance_enabled() {
-        setDisplayWhiteBalanceEnabled(true);
-        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
-        mBinderService.setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
-        startService();
-        assertDwbActive(true);
-    }
-
-    @Test
-    public void displayWhiteBalance_disabledAfterNightDisplayEnabled() {
-        setDisplayWhiteBalanceEnabled(true);
-        startService();
-        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        /* Since we are using FakeSettingsProvider which could not trigger observer change,
-         * force an update here.*/
-        mCds.updateDisplayWhiteBalanceStatus();
-        assertDwbActive(false);
-    }
-
-    @Test
-    public void displayWhiteBalance_enabledAfterNightDisplayDisabled() {
-        setDisplayWhiteBalanceEnabled(true);
-        startService();
-        setNightDisplayActivated(true /* activated */, -30 /* lastActivatedTimeOffset */);
-
-        mCds.updateDisplayWhiteBalanceStatus();
-        assertDwbActive(false);
-
-        setNightDisplayActivated(false /* activated */, -30 /* lastActivatedTimeOffset */);
-        mCds.updateDisplayWhiteBalanceStatus();
-        assertDwbActive(true);
-    }
-
-    @Test
-    public void displayWhiteBalance_enabledAfterLinearColorModeSelected() {
-        if (!isColorModeValid(ColorDisplayManager.COLOR_MODE_SATURATED)) {
-            return;
-        }
-        setDisplayWhiteBalanceEnabled(true);
-        mBinderService.setColorMode(ColorDisplayManager.COLOR_MODE_SATURATED);
-        startService();
-        assertDwbActive(false);
-
-        mBinderService.setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
-        mCds.updateDisplayWhiteBalanceStatus();
-        assertDwbActive(true);
-    }
-
-    @Test
-    public void displayWhiteBalance_disabledWhileAccessibilityColorCorrectionEnabled() {
-        setDisplayWhiteBalanceEnabled(true);
-        setAccessibilityColorCorrection(true);
-        startService();
-        assertDwbActive(false);
-
-        setAccessibilityColorCorrection(false);
-        mCds.updateDisplayWhiteBalanceStatus();
-        assertDwbActive(true);
-    }
-
-    @Test
-    public void displayWhiteBalance_disabledWhileAccessibilityColorInversionEnabled() {
-        setDisplayWhiteBalanceEnabled(true);
-        setAccessibilityColorInversion(true);
-        startService();
-        assertDwbActive(false);
-
-        setAccessibilityColorInversion(false);
-        mCds.updateDisplayWhiteBalanceStatus();
-        assertDwbActive(true);
-    }
-
-    @Test
-    public void compositionColorSpaces_noResources() {
-        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorModes))
-            .thenReturn(new int[] {});
-        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorSpaces))
-            .thenReturn(new int[] {});
-        setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
-        startService();
-        verify(mDisplayTransformManager).setColorMode(
-                eq(ColorDisplayManager.COLOR_MODE_NATURAL), any(), eq(Display.COLOR_MODE_INVALID));
-    }
-
-    @Test
-    public void compositionColorSpaces_invalidResources() {
-        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorModes))
-            .thenReturn(new int[] {
-               ColorDisplayManager.COLOR_MODE_NATURAL,
-               // Missing second color mode
-            });
-        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorSpaces))
-            .thenReturn(new int[] {
-               Display.COLOR_MODE_SRGB,
-               Display.COLOR_MODE_DISPLAY_P3
-            });
-        setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
-        startService();
-        verify(mDisplayTransformManager).setColorMode(
-                eq(ColorDisplayManager.COLOR_MODE_NATURAL), any(), eq(Display.COLOR_MODE_INVALID));
-    }
-
-    @Test
-    public void compositionColorSpaces_validResources_validColorMode() {
-        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorModes))
-            .thenReturn(new int[] {
-               ColorDisplayManager.COLOR_MODE_NATURAL
-            });
-        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorSpaces))
-            .thenReturn(new int[] {
-               Display.COLOR_MODE_SRGB,
-            });
-        setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL);
-        startService();
-        verify(mDisplayTransformManager).setColorMode(
-                eq(ColorDisplayManager.COLOR_MODE_NATURAL), any(), eq(Display.COLOR_MODE_SRGB));
-    }
-
-    @Test
-    public void compositionColorSpaces_validResources_invalidColorMode() {
-        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorModes))
-            .thenReturn(new int[] {
-               ColorDisplayManager.COLOR_MODE_NATURAL
-            });
-        when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorSpaces))
-            .thenReturn(new int[] {
-               Display.COLOR_MODE_SRGB,
-            });
-        setColorMode(ColorDisplayManager.COLOR_MODE_BOOSTED);
-        startService();
-        verify(mDisplayTransformManager).setColorMode(
-                eq(ColorDisplayManager.COLOR_MODE_BOOSTED), any(), eq(Display.COLOR_MODE_INVALID));
-    }
-
-    @Test
-    public void getColorMode_noAvailableModes_returnsNotSet() {
-        when(mResourcesSpy.getIntArray(R.array.config_availableColorModes))
-                .thenReturn(new int[] {});
-        startService();
-        verify(mDisplayTransformManager, never()).setColorMode(anyInt(), any(), anyInt());
-        assertThat(mBinderService.getColorMode()).isEqualTo(-1);
-    }
-
-    /**
-     * Configures Night display to use a custom schedule.
-     *
-     * @param startTimeOffset the offset relative to now to activate Night display (in minutes)
-     * @param endTimeOffset the offset relative to now to deactivate Night display (in minutes)
-     */
-    private void setAutoModeCustom(int startTimeOffset, int endTimeOffset) {
-        mBinderService.setNightDisplayAutoMode(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME);
-        mBinderService.setNightDisplayCustomStartTime(
-                new Time(getLocalTimeRelativeToNow(startTimeOffset)));
-        mBinderService
-                .setNightDisplayCustomEndTime(new Time(getLocalTimeRelativeToNow(endTimeOffset)));
-    }
-
-    /**
-     * Configures Night display to use the twilight schedule.
-     *
-     * @param sunsetOffset the offset relative to now for sunset (in minutes)
-     * @param sunriseOffset the offset relative to now for sunrise (in minutes)
-     */
-    private void setAutoModeTwilight(int sunsetOffset, int sunriseOffset) {
-        mBinderService.setNightDisplayAutoMode(ColorDisplayManager.AUTO_MODE_TWILIGHT);
-        mTwilightManager.setTwilightState(
-                getTwilightStateRelativeToNow(sunsetOffset, sunriseOffset));
-    }
-
-    /**
-     * Configures the Night display activated state.
-     *
-     * @param activated {@code true} if Night display should be activated
-     * @param lastActivatedTimeOffset the offset relative to now to record that Night display was
-     * activated (in minutes)
-     */
-    private void setNightDisplayActivated(boolean activated, int lastActivatedTimeOffset) {
-        mBinderService.setNightDisplayActivated(activated);
-        Secure.putStringForUser(mContext.getContentResolver(),
-                Secure.NIGHT_DISPLAY_LAST_ACTIVATED_TIME,
-                LocalDateTime.now().plusMinutes(lastActivatedTimeOffset).toString(),
-                mUserId);
-    }
-
-    /**
-     * Configures the Accessibility color correction setting state.
-     *
-     * @param state {@code true} if color inversion should be activated
-     */
-    private void setAccessibilityColorCorrection(boolean state) {
-        Secure.putIntForUser(mContext.getContentResolver(),
-                Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, state ? 1 : 0, mUserId);
-    }
-
-    /**
-     * Configures the Accessibility color inversion setting state.
-     *
-     * @param state {@code true} if color inversion should be activated
-     */
-    private void setAccessibilityColorInversion(boolean state) {
-        Secure.putIntForUser(mContext.getContentResolver(),
-                Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, state ? 1 : 0, mUserId);
-    }
-
-    /**
-     * Configures the Display White Balance setting state.
-     *
-     * @param enabled {@code true} if display white balance should be enabled
-     */
-    private void setDisplayWhiteBalanceEnabled(boolean enabled) {
-        Secure.putIntForUser(mContext.getContentResolver(),
-                Secure.DISPLAY_WHITE_BALANCE_ENABLED, enabled ? 1 : 0, mUserId);
-    }
-
-    /**
-     * Configures color mode.
-     */
-    private void setColorMode(int colorMode) {
-        mBinderService.setColorMode(colorMode);
-    }
-
-    /**
-     * Returns whether the color mode is valid on the device the tests are running on.
-     */
-    private boolean isColorModeValid(int mode) {
-        final int[] availableColorModes = mContext.getResources().getIntArray(
-                R.array.config_availableColorModes);
-        if (availableColorModes != null) {
-            for (int availableMode : availableColorModes) {
-                if (mode == availableMode) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Convenience method to start {@link #mCds}.
-     */
-    private void startService() {
-        Secure.putIntForUser(mContext.getContentResolver(), Secure.USER_SETUP_COMPLETE, 1, mUserId);
-
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            mCds.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
-            mCds.onUserChanged(mUserId);
-        });
-    }
-
-    /**
-     * Convenience method for asserting whether Night display should be activated.
-     *
-     * @param activated the expected activated state of Night display
-     */
-    private void assertActivated(boolean activated) {
-        assertWithMessage("Incorrect Night display activated state")
-                .that(mBinderService.isNightDisplayActivated())
-                .isEqualTo(activated);
-    }
-
-    /**
-     * Convenience method for asserting that the active color mode matches expectation.
-     *
-     * @param mode the expected active color mode.
-     */
-    private void assertActiveColorMode(int mode) {
-        assertWithMessage("Unexpected color mode setting")
-                .that(mBinderService.getColorMode())
-                .isEqualTo(mode);
-    }
-
-    /**
-     * Convenience method for asserting that the user chosen color mode matches expectation.
-     *
-     * @param mode the expected color mode setting.
-     */
-    private void assertUserColorMode(int mode) {
-        final int actualMode = System.getIntForUser(mContext.getContentResolver(),
-                System.DISPLAY_COLOR_MODE, -1, mUserId);
-        assertWithMessage("Unexpected color mode setting")
-                .that(actualMode)
-                .isEqualTo(mode);
-    }
-
-    /**
-     * Convenience method for asserting that the DWB active status matches expectation.
-     *
-     * @param enabled the expected active status.
-     */
-    private void assertDwbActive(boolean enabled) {
-        assertWithMessage("Incorrect Display White Balance state")
-                .that(mCds.mDisplayWhiteBalanceTintController.isActivated())
-                .isEqualTo(enabled);
-    }
-
-    /**
-     * Convenience for making a {@link LocalTime} instance with an offset relative to now.
-     *
-     * @param offsetMinutes the offset relative to now (in minutes)
-     * @return the LocalTime instance
-     */
-    private static LocalTime getLocalTimeRelativeToNow(int offsetMinutes) {
-        final Calendar c = Calendar.getInstance();
-        c.add(Calendar.MINUTE, offsetMinutes);
-        return LocalTime.of(c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE));
-    }
-
-    /**
-     * Convenience for making a {@link TwilightState} instance with sunrise/sunset relative to now.
-     *
-     * @param sunsetOffset the offset relative to now for sunset (in minutes)
-     * @param sunriseOffset the offset relative to now for sunrise (in minutes)
-     * @return the TwilightState instance
-     */
-    private static TwilightState getTwilightStateRelativeToNow(int sunsetOffset,
-            int sunriseOffset) {
-        final LocalTime sunset = getLocalTimeRelativeToNow(sunsetOffset);
-        final LocalTime sunrise = getLocalTimeRelativeToNow(sunriseOffset);
-
-        final LocalDateTime now = LocalDateTime.now();
-        final ZoneId zoneId = ZoneId.systemDefault();
-
-        long sunsetMillis = ColorDisplayService.getDateTimeBefore(sunset, now)
-                .atZone(zoneId)
-                .toInstant()
-                .toEpochMilli();
-        long sunriseMillis = ColorDisplayService.getDateTimeBefore(sunrise, now)
-                .atZone(zoneId)
-                .toInstant()
-                .toEpochMilli();
-        if (sunsetMillis < sunriseMillis) {
-            sunsetMillis = ColorDisplayService.getDateTimeAfter(sunset, now)
-                    .atZone(zoneId)
-                    .toInstant()
-                    .toEpochMilli();
-        } else {
-            sunriseMillis = ColorDisplayService.getDateTimeAfter(sunrise, now)
-                    .atZone(zoneId)
-                    .toInstant()
-                    .toEpochMilli();
-        }
-
-        return new TwilightState(sunriseMillis, sunsetMillis);
-    }
-
-    private static class MockTwilightManager implements TwilightManager {
-
-        private final Map<TwilightListener, Handler> mListeners = new HashMap<>();
-        private TwilightState mTwilightState;
-
-        /**
-         * Updates the TwilightState and notifies any registered listeners.
-         *
-         * @param state the new TwilightState to use
-         */
-        void setTwilightState(TwilightState state) {
-            synchronized (mListeners) {
-                mTwilightState = state;
-
-                final CountDownLatch latch = new CountDownLatch(mListeners.size());
-                for (Map.Entry<TwilightListener, Handler> entry : mListeners.entrySet()) {
-                    entry.getValue().post(new Runnable() {
-                        @Override
-                        public void run() {
-                            entry.getKey().onTwilightStateChanged(state);
-                            latch.countDown();
-                        }
-                    });
-                }
-
-                try {
-                    latch.await(5, TimeUnit.SECONDS);
-                } catch (InterruptedException e) {
-                    throw new RuntimeException(e);
-                }
-            }
-        }
-
-        @Override
-        public void registerListener(@NonNull TwilightListener listener, @NonNull Handler handler) {
-            synchronized (mListeners) {
-                mListeners.put(listener, handler);
-            }
-        }
-
-        @Override
-        public void unregisterListener(@NonNull TwilightListener listener) {
-            synchronized (mListeners) {
-                mListeners.remove(listener);
-            }
-        }
-
-        @Override
-        public TwilightState getLastTwilightState() {
-            return mTwilightState;
-        }
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/display/whitebalance/AmbientLuxTest.java b/services/tests/servicestests/src/com/android/server/display/whitebalance/AmbientLuxTest.java
deleted file mode 100644
index ac97911..0000000
--- a/services/tests/servicestests/src/com/android/server/display/whitebalance/AmbientLuxTest.java
+++ /dev/null
@@ -1,559 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.whitebalance;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyLong;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doAnswer;
-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.content.ContextWrapper;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.hardware.Sensor;
-import android.hardware.SensorManager;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.TypedValue;
-
-import androidx.test.InstrumentationRegistry;
-
-import com.android.internal.R;
-import com.android.server.LocalServices;
-import com.android.server.display.TestUtils;
-import com.android.server.display.color.ColorDisplayService;
-import com.android.server.display.utils.AmbientFilter;
-import com.android.server.display.utils.AmbientFilterStubber;
-
-import com.google.common.collect.ImmutableList;
-
-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;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import java.util.List;
-
-@RunWith(JUnit4.class)
-public final class AmbientLuxTest {
-
-    private static final float ALLOWED_ERROR_DELTA = 0.001f;
-    private static final int AMBIENT_COLOR_TYPE = 20705;
-    private static final String AMBIENT_COLOR_TYPE_STR = "colorSensoryDensoryDoc";
-    private static final float LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE = 5432.1f;
-    private static final float HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE = 3456.7f;
-
-    private Handler mHandler = new Handler(Looper.getMainLooper());
-    private Sensor mLightSensor;
-    private Sensor mAmbientColorSensor;
-    private ContextWrapper mContextSpy;
-    private Resources mResourcesSpy;
-
-    @Mock private SensorManager mSensorManagerMock;
-
-    @Mock private TypedArray mBrightnesses;
-    @Mock private TypedArray mBiases;
-    @Mock private TypedArray mHighLightBrightnesses;
-    @Mock private TypedArray mHighLightBiases;
-    @Mock private TypedArray mAmbientColorTemperatures;
-    @Mock private TypedArray mDisplayColorTemperatures;
-    @Mock private TypedArray mStrongAmbientColorTemperatures;
-    @Mock private TypedArray mStrongDisplayColorTemperatures;
-    @Mock private ColorDisplayService.ColorDisplayServiceInternal mColorDisplayServiceInternalMock;
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        mLightSensor = TestUtils.createSensor(Sensor.TYPE_LIGHT, null);
-        mAmbientColorSensor = TestUtils.createSensor(AMBIENT_COLOR_TYPE, AMBIENT_COLOR_TYPE_STR);
-        mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
-        mResourcesSpy = spy(mContextSpy.getResources());
-        when(mContextSpy.getResources()).thenReturn(mResourcesSpy);
-        when(mSensorManagerMock.getDefaultSensor(Sensor.TYPE_LIGHT)).thenReturn(mLightSensor);
-        final List<Sensor> sensorList = ImmutableList.of(mLightSensor, mAmbientColorSensor);
-        when(mSensorManagerMock.getSensorList(Sensor.TYPE_ALL)).thenReturn(sensorList);
-        when(mResourcesSpy.getString(
-                R.string.config_displayWhiteBalanceColorTemperatureSensorName))
-                .thenReturn(AMBIENT_COLOR_TYPE_STR);
-        when(mResourcesSpy.getInteger(
-                R.integer.config_displayWhiteBalanceDecreaseDebounce))
-                .thenReturn(0);
-        when(mResourcesSpy.getInteger(
-                R.integer.config_displayWhiteBalanceIncreaseDebounce))
-                .thenReturn(0);
-        mockResourcesFloat(R.dimen.config_displayWhiteBalanceLowLightAmbientColorTemperature,
-                LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE);
-        mockResourcesFloat(R.dimen.config_displayWhiteBalanceHighLightAmbientColorTemperature,
-                HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE);
-        when(mResourcesSpy.obtainTypedArray(
-                R.array.config_displayWhiteBalanceAmbientColorTemperatures))
-                .thenReturn(mAmbientColorTemperatures);
-        when(mResourcesSpy.obtainTypedArray(
-                R.array.config_displayWhiteBalanceDisplayColorTemperatures))
-                .thenReturn(mDisplayColorTemperatures);
-        when(mResourcesSpy.obtainTypedArray(
-                R.array.config_displayWhiteBalanceStrongAmbientColorTemperatures))
-                .thenReturn(mStrongAmbientColorTemperatures);
-        when(mResourcesSpy.obtainTypedArray(
-                R.array.config_displayWhiteBalanceStrongDisplayColorTemperatures))
-                .thenReturn(mStrongDisplayColorTemperatures);
-
-        when(mResourcesSpy.obtainTypedArray(
-                R.array.config_displayWhiteBalanceLowLightAmbientBrightnesses))
-                .thenReturn(mBrightnesses);
-        when(mResourcesSpy.obtainTypedArray(
-                R.array.config_displayWhiteBalanceLowLightAmbientBiases))
-                .thenReturn(mBiases);
-        when(mResourcesSpy.obtainTypedArray(
-                R.array.config_displayWhiteBalanceHighLightAmbientBrightnesses))
-                .thenReturn(mHighLightBrightnesses);
-        when(mResourcesSpy.obtainTypedArray(
-                R.array.config_displayWhiteBalanceHighLightAmbientBiases))
-                .thenReturn(mHighLightBiases);
-        mockThrottler();
-        LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class);
-        LocalServices.addService(ColorDisplayService.ColorDisplayServiceInternal.class,
-                mColorDisplayServiceInternalMock);
-    }
-
-    @Test
-    public void testCalculateAdjustedBrightnessNits() {
-        doReturn(0.9f).when(mColorDisplayServiceInternalMock).getDisplayWhiteBalanceLuminance();
-        DisplayWhiteBalanceController controller =
-                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
-        final float adjustedNits = controller.calculateAdjustedBrightnessNits(500f);
-        assertEquals(/* expected= */ 550f, adjustedNits, /* delta= */ 0.001);
-    }
-
-    @Test
-    public void testNoSpline() throws Exception {
-        setBrightnesses();
-        setBiases();
-
-        DisplayWhiteBalanceController controller =
-                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
-        final float ambientColorTemperature = 8000.0f;
-        setEstimatedColorTemperature(controller, ambientColorTemperature);
-        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
-
-        for (float luxOverride = 0.1f; luxOverride <= 10000; luxOverride *= 10) {
-            setEstimatedBrightnessAndUpdate(controller, luxOverride);
-            assertEquals(controller.mPendingAmbientColorTemperature,
-                    ambientColorTemperature, 0.001);
-        }
-    }
-
-    @Test
-    public void testSpline_OneSegment() throws Exception {
-        final float lowerBrightness = 10.0f;
-        final float upperBrightness = 50.0f;
-        setBrightnesses(lowerBrightness, upperBrightness);
-        setBiases(0.0f, 1.0f);
-
-        DisplayWhiteBalanceController controller =
-                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
-        final float ambientColorTemperature = 8000.0f;
-        setEstimatedColorTemperature(controller, ambientColorTemperature);
-        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
-
-        for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
-            setEstimatedBrightnessAndUpdate(controller,
-                    mix(lowerBrightness, upperBrightness, t));
-            assertEquals(controller.mPendingAmbientColorTemperature,
-                    mix(LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE, ambientColorTemperature, t), 0.001);
-        }
-
-        setEstimatedBrightnessAndUpdate(controller, 0.0f);
-        assertEquals(controller.mPendingAmbientColorTemperature,
-                LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE, 0.001);
-
-        setEstimatedBrightnessAndUpdate(controller, upperBrightness + 1.0f);
-        assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature, 0.001);
-    }
-
-    @Test
-    public void testSpline_TwoSegments() throws Exception {
-        final float brightness0 = 10.0f;
-        final float brightness1 = 50.0f;
-        final float brightness2 = 60.0f;
-        setBrightnesses(brightness0, brightness1, brightness2);
-        final float bias0 = 0.0f;
-        final float bias1 = 0.25f;
-        final float bias2 = 1.0f;
-        setBiases(bias0, bias1, bias2);
-
-        DisplayWhiteBalanceController controller =
-                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
-        final float ambientColorTemperature = 8000.0f;
-        setEstimatedColorTemperature(controller, ambientColorTemperature);
-        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
-
-        for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
-            float luxOverride = mix(brightness0, brightness1, t);
-            setEstimatedBrightnessAndUpdate(controller, luxOverride);
-            float bias = mix(bias0, bias1, t);
-            assertEquals(controller.mPendingAmbientColorTemperature,
-                    mix(LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE, ambientColorTemperature, bias), 0.001);
-        }
-
-        for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
-            float luxOverride = mix(brightness1, brightness2, t);
-            setEstimatedBrightnessAndUpdate(controller, luxOverride);
-            float bias = mix(bias1, bias2, t);
-            assertEquals(controller.mPendingAmbientColorTemperature,
-                    mix(LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE, ambientColorTemperature, bias), 0.001);
-        }
-
-        setEstimatedBrightnessAndUpdate(controller, 0.0f);
-        assertEquals(controller.mPendingAmbientColorTemperature,
-                LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE, 0.001);
-
-        setEstimatedBrightnessAndUpdate(controller, brightness2 + 1.0f);
-        assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature, 0.001);
-    }
-
-    @Test
-    public void testSpline_VerticalSegment() throws Exception {
-        final float lowerBrightness = 10.0f;
-        final float upperBrightness = 10.0f;
-        setBrightnesses(lowerBrightness, upperBrightness);
-        setBiases(0.0f, 1.0f);
-
-        DisplayWhiteBalanceController controller =
-                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
-        final float ambientColorTemperature = 8000.0f;
-        setEstimatedColorTemperature(controller, ambientColorTemperature);
-        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
-
-        setEstimatedBrightnessAndUpdate(controller, 0.0f);
-        assertEquals(controller.mPendingAmbientColorTemperature,
-                LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE, 0.001);
-
-        setEstimatedBrightnessAndUpdate(controller, upperBrightness + 1.0f);
-        assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature, 0.001);
-    }
-
-    @Test
-    public void testSpline_InvalidEndBias() throws Exception {
-        setBrightnesses(10.0f, 1000.0f);
-        setBiases(0.0f, 2.0f);
-
-        DisplayWhiteBalanceController controller =
-                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
-        final float ambientColorTemperature = 8000.0f;
-        setEstimatedColorTemperature(controller, ambientColorTemperature);
-        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
-
-        for (float luxOverride = 0.1f; luxOverride <= 10000; luxOverride *= 10) {
-        setEstimatedBrightnessAndUpdate(controller, luxOverride);
-        assertEquals(controller.mPendingAmbientColorTemperature,
-                ambientColorTemperature, 0.001);
-        }
-    }
-
-    @Test
-    public void testSpline_InvalidBeginBias() throws Exception {
-        setBrightnesses(10.0f, 1000.0f);
-        setBiases(0.1f, 1.0f);
-
-        DisplayWhiteBalanceController controller =
-                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
-        final float ambientColorTemperature = 8000.0f;
-        setEstimatedColorTemperature(controller, ambientColorTemperature);
-        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
-
-        for (float luxOverride = 0.1f; luxOverride <= 10000; luxOverride *= 10) {
-        setEstimatedBrightnessAndUpdate(controller, luxOverride);
-        assertEquals(controller.mPendingAmbientColorTemperature,
-                ambientColorTemperature, 0.001);
-        }
-    }
-
-    @Test
-    public void testSpline_OneSegmentHighLight() throws Exception {
-        final float lowerBrightness = 10.0f;
-        final float upperBrightness = 50.0f;
-        setHighLightBrightnesses(lowerBrightness, upperBrightness);
-        setHighLightBiases(0.0f, 1.0f);
-
-        DisplayWhiteBalanceController controller =
-                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
-        final float ambientColorTemperature = 8000.0f;
-        setEstimatedColorTemperature(controller, ambientColorTemperature);
-        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
-
-        for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
-            setEstimatedBrightnessAndUpdate(controller,
-                    mix(lowerBrightness, upperBrightness, t));
-            assertEquals(controller.mPendingAmbientColorTemperature,
-                    mix(HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE, ambientColorTemperature, 1.0f - t),
-                    0.001);
-        }
-
-        setEstimatedBrightnessAndUpdate(controller, upperBrightness + 1.0f);
-        assertEquals(controller.mPendingAmbientColorTemperature,
-                HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE, 0.001);
-
-        setEstimatedBrightnessAndUpdate(controller, 0.0f);
-        assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature, 0.001);
-    }
-
-    @Test
-    public void testSpline_TwoSegmentsHighLight() throws Exception {
-        final float brightness0 = 10.0f;
-        final float brightness1 = 50.0f;
-        final float brightness2 = 60.0f;
-        setHighLightBrightnesses(brightness0, brightness1, brightness2);
-        final float bias0 = 0.0f;
-        final float bias1 = 0.25f;
-        final float bias2 = 1.0f;
-        setHighLightBiases(bias0, bias1, bias2);
-
-        DisplayWhiteBalanceController controller =
-                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
-        final float ambientColorTemperature = 6000.0f;
-        setEstimatedColorTemperature(controller, ambientColorTemperature);
-        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
-
-        for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
-            float luxOverride = mix(brightness0, brightness1, t);
-            setEstimatedBrightnessAndUpdate(controller, luxOverride);
-            float bias = mix(bias0, bias1, t);
-            assertEquals(controller.mPendingAmbientColorTemperature,
-                    mix(HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE, ambientColorTemperature, 1.0f - bias),
-                    0.01);
-        }
-
-        for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
-            float luxOverride = mix(brightness1, brightness2, t);
-            setEstimatedBrightnessAndUpdate(controller, luxOverride);
-            float bias = mix(bias1, bias2, t);
-            assertEquals(controller.mPendingAmbientColorTemperature,
-                    mix(HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE, ambientColorTemperature, 1.0f - bias),
-                    0.01);
-        }
-
-        setEstimatedBrightnessAndUpdate(controller, brightness2 + 1.0f);
-        assertEquals(controller.mPendingAmbientColorTemperature,
-                HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE, 0.001);
-
-        setEstimatedBrightnessAndUpdate(controller, 0.0f);
-        assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature, 0.001);
-    }
-
-    @Test
-    public void testSpline_InvalidCombinations() throws Exception {
-            setBrightnesses(100.0f, 200.0f);
-            setBiases(0.0f, 1.0f);
-            setHighLightBrightnesses(150.0f, 250.0f);
-            setHighLightBiases(0.0f, 1.0f);
-
-            DisplayWhiteBalanceController controller =
-                    DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
-            final float ambientColorTemperature = 8000.0f;
-            setEstimatedColorTemperature(controller, ambientColorTemperature);
-            controller.mBrightnessFilter = spy(new AmbientFilterStubber());
-
-            for (float luxOverride = 0.1f; luxOverride <= 10000; luxOverride *= 10) {
-                setEstimatedBrightnessAndUpdate(controller, luxOverride);
-                assertEquals(controller.mPendingAmbientColorTemperature,
-                        ambientColorTemperature, 0.001);
-            }
-    }
-
-    @Test
-    public void testStrongMode() {
-        final float lowerBrightness = 10.0f;
-        final float upperBrightness = 50.0f;
-        setBrightnesses(lowerBrightness, upperBrightness);
-        setBiases(0.0f, 1.0f);
-        final int ambientColorTempLow = 6000;
-        final int ambientColorTempHigh = 8000;
-        final int displayColorTempLow = 6400;
-        final int displayColorTempHigh = 7400;
-        setStrongAmbientColorTemperatures(ambientColorTempLow, ambientColorTempHigh);
-        setStrongDisplayColorTemperatures(displayColorTempLow, displayColorTempHigh);
-
-        DisplayWhiteBalanceController controller =
-                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
-        controller.setStrongModeEnabled(true);
-        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
-
-        for (float ambientTempFraction = 0.0f; ambientTempFraction <= 1.0f;
-                ambientTempFraction += 0.1f) {
-            final float ambientTemp =
-                    (ambientColorTempHigh - ambientColorTempLow) * ambientTempFraction
-                            + ambientColorTempLow;
-            setEstimatedColorTemperature(controller, ambientTemp);
-            for (float brightnessFraction = 0.0f; brightnessFraction <= 1.0f;
-                    brightnessFraction += 0.1f) {
-                setEstimatedBrightnessAndUpdate(controller,
-                        mix(lowerBrightness, upperBrightness, brightnessFraction));
-                assertEquals(controller.mPendingAmbientColorTemperature,
-                        mix(LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE,
-                                mix(displayColorTempLow, displayColorTempHigh, ambientTempFraction),
-                                brightnessFraction),
-                        ALLOWED_ERROR_DELTA);
-            }
-        }
-    }
-
-    @Test
-    public void testLowLight_DefaultAmbient() throws Exception {
-        final float lowerBrightness = 10.0f;
-        final float upperBrightness = 50.0f;
-        setBrightnesses(lowerBrightness, upperBrightness);
-        setBiases(0.0f, 1.0f);
-
-        DisplayWhiteBalanceController controller =
-                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
-        final float ambientColorTemperature = -1.0f;
-        setEstimatedColorTemperature(controller, ambientColorTemperature);
-        controller.mBrightnessFilter = spy(new AmbientFilterStubber());
-
-        for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
-            setEstimatedBrightnessAndUpdate(controller,
-                    mix(lowerBrightness, upperBrightness, t));
-            assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature,
-                        0.001);
-        }
-
-        setEstimatedBrightnessAndUpdate(controller, 0.0f);
-        assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature, 0.001);
-
-        setEstimatedBrightnessAndUpdate(controller, upperBrightness + 1.0f);
-        assertEquals(controller.mPendingAmbientColorTemperature, ambientColorTemperature, 0.001);
-    }
-
-    @Test
-    public void testWhiteBalance_updateWithEmptyFilter() throws Exception {
-        setAmbientColorTemperatures(5300.0f, 6000.0f, 7000.0f, 8000.0f);
-        setDisplayColorTemperatures(6300.0f, 6400.0f, 6850.0f, 7450.0f);
-        DisplayWhiteBalanceController controller =
-                DisplayWhiteBalanceFactory.create(mHandler, mSensorManagerMock, mResourcesSpy);
-        controller.updateAmbientColorTemperature();
-        assertEquals(-1.0f, controller.mPendingAmbientColorTemperature, 0);
-    }
-
-    void mockThrottler() {
-        when(mResourcesSpy.getInteger(
-                R.integer.config_displayWhiteBalanceDecreaseDebounce)).thenReturn(0);
-        when(mResourcesSpy.getInteger(
-                R.integer.config_displayWhiteBalanceIncreaseDebounce)).thenReturn(0);
-        TypedArray base = mResourcesSpy.obtainTypedArray(
-                R.array.config_displayWhiteBalanceBaseThresholds);
-        TypedArray inc = mResourcesSpy.obtainTypedArray(
-                R.array.config_displayWhiteBalanceIncreaseThresholds);
-        TypedArray dec = mResourcesSpy.obtainTypedArray(
-                R.array.config_displayWhiteBalanceDecreaseThresholds);
-        base = spy(base);
-        inc = spy(inc);
-        dec = spy(dec);
-        when(mResourcesSpy.obtainTypedArray(
-                R.array.config_displayWhiteBalanceBaseThresholds)).thenReturn(base);
-        when(mResourcesSpy.obtainTypedArray(
-                R.array.config_displayWhiteBalanceIncreaseThresholds)).thenReturn(inc);
-        when(mResourcesSpy.obtainTypedArray(
-                R.array.config_displayWhiteBalanceDecreaseThresholds)).thenReturn(dec);
-        setFloatArrayResource(base, new float[]{0.0f});
-        setFloatArrayResource(inc, new float[]{0.0f});
-        setFloatArrayResource(dec, new float[]{0.0f});
-    }
-
-    private void mockResourcesFloat(int id, float floatValue) {
-        doAnswer(new Answer<Void>() {
-            public Void answer(InvocationOnMock invocation) {
-                TypedValue value = (TypedValue)invocation.getArgument(1);
-                value.type = TypedValue.TYPE_FLOAT;
-                value.data = Float.floatToIntBits(floatValue);
-                return null;
-            }
-        }).when(mResourcesSpy).getValue(
-                eq(id),
-                any(TypedValue.class), eq(true));
-    }
-
-    private void setEstimatedColorTemperature(DisplayWhiteBalanceController controller,
-                                              float ambientColorTemperature) {
-        AmbientFilter colorTemperatureFilter = spy(new AmbientFilterStubber());
-        controller.mColorTemperatureFilter = colorTemperatureFilter;
-        when(colorTemperatureFilter.getEstimate(anyLong())).thenReturn(ambientColorTemperature);
-    }
-
-    private void setEstimatedBrightnessAndUpdate(DisplayWhiteBalanceController controller,
-                                                 float brightness) {
-        when(controller.mBrightnessFilter.getEstimate(anyLong())).thenReturn(brightness);
-        controller.updateAmbientColorTemperature();
-    }
-
-    private void setBrightnesses(float... vals) {
-        setFloatArrayResource(mBrightnesses, vals);
-    }
-
-    private void setBiases(float... vals) {
-        setFloatArrayResource(mBiases, vals);
-    }
-
-    private void setHighLightBrightnesses(float... vals) {
-        setFloatArrayResource(mHighLightBrightnesses, vals);
-    }
-
-    private void setHighLightBiases(float... vals) {
-        setFloatArrayResource(mHighLightBiases, vals);
-    }
-
-    private void setAmbientColorTemperatures(float... vals) {
-        setFloatArrayResource(mAmbientColorTemperatures, vals);
-    }
-
-    private void setDisplayColorTemperatures(float... vals) {
-        setFloatArrayResource(mDisplayColorTemperatures, vals);
-    }
-
-    private void setStrongAmbientColorTemperatures(float... vals) {
-        setFloatArrayResource(mStrongAmbientColorTemperatures, vals);
-    }
-
-    private void setStrongDisplayColorTemperatures(float... vals) {
-        setFloatArrayResource(mStrongDisplayColorTemperatures, vals);
-    }
-
-    private void setFloatArrayResource(TypedArray array, float[] vals) {
-        when(array.length()).thenReturn(vals.length);
-        for (int i = 0; i < vals.length; i++) {
-            when(array.getFloat(i, Float.NaN)).thenReturn(vals[i]);
-        }
-    }
-
-    private TypedArray createTypedArray() throws Exception {
-        TypedArray mockArray = mock(TypedArray.class);
-        return mockArray;
-    }
-
-    private static float mix(float a, float b, float t) {
-        return (1.0f - t) * a + t * b;
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/input/AmbientKeyboardBacklightControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/AmbientKeyboardBacklightControllerTests.kt
new file mode 100644
index 0000000..98b4628
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/input/AmbientKeyboardBacklightControllerTests.kt
@@ -0,0 +1,355 @@
+/*
+ * 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.
+ */
+
+package com.android.server.input
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.res.Resources
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.hardware.SensorManager
+import android.hardware.display.DisplayManagerInternal
+import android.hardware.input.InputSensorInfo
+import android.os.Handler
+import android.os.test.TestLooper
+import android.platform.test.annotations.Presubmit
+import android.util.TypedValue
+import android.view.Display
+import android.view.DisplayInfo
+import androidx.test.core.app.ApplicationProvider
+import com.android.internal.R
+import com.android.server.LocalServices
+import com.android.server.input.AmbientKeyboardBacklightController.HYSTERESIS_THRESHOLD
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertThrows
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+
+/**
+ * Tests for {@link AmbientKeyboardBacklightController}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:AmbientKeyboardBacklightControllerTests
+ */
+@Presubmit
+class AmbientKeyboardBacklightControllerTests {
+
+    companion object {
+        const val DEFAULT_DISPLAY_UNIQUE_ID = "uniqueId_1"
+        const val SENSOR_NAME = "test_sensor_name"
+        const val SENSOR_TYPE = "test_sensor_type"
+    }
+
+    @get:Rule
+    val rule = MockitoJUnit.rule()!!
+
+    private lateinit var context: Context
+    private lateinit var testLooper: TestLooper
+    private lateinit var ambientController: AmbientKeyboardBacklightController
+
+    @Mock
+    private lateinit var resources: Resources
+
+    @Mock
+    private lateinit var lightSensorInfo: InputSensorInfo
+
+    @Mock
+    private lateinit var sensorManager: SensorManager
+
+    @Mock
+    private lateinit var displayManagerInternal: DisplayManagerInternal
+    private lateinit var lightSensor: Sensor
+
+    private var currentDisplayInfo = DisplayInfo()
+    private var lastBrightnessCallback: Int = 0
+    private var listenerRegistered: Boolean = false
+    private var listenerRegistrationCount: Int = 0
+
+    @Before
+    fun setup() {
+        context = spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+        `when`(context.resources).thenReturn(resources)
+        setupBrightnessSteps()
+        setupSensor()
+        testLooper = TestLooper()
+        ambientController = AmbientKeyboardBacklightController(context, testLooper.looper)
+        ambientController.systemRunning()
+        testLooper.dispatchAll()
+    }
+
+    private fun setupBrightnessSteps() {
+        val brightnessValues = intArrayOf(100, 200, 0)
+        val decreaseThresholds = intArrayOf(-1, 900, 1900)
+        val increaseThresholds = intArrayOf(1000, 2000, -1)
+        `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightBrightnessValues))
+            .thenReturn(brightnessValues)
+        `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightDecreaseLuxThreshold))
+            .thenReturn(decreaseThresholds)
+        `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightIncreaseLuxThreshold))
+            .thenReturn(increaseThresholds)
+        `when`(
+            resources.getValue(
+                eq(R.dimen.config_autoKeyboardBrightnessSmoothingConstant),
+                any(TypedValue::class.java),
+                anyBoolean()
+            )
+        ).then {
+            val args = it.arguments
+            val outValue = args[1] as TypedValue
+            outValue.data = java.lang.Float.floatToRawIntBits(1.0f)
+            Unit
+        }
+    }
+
+    private fun setupSensor() {
+        LocalServices.removeServiceForTest(DisplayManagerInternal::class.java)
+        LocalServices.addService(DisplayManagerInternal::class.java, displayManagerInternal)
+        currentDisplayInfo.uniqueId = DEFAULT_DISPLAY_UNIQUE_ID
+        `when`(displayManagerInternal.getDisplayInfo(Display.DEFAULT_DISPLAY)).thenReturn(
+            currentDisplayInfo
+        )
+        val sensorData = DisplayManagerInternal.AmbientLightSensorData(SENSOR_NAME, SENSOR_TYPE)
+        `when`(displayManagerInternal.getAmbientLightSensorData(Display.DEFAULT_DISPLAY))
+            .thenReturn(sensorData)
+
+        `when`(lightSensorInfo.name).thenReturn(SENSOR_NAME)
+        `when`(lightSensorInfo.stringType).thenReturn(SENSOR_TYPE)
+        lightSensor = Sensor(lightSensorInfo)
+        `when`(context.getSystemService(eq(Context.SENSOR_SERVICE))).thenReturn(sensorManager)
+        `when`(sensorManager.getSensorList(anyInt())).thenReturn(listOf(lightSensor))
+        `when`(
+            sensorManager.registerListener(
+                any(),
+                eq(lightSensor),
+                anyInt(),
+                any(Handler::class.java)
+            )
+        ).then {
+            listenerRegistered = true
+            listenerRegistrationCount++
+            true
+        }
+        `when`(
+            sensorManager.unregisterListener(
+                any(SensorEventListener::class.java),
+                eq(lightSensor)
+            )
+        ).then {
+            listenerRegistered = false
+            Unit
+        }
+    }
+
+    private fun setupSensorWithInitialLux(luxValue: Float) {
+        ambientController.registerAmbientBacklightListener { brightnessValue: Int ->
+            lastBrightnessCallback = brightnessValue
+        }
+        sendAmbientLuxValue(luxValue)
+        testLooper.dispatchAll()
+    }
+
+    @Test
+    fun testInitialAmbientLux_sendsCallbackImmediately() {
+        setupSensorWithInitialLux(500F)
+
+        assertEquals(
+            "Should receive immediate callback for first lux change",
+            100,
+            lastBrightnessCallback
+        )
+    }
+
+    @Test
+    fun testBrightnessIncrease_afterInitialLuxChanges() {
+        setupSensorWithInitialLux(500F)
+
+        // Current state: Step 1 [value = 100, increaseThreshold = 1000, decreaseThreshold = -1]
+        repeat(HYSTERESIS_THRESHOLD) {
+            sendAmbientLuxValue(1500F)
+        }
+        testLooper.dispatchAll()
+
+        assertEquals(
+            "Should receive brightness change callback for increasing lux change",
+            200,
+            lastBrightnessCallback
+        )
+    }
+
+    @Test
+    fun testBrightnessDecrease_afterInitialLuxChanges() {
+        setupSensorWithInitialLux(1500F)
+
+        // Current state: Step 2 [value = 200, increaseThreshold = 2000, decreaseThreshold = 900]
+        repeat(HYSTERESIS_THRESHOLD) {
+            sendAmbientLuxValue(500F)
+        }
+        testLooper.dispatchAll()
+
+        assertEquals(
+            "Should receive brightness change callback for decreasing lux change",
+            100,
+            lastBrightnessCallback
+        )
+    }
+
+    @Test
+    fun testRegisterAmbientListener_throwsExceptionOnRegisteringDuplicate() {
+        val ambientListener =
+            AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { }
+        ambientController.registerAmbientBacklightListener(ambientListener)
+
+        assertThrows(IllegalStateException::class.java) {
+            ambientController.registerAmbientBacklightListener(
+                ambientListener
+            )
+        }
+    }
+
+    @Test
+    fun testUnregisterAmbientListener_throwsExceptionOnUnregisteringNonExistent() {
+        val ambientListener =
+            AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { }
+        assertThrows(IllegalStateException::class.java) {
+            ambientController.unregisterAmbientBacklightListener(
+                ambientListener
+            )
+        }
+    }
+
+    @Test
+    fun testSensorListenerRegistered_onRegisterUnregisterAmbientListener() {
+        assertEquals(
+            "Should not have a sensor listener registered at init",
+            0,
+            listenerRegistrationCount
+        )
+        assertFalse("Should not have a sensor listener registered at init", listenerRegistered)
+
+        val ambientListener1 =
+            AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { }
+        ambientController.registerAmbientBacklightListener(ambientListener1)
+        assertEquals(
+            "Should register a new sensor listener", 1, listenerRegistrationCount
+        )
+        assertTrue("Should have sensor listener registered", listenerRegistered)
+
+        val ambientListener2 =
+            AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { }
+        ambientController.registerAmbientBacklightListener(ambientListener2)
+        assertEquals(
+            "Should not register a new sensor listener when adding a second ambient listener",
+            1,
+            listenerRegistrationCount
+        )
+        assertTrue("Should have sensor listener registered", listenerRegistered)
+
+        ambientController.unregisterAmbientBacklightListener(ambientListener1)
+        assertTrue("Should have sensor listener registered", listenerRegistered)
+
+        ambientController.unregisterAmbientBacklightListener(ambientListener2)
+        assertFalse(
+            "Should not have sensor listener registered if there are no ambient listeners",
+            listenerRegistered
+        )
+    }
+
+    @Test
+    fun testDisplayChange_shouldNotReRegisterListener_ifUniqueIdSame() {
+        setupSensorWithInitialLux(0F)
+
+        val count = listenerRegistrationCount
+        ambientController.onDisplayChanged(Display.DEFAULT_DISPLAY)
+        testLooper.dispatchAll()
+
+        assertEquals(
+            "Should not re-register listener on display change if unique is same",
+            count,
+            listenerRegistrationCount
+        )
+    }
+
+    @Test
+    fun testDisplayChange_shouldReRegisterListener_ifUniqueIdChanges() {
+        setupSensorWithInitialLux(0F)
+
+        val count = listenerRegistrationCount
+        currentDisplayInfo.uniqueId = "xyz"
+        ambientController.onDisplayChanged(Display.DEFAULT_DISPLAY)
+        testLooper.dispatchAll()
+
+        assertEquals(
+            "Should re-register listener on display change if unique id changed",
+            count + 1,
+            listenerRegistrationCount
+        )
+    }
+
+    @Test
+    fun testBrightnessDoesntChange_betweenIncreaseAndDecreaseThresholds() {
+        setupSensorWithInitialLux(1001F)
+
+        // Previous state: Step 1 [value = 100, increaseThreshold = 1000, decreaseThreshold = -1]
+        // Current state: Step 2 [value = 200, increaseThreshold = 2000, decreaseThreshold = 900]
+        lastBrightnessCallback = -1
+        repeat(HYSTERESIS_THRESHOLD) {
+            sendAmbientLuxValue(999F)
+        }
+        testLooper.dispatchAll()
+
+        assertEquals(
+            "Should not receive any callback for brightness change",
+            -1,
+            lastBrightnessCallback
+        )
+    }
+
+    @Test
+    fun testBrightnessDoesntChange_onChangeOccurringLessThanHysteresisThreshold() {
+        setupSensorWithInitialLux(1001F)
+
+        // Previous state: Step 1 [value = 100, increaseThreshold = 1000, decreaseThreshold = -1]
+        // Current state: Step 2 [value = 200, increaseThreshold = 2000, decreaseThreshold = 900]
+        lastBrightnessCallback = -1
+        repeat(HYSTERESIS_THRESHOLD - 1) {
+            sendAmbientLuxValue(2001F)
+        }
+        testLooper.dispatchAll()
+
+        assertEquals(
+            "Should not receive any callback for brightness change",
+            -1,
+            lastBrightnessCallback
+        )
+    }
+
+    private fun sendAmbientLuxValue(luxValue: Float) {
+        ambientController.onSensorChanged(SensorEvent(lightSensor, 0, 0, floatArrayOf(luxValue)))
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
index 416b1f4..c36122b 100644
--- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
@@ -40,8 +40,7 @@
 import com.android.server.input.BatteryController.BluetoothBatteryManager
 import com.android.server.input.BatteryController.BluetoothBatteryManager.BluetoothBatteryListener
 import com.android.server.input.BatteryController.POLLING_PERIOD_MILLIS
-import com.android.server.input.BatteryController.UEventManager
-import com.android.server.input.BatteryController.UEventManager.UEventBatteryListener
+import com.android.server.input.BatteryController.UEventBatteryListener
 import com.android.server.input.BatteryController.USI_BATTERY_VALIDITY_DURATION_MILLIS
 import org.hamcrest.Description
 import org.hamcrest.Matcher
diff --git a/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt b/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt
index 677144c..72066ea 100644
--- a/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt
@@ -23,9 +23,12 @@
 import android.os.IInputConstants
 import android.os.test.TestLooper
 import android.platform.test.annotations.Presubmit
+import android.provider.Settings
+import android.test.mock.MockContentResolver
 import android.view.Display
 import android.view.PointerIcon
 import androidx.test.InstrumentationRegistry
+import com.android.internal.util.test.FakeSettingsProvider
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -33,6 +36,8 @@
 import org.junit.Rule
 import org.junit.Test
 import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyFloat
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
@@ -44,7 +49,9 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
 import org.mockito.junit.MockitoJUnit
+import org.mockito.stubbing.OngoingStubbing
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 
@@ -58,7 +65,10 @@
 class InputManagerServiceTests {
 
     @get:Rule
-    val rule = MockitoJUnit.rule()!!
+    val mockitoRule = MockitoJUnit.rule()!!
+
+    @get:Rule
+    val fakeSettingsProviderRule = FakeSettingsProvider.rule()!!
 
     @Mock
     private lateinit var native: NativeInputManagerService
@@ -66,17 +76,25 @@
     @Mock
     private lateinit var wmCallbacks: InputManagerService.WindowManagerCallbacks
 
+    @Mock
+    private lateinit var uEventManager: UEventManager
+
     private lateinit var service: InputManagerService
     private lateinit var localService: InputManagerInternal
     private lateinit var context: Context
     private lateinit var testLooper: TestLooper
+    private lateinit var contentResolver: MockContentResolver
 
     @Before
     fun setup() {
         context = spy(ContextWrapper(InstrumentationRegistry.getContext()))
+        contentResolver = MockContentResolver(context)
+        contentResolver.addProvider(Settings.AUTHORITY, FakeSettingsProvider())
+        whenever(context.contentResolver).thenReturn(contentResolver)
         testLooper = TestLooper()
         service =
-            InputManagerService(object : InputManagerService.Injector(context, testLooper.looper) {
+            InputManagerService(object : InputManagerService.Injector(
+                    context, testLooper.looper, uEventManager) {
                 override fun getNativeService(
                     service: InputManagerService?
                 ): NativeInputManagerService {
@@ -92,9 +110,36 @@
     }
 
     @Test
+    fun testStart() {
+        verifyZeroInteractions(native)
+
+        service.start()
+        verify(native).start()
+    }
+
+    @Test
+    fun testInputSettingsUpdatedOnSystemRunning() {
+        verifyZeroInteractions(native)
+
+        service.systemRunning()
+
+        verify(native).setPointerSpeed(anyInt())
+        verify(native).setTouchpadPointerSpeed(anyInt())
+        verify(native).setTouchpadNaturalScrollingEnabled(anyBoolean())
+        verify(native).setTouchpadTapToClickEnabled(anyBoolean())
+        verify(native).setTouchpadRightClickZoneEnabled(anyBoolean())
+        verify(native).setShowTouches(anyBoolean())
+        verify(native).reloadPointerIcons()
+        verify(native).notifyKeyGestureTimeoutsChanged()
+        verify(native).setMotionClassifierEnabled(anyBoolean())
+        verify(native).setMaximumObscuringOpacityForTouch(anyFloat())
+        verify(native).setStylusPointerIconEnabled(anyBoolean())
+    }
+
+    @Test
     fun testPointerDisplayUpdatesWhenDisplayViewportsChanged() {
         val displayId = 123
-        `when`(wmCallbacks.pointerDisplayId).thenReturn(displayId)
+        whenever(wmCallbacks.pointerDisplayId).thenReturn(displayId)
         val viewports = listOf<DisplayViewport>()
         localService.setDisplayViewports(viewports)
         verify(native).setDisplayViewports(any(Array<DisplayViewport>::class.java))
@@ -337,3 +382,5 @@
         thread.join(100 /*millis*/)
     }
 }
+
+private fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall)
diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
index 64c05dc..3f4a4fb 100644
--- a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
@@ -16,6 +16,7 @@
 
 package com.android.server.input
 
+import android.animation.ValueAnimator
 import android.content.Context
 import android.content.ContextWrapper
 import android.graphics.Color
@@ -29,11 +30,15 @@
 import android.os.test.TestLooper
 import android.platform.test.annotations.Presubmit
 import android.view.InputDevice
+import androidx.test.annotation.UiThreadTest
 import androidx.test.core.app.ApplicationProvider
-import com.android.server.input.KeyboardBacklightController.BRIGHTNESS_VALUE_FOR_LEVEL
+import com.android.server.input.KeyboardBacklightController.DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL
+import com.android.server.input.KeyboardBacklightController.MAX_BRIGHTNESS_CHANGE_STEPS
 import com.android.server.input.KeyboardBacklightController.USER_INACTIVITY_THRESHOLD_MILLIS
 import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
@@ -63,12 +68,20 @@
         .build()
 
 private fun createLight(lightId: Int, lightType: Int): Light =
+    createLight(
+        lightId,
+        lightType,
+        null
+    )
+
+private fun createLight(lightId: Int, lightType: Int, suggestedBrightnessLevels: IntArray?): Light =
     Light(
         lightId,
         "Light $lightId",
         1,
         lightType,
-        Light.LIGHT_CAPABILITY_BRIGHTNESS
+        Light.LIGHT_CAPABILITY_BRIGHTNESS,
+        suggestedBrightnessLevels
     )
 /**
  * Tests for {@link KeyboardBacklightController}.
@@ -92,6 +105,8 @@
     private lateinit var iInputManager: IInputManager
     @Mock
     private lateinit var native: NativeInputManagerService
+    @Mock
+    private lateinit var uEventManager: UEventManager
     private lateinit var keyboardBacklightController: KeyboardBacklightController
     private lateinit var context: Context
     private lateinit var dataStore: PersistentDataStore
@@ -99,6 +114,7 @@
     private var lightColorMap: HashMap<Int, Int> = HashMap()
     private var lastBacklightState: KeyboardBacklightState? = null
     private var sysfsNodeChanges = 0
+    private var lastAnimationValues = IntArray(2)
 
     @Before
     fun setup() {
@@ -115,8 +131,8 @@
             override fun finishWrite(fos: FileOutputStream?, success: Boolean) {}
         })
         testLooper = TestLooper()
-        keyboardBacklightController =
-            KeyboardBacklightController(context, native, dataStore, testLooper.looper)
+        keyboardBacklightController = KeyboardBacklightController(context, native, dataStore,
+                testLooper.looper, FakeAnimatorFactory(), uEventManager)
         InputManagerGlobal.resetInstance(iInputManager)
         val inputManager = InputManager(context)
         `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager)
@@ -125,6 +141,10 @@
             val args = it.arguments
             lightColorMap.put(args[1] as Int, args[2] as Int)
         }
+        `when`(native.getLightColor(anyInt(), anyInt())).thenAnswer {
+            val args = it.arguments
+            lightColorMap.getOrDefault(args[1] as Int, 0)
+        }
         lightColorMap.clear()
         `when`(native.sysfsNodeChanged(any())).then {
             sysfsNodeChanges++
@@ -138,271 +158,257 @@
 
     @Test
     fun testKeyboardBacklightIncrementDecrement() {
-        val keyboardWithBacklight = createKeyboard(DEVICE_ID)
-        val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
-        `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
-        `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
-        keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = false,
+            ambientControlEnabled = false
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
 
-        for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) {
-            incrementKeyboardBacklight(DEVICE_ID)
-            assertEquals(
-                "Light value for level $level mismatched",
-                Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
-                lightColorMap[LIGHT_ID]
-            )
-            assertEquals(
-                "Light value for level $level must be correctly stored in the datastore",
-                BRIGHTNESS_VALUE_FOR_LEVEL[level],
-                dataStore.getKeyboardBacklightBrightness(
-                    keyboardWithBacklight.descriptor,
-                    LIGHT_ID
-                ).asInt
-            )
+            assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight,
+                    DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL)
         }
-
-        // Increment above max level
-        incrementKeyboardBacklight(DEVICE_ID)
-        assertEquals(
-            "Light value for max level mismatched",
-            Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
-            lightColorMap[LIGHT_ID]
-        )
-        assertEquals(
-            "Light value for max level must be correctly stored in the datastore",
-            MAX_BRIGHTNESS,
-            dataStore.getKeyboardBacklightBrightness(
-                keyboardWithBacklight.descriptor,
-                LIGHT_ID
-            ).asInt
-        )
-
-        for (level in BRIGHTNESS_VALUE_FOR_LEVEL.size - 2 downTo 0) {
-            decrementKeyboardBacklight(DEVICE_ID)
-            assertEquals(
-                "Light value for level $level mismatched",
-                Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
-                lightColorMap[LIGHT_ID]
-            )
-            assertEquals(
-                "Light value for level $level must be correctly stored in the datastore",
-                BRIGHTNESS_VALUE_FOR_LEVEL[level],
-                dataStore.getKeyboardBacklightBrightness(
-                    keyboardWithBacklight.descriptor,
-                    LIGHT_ID
-                ).asInt
-            )
-        }
-
-        // Decrement below min level
-        decrementKeyboardBacklight(DEVICE_ID)
-        assertEquals(
-            "Light value for min level mismatched",
-            Color.argb(0, 0, 0, 0),
-            lightColorMap[LIGHT_ID]
-        )
-        assertEquals(
-            "Light value for min level must be correctly stored in the datastore",
-            0,
-            dataStore.getKeyboardBacklightBrightness(
-                keyboardWithBacklight.descriptor,
-                LIGHT_ID
-            ).asInt
-        )
     }
 
     @Test
     fun testKeyboardWithoutBacklight() {
-        val keyboardWithoutBacklight = createKeyboard(DEVICE_ID)
-        val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT)
-        `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight)
-        `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight))
-        keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = false,
+            ambientControlEnabled = false
+        ).use {
+            val keyboardWithoutBacklight = createKeyboard(DEVICE_ID)
+            val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight))
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
 
-        incrementKeyboardBacklight(DEVICE_ID)
-        assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty())
+            incrementKeyboardBacklight(DEVICE_ID)
+            assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty())
+        }
     }
 
     @Test
     fun testKeyboardWithMultipleLight() {
-        val keyboardWithBacklight = createKeyboard(DEVICE_ID)
-        val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
-        val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT)
-        `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
-        `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(
-            listOf(
-                keyboardBacklight,
-                keyboardInputLight
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = false,
+            ambientControlEnabled = false
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+            val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(
+                listOf(
+                    keyboardBacklight,
+                    keyboardInputLight
+                )
             )
-        )
-        keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
 
-        incrementKeyboardBacklight(DEVICE_ID)
-        assertEquals("Only keyboard backlights should change", 1, lightColorMap.size)
-        assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID])
-        assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID])
+            incrementKeyboardBacklight(DEVICE_ID)
+            assertEquals("Only keyboard backlights should change", 1, lightColorMap.size)
+            assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID])
+            assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID])
+        }
     }
 
     @Test
     fun testRestoreBacklightOnInputDeviceAdded() {
-        val keyboardWithBacklight = createKeyboard(DEVICE_ID)
-        val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
-        `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
-        `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = false,
+            ambientControlEnabled = false
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
 
-        for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) {
-            dataStore.setKeyboardBacklightBrightness(
+            for (level in 1 until DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size) {
+                dataStore.setKeyboardBacklightBrightness(
                     keyboardWithBacklight.descriptor,
                     LIGHT_ID,
-                    BRIGHTNESS_VALUE_FOR_LEVEL[level] - 1
+                    DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[level] - 1
+                )
+
+                keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+                keyboardBacklightController.notifyUserActivity()
+                testLooper.dispatchNext()
+                assertEquals(
+                    "Keyboard backlight level should be restored to the level saved in the " +
+                            "data store",
+                    Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
+                    lightColorMap[LIGHT_ID]
+                )
+                keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID)
+            }
+        }
+    }
+
+    @Test
+    fun testRestoreBacklightOnInputDeviceChanged() {
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = false,
+            ambientControlEnabled = false
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            dataStore.setKeyboardBacklightBrightness(
+                keyboardWithBacklight.descriptor,
+                LIGHT_ID,
+                MAX_BRIGHTNESS
+            )
+
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+            keyboardBacklightController.notifyUserActivity()
+            testLooper.dispatchNext()
+            assertTrue(
+                "Keyboard backlight should not be changed until its added",
+                lightColorMap.isEmpty()
+            )
+
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+            keyboardBacklightController.onInputDeviceChanged(DEVICE_ID)
+            keyboardBacklightController.notifyUserActivity()
+            testLooper.dispatchNext()
+            assertEquals(
+                "Keyboard backlight level should be restored to the level saved in the data store",
+                Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
+                lightColorMap[LIGHT_ID]
+            )
+        }
+    }
+
+    @Test
+    fun testKeyboardBacklight_registerUnregisterListener() {
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = false,
+            ambientControlEnabled = false
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+            val maxLevel = DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size - 1
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+
+            // Register backlight listener
+            val listener = KeyboardBacklightListener()
+            keyboardBacklightController.registerKeyboardBacklightListener(listener, 0)
+
+            lastBacklightState = null
+            keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
+            testLooper.dispatchNext()
+
+            assertEquals(
+                "Backlight state device Id should be $DEVICE_ID",
+                DEVICE_ID,
+                lastBacklightState!!.deviceId
+            )
+            assertEquals(
+                "Backlight state brightnessLevel should be 1",
+                1,
+                lastBacklightState!!.brightnessLevel
+            )
+            assertEquals(
+                "Backlight state maxBrightnessLevel should be $maxLevel",
+                maxLevel,
+                lastBacklightState!!.maxBrightnessLevel
+            )
+            assertEquals(
+                "Backlight state isTriggeredByKeyPress should be true",
+                true,
+                lastBacklightState!!.isTriggeredByKeyPress
+            )
+
+            // Unregister listener
+            keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0)
+
+            lastBacklightState = null
+            incrementKeyboardBacklight(DEVICE_ID)
+
+            assertNull("Listener should not receive any updates", lastBacklightState)
+        }
+    }
+
+    @Test
+    fun testKeyboardBacklight_userActivity() {
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = false,
+            ambientControlEnabled = false
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+            dataStore.setKeyboardBacklightBrightness(
+                keyboardWithBacklight.descriptor,
+                LIGHT_ID,
+                MAX_BRIGHTNESS
             )
 
             keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
             keyboardBacklightController.notifyUserActivity()
             testLooper.dispatchNext()
             assertEquals(
-                    "Keyboard backlight level should be restored to the level saved in the data " +
-                            "store",
-                    Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
-                    lightColorMap[LIGHT_ID]
+                "Keyboard backlight level should be restored to the level saved in the data store",
+                Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
+                lightColorMap[LIGHT_ID]
             )
-            keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID)
+
+            testLooper.moveTimeForward(USER_INACTIVITY_THRESHOLD_MILLIS + 1000)
+            testLooper.dispatchNext()
+            assertEquals(
+                "Keyboard backlight level should be turned off after inactivity",
+                0,
+                lightColorMap[LIGHT_ID]
+            )
         }
     }
 
     @Test
-    fun testRestoreBacklightOnInputDeviceChanged() {
-        val keyboardWithBacklight = createKeyboard(DEVICE_ID)
-        val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
-        `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
-        dataStore.setKeyboardBacklightBrightness(
-            keyboardWithBacklight.descriptor,
-            LIGHT_ID,
-            MAX_BRIGHTNESS
-        )
-
-        keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
-        keyboardBacklightController.notifyUserActivity()
-        testLooper.dispatchNext()
-        assertTrue(
-            "Keyboard backlight should not be changed until its added",
-            lightColorMap.isEmpty()
-        )
-
-        `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
-        keyboardBacklightController.onInputDeviceChanged(DEVICE_ID)
-        keyboardBacklightController.notifyUserActivity()
-        testLooper.dispatchNext()
-        assertEquals(
-            "Keyboard backlight level should be restored to the level saved in the data store",
-            Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
-            lightColorMap[LIGHT_ID]
-        )
-    }
-
-    @Test
-    fun testKeyboardBacklight_registerUnregisterListener() {
-        val keyboardWithBacklight = createKeyboard(DEVICE_ID)
-        val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
-        `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
-        `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
-        keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
-
-        // Register backlight listener
-        val listener = KeyboardBacklightListener()
-        keyboardBacklightController.registerKeyboardBacklightListener(listener, 0)
-
-        lastBacklightState = null
-        keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
-        testLooper.dispatchNext()
-
-        assertEquals(
-            "Backlight state device Id should be $DEVICE_ID",
-            DEVICE_ID,
-            lastBacklightState!!.deviceId
-        )
-        assertEquals(
-            "Backlight state brightnessLevel should be " + 1,
-            1,
-            lastBacklightState!!.brightnessLevel
-        )
-        assertEquals(
-            "Backlight state maxBrightnessLevel should be " + (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1),
-            (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1),
-            lastBacklightState!!.maxBrightnessLevel
-        )
-        assertEquals(
-            "Backlight state isTriggeredByKeyPress should be true",
-            true,
-            lastBacklightState!!.isTriggeredByKeyPress
-        )
-
-        // Unregister listener
-        keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0)
-
-        lastBacklightState = null
-        incrementKeyboardBacklight(DEVICE_ID)
-
-        assertNull("Listener should not receive any updates", lastBacklightState)
-    }
-
-    @Test
-    fun testKeyboardBacklight_userActivity() {
-        val keyboardWithBacklight = createKeyboard(DEVICE_ID)
-        val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
-        `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
-        `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
-        dataStore.setKeyboardBacklightBrightness(
-            keyboardWithBacklight.descriptor,
-            LIGHT_ID,
-            MAX_BRIGHTNESS
-        )
-
-        keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
-        keyboardBacklightController.notifyUserActivity()
-        testLooper.dispatchNext()
-        assertEquals(
-            "Keyboard backlight level should be restored to the level saved in the data store",
-            Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
-            lightColorMap[LIGHT_ID]
-        )
-
-        testLooper.moveTimeForward(USER_INACTIVITY_THRESHOLD_MILLIS + 1000)
-        testLooper.dispatchNext()
-        assertEquals(
-            "Keyboard backlight level should be turned off after inactivity",
-            0,
-            lightColorMap[LIGHT_ID]
-        )
-    }
-
-    @Test
     fun testKeyboardBacklight_displayOnOff() {
-        val keyboardWithBacklight = createKeyboard(DEVICE_ID)
-        val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
-        `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
-        `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
-        dataStore.setKeyboardBacklightBrightness(
-            keyboardWithBacklight.descriptor,
-            LIGHT_ID,
-            MAX_BRIGHTNESS
-        )
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = false,
+            ambientControlEnabled = false
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+            dataStore.setKeyboardBacklightBrightness(
+                keyboardWithBacklight.descriptor,
+                LIGHT_ID,
+                MAX_BRIGHTNESS
+            )
 
-        keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
-        keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */)
-        assertEquals(
-            "Keyboard backlight level should be restored to the level saved in the data " +
-                    "store when display turned on",
-            Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
-            lightColorMap[LIGHT_ID]
-        )
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+            keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */)
+            assertEquals(
+                "Keyboard backlight level should be restored to the level saved in the data " +
+                        "store when display turned on",
+                Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
+                lightColorMap[LIGHT_ID]
+            )
 
-        keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */)
-        assertEquals(
-            "Keyboard backlight level should be turned off after display is turned off",
-            0,
-            lightColorMap[LIGHT_ID]
-        )
+            keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */)
+            assertEquals(
+                "Keyboard backlight level should be turned off after display is turned off",
+                0,
+                lightColorMap[LIGHT_ID]
+            )
+        }
     }
 
     @Test
@@ -463,6 +469,316 @@
         )
     }
 
+    @Test
+    @UiThreadTest
+    fun testKeyboardBacklightAnimation_onChangeLevels() {
+        KeyboardBacklightFlags(
+            animationEnabled = true,
+            customLevelsEnabled = false,
+            ambientControlEnabled = false
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+
+            incrementKeyboardBacklight(DEVICE_ID)
+            assertEquals(
+                "Should start animation from level 0",
+                DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[0],
+                lastAnimationValues[0]
+            )
+            assertEquals(
+                "Should start animation to level 1",
+                DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1],
+                lastAnimationValues[1]
+            )
+        }
+    }
+
+    @Test
+    fun testKeyboardBacklightPreferredLevels() {
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = true,
+            ambientControlEnabled = false
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val suggestedLevels = intArrayOf(0, 22, 63, 135, 196, 255)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT,
+                    suggestedLevels)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+
+            assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight,
+                    suggestedLevels)
+        }
+    }
+
+    @Test
+    fun testKeyboardBacklightPreferredLevels_moreThanMax_shouldUseDefault() {
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = true,
+            ambientControlEnabled = false
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val suggestedLevels = IntArray(MAX_BRIGHTNESS_CHANGE_STEPS + 1) { 10 * (it + 1) }
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT,
+                    suggestedLevels)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+
+            assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight,
+                    DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL)
+        }
+    }
+
+    @Test
+    fun testKeyboardBacklightPreferredLevels_mustHaveZeroAndMaxBrightnessAsBounds() {
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = true,
+            ambientControlEnabled = false
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val suggestedLevels = intArrayOf(22, 63, 135, 196)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT,
+                    suggestedLevels)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+
+            // Framework will add the lowest and maximum levels if not provided via config
+            assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight,
+                    intArrayOf(0, 22, 63, 135, 196, 255))
+        }
+    }
+
+    @Test
+    fun testKeyboardBacklightPreferredLevels_dropsOutOfBoundsLevels() {
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = true,
+            ambientControlEnabled = false
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val suggestedLevels = intArrayOf(22, 63, 135, 400, 196, 1000)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT,
+                    suggestedLevels)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+
+            // Framework will drop out of bound levels in the config
+            assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight,
+                    intArrayOf(0, 22, 63, 135, 196, 255))
+        }
+    }
+
+    @Test
+    fun testAmbientBacklightControl_doesntRestoreBacklightLevel() {
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = false,
+            ambientControlEnabled = true
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+
+            dataStore.setKeyboardBacklightBrightness(
+                keyboardWithBacklight.descriptor,
+                LIGHT_ID,
+                DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1]
+            )
+
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+            keyboardBacklightController.notifyUserActivity()
+            testLooper.dispatchNext()
+            assertNull(
+                "Keyboard backlight level should not be restored to the saved level",
+                lightColorMap[LIGHT_ID]
+            )
+        }
+    }
+
+    @Test
+    fun testAmbientBacklightControl_doesntBackupBacklightLevel() {
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = false,
+            ambientControlEnabled = true
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+            incrementKeyboardBacklight(DEVICE_ID)
+            assertFalse(
+                "Light value should not be backed up if ambient control is enabled",
+                dataStore.getKeyboardBacklightBrightness(
+                    keyboardWithBacklight.descriptor, LIGHT_ID
+                ).isPresent
+            )
+        }
+    }
+
+    @Test
+    fun testAmbientBacklightControl_incrementLevel_afterAmbientChange() {
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = false,
+            ambientControlEnabled = true
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+            sendAmbientBacklightValue(1)
+            assertEquals(
+                "Light value should be changed to ambient provided value",
+                Color.argb(1, 0, 0, 0),
+                lightColorMap[LIGHT_ID]
+            )
+
+            incrementKeyboardBacklight(DEVICE_ID)
+
+            assertEquals(
+                "Light value for level after increment post Ambient change is mismatched",
+                Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], 0, 0, 0),
+                lightColorMap[LIGHT_ID]
+            )
+        }
+    }
+
+    @Test
+    fun testAmbientBacklightControl_decrementLevel_afterAmbientChange() {
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = false,
+            ambientControlEnabled = true
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+            sendAmbientBacklightValue(254)
+            assertEquals(
+                "Light value should be changed to ambient provided value",
+                Color.argb(254, 0, 0, 0),
+                lightColorMap[LIGHT_ID]
+            )
+
+            decrementKeyboardBacklight(DEVICE_ID)
+
+            val numLevels = DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size
+            assertEquals(
+                "Light value for level after decrement post Ambient change is mismatched",
+                Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[numLevels - 2], 0, 0, 0),
+                lightColorMap[LIGHT_ID]
+            )
+        }
+    }
+
+    @Test
+    fun testAmbientBacklightControl_ambientChanges_afterManualChange() {
+        KeyboardBacklightFlags(
+            animationEnabled = false,
+            customLevelsEnabled = false,
+            ambientControlEnabled = true
+        ).use {
+            val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+            val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+            `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+            `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+            keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+            incrementKeyboardBacklight(DEVICE_ID)
+            assertEquals(
+                "Light value should be changed to the first level",
+                Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], 0, 0, 0),
+                lightColorMap[LIGHT_ID]
+            )
+
+            sendAmbientBacklightValue(100)
+            assertNotEquals(
+                "Light value should not change based on ambient changes after manual changes",
+                Color.argb(100, 0, 0, 0),
+                lightColorMap[LIGHT_ID]
+            )
+        }
+    }
+
+    private fun assertIncrementDecrementForLevels(
+        device: InputDevice,
+        light: Light,
+        expectedLevels: IntArray
+    ) {
+        val deviceId = device.id
+        val lightId = light.id
+        for (level in 1 until expectedLevels.size) {
+            incrementKeyboardBacklight(deviceId)
+            assertEquals(
+                "Light value for level $level mismatched",
+                Color.argb(expectedLevels[level], 0, 0, 0),
+                lightColorMap[lightId]
+            )
+            assertEquals(
+                "Light value for level $level must be correctly stored in the datastore",
+                expectedLevels[level],
+                dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt
+            )
+        }
+
+        // Increment above max level
+        incrementKeyboardBacklight(deviceId)
+        assertEquals(
+            "Light value for max level mismatched",
+            Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
+            lightColorMap[lightId]
+        )
+        assertEquals(
+            "Light value for max level must be correctly stored in the datastore",
+            MAX_BRIGHTNESS,
+            dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt
+        )
+
+        for (level in expectedLevels.size - 2 downTo 0) {
+            decrementKeyboardBacklight(deviceId)
+            assertEquals(
+                "Light value for level $level mismatched",
+                Color.argb(expectedLevels[level], 0, 0, 0),
+                lightColorMap[lightId]
+            )
+            assertEquals(
+                "Light value for level $level must be correctly stored in the datastore",
+                expectedLevels[level],
+                dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt
+            )
+        }
+
+        // Decrement below min level
+        decrementKeyboardBacklight(deviceId)
+        assertEquals(
+            "Light value for min level mismatched",
+            Color.argb(0, 0, 0, 0),
+            lightColorMap[lightId]
+        )
+        assertEquals(
+            "Light value for min level must be correctly stored in the datastore",
+            0,
+            dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt
+        )
+    }
+
     inner class KeyboardBacklightListener : IKeyboardBacklightListener.Stub() {
         override fun onBrightnessChanged(
             deviceId: Int,
@@ -490,10 +806,41 @@
         testLooper.dispatchAll()
     }
 
+    private fun sendAmbientBacklightValue(brightnessValue: Int) {
+        keyboardBacklightController.handleAmbientLightValueChanged(brightnessValue)
+        keyboardBacklightController.notifyUserActivity()
+        testLooper.dispatchAll()
+    }
+
     class KeyboardBacklightState(
         val deviceId: Int,
         val brightnessLevel: Int,
         val maxBrightnessLevel: Int,
         val isTriggeredByKeyPress: Boolean
     )
+
+    private inner class KeyboardBacklightFlags constructor(
+        animationEnabled: Boolean,
+        customLevelsEnabled: Boolean,
+        ambientControlEnabled: Boolean
+    ) : AutoCloseable {
+        init {
+            InputFeatureFlagProvider.setKeyboardBacklightAnimationEnabled(animationEnabled)
+            InputFeatureFlagProvider.setKeyboardBacklightCustomLevelsEnabled(customLevelsEnabled)
+            InputFeatureFlagProvider
+                .setAmbientKeyboardBacklightControlEnabled(ambientControlEnabled)
+        }
+
+        override fun close() {
+            InputFeatureFlagProvider.clearOverrides()
+        }
+    }
+
+    private inner class FakeAnimatorFactory : KeyboardBacklightController.AnimatorFactory {
+        override fun makeIntAnimator(from: Int, to: Int): ValueAnimator {
+            lastAnimationValues[0] = from
+            lastAnimationValues[1] = to
+            return ValueAnimator.ofInt(from, to)
+        }
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardMetricsCollectorTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardMetricsCollectorTests.kt
new file mode 100644
index 0000000..c9724a3
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/input/KeyboardMetricsCollectorTests.kt
@@ -0,0 +1,179 @@
+/*
+ * 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.
+ */
+
+package com.android.server.input
+
+import android.hardware.input.KeyboardLayout
+import android.icu.util.ULocale
+import android.platform.test.annotations.Presubmit
+import android.view.InputDevice
+import android.view.inputmethod.InputMethodSubtype
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertThrows
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+private fun createKeyboard(
+    deviceId: Int,
+    vendorId: Int,
+    productId: Int,
+    languageTag: String?,
+    layoutType: String?
+): InputDevice =
+    InputDevice.Builder()
+        .setId(deviceId)
+        .setName("Device $deviceId")
+        .setDescriptor("descriptor $deviceId")
+        .setSources(InputDevice.SOURCE_KEYBOARD)
+        .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
+        .setExternal(true)
+        .setVendorId(vendorId)
+        .setProductId(productId)
+        .setKeyboardLanguageTag(languageTag)
+        .setKeyboardLayoutType(layoutType)
+        .build()
+
+private fun createImeSubtype(
+    imeSubtypeId: Int,
+    languageTag: String,
+    layoutType: String
+): InputMethodSubtype =
+    InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(imeSubtypeId)
+        .setPhysicalKeyboardHint(ULocale.forLanguageTag(languageTag), layoutType).build()
+
+/**
+ * Tests for {@link KeyboardMetricsCollector}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:KeyboardMetricsCollectorTests
+ */
+@Presubmit
+class KeyboardMetricsCollectorTests {
+
+    companion object {
+        const val DEVICE_ID = 1
+        const val DEFAULT_VENDOR_ID = 123
+        const val DEFAULT_PRODUCT_ID = 456
+    }
+
+    @Test
+    fun testCreateKeyboardConfigurationEvent_throwsExceptionWithoutAnyLayoutConfiguration() {
+        assertThrows(IllegalStateException::class.java) {
+            KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder(
+                createKeyboard(
+                    DEVICE_ID,
+                    DEFAULT_VENDOR_ID,
+                    DEFAULT_PRODUCT_ID,
+                    null,
+                    null
+                )
+            ).build()
+        }
+    }
+
+    @Test
+    fun testCreateKeyboardConfigurationEvent_throwsExceptionWithInvalidLayoutSelectionCriteria() {
+        assertThrows(IllegalStateException::class.java) {
+            KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder(
+                createKeyboard(
+                    DEVICE_ID,
+                    DEFAULT_VENDOR_ID,
+                    DEFAULT_PRODUCT_ID,
+                    null,
+                    null
+                )
+            ).addLayoutSelection(createImeSubtype(1, "en-US", "qwerty"), null, 123).build()
+        }
+    }
+
+    @Test
+    fun testCreateKeyboardConfigurationEvent_withMultipleConfigurations() {
+        val builder = KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder(
+            createKeyboard(
+                DEVICE_ID,
+                DEFAULT_VENDOR_ID,
+                DEFAULT_PRODUCT_ID,
+                "de-CH",
+                "qwertz"
+            )
+        )
+        val event = builder.addLayoutSelection(
+            createImeSubtype(1, "en-US", "qwerty"),
+            KeyboardLayout(null, "English(US)(Qwerty)", null, 0, null, 0, 0, 0),
+            KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD
+        ).addLayoutSelection(
+            createImeSubtype(2, "en-US", "azerty"),
+            null,
+            KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER
+        ).addLayoutSelection(
+            createImeSubtype(3, "en-US", "qwerty"),
+            KeyboardLayout(null, "German", null, 0, null, 0, 0, 0),
+            KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE
+        ).setIsFirstTimeConfiguration(true).build()
+
+        assertEquals(
+            "KeyboardConfigurationEvent should pick vendor ID from provided InputDevice",
+            DEFAULT_VENDOR_ID,
+            event.vendorId
+        )
+        assertEquals(
+            "KeyboardConfigurationEvent should pick product ID from provided InputDevice",
+            DEFAULT_PRODUCT_ID,
+            event.productId
+        )
+        assertTrue(event.isFirstConfiguration)
+
+        assertEquals(
+            "KeyboardConfigurationEvent should contain 3 configurations provided",
+            3,
+            event.layoutConfigurations.size
+        )
+        assertExpectedLayoutConfiguration(
+            event.layoutConfigurations[0],
+            "en-US",
+            KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwerty"),
+            "English(US)(Qwerty)",
+            KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD
+        )
+        assertExpectedLayoutConfiguration(
+            event.layoutConfigurations[1],
+            "en-US",
+            KeyboardLayout.LayoutType.getLayoutTypeEnumValue("azerty"),
+            KeyboardMetricsCollector.DEFAULT_LAYOUT,
+            KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER
+        )
+        assertExpectedLayoutConfiguration(
+            event.layoutConfigurations[2],
+            "de-CH",
+            KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwertz"),
+            "German",
+            KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE
+        )
+    }
+
+    private fun assertExpectedLayoutConfiguration(
+        configuration: KeyboardMetricsCollector.LayoutConfiguration,
+        expectedLanguageTag: String,
+        expectedLayoutType: Int,
+        expectedSelectedLayout: String,
+        expectedLayoutSelectionCriteria: Int
+    ) {
+        assertEquals(expectedLanguageTag, configuration.keyboardLanguageTag)
+        assertEquals(expectedLayoutType, configuration.keyboardLayoutType)
+        assertEquals(expectedSelectedLayout, configuration.keyboardLayoutName)
+        assertEquals(expectedLayoutSelectionCriteria, configuration.layoutSelectionCriteria)
+    }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index e960e99..fe2ac17 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -254,6 +254,8 @@
                 .thenReturn(true);
         when(res.getBoolean(eq(com.android.internal.R.bool.config_strongAuthRequiredOnBoot)))
                 .thenReturn(true);
+        when(res.getBoolean(eq(com.android.internal.R.bool.config_repairModeSupported)))
+                .thenReturn(true);
         return res;
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java
index 36dc6c5..a029db9 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java
@@ -84,6 +84,11 @@
     }
 
     @Override
+    File getRepairModePersistentDataFile() {
+        return remapToStorageDir(super.getRepairModePersistentDataFile());
+    }
+
+    @Override
     PersistentDataBlockManagerInternal getPersistentDataBlockManager() {
         return mPersistentDataBlockManager;
     }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
index 10ed882..23f14f8 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
@@ -457,6 +457,31 @@
         assertEquals(2, PersistentData.TYPE_SP_WEAVER);
     }
 
+    @Test
+    public void testRepairMode_emptyPersistentData() {
+        assertSame(PersistentData.NONE, mStorage.readPersistentDataBlock());
+    }
+
+    @Test
+    public void testRepairMode_writeGatekeeperPersistentData() {
+        mStorage.writeRepairModePersistentData(
+                PersistentData.TYPE_SP_GATEKEEPER, SOME_USER_ID, PAYLOAD);
+
+        final PersistentData data = mStorage.readRepairModePersistentData();
+        assertEquals(PersistentData.TYPE_SP_GATEKEEPER, data.type);
+        assertArrayEquals(PAYLOAD, data.payload);
+    }
+
+    @Test
+    public void testRepairMode_writeWeaverPersistentData() {
+        mStorage.writeRepairModePersistentData(
+                PersistentData.TYPE_SP_WEAVER, SOME_USER_ID, PAYLOAD);
+
+        final PersistentData data = mStorage.readRepairModePersistentData();
+        assertEquals(PersistentData.TYPE_SP_WEAVER, data.type);
+        assertArrayEquals(PAYLOAD, data.payload);
+    }
+
     private static void assertArrayEquals(byte[] expected, byte[] actual) {
         if (!Arrays.equals(expected, actual)) {
             fail("expected:<" + Arrays.toString(expected) +
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockscreenRepairModeTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenRepairModeTest.java
new file mode 100644
index 0000000..70150c5
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenRepairModeTest.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.locksettings;
+
+import static com.android.internal.widget.LockPatternUtils.USER_REPAIR_MODE;
+import static com.android.internal.widget.LockPatternUtils.VERIFY_FLAG_WRITE_REPAIR_MODE_PW;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import android.app.PropertyInvalidatedCache;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.VerifyCredentialResponse;
+import com.android.server.locksettings.LockSettingsStorage.PersistentData;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class LockscreenRepairModeTest extends BaseLockSettingsServiceTests {
+
+    @Before
+    public void setUp() throws Exception {
+        PropertyInvalidatedCache.disableForTestMode();
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void verifyPin_writeRepairModePW() {
+        mService.setLockCredential(newPin("1234"), nonePassword(), PRIMARY_USER_ID);
+        assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData());
+
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(
+                        newPin("1234"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW)
+                        .getResponseCode());
+        assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PIN,
+                mService.getCredentialType(USER_REPAIR_MODE));
+    }
+
+    @Test
+    public void verifyPattern_writeRepairModePW() {
+        mService.setLockCredential(newPattern("4321"), nonePassword(), PRIMARY_USER_ID);
+        assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData());
+
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(
+                        newPattern("4321"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW)
+                        .getResponseCode());
+        assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
+                mService.getCredentialType(USER_REPAIR_MODE));
+    }
+
+    @Test
+    public void verifyPassword_writeRepairModePW() {
+        mService.setLockCredential(newPassword("4321"), nonePassword(), PRIMARY_USER_ID);
+        assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData());
+
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(
+                        newPassword("4321"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW)
+                        .getResponseCode());
+        assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
+                mService.getCredentialType(USER_REPAIR_MODE));
+    }
+
+    @Test
+    public void verifyCredential_writeRepairModePW_repairModeActive() {
+        mService.setLockCredential(newPin("1234"), nonePassword(), PRIMARY_USER_ID);
+        assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData());
+
+        setRepairModeActive(true);
+        assertEquals(VerifyCredentialResponse.RESPONSE_ERROR,
+                mService.verifyCredential(
+                                newPin("1234"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW)
+                        .getResponseCode());
+        assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData());
+    }
+
+    @Test
+    public void deleteRepairModePersistentData() {
+        mService.setLockCredential(newPin("1234"), nonePassword(), PRIMARY_USER_ID);
+        assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData());
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(
+                                newPin("1234"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW)
+                        .getResponseCode());
+        assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PIN,
+                mService.getCredentialType(USER_REPAIR_MODE));
+
+        mService.deleteRepairModePersistentDataIfNeeded();
+        assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData());
+    }
+
+    @Test
+    public void verifyPin_userRepairMode() {
+        mService.setLockCredential(newPin("1234"), nonePassword(), PRIMARY_USER_ID);
+        assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData());
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(
+                        newPin("1234"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW)
+                        .getResponseCode());
+        setRepairModeActive(true);
+
+        assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PIN,
+                mService.getCredentialType(USER_REPAIR_MODE));
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(newPin("1234"), USER_REPAIR_MODE, 0 /* flags */)
+                        .getResponseCode());
+    }
+
+    @Test
+    public void verifyPattern_userRepairMode() {
+        mService.setLockCredential(newPattern("4321"), nonePassword(), PRIMARY_USER_ID);
+        assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData());
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(
+                        newPattern("4321"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW)
+                        .getResponseCode());
+        setRepairModeActive(true);
+
+        assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
+                mService.getCredentialType(USER_REPAIR_MODE));
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(newPattern("4321"), USER_REPAIR_MODE, 0 /* flags */)
+                        .getResponseCode());
+    }
+
+    @Test
+    public void verifyPassword_userRepairMode() {
+        mService.setLockCredential(newPassword("4321"), nonePassword(), PRIMARY_USER_ID);
+        assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData());
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(
+                        newPassword("4321"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW)
+                        .getResponseCode());
+        setRepairModeActive(true);
+
+        assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
+                mService.getCredentialType(USER_REPAIR_MODE));
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(newPassword("4321"), USER_REPAIR_MODE, 0 /* flags */)
+                        .getResponseCode());
+    }
+
+    @Test
+    public void verifyCredential_userRepairMode_repairModeIsNotActive() {
+        mService.setLockCredential(newPin("1234"), nonePassword(), PRIMARY_USER_ID);
+        assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData());
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(
+                                newPin("1234"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW)
+                        .getResponseCode());
+
+        assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PIN,
+                mService.getCredentialType(USER_REPAIR_MODE));
+        assertEquals(VerifyCredentialResponse.RESPONSE_ERROR,
+                mService.verifyCredential(newPin("1234"), USER_REPAIR_MODE, 0 /* flags */)
+                        .getResponseCode());
+    }
+
+    @Test
+    public void verifyCredential_userRepairMode_wrongPin() {
+        mService.setLockCredential(newPin("1234"), nonePassword(), PRIMARY_USER_ID);
+        assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData());
+        assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+                mService.verifyCredential(
+                                newPin("1234"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW)
+                        .getResponseCode());
+        setRepairModeActive(true);
+
+        assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PIN,
+                mService.getCredentialType(USER_REPAIR_MODE));
+        assertEquals(VerifyCredentialResponse.RESPONSE_ERROR,
+                mService.verifyCredential(newPin("5678"), USER_REPAIR_MODE, 0 /* flags */)
+                        .getResponseCode());
+    }
+
+    private void setRepairModeActive(boolean active) {
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.REPAIR_MODE_ACTIVE, active ? 1 : 0);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index c42928e..bb8b986 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -38,6 +38,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 static org.testng.Assert.assertThrows;
 
 import android.app.ActivityManagerInternal;
@@ -49,10 +50,14 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.media.projection.IMediaProjection;
 import android.media.projection.IMediaProjectionCallback;
+import android.media.projection.IMediaProjectionWatcherCallback;
 import android.media.projection.ReviewGrantedConsentResult;
+import android.os.Binder;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
 import android.view.ContentRecordingSession;
 
@@ -86,6 +91,7 @@
     private static final int UID = 10;
     private static final String PACKAGE_NAME = "test.package";
     private final ApplicationInfo mAppInfo = new ApplicationInfo();
+    private final TestLooper mTestLooper = new TestLooper();
     private static final ContentRecordingSession DISPLAY_SESSION =
             ContentRecordingSession.createDisplaySession(DEFAULT_DISPLAY);
     // Callback registered by an app on a MediaProjection instance.
@@ -110,6 +116,14 @@
                 }
             };
 
+    private final MediaProjectionManagerService.Injector mTestLooperInjector =
+            new MediaProjectionManagerService.Injector() {
+                @Override
+                Looper createCallbackLooper() {
+                    return mTestLooper.getLooper();
+                }
+            };
+
     private Context mContext;
     private MediaProjectionManagerService mService;
     private OffsettableClock mClock;
@@ -122,12 +136,15 @@
     private WindowManagerInternal mWindowManagerInternal;
     @Mock
     private PackageManager mPackageManager;
+    @Mock
+    private IMediaProjectionWatcherCallback mWatcherCallback;
     @Captor
     private ArgumentCaptor<ContentRecordingSession> mSessionCaptor;
 
     @Before
     public void setup() throws Exception {
         MockitoAnnotations.initMocks(this);
+        when(mWatcherCallback.asBinder()).thenReturn(new Binder());
 
         LocalServices.removeServiceForTest(ActivityManagerInternal.class);
         LocalServices.addService(ActivityManagerInternal.class, mAmInternal);
@@ -671,6 +688,59 @@
         assertThat(mService.isCurrentProjection(projection)).isTrue();
     }
 
+    @Test
+    public void setContentRecordingSession_successful_notifiesListeners()
+            throws Exception {
+        mService.addCallback(mWatcherCallback);
+        MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+        projection.start(mIMediaProjectionCallback);
+
+        doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
+                any(ContentRecordingSession.class));
+        mService.setContentRecordingSession(DISPLAY_SESSION);
+
+        verify(mWatcherCallback).onRecordingSessionSet(
+                projection.getProjectionInfo(),
+                DISPLAY_SESSION
+        );
+    }
+
+    @Test
+    public void setContentRecordingSession_notifiesListenersOnCallbackLooper()
+            throws Exception {
+        mService = new MediaProjectionManagerService(mContext, mTestLooperInjector);
+        mService.addCallback(mWatcherCallback);
+        MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+        projection.start(mIMediaProjectionCallback);
+        doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
+                any(ContentRecordingSession.class));
+
+        mService.setContentRecordingSession(DISPLAY_SESSION);
+        // Callback not notified yet, as test looper hasn't dispatched the message yet
+        verify(mWatcherCallback, never()).onRecordingSessionSet(any(), any());
+
+        mTestLooper.dispatchAll();
+        // Message dispatched on test looper. Callback should now be notified.
+        verify(mWatcherCallback).onRecordingSessionSet(
+                projection.getProjectionInfo(),
+                DISPLAY_SESSION
+        );
+    }
+
+    @Test
+    public void setContentRecordingSession_failure_doesNotNotifyListeners()
+            throws Exception {
+        mService.addCallback(mWatcherCallback);
+        MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+        projection.start(mIMediaProjectionCallback);
+
+        doReturn(false).when(mWindowManagerInternal).setContentRecordingSession(
+                any(ContentRecordingSession.class));
+        mService.setContentRecordingSession(DISPLAY_SESSION);
+
+        verify(mWatcherCallback, never()).onRecordingSessionSet(any(), any());
+    }
+
     private void verifySetSessionWithContent(@ContentRecordingSession.RecordContent int content) {
         verify(mWindowManagerInternal, atLeastOnce()).setContentRecordingSession(
                 mSessionCaptor.capture());
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceCreateProfileTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceCreateProfileTest.java
index fdf94be..39cc653 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceCreateProfileTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceCreateProfileTest.java
@@ -182,7 +182,7 @@
         UserInfo secondaryUser = addUser();
         UserInfo profile = addProfile(secondaryUser);
         // Add the profile it to the users being removed.
-        mUserManagerService.addRemovingUserIdLocked(profile.id);
+        mUserManagerService.addRemovingUserId(profile.id);
         // We should reuse the badge from the profile being removed.
         assertEquals("Badge index not reused while removing a user", 0,
                 mUserManagerService.getFreeProfileBadgeLU(secondaryUser.id,
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceIdRecyclingTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceIdRecyclingTest.java
index 1f4c9f8..b6fd65e 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceIdRecyclingTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceIdRecyclingTest.java
@@ -111,7 +111,7 @@
 
     private void removeUser(int userId) {
         mUserManagerService.removeUserInfo(userId);
-        mUserManagerService.addRemovingUserIdLocked(userId);
+        mUserManagerService.addRemovingUserId(userId);
     }
 
     private void assertNoNextIdAvailable(String message) {
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 4af0323..592be2d 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -70,6 +70,9 @@
 
         LocalServices.removeServiceForTest(UserManagerInternal.class);
         mUserManagerService = new UserManagerService(InstrumentationRegistry.getContext());
+        // Put the current user to mUsers. UMS can't find userlist.xml, and fallbackToSingleUserLP.
+        mUserManagerService.putUserInfo(
+                new UserInfo(ActivityManager.getCurrentUser(), "Current User", 0));
 
         restrictionsFile = new File(mContext.getCacheDir(), "restrictions.xml");
         restrictionsFile.delete();
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index 6bcda3f..dc86e5f 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -53,6 +53,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -545,23 +547,42 @@
     public void testRemoveUserWhenPossible_withProfiles() throws Exception {
         assumeHeadlessModeEnabled();
         assumeCloneEnabled();
-        final UserInfo parentUser = createUser("Human User", /* flags= */ 0);
-        final UserInfo cloneProfileUser = createProfileForUser("Clone Profile user",
+        final List<String> profileTypesToCreate = Arrays.asList(
                 UserManager.USER_TYPE_PROFILE_CLONE,
-                parentUser.id);
+                UserManager.USER_TYPE_PROFILE_MANAGED
+        );
 
-        final UserInfo workProfileUser = createProfileForUser("Work Profile user",
-                UserManager.USER_TYPE_PROFILE_MANAGED,
-                parentUser.id);
+        final UserInfo parentUser = createUser("Human User", /* flags= */ 0);
+        assertWithMessage("Could not create parent user")
+                .that(parentUser).isNotNull();
+
+        final List<Integer> profileIds = new ArrayList<>();
+        for (String profileType : profileTypesToCreate) {
+            final String name = profileType.substring(profileType.lastIndexOf('.') + 1);
+            if (mUserManager.canAddMoreProfilesToUser(profileType, parentUser.id)) {
+                final UserInfo profile = createProfileForUser(name, profileType, parentUser.id);
+                assertWithMessage("Could not create " + name)
+                        .that(profile).isNotNull();
+                profileIds.add(profile.id);
+            } else {
+                Slog.w(TAG, "Can not add " + name + " to user #" + parentUser.id);
+            }
+        }
+
+        // Test shouldn't pass or fail unless it's allowed to add profiles to secondary users.
+        assumeTrue("Not possible to create any profiles to user #" + parentUser.id,
+                profileIds.size() > 0);
 
         assertThat(mUserManager.removeUserWhenPossible(parentUser.getUserHandle(),
                 /* overrideDevicePolicy= */ false))
                 .isEqualTo(UserManager.REMOVE_RESULT_REMOVED);
         waitForUserRemoval(parentUser.id);
 
-        assertThat(hasUser(parentUser.id)).isFalse();
-        assertThat(hasUser(cloneProfileUser.id)).isFalse();
-        assertThat(hasUser(workProfileUser.id)).isFalse();
+        assertWithMessage("Parent user still exists")
+                .that(hasUser(parentUser.id)).isFalse();
+        profileIds.forEach(id ->
+                assertWithMessage("Profile still exists")
+                        .that(hasUser(id)).isFalse());
     }
 
     /** Tests creating a FULL user via specifying userType. */
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 4347dd6..e49b290 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -83,6 +83,7 @@
 import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.SHOW_STICKY_HUN_FOR_DENIED_FSI;
 import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.WAKE_LOCK_FOR_POSTING_NOTIFICATION;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
+import static com.android.server.notification.NotificationManagerService.DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE;
 import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_ADJUSTED;
 import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_POSTED;
 import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_UPDATED;
@@ -705,6 +706,7 @@
 
     @After
     public void assertAllTrackersFinishedOrCancelled() {
+        waitForIdle(); // Finish async work.
         // Verify that no trackers were left dangling.
         for (PostNotificationTracker tracker : mPostNotificationTrackerFactory.mCreatedTrackers) {
             assertThat(tracker.isOngoing()).isFalse();
@@ -865,13 +867,18 @@
     }
 
     private NotificationRecord generateNotificationRecord(NotificationChannel channel, int userId) {
+        return generateNotificationRecord(channel, 1, userId);
+    }
+
+    private NotificationRecord generateNotificationRecord(NotificationChannel channel, int id,
+            int userId) {
         if (channel == null) {
             channel = mTestNotificationChannel;
         }
         Notification.Builder nb = new Notification.Builder(mContext, channel.getId())
                 .setContentTitle("foo")
                 .setSmallIcon(android.R.drawable.sym_def_app_icon);
-        StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, "tag", mUid, 0,
+        StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, id, "tag", mUid, 0,
                 nb.build(), new UserHandle(userId), null, 0);
         return new NotificationRecord(mContext, sbn, channel);
     }
@@ -10732,6 +10739,51 @@
     }
 
     @Test
+    public void testUngroupingAutoSummary_differentUsers() throws Exception {
+        NotificationRecord nr0 =
+                generateNotificationRecord(mTestNotificationChannel, 0, USER_SYSTEM);
+        NotificationRecord nr1 =
+                generateNotificationRecord(mTestNotificationChannel, 1, USER_SYSTEM);
+
+        // add notifications + summary for USER_SYSTEM
+        mService.addNotification(nr0);
+        mService.addNotification(nr1);
+        mService.addNotification(mService.createAutoGroupSummary(nr1.getUserId(),
+                nr1.getSbn().getPackageName(), nr1.getKey(), GroupHelper.BASE_FLAGS));
+
+        // add notifications + summary for USER_ALL
+        NotificationRecord nr0_all =
+                generateNotificationRecord(mTestNotificationChannel, 2, UserHandle.USER_ALL);
+        NotificationRecord nr1_all =
+                generateNotificationRecord(mTestNotificationChannel, 3, UserHandle.USER_ALL);
+
+        mService.addNotification(nr0_all);
+        mService.addNotification(nr1_all);
+        mService.addNotification(mService.createAutoGroupSummary(nr0_all.getUserId(),
+                nr0_all.getSbn().getPackageName(), nr0_all.getKey(), GroupHelper.BASE_FLAGS));
+
+        // cancel both children for USER_ALL
+        mBinderService.cancelNotificationWithTag(PKG, PKG, nr0_all.getSbn().getTag(),
+                nr0_all.getSbn().getId(), UserHandle.USER_ALL);
+        mBinderService.cancelNotificationWithTag(PKG, PKG, nr1_all.getSbn().getTag(),
+                nr1_all.getSbn().getId(), UserHandle.USER_ALL);
+        waitForIdle();
+
+        // group helper would send 'remove summary' event
+        mService.clearAutogroupSummaryLocked(UserHandle.USER_ALL,
+                nr0_all.getSbn().getPackageName());
+        waitForIdle();
+
+        // make sure the right summary was removed
+        assertThat(mService.getNotificationCount(nr0_all.getSbn().getPackageName(),
+                UserHandle.USER_ALL, 0, null)).isEqualTo(0);
+
+        // the USER_SYSTEM notifications + summary were not removed
+        assertThat(mService.getNotificationCount(nr0.getSbn().getPackageName(),
+                USER_SYSTEM, 0, null)).isEqualTo(3);
+    }
+
+    @Test
     public void testStrongAuthTracker_isInLockDownMode() {
         mStrongAuthTracker.setGetStrongAuthForUserReturnValue(
                 STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
@@ -11948,6 +12000,103 @@
         assertFalse(n.isUserInitiatedJob());
     }
 
+    @Test
+    public void enqueue_updatesEnqueueRate() throws Exception {
+        Notification n = generateNotificationRecord(null).getNotification();
+
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, mUserId);
+        // Don't waitForIdle() here. We want to verify the "intermediate" state.
+
+        verify(mUsageStats).registerEnqueuedByApp(eq(PKG));
+        verify(mUsageStats).registerEnqueuedByAppAndAccepted(eq(PKG));
+        verify(mUsageStats, never()).registerPostedByApp(any());
+
+        waitForIdle();
+    }
+
+    @Test
+    public void enqueue_withPost_updatesEnqueueRateAndPost() throws Exception {
+        Notification n = generateNotificationRecord(null).getNotification();
+
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, mUserId);
+        waitForIdle();
+
+        verify(mUsageStats).registerEnqueuedByApp(eq(PKG));
+        verify(mUsageStats).registerEnqueuedByAppAndAccepted(eq(PKG));
+        verify(mUsageStats).registerPostedByApp(any());
+    }
+
+    @Test
+    public void enqueueNew_whenOverEnqueueRate_accepts() throws Exception {
+        Notification n = generateNotificationRecord(null).getNotification();
+        when(mUsageStats.getAppEnqueueRate(eq(PKG)))
+                .thenReturn(DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE + 1f);
+
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, n, mUserId);
+        waitForIdle();
+
+        assertThat(mService.mNotificationsByKey).hasSize(1);
+        verify(mUsageStats).registerEnqueuedByApp(eq(PKG));
+        verify(mUsageStats).registerEnqueuedByAppAndAccepted(eq(PKG));
+        verify(mUsageStats).registerPostedByApp(any());
+    }
+
+    @Test
+    public void enqueueUpdate_whenBelowMaxEnqueueRate_accepts() throws Exception {
+        // Post the first version.
+        Notification original = generateNotificationRecord(null).getNotification();
+        original.when = 111;
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, original, mUserId);
+        waitForIdle();
+        assertThat(mService.mNotificationList).hasSize(1);
+        assertThat(mService.mNotificationList.get(0).getNotification().when).isEqualTo(111);
+
+        reset(mUsageStats);
+        when(mUsageStats.getAppEnqueueRate(eq(PKG)))
+                .thenReturn(DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE - 1f);
+
+        // Post the update.
+        Notification update = generateNotificationRecord(null).getNotification();
+        update.when = 222;
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, update, mUserId);
+        waitForIdle();
+
+        verify(mUsageStats).registerEnqueuedByApp(eq(PKG));
+        verify(mUsageStats).registerEnqueuedByAppAndAccepted(eq(PKG));
+        verify(mUsageStats, never()).registerPostedByApp(any());
+        verify(mUsageStats).registerUpdatedByApp(any(), any());
+        assertThat(mService.mNotificationList).hasSize(1);
+        assertThat(mService.mNotificationList.get(0).getNotification().when).isEqualTo(222);
+    }
+
+    @Test
+    public void enqueueUpdate_whenAboveMaxEnqueueRate_rejects() throws Exception {
+        // Post the first version.
+        Notification original = generateNotificationRecord(null).getNotification();
+        original.when = 111;
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, original, mUserId);
+        waitForIdle();
+        assertThat(mService.mNotificationList).hasSize(1);
+        assertThat(mService.mNotificationList.get(0).getNotification().when).isEqualTo(111);
+
+        reset(mUsageStats);
+        when(mUsageStats.getAppEnqueueRate(eq(PKG)))
+                .thenReturn(DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE + 1f);
+
+        // Post the update.
+        Notification update = generateNotificationRecord(null).getNotification();
+        update.when = 222;
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, update, mUserId);
+        waitForIdle();
+
+        verify(mUsageStats).registerEnqueuedByApp(eq(PKG));
+        verify(mUsageStats, never()).registerEnqueuedByAppAndAccepted(any());
+        verify(mUsageStats, never()).registerPostedByApp(any());
+        verify(mUsageStats, never()).registerUpdatedByApp(any(), any());
+        assertThat(mService.mNotificationList).hasSize(1);
+        assertThat(mService.mNotificationList.get(0).getNotification().when).isEqualTo(111); // old
+    }
+
     private void setDpmAppOppsExemptFromDismissal(boolean isOn) {
         DeviceConfig.setProperty(
                 DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordLoggerTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordLoggerTest.java
index beab107..0b147c3 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordLoggerTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordLoggerTest.java
@@ -18,6 +18,8 @@
 
 import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
 import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
+import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_POSTED;
+import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_UPDATED;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -33,6 +35,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.UiServiceTestCase;
+import com.android.internal.util.FrameworkStatsLog;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -155,4 +158,54 @@
         // Then: should return false
         assertFalse(NotificationRecordLogger.isNonDismissible(p.r));
     }
+
+    @Test
+    public void testGetFsiState_stickyHunFlagDisabled_zero() {
+        final int fsiState = NotificationRecordLogger.getFsiState(
+                /* isStickyHunFlagEnabled= */ false,
+                /* hasFullScreenIntent= */ true,
+                /* hasFsiRequestedButDeniedFlag= */ true,
+                /* eventType= */ NOTIFICATION_POSTED);
+        assertEquals(0, fsiState);
+    }
+
+    @Test
+    public void testGetFsiState_isUpdate_zero() {
+        final int fsiState = NotificationRecordLogger.getFsiState(
+                /* isStickyHunFlagEnabled= */ true,
+                /* hasFullScreenIntent= */ true,
+                /* hasFsiRequestedButDeniedFlag= */ true,
+                /* eventType= */ NOTIFICATION_UPDATED);
+        assertEquals(0, fsiState);
+    }
+
+    @Test
+    public void testGetFsiState_hasFsi_allowedEnum() {
+        final int fsiState = NotificationRecordLogger.getFsiState(
+                /* isStickyHunFlagEnabled= */ true,
+                /* hasFullScreenIntent= */ true,
+                /* hasFsiRequestedButDeniedFlag= */ false,
+                /* eventType= */ NOTIFICATION_POSTED);
+        assertEquals(FrameworkStatsLog.NOTIFICATION_REPORTED__FSI_STATE__FSI_ALLOWED, fsiState);
+    }
+
+    @Test
+    public void testGetFsiState_fsiPermissionDenied_deniedEnum() {
+        final int fsiState = NotificationRecordLogger.getFsiState(
+                /* isStickyHunFlagEnabled= */ true,
+                /* hasFullScreenIntent= */ false,
+                /* hasFsiRequestedButDeniedFlag= */ true,
+                /* eventType= */ NOTIFICATION_POSTED);
+        assertEquals(FrameworkStatsLog.NOTIFICATION_REPORTED__FSI_STATE__FSI_DENIED, fsiState);
+    }
+
+    @Test
+    public void testGetFsiState_noFsi_noFsiEnum() {
+        final int fsiState = NotificationRecordLogger.getFsiState(
+                /* isStickyHunFlagEnabled= */ true,
+                /* hasFullScreenIntent= */ false,
+                /* hasFsiRequestedButDeniedFlag= */ false,
+                /* eventType= */ NOTIFICATION_POSTED);
+        assertEquals(FrameworkStatsLog.NOTIFICATION_REPORTED__FSI_STATE__NO_FSI, fsiState);
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
index 6668f85..d32289d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
@@ -34,6 +34,7 @@
 import android.os.IBinder;
 import android.os.Parcel;
 import android.util.Log;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.RemoteViews;
 
@@ -95,7 +96,8 @@
     // Types that we can't really produce. No methods receiving these parameters will be invoked.
     private static final ImmutableSet<Class<?>> UNUSABLE_TYPES =
             ImmutableSet.of(Consumer.class, IBinder.class, MediaSession.Token.class, Parcel.class,
-                    PrintWriter.class, Resources.Theme.class, View.class);
+                    PrintWriter.class, Resources.Theme.class, View.class,
+                    LayoutInflater.Factory2.class);
 
     // Maximum number of times we allow generating the same class recursively.
     // E.g. new RemoteViews.addView(new RemoteViews()) but stop there.
@@ -115,11 +117,19 @@
             PREFERRED_CONSTRUCTORS = ImmutableMap.of(
                     Notification.Builder.class,
                     Notification.Builder.class.getConstructor(Context.class, String.class));
+
+            EXCLUDED_SETTERS_OVERLOADS = ImmutableMultimap.<Class<?>, Method>builder()
+                    .put(RemoteViews.class,
+                            // b/245950570: Tries to connect to service and will crash.
+                            RemoteViews.class.getMethod("setRemoteAdapter",
+                                    int.class, Intent.class))
+                    .build();
         } catch (NoSuchMethodException e) {
             throw new RuntimeException(e);
         }
     }
 
+    // Setters that shouldn't be called, for various reasons (but NOT because they are KNOWN_BAD).
     private static final Multimap<Class<?>, String> EXCLUDED_SETTERS =
             ImmutableMultimap.<Class<?>, String>builder()
                     // Handled by testAllStyles().
@@ -134,6 +144,9 @@
                     .put(RemoteViews.class, "mergeRemoteViews")
                     .build();
 
+    // Same as above, but specific overloads that should not be called.
+    private static final Multimap<Class<?>, Method> EXCLUDED_SETTERS_OVERLOADS;
+
     private Context mContext;
 
     @Rule
@@ -146,10 +159,12 @@
 
     @Test // This is a meta-test, checks that the generators are not broken.
     public void verifyTest() {
-        Generated<Notification> notification = buildNotification(mContext, /* styleClass= */ null,
-                /* extenderClass= */ null, /* actionExtenderClass= */ null,
+        Generated<Notification> notification = buildNotification(mContext,
+                /* styleClass= */ Notification.MessagingStyle.class,
+                /* extenderClass= */ Notification.WearableExtender.class,
+                /* actionExtenderClass= */ Notification.Action.WearableExtender.class,
                 /* includeRemoteViews= */ true);
-        assertThat(notification.includedUris.size()).isAtLeast(20);
+        assertThat(notification.includedUris.size()).isAtLeast(730);
     }
 
     @Test
@@ -479,6 +494,7 @@
                         || method.getReturnType().equals(clazz))
                         && method.getParameterCount() >= 1
                         && !EXCLUDED_SETTERS.containsEntry(clazz, method.getName())
+                        && !EXCLUDED_SETTERS_OVERLOADS.containsEntry(clazz, method)
                         && Arrays.stream(method.getParameterTypes())
                             .noneMatch(excludingParameterTypes::contains)) {
                     methods.put(method.getName(), method);
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 48ad86d..47340c1 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -36,6 +36,9 @@
 
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES;
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED;
 import static com.android.os.AtomsProto.PackageNotificationChannelPreferences.CHANNEL_ID_FIELD_NUMBER;
 import static com.android.os.AtomsProto.PackageNotificationChannelPreferences.CHANNEL_NAME_FIELD_NUMBER;
 import static com.android.os.AtomsProto.PackageNotificationChannelPreferences.IMPORTANCE_FIELD_NUMBER;
@@ -48,7 +51,6 @@
 import static com.android.server.notification.PreferencesHelper.NOTIFICATION_CHANNEL_COUNT_LIMIT;
 import static com.android.server.notification.PreferencesHelper.NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT;
 import static com.android.server.notification.PreferencesHelper.UNKNOWN_UID;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.assertNull;
@@ -102,6 +104,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.permission.PermissionManager;
 import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.provider.Settings.Secure;
@@ -185,6 +188,7 @@
     @Mock Context mContext;
     @Mock ZenModeHelper mMockZenModeHelper;
     @Mock AppOpsManager mAppOpsManager;
+    @Mock PermissionManager mPermissionManager;
 
     private NotificationManager.Policy mTestNotificationPolicy;
 
@@ -218,6 +222,8 @@
                 InstrumentationRegistry.getContext().getResources());
         when(mContext.getContentResolver()).thenReturn(
                 InstrumentationRegistry.getContext().getContentResolver());
+        when(mPm.getPermissionFlags(any(), any(), any()))
+                .thenReturn(PackageManager.FLAG_PERMISSION_USER_SET);
         when(mContext.getPackageManager()).thenReturn(mPm);
         when(mContext.getApplicationInfo()).thenReturn(legacy);
         // most tests assume badging is enabled
@@ -303,7 +309,8 @@
         mStatsEventBuilderFactory = new WrappedSysUiStatsEvent.WrappedBuilderFactory();
 
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
+                mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager,
+                mStatsEventBuilderFactory, false);
         resetZenModeHelper();
 
         mAudioAttributes = new AudioAttributes.Builder()
@@ -645,7 +652,7 @@
     @Test
     public void testReadXml_oldXml_migrates() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, true);
+                mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mStatsEventBuilderFactory, true);
 
         String xml = "<ranking version=\"2\">\n"
                 + "<package name=\"" + PKG_N_MR1 + "\" uid=\"" + UID_N_MR1
@@ -711,7 +718,7 @@
     @Test
     public void testReadXml_oldXml_backup_migratesWhenPkgInstalled() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
+                mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
 
         when(mPm.getPackageUidAsUser("pkg1", USER_SYSTEM)).thenReturn(UNKNOWN_UID);
         when(mPm.getPackageUidAsUser("pkg2", USER_SYSTEM)).thenReturn(UNKNOWN_UID);
@@ -789,7 +796,7 @@
     @Test
     public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, true);
+                mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mStatsEventBuilderFactory, true);
 
         String xml = "<ranking version=\"3\">\n"
                 + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
@@ -846,7 +853,7 @@
     @Test
     public void testReadXml_newXml_permissionNotificationOff() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
+                mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
 
         String xml = "<ranking version=\"3\">\n"
                 + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
@@ -903,7 +910,7 @@
     @Test
     public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, true);
+                mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mStatsEventBuilderFactory, true);
 
         String xml = "<ranking version=\"4\">\n"
                 + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n"
@@ -959,7 +966,7 @@
     @Test
     public void testReadXml_oldXml_migration_NoUid() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
+                mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
 
         when(mPm.getPackageUidAsUser("something", USER_SYSTEM)).thenReturn(UNKNOWN_UID);
         String xml = "<ranking version=\"2\">\n"
@@ -992,7 +999,7 @@
     @Test
     public void testReadXml_newXml_noMigration_NoUid() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
+                mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
 
         when(mPm.getPackageUidAsUser("something", USER_SYSTEM)).thenReturn(UNKNOWN_UID);
         String xml = "<ranking version=\"3\">\n"
@@ -1024,7 +1031,7 @@
     @Test
     public void testChannelXmlForNonBackup_postMigration() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
+                mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
 
         ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> appPermissions = new ArrayMap<>();
         appPermissions.put(new Pair<>(1, "first"), new Pair<>(true, false));
@@ -1110,7 +1117,7 @@
     @Test
     public void testChannelXmlForBackup_postMigration() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
+                mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
 
         ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> appPermissions = new ArrayMap<>();
         appPermissions.put(new Pair<>(1, "first"), new Pair<>(true, false));
@@ -1202,7 +1209,7 @@
     @Test
     public void testChannelXmlForBackup_postMigration_noExternal() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
+                mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
 
         ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> appPermissions = new ArrayMap<>();
         appPermissions.put(new Pair<>(UID_P, PKG_P), new Pair<>(true, false));
@@ -1287,7 +1294,7 @@
     @Test
     public void testChannelXmlForBackup_postMigration_noLocalSettings() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
+                mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
 
         ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> appPermissions = new ArrayMap<>();
         appPermissions.put(new Pair<>(1, "first"), new Pair<>(true, false));
@@ -1498,7 +1505,7 @@
                 new FileNotFoundException("")).thenReturn(resId);
 
         mHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
+                mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false);
 
         NotificationChannel channel =
                 new NotificationChannel("id", "name", IMPORTANCE_LOW);
@@ -2443,7 +2450,7 @@
                 NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
         when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy);
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
 
 
@@ -2474,7 +2481,7 @@
                 NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
         when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy);
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
 
         // create notification channel that can bypass dnd, but app is blocked
@@ -2498,7 +2505,7 @@
                 NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
         when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy);
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
 
         // create notification channel that can bypass dnd, but app is blocked
@@ -2552,7 +2559,7 @@
                 NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
         when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy);
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
         assertFalse(mHelper.areChannelsBypassingDnd());
         verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean());
@@ -2565,7 +2572,7 @@
         mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, 0, 0);
         when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy);
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
         assertFalse(mHelper.areChannelsBypassingDnd());
         verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyBoolean());
@@ -3668,7 +3675,7 @@
                 + "</package>\n"
                 + "</ranking>\n";
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
         loadByteArrayXml(preQXml.getBytes(), true, USER_SYSTEM);
 
@@ -3682,7 +3689,7 @@
 
         ByteArrayOutputStream baos = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL);
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
         loadStreamXml(baos, false, UserHandle.USER_ALL);
 
@@ -3752,7 +3759,7 @@
 
         ByteArrayOutputStream baos = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL);
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
         loadStreamXml(baos, false, UserHandle.USER_ALL);
 
@@ -3765,7 +3772,7 @@
 
         ByteArrayOutputStream baos = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL);
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
         loadStreamXml(baos, false, UserHandle.USER_ALL);
 
@@ -3779,7 +3786,7 @@
 
         ByteArrayOutputStream baos = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL);
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
         loadStreamXml(baos, false, UserHandle.USER_ALL);
 
@@ -3795,7 +3802,7 @@
 
         ByteArrayOutputStream baos = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL);
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
         loadStreamXml(baos, false, UserHandle.USER_ALL);
 
@@ -3851,7 +3858,7 @@
 
         ByteArrayOutputStream baos = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL);
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
         loadStreamXml(baos, false, UserHandle.USER_ALL);
 
@@ -3889,7 +3896,7 @@
 
         ByteArrayOutputStream baos = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL);
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
         loadStreamXml(baos, false, UserHandle.USER_ALL);
 
@@ -4547,7 +4554,7 @@
     @Test
     public void testPlaceholderConversationId_shortcutRequired() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
 
         final String xml = "<ranking version=\"1\">\n"
@@ -4567,7 +4574,7 @@
     @Test
     public void testNormalConversationId_shortcutRequired() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
 
         final String xml = "<ranking version=\"1\">\n"
@@ -4587,7 +4594,7 @@
     @Test
     public void testNoConversationId_shortcutRequired() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
 
         final String xml = "<ranking version=\"1\">\n"
@@ -4607,7 +4614,7 @@
     @Test
     public void testDeleted_noTime() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
 
         final String xml = "<ranking version=\"1\">\n"
@@ -4627,7 +4634,7 @@
     @Test
     public void testDeleted_twice() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
 
         mHelper.createNotificationChannel(
@@ -4642,7 +4649,7 @@
     @Test
     public void testDeleted_recentTime() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
 
         mHelper.createNotificationChannel(
@@ -4661,7 +4668,7 @@
                 null);
         parser.nextTag();
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
         mHelper.readXml(parser, true, USER_SYSTEM);
 
@@ -4673,7 +4680,7 @@
     @Test
     public void testUnDelete_time() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
 
         mHelper.createNotificationChannel(
@@ -4695,7 +4702,7 @@
     @Test
     public void testDeleted_longTime() throws Exception {
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
-                mPermissionHelper, mLogger,
+                mPermissionHelper, mPermissionManager, mLogger,
                 mAppOpsManager, mStatsEventBuilderFactory, false);
 
         long time = System.currentTimeMillis() - (DateUtils.DAY_IN_MILLIS * 30);
@@ -5464,4 +5471,93 @@
 
         verifyZeroInteractions(mHandler);
     }
+
+    @Test
+    public void testGetFsiState_flagDisabled_zero() {
+        final int fsiState = mHelper.getFsiState("pkg", /* uid= */ 0,
+                /* requestedFsiPermission */ true, /* isFlagEnabled= */ false);
+
+        assertEquals(0, fsiState);
+    }
+
+    @Test
+    public void testGetFsiState_appDidNotRequest_enumNotRequested() {
+        final int fsiState = mHelper.getFsiState("pkg", /* uid= */ 0,
+                /* requestedFsiPermission */ false, /* isFlagEnabled= */ true);
+
+        assertEquals(PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED, fsiState);
+    }
+
+    @Test
+    public void testGetFsiState_permissionGranted_enumGranted() {
+        when(mPermissionManager.checkPermissionForPreflight(any(), any()))
+                .thenReturn(PermissionManager.PERMISSION_GRANTED);
+
+        final int fsiState = mHelper.getFsiState("pkg", /* uid= */ 0,
+                /* requestedFsiPermission= */ true, /* isFlagEnabled= */ true);
+
+        assertEquals(PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED, fsiState);
+    }
+
+    @Test
+    public void testGetFsiState_permissionSoftDenied_enumDenied() {
+        when(mPermissionManager.checkPermissionForPreflight(any(), any()))
+                .thenReturn(PermissionManager.PERMISSION_SOFT_DENIED);
+
+        final int fsiState = mHelper.getFsiState("pkg", /* uid= */ 0,
+                /* requestedFsiPermission = */ true, /* isFlagEnabled= */ true);
+
+        assertEquals(PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED, fsiState);
+    }
+
+    @Test
+    public void testGetFsiState_permissionHardDenied_enumDenied() {
+        when(mPermissionManager.checkPermissionForPreflight(any(), any()))
+                .thenReturn(PermissionManager.PERMISSION_HARD_DENIED);
+
+        final int fsiState = mHelper.getFsiState("pkg", /* uid= */ 0,
+                /* requestedFsiPermission = */ true, /* isFlagEnabled= */ true);
+
+        assertEquals(PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED, fsiState);
+    }
+
+    @Test
+    public void testIsFsiPermissionUserSet_appDidNotRequest_false() {
+        final boolean isUserSet = mHelper.isFsiPermissionUserSet("pkg", /* uid= */ 0,
+                /* fsiState = */ PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED,
+                /* currentPermissionFlags= */ PackageManager.FLAG_PERMISSION_USER_SET,
+                /* isStickyHunFlagEnabled= */ true);
+
+        assertFalse(isUserSet);
+    }
+
+    @Test
+    public void testIsFsiPermissionUserSet_flagDisabled_false() {
+        final boolean isUserSet = mHelper.isFsiPermissionUserSet("pkg", /* uid= */ 0,
+                /* fsiState = */ PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED,
+                /* currentPermissionFlags= */ PackageManager.FLAG_PERMISSION_USER_SET,
+                /* isStickyHunFlagEnabled= */ false);
+
+        assertFalse(isUserSet);
+    }
+
+    @Test
+    public void testIsFsiPermissionUserSet_userSet_true() {
+        final boolean isUserSet = mHelper.isFsiPermissionUserSet("pkg", /* uid= */ 0,
+                /* fsiState = */ PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED,
+                /* currentPermissionFlags= */ PackageManager.FLAG_PERMISSION_USER_SET,
+                /* isStickyHunFlagEnabled= */ true);
+
+        assertTrue(isUserSet);
+    }
+
+    @Test
+    public void testIsFsiPermissionUserSet_notUserSet_false() {
+        final boolean isUserSet = mHelper.isFsiPermissionUserSet("pkg", /* uid= */ 0,
+                /* fsiState = */ PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED,
+                /* currentPermissionFlags= */ ~PackageManager.FLAG_PERMISSION_USER_SET,
+                /* isStickyHunFlagEnabled= */ true);
+
+        assertFalse(isUserSet);
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RateEstimatorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RateEstimatorTest.java
index 68aa1dc..131aaa0 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RateEstimatorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RateEstimatorTest.java
@@ -15,8 +15,9 @@
  */
 package com.android.server.notification;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.util.concurrent.TimeUnit.HOURS;
 
 import android.test.suitebuilder.annotation.SmallTest;
 
@@ -42,110 +43,120 @@
 
     @Test
     public void testRunningTimeBackwardDoesntExplodeUpdate() throws Exception {
-        assertUpdateTime(mTestStartTime);
-        assertUpdateTime(mTestStartTime - 1000L);
+        updateAndVerifyRate(mTestStartTime);
+        updateAndVerifyRate(mTestStartTime - 1000L);
     }
 
     @Test
     public void testRunningTimeBackwardDoesntExplodeGet() throws Exception {
-        assertUpdateTime(mTestStartTime);
+        updateAndVerifyRate(mTestStartTime);
         final float rate = mEstimator.getRate(mTestStartTime - 1000L);
-        assertFalse(Float.isInfinite(rate));
-        assertFalse(Float.isNaN(rate));
+        assertThat(rate).isFinite();
     }
 
     @Test
     public void testInstantaneousEventsDontExplodeUpdate() throws Exception {
-        assertUpdateTime(mTestStartTime);
-        assertUpdateTime(mTestStartTime);
+        updateAndVerifyRate(mTestStartTime);
+        updateAndVerifyRate(mTestStartTime);
     }
 
     @Test
     public void testInstantaneousEventsDontExplodeGet() throws Exception {
-        assertUpdateTime(mTestStartTime);
-        assertUpdateTime(mTestStartTime);
+        updateAndVerifyRate(mTestStartTime);
+        updateAndVerifyRate(mTestStartTime);
         final float rate = mEstimator.getRate(mTestStartTime);
-        assertFalse(Float.isInfinite(rate));
-        assertFalse(Float.isNaN(rate));
+        assertThat(rate).isFinite();
     }
 
     @Test
     public void testInstantaneousBurstIsEstimatedUnderTwoPercent() throws Exception {
-        assertUpdateTime(mTestStartTime);
+        updateAndVerifyRate(mTestStartTime);
         long eventStart = mTestStartTime + 1000; // start event a long time after initialization
         long nextEventTime = postEvents(eventStart, 0, 5); // five events at \inf
         final float rate = mEstimator.getRate(nextEventTime);
-        assertLessThan("Rate", rate, 20f);
+        assertThat(rate).isLessThan(20f);
     }
 
     @Test
     public void testCompactBurstIsEstimatedUnderTwoPercent() throws Exception {
-        assertUpdateTime(mTestStartTime);
+        updateAndVerifyRate(mTestStartTime);
         long eventStart = mTestStartTime + 1000; // start event a long time after initialization
         long nextEventTime = postEvents(eventStart, 1, 5); // five events at 1000Hz
         final float rate = mEstimator.getRate(nextEventTime);
-        assertLessThan("Rate", rate, 20f);
+        assertThat(rate).isLessThan(20f);
     }
 
     @Test
     public void testSustained1000HzBurstIsEstimatedOverNinetyPercent() throws Exception {
-        assertUpdateTime(mTestStartTime);
+        updateAndVerifyRate(mTestStartTime);
         long eventStart = mTestStartTime + 1000; // start event a long time after initialization
         long nextEventTime = postEvents(eventStart, 1, 100); // one hundred events at 1000Hz
         final float rate = mEstimator.getRate(nextEventTime);
-        assertGreaterThan("Rate", rate, 900f);
+        assertThat(rate).isGreaterThan(900f);
     }
 
     @Test
     public void testSustained100HzBurstIsEstimatedOverNinetyPercent() throws Exception {
-        assertUpdateTime(mTestStartTime);
+        updateAndVerifyRate(mTestStartTime);
         long eventStart = mTestStartTime + 1000; // start event a long time after initialization
         long nextEventTime = postEvents(eventStart, 10, 100); // one hundred events at 100Hz
         final float rate = mEstimator.getRate(nextEventTime);
 
-        assertGreaterThan("Rate", rate, 90f);
+        assertThat(rate).isGreaterThan(90f);
     }
 
     @Test
     public void testRecoverQuicklyAfterSustainedBurst() throws Exception {
-        assertUpdateTime(mTestStartTime);
+        updateAndVerifyRate(mTestStartTime);
         long eventStart = mTestStartTime + 1000; // start event a long time after initialization
-        long nextEventTime = postEvents(eventStart, 10, 1000); // one hundred events at 100Hz
-        final float rate = mEstimator.getRate(nextEventTime + 5000L); // two seconds later
-        assertLessThan("Rate", rate, 2f);
+        long nextEventTime = postEvents(eventStart, 10, 1000); // one thousand events at 100Hz
+        final float rate = mEstimator.getRate(nextEventTime + 5000L); // five seconds later
+        assertThat(rate).isLessThan(2f);
     }
 
     @Test
     public void testEstimateShouldNotOvershoot() throws Exception {
-        assertUpdateTime(mTestStartTime);
+        updateAndVerifyRate(mTestStartTime);
         long eventStart = mTestStartTime + 1000; // start event a long time after initialization
-        long nextEventTime = postEvents(eventStart, 1, 1000); // one thousand events at 1000Hz
+        long nextEventTime = postEvents(eventStart, 1, 5000); // five thousand events at 1000Hz
         final float rate = mEstimator.getRate(nextEventTime);
-        assertLessThan("Rate", rate, 1000f);
+        assertThat(rate).isAtMost(1000f);
     }
 
     @Test
     public void testGetRateWithoutUpdate() throws Exception {
         final float rate = mEstimator.getRate(mTestStartTime);
-        assertLessThan("Rate", rate, 0.1f);
+        assertThat(rate).isLessThan(0.1f);
     }
 
     @Test
     public void testGetRateWithOneUpdate() throws Exception {
-        assertUpdateTime(mTestStartTime);
+        updateAndVerifyRate(mTestStartTime);
         final float rate = mEstimator.getRate(mTestStartTime+1);
-        assertLessThan("Rate", rate, 1f);
+        assertThat(rate).isLessThan(1f);
     }
 
-    private void assertLessThan(String label, float a, float b)  {
-        assertTrue(String.format("%s was %f, but should be less than %f", label, a, b), a <= b);
+    @Test
+    public void testEstimateCatchesUpQuickly() {
+        long nextEventTime = postEvents(mTestStartTime, 100, 30); // 30 events at 10Hz
+
+        final float firstBurstRate = mEstimator.getRate(nextEventTime);
+        assertThat(firstBurstRate).isWithin(2f).of(10);
+
+        nextEventTime += HOURS.toMillis(3); // 3 hours later...
+        nextEventTime = postEvents(nextEventTime, 100, 30); // same burst of 30 events at 10Hz
+
+        // Catching up. Rate is not yet 10, since we had a long period of inactivity...
+        float secondBurstRate = mEstimator.getRate(nextEventTime);
+        assertThat(secondBurstRate).isWithin(1f).of(6);
+
+        // ... but after a few more events, we are there.
+        nextEventTime = postEvents(nextEventTime, 100, 10); // 10 more events at 10Hz
+        secondBurstRate = mEstimator.getRate(nextEventTime);
+        assertThat(secondBurstRate).isWithin(1f).of(10);
     }
 
-    private void assertGreaterThan(String label, float a, float b)  {
-        assertTrue(String.format("%s was %f, but should be more than %f", label, a, b), a >= b);
-    }
-
-    /** @returns the next event time. */
+    /** @return the next event time. */
     private long postEvents(long start, long dt, int num) {
         long time = start;
         for (int i = 0; i < num; i++) {
@@ -155,9 +166,8 @@
         return time;
     }
 
-    private void assertUpdateTime(long time) {
-        final float rate = mEstimator.update(time);
-        assertFalse(Float.isInfinite(rate));
-        assertFalse(Float.isNaN(rate));
+    private void updateAndVerifyRate(long time) {
+        mEstimator.update(time);
+        assertThat(mEstimator.getRate(time)).isFinite();
     }
-}
+}
\ No newline at end of file
diff --git a/services/tests/wmtests/Android.bp b/services/tests/wmtests/Android.bp
index e5371975..5f48f3c 100644
--- a/services/tests/wmtests/Android.bp
+++ b/services/tests/wmtests/Android.bp
@@ -82,7 +82,10 @@
     ],
 
     platform_apis: true,
-    test_suites: ["device-tests"],
+    test_suites: [
+        "device-tests",
+        "automotive-tests",
+    ],
 
     certificate: "platform",
 
diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml
index 37e5da5..9c79375 100644
--- a/services/tests/wmtests/AndroidManifest.xml
+++ b/services/tests/wmtests/AndroidManifest.xml
@@ -96,6 +96,19 @@
             android:theme="@style/WhiteBackgroundTheme"
             android:exported="true"/>
 
+        <activity android:name="com.android.server.wm.TrustedPresentationCallbackTest$TestActivity"
+            android:exported="true"
+            android:showWhenLocked="true"
+            android:turnScreenOn="true" />
+
+        <activity
+            android:name="androidx.test.core.app.InstrumentationActivityInvoker$EmptyActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
+
         <service android:name="android.view.cts.surfacevalidator.LocalMediaProjectionService"
             android:foregroundServiceType="mediaProjection"
             android:enabled="true">
diff --git a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java
index 5863e9d..6a9f283 100644
--- a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java
@@ -58,9 +58,20 @@
         mPhoneWindowManager.overrideCanStartDreaming(true);
         sendKey(KEYCODE_POWER);
         mPhoneWindowManager.assertDreamRequest();
+        mPhoneWindowManager.overrideIsDreaming(true);
         mPhoneWindowManager.assertLockedAfterAppTransitionFinished();
     }
 
+    @Test
+    public void testAppTransitionFinishedCalledAfterDreamStoppedWillNotLockAgain() {
+        mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_DREAM_OR_SLEEP);
+        mPhoneWindowManager.overrideCanStartDreaming(true);
+        sendKey(KEYCODE_POWER);
+        mPhoneWindowManager.assertDreamRequest();
+        mPhoneWindowManager.overrideIsDreaming(false);
+        mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished();
+    }
+
     /**
      * Power double-press to launch camera does not lock device when the single press behavior is to
      * dream.
@@ -72,7 +83,7 @@
         sendKey(KEYCODE_POWER);
         sendKey(KEYCODE_POWER);
         mPhoneWindowManager.assertCameraLaunch();
-        mPhoneWindowManager.assertWillNotLockAfterAppTransitionFinished();
+        mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished();
     }
 
     /**
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index 676bfb0..2015ae9 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -39,6 +39,7 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
 import static com.android.server.policy.WindowManagerPolicy.ACTION_PASS_TO_USER;
 
 import static java.util.Collections.unmodifiableMap;
@@ -59,7 +60,7 @@
 
 class ShortcutKeyTestBase {
     TestPhoneWindowManager mPhoneWindowManager;
-    final Context mContext = getInstrumentation().getTargetContext();
+    final Context mContext = spy(getInstrumentation().getTargetContext());
 
     /** Modifier key to meta state */
     private static final Map<Integer, Integer> MODIFIER;
diff --git a/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java
index 280afba..3bb86a7 100644
--- a/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java
@@ -316,4 +316,16 @@
         pressKey(KEYCODE_POWER, 0 /* pressTime */);
         assertTrue(mShortPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS));
     }
+
+    // Verify short press should not be triggered if no very long press behavior defined but the
+    // press time exceeded the very long press timeout.
+    @Test
+    public void testTimeoutExceedVeryLongPress() throws InterruptedException {
+        mVeryLongPressOnPowerBehavior = false;
+
+        pressKey(KEYCODE_POWER, mVeryLongPressTime + 50);
+        assertTrue(mLongPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS));
+        assertEquals(mVeryLongPressed.getCount(), 1);
+        assertEquals(mShortPressed.getCount(), 1);
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
new file mode 100644
index 0000000..fe8017e
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.policy;
+
+import static android.view.KeyEvent.KEYCODE_STEM_PRIMARY;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+/**
+ * Test class for stem key gesture.
+ *
+ * Build/Install/Run:
+ * atest WmTests:StemKeyGestureTests
+ */
+public class StemKeyGestureTests extends ShortcutKeyTestBase {
+    @Mock private Resources mResources;
+
+    /**
+     * Stem single key should not launch behavior during set up.
+     */
+    @Test
+    public void stemSingleKey_duringSetup_doNothing() {
+        stemKeySetup(
+                () -> overrideBehavior(
+                        com.android.internal.R.integer.config_shortPressOnStemPrimaryBehavior,
+                        SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS));
+        mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false);
+        mPhoneWindowManager.overrideIsUserSetupComplete(false);
+
+        sendKey(KEYCODE_STEM_PRIMARY);
+
+        mPhoneWindowManager.assertNotOpenAllAppView();
+    }
+
+    /**
+     * Stem single key should launch all app after set up.
+     */
+    @Test
+    public void stemSingleKey_AfterSetup_openAllApp() {
+        stemKeySetup(
+                () -> overrideBehavior(
+                        com.android.internal.R.integer.config_shortPressOnStemPrimaryBehavior,
+                        SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS));
+        mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false);
+        mPhoneWindowManager.overrideIsUserSetupComplete(true);
+
+        sendKey(KEYCODE_STEM_PRIMARY);
+
+        mPhoneWindowManager.assertOpenAllAppView();
+    }
+
+    private void stemKeySetup(Runnable behaviorOverrideRunnable) {
+        super.tearDown();
+        setupResourcesMock();
+        behaviorOverrideRunnable.run();
+        super.setUp();
+    }
+
+    private void setupResourcesMock() {
+        Resources realResources = mContext.getResources();
+
+        mResources = Mockito.mock(Resources.class);
+        doReturn(mResources).when(mContext).getResources();
+
+        doAnswer(invocation -> realResources.getXml((Integer) invocation.getArguments()[0]))
+                .when(mResources).getXml(anyInt());
+        doAnswer(invocation -> realResources.getString((Integer) invocation.getArguments()[0]))
+                .when(mResources).getString(anyInt());
+        doAnswer(invocation -> realResources.getBoolean((Integer) invocation.getArguments()[0]))
+                .when(mResources).getBoolean(anyInt());
+    }
+
+    private void overrideBehavior(int resId, int expectedBehavior) {
+        doReturn(expectedBehavior).when(mResources).getInteger(eq(resId));
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index d383024..766a88f 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -16,6 +16,7 @@
 
 package com.android.server.policy;
 
+import static android.os.Build.HW_TIMEOUT_MULTIPLIER;
 import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.STATE_ON;
@@ -43,8 +44,11 @@
 import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE;
 
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.after;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mockingDetails;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.withSettings;
@@ -61,9 +65,11 @@
 import android.media.AudioManagerInternal;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.IBinder;
 import android.os.PowerManager;
 import android.os.PowerManagerInternal;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.os.Vibrator;
 import android.service.dreams.DreamManagerInternal;
 import android.telecom.TelecomManager;
@@ -79,6 +85,7 @@
 import com.android.server.input.InputManagerInternal;
 import com.android.server.inputmethod.InputMethodManagerInternal;
 import com.android.server.pm.UserManagerInternal;
+import com.android.server.policy.keyguard.KeyguardServiceDelegate;
 import com.android.server.statusbar.StatusBarManagerInternal;
 import com.android.server.vr.VrManagerInternal;
 import com.android.server.wm.ActivityTaskManagerInternal;
@@ -100,6 +107,8 @@
 
 class TestPhoneWindowManager {
     private static final long SHORTCUT_KEY_DELAY_MILLIS = 150;
+    private static final long TEST_SINGLE_KEY_DELAY_MILLIS
+            = SingleKeyGestureDetector.MULTI_PRESS_TIMEOUT + 1000L * HW_TIMEOUT_MULTIPLIER;
 
     private PhoneWindowManager mPhoneWindowManager;
     private Context mContext;
@@ -134,6 +143,8 @@
 
     @Mock private StatusBarManagerInternal mStatusBarManagerInternal;
 
+    @Mock private KeyguardServiceDelegate mKeyguardServiceDelegate;
+
     private StaticMockitoSession mMockitoSession;
     private HandlerThread mHandlerThread;
     private Handler mHandler;
@@ -151,6 +162,10 @@
         Supplier<GlobalActions> getGlobalActionsFactory() {
             return () -> mGlobalActions;
         }
+
+        KeyguardServiceDelegate getKeyguardServiceDelegate() {
+            return mKeyguardServiceDelegate;
+        }
     }
 
     TestPhoneWindowManager(Context context) {
@@ -158,12 +173,12 @@
         mHandlerThread = new HandlerThread("fake window manager");
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
-        mHandler.runWithScissors(()-> setUp(context),  0 /* timeout */);
+        mContext = mockingDetails(context).isSpy() ? context : spy(context);
+        mHandler.runWithScissors(this::setUp,  0 /* timeout */);
     }
 
-    private void setUp(Context context) {
+    private void setUp() {
         mPhoneWindowManager = spy(new PhoneWindowManager());
-        mContext = spy(context);
 
         // Use stubOnly() to reduce memory usage if it doesn't need verification.
         final MockSettings spyStubOnly = withSettings().stubOnly()
@@ -251,6 +266,7 @@
         overrideLaunchAccessibility();
         doReturn(false).when(mPhoneWindowManager).keyguardOn();
         doNothing().when(mContext).startActivityAsUser(any(), any());
+        doNothing().when(mContext).startActivityAsUser(any(), any(), any());
         Mockito.reset(mContext);
     }
 
@@ -329,6 +345,10 @@
         doReturn(canDream).when(mDreamManagerInternal).canStartDreaming(anyBoolean());
     }
 
+    void overrideIsDreaming(boolean isDreaming) {
+        doReturn(isDreaming).when(mDreamManagerInternal).isDreaming();
+    }
+
     void overrideDisplayState(int state) {
         doReturn(state).when(mDisplay).getState();
         doReturn(state == STATE_ON).when(mDisplayPolicy).isAwake();
@@ -381,6 +401,14 @@
         doNothing().when(mPhoneWindowManager).launchHomeFromHotKey(anyInt());
     }
 
+    void overrideIsUserSetupComplete(boolean isCompleted) {
+        doReturn(isCompleted).when(mPhoneWindowManager).isUserSetupComplete();
+    }
+
+    void setKeyguardServiceDelegateIsShowing(boolean isShowing) {
+        doReturn(isShowing).when(mKeyguardServiceDelegate).isShowing();
+    }
+
     /**
      * Below functions will check the policy behavior could be invoked.
      */
@@ -497,21 +525,42 @@
         verify(mInputManagerInternal).toggleCapsLock(anyInt());
     }
 
-    void assertWillNotLockAfterAppTransitionFinished() {
-        Assert.assertFalse(mPhoneWindowManager.mLockAfterAppTransitionFinished);
-    }
-
     void assertLockedAfterAppTransitionFinished() {
         ArgumentCaptor<AppTransitionListener> transitionCaptor =
                 ArgumentCaptor.forClass(AppTransitionListener.class);
         verify(mWindowManagerInternal).registerAppTransitionListener(
                 transitionCaptor.capture());
-        transitionCaptor.getValue().onAppTransitionFinishedLocked(any());
+        final IBinder token = mock(IBinder.class);
+        transitionCaptor.getValue().onAppTransitionFinishedLocked(token);
         verify(mPhoneWindowManager).lockNow(null);
     }
 
+    void assertDidNotLockAfterAppTransitionFinished() {
+        ArgumentCaptor<AppTransitionListener> transitionCaptor =
+                ArgumentCaptor.forClass(AppTransitionListener.class);
+        verify(mWindowManagerInternal).registerAppTransitionListener(
+                transitionCaptor.capture());
+        final IBinder token = mock(IBinder.class);
+        transitionCaptor.getValue().onAppTransitionFinishedLocked(token);
+        verify(mPhoneWindowManager, never()).lockNow(null);
+    }
+
     void assertGoToHomescreen() {
         waitForIdle();
         verify(mPhoneWindowManager).launchHomeFromHotKey(anyInt());
     }
+
+    void assertOpenAllAppView() {
+        waitForIdle();
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext, timeout(TEST_SINGLE_KEY_DELAY_MILLIS))
+                .startActivityAsUser(intentCaptor.capture(), isNull(), any(UserHandle.class));
+        Assert.assertEquals(Intent.ACTION_ALL_APPS, intentCaptor.getValue().getAction());
+    }
+
+    void assertNotOpenAllAppView() {
+        waitForIdle();
+        verify(mContext, after(TEST_SINGLE_KEY_DELAY_MILLIS).never())
+                .startActivityAsUser(any(Intent.class), any(), any(UserHandle.class));
+    }
 }
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 e881039..3db53eb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3334,7 +3334,7 @@
 
         // app1 requests IME visible.
         app1.setRequestedVisibleTypes(ime(), ime());
-        mDisplayContent.getInsetsStateController().onInsetsModified(app1);
+        mDisplayContent.getInsetsStateController().onRequestedVisibleTypesChanged(app1);
 
         // Verify app1's IME insets is visible and app2's IME insets frozen flag set.
         assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible());
@@ -3403,7 +3403,7 @@
         assertFalse(activity2.mImeInsetsFrozenUntilStartInput);
 
         app1.setRequestedVisibleTypes(ime());
-        controller.onInsetsModified(app1);
+        controller.onRequestedVisibleTypesChanged(app1);
 
         // Expect all activities in split-screen will get IME insets visible state
         assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible());
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 2b589bf..d179338 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -1702,6 +1702,9 @@
 
     @Test
     public void testRecordActivityMovementBeforeDeliverToTop() {
+        // Mock recents as task is only marked to be in recents
+        mAtm.mTaskSupervisor.setRecentTasks(mock(RecentTasks.class));
+
         final Task task = new TaskBuilder(mAtm.mTaskSupervisor).build();
         final ActivityRecord activityBot = new ActivityBuilder(mAtm).setTask(task).build();
         final ActivityRecord activityTop = new ActivityBuilder(mAtm).setTask(task).build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index 3a456fb..dc4e47d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.util.DisplayMetrics.DENSITY_DEFAULT;
 
@@ -80,13 +81,27 @@
     }
 
     @Test
-    public void testReturnsSkipIfTaskNotUsingActivityTypeStandard() {
+    public void testReturnsSkipIfTaskNotUsingActivityTypeStandardOrUndefined() {
         final Task task = new TaskBuilder(mSupervisor).setActivityType(
                 ACTIVITY_TYPE_ASSISTANT).build();
         assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(task).calculate());
     }
 
     @Test
+    public void testReturnsDoneIfTaskUsingActivityTypeStandard() {
+        final Task task = new TaskBuilder(mSupervisor).setActivityType(
+                ACTIVITY_TYPE_STANDARD).build();
+        assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate());
+    }
+
+    @Test
+    public void testReturnsDoneIfTaskUsingActivityTypeUndefined() {
+        final Task task = new TaskBuilder(mSupervisor).setActivityType(
+                ACTIVITY_TYPE_UNDEFINED).build();
+        assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate());
+    }
+
+    @Test
     public void testReturnsSkipIfCurrentParamsHasBounds() {
         final Task task = new TaskBuilder(mSupervisor).setActivityType(
                 ACTIVITY_TYPE_STANDARD).build();
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 340b591..bdd178b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -797,6 +797,7 @@
         final int baseDensity = 320;
         final float baseXDpi = 60;
         final float baseYDpi = 60;
+        final int originalMinTaskSizeDp = displayContent.mMinSizeOfResizeableTaskDp;
 
         displayContent.mInitialDisplayWidth = baseWidth;
         displayContent.mInitialDisplayHeight = baseHeight;
@@ -814,6 +815,9 @@
         displayContent.setForcedDensity(forcedDensity, 0 /* userId */);
         verifySizes(displayContent, baseWidth, baseHeight, forcedDensity);
 
+        // Verify that minimal task size (dp) doesn't change with density of display.
+        assertEquals(originalMinTaskSizeDp, displayContent.mMinSizeOfResizeableTaskDp);
+
         // Verify that forcing resolution won't affect the already forced density.
         displayContent.setForcedSize(1800, 1200);
         verifySizes(displayContent, 1800, 1200, forcedDensity);
@@ -1811,30 +1815,6 @@
         assertFalse(mDisplayContent.hasTopFixedRotationLaunchingApp());
     }
 
-    /**
-     * Creates different types of displays, verifies that minimal task size doesn't change
-     * with density of display.
-     */
-    @Test
-    public void testCalculatesDisplaySpecificMinTaskSizes() {
-        DisplayContent defaultTestDisplay =
-                new TestDisplayContent.Builder(mAtm, 1000, 2000).build();
-        final int defaultMinTaskSize = defaultTestDisplay.mMinSizeOfResizeableTaskDp;
-        DisplayContent firstDisplay = new TestDisplayContent.Builder(mAtm, 1000, 2000)
-                .setDensityDpi(300)
-                .updateDisplayMetrics()
-                .setDefaultMinTaskSizeDp(defaultMinTaskSize + 10)
-                .build();
-        assertEquals(defaultMinTaskSize + 10, firstDisplay.mMinSizeOfResizeableTaskDp);
-
-        DisplayContent secondDisplay = new TestDisplayContent.Builder(mAtm, 200, 200)
-                .setDensityDpi(320)
-                .updateDisplayMetrics()
-                .setDefaultMinTaskSizeDp(defaultMinTaskSize + 20)
-                .build();
-        assertEquals(defaultMinTaskSize + 20, secondDisplay.mMinSizeOfResizeableTaskDp);
-    }
-
     @Test
     public void testRecentsNotRotatingWithFixedRotation() {
         unblockDisplayRotation(mDisplayContent);
@@ -2066,6 +2046,7 @@
 
         // Once transition starts, rotation is applied and transition shows DC rotating.
         testPlayer.startTransition();
+        waitUntilHandlersIdle();
         assertNotEquals(origRot, dc.getConfiguration().windowConfiguration.getRotation());
         assertNotNull(testPlayer.mLastReady);
         assertTrue(testPlayer.mController.isPlaying());
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index b0609dd..dd90e04 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -182,6 +182,44 @@
                 dimmingNonImTarget, imeNonDrawNavBar, NAV_BAR_BOTTOM));
     }
 
+    @Test
+    public void testChooseNavigationBackgroundWindow() {
+        final WindowState drawBarWin = createOpaqueFullscreen(false);
+        final WindowState nonDrawBarWin = createDimmingDialogWindow(true);
+
+        final WindowState visibleIme = createInputMethodWindow(true, true, false);
+        final WindowState invisibleIme = createInputMethodWindow(false, true, false);
+        final WindowState nonDrawBarIme = createInputMethodWindow(true, false, false);
+
+        assertEquals(drawBarWin, DisplayPolicy.chooseNavigationBackgroundWindow(
+                drawBarWin, null, NAV_BAR_BOTTOM));
+        assertNull(DisplayPolicy.chooseNavigationBackgroundWindow(
+                null, null, NAV_BAR_BOTTOM));
+        assertNull(DisplayPolicy.chooseNavigationBackgroundWindow(
+                nonDrawBarWin, null, NAV_BAR_BOTTOM));
+
+        assertEquals(visibleIme, DisplayPolicy.chooseNavigationBackgroundWindow(
+                drawBarWin, visibleIme, NAV_BAR_BOTTOM));
+        assertEquals(visibleIme, DisplayPolicy.chooseNavigationBackgroundWindow(
+                null, visibleIme, NAV_BAR_BOTTOM));
+        assertEquals(visibleIme, DisplayPolicy.chooseNavigationBackgroundWindow(
+                nonDrawBarWin, visibleIme, NAV_BAR_BOTTOM));
+
+        assertEquals(drawBarWin, DisplayPolicy.chooseNavigationBackgroundWindow(
+                drawBarWin, invisibleIme, NAV_BAR_BOTTOM));
+        assertNull(DisplayPolicy.chooseNavigationBackgroundWindow(
+                null, invisibleIme, NAV_BAR_BOTTOM));
+        assertNull(DisplayPolicy.chooseNavigationBackgroundWindow(
+                nonDrawBarWin, invisibleIme, NAV_BAR_BOTTOM));
+
+        assertEquals(drawBarWin, DisplayPolicy.chooseNavigationBackgroundWindow(
+                drawBarWin, nonDrawBarIme, NAV_BAR_BOTTOM));
+        assertNull(DisplayPolicy.chooseNavigationBackgroundWindow(
+                null, nonDrawBarIme, NAV_BAR_BOTTOM));
+        assertNull(DisplayPolicy.chooseNavigationBackgroundWindow(
+                nonDrawBarWin, nonDrawBarIme, NAV_BAR_BOTTOM));
+    }
+
     @SetupWindows(addWindows = W_NAVIGATION_BAR)
     @Test
     public void testUpdateLightNavigationBarLw() {
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 8e015d4..769a309 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -103,8 +103,7 @@
     public void setUp() throws Exception {
         mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration;
         spyOn(mLetterboxConfiguration);
-        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
-                    /* checkDeviceConfig */ anyBoolean()))
+        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled())
                 .thenReturn(true);
         when(mLetterboxConfiguration.isCameraCompatRefreshEnabled())
                 .thenReturn(true);
@@ -177,8 +176,7 @@
 
     @Test
     public void testOnScreenRotationAnimationFinished_treatmentNotEnabled_doNotShowToast() {
-        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
-                    /* checkDeviceConfig */ anyBoolean()))
+        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled())
                 .thenReturn(false);
         spyOn(mDisplayRotationCompatPolicy);
 
@@ -238,8 +236,7 @@
 
     @Test
     public void testTreatmentNotEnabled_noForceRotationOrRefresh() throws Exception {
-        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
-                    /* checkDeviceConfig */ anyBoolean()))
+        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled())
                 .thenReturn(false);
 
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -253,8 +250,7 @@
 
     @Test
     public void testTreatmentDisabledViaDeviceConfig_noForceRotationOrRefresh() throws Exception {
-        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
-                    /* checkDeviceConfig */ true))
+        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled())
                 .thenReturn(false);
 
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
index d29b18f..b105703 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
@@ -31,7 +31,6 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 
 import android.platform.test.annotations.Presubmit;
 import android.view.Surface;
@@ -79,8 +78,11 @@
         when(mDisplayContent.getIgnoreOrientationRequest()).thenReturn(true);
 
         mMockLetterboxConfiguration = mock(LetterboxConfiguration.class);
-        when(mMockLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled(
-                /* checkDeviceConfig */ anyBoolean())).thenReturn(true);
+        when(mMockLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled())
+                .thenReturn(true);
+        when(mMockLetterboxConfiguration
+                .isDisplayRotationImmersiveAppCompatPolicyEnabledAtBuildTime())
+                    .thenReturn(true);
 
         mPolicy = DisplayRotationImmersiveAppCompatPolicy.createIfNeeded(
                 mMockLetterboxConfiguration, createDisplayRotationMock(),
@@ -204,8 +206,8 @@
 
     @Test
     public void testRotationChoiceEnforcedOnly_featureFlagDisabled_lockNotEnforced() {
-        when(mMockLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled(
-                /* checkDeviceConfig */ true)).thenReturn(false);
+        when(mMockLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled())
+                .thenReturn(false);
 
         assertIsRotationLockEnforcedReturnsFalseForAllRotations();
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
index a8fc25f..994dcf1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
@@ -31,17 +31,12 @@
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL;
 
-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.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.verify;
 
 import android.app.StatusBarManager;
@@ -268,8 +263,6 @@
         navBar.setHasSurface(true);
         navBarProvider.setServerVisible(true);
         final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();
-        spyOn(policy);
-        doNothing().when(policy).startAnimation(anyBoolean(), any());
 
         // Make both system bars invisible.
         mAppWindow.setRequestedVisibleTypes(
@@ -305,8 +298,6 @@
         addNavigationBar().getControllableInsetProvider().setServerVisible(true);
 
         final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();
-        spyOn(policy);
-        doNothing().when(policy).startAnimation(anyBoolean(), any());
         policy.updateBarControlTarget(mAppWindow);
         policy.showTransient(navigationBars() | statusBars(),
                 true /* isGestureOnSystemBar */);
@@ -341,8 +332,6 @@
         mAppWindow.mAboveInsetsState.addSource(navBarSource);
         mAppWindow.mAboveInsetsState.addSource(statusBarSource);
         final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();
-        spyOn(policy);
-        doNothing().when(policy).startAnimation(anyBoolean(), any());
         policy.updateBarControlTarget(mAppWindow);
         policy.showTransient(navigationBars() | statusBars(),
                 true /* isGestureOnSystemBar */);
@@ -370,7 +359,7 @@
 
         mAppWindow.setRequestedVisibleTypes(
                 navigationBars() | statusBars(), navigationBars() | statusBars());
-        policy.onInsetsModified(mAppWindow);
+        policy.onRequestedVisibleTypesChanged(mAppWindow);
         waitUntilWindowAnimatorIdle();
 
         controls = mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow);
@@ -390,8 +379,6 @@
         final WindowState app2 = addWindow(TYPE_APPLICATION, "app");
 
         final InsetsPolicy policy = mDisplayContent.getInsetsPolicy();
-        spyOn(policy);
-        doNothing().when(policy).startAnimation(anyBoolean(), any());
         policy.updateBarControlTarget(app);
         policy.showTransient(navigationBars() | statusBars(),
                 true /* isGestureOnSystemBar */);
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
index d7e736d..114796d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
@@ -192,17 +192,16 @@
         // This can be the IME z-order target while app cannot be the IME z-order target.
         // This is also the only IME control target in this test, so IME won't be invisible caused
         // by the control-target change.
-        mDisplayContent.updateImeInputAndControlTarget(
-                createWindow(null, TYPE_APPLICATION, "base"));
+        final WindowState base = createWindow(null, TYPE_APPLICATION, "base");
+        mDisplayContent.updateImeInputAndControlTarget(base);
 
         // Make IME and stay visible during the test.
         mImeWindow.setHasSurface(true);
         getController().getOrCreateSourceProvider(ID_IME, ime())
                 .setWindowContainer(mImeWindow, null, null);
-        getController().onImeControlTargetChanged(
-                mDisplayContent.getImeInputTarget().getWindowState());
-        mDisplayContent.getImeInputTarget().getWindowState().setRequestedVisibleTypes(ime(), ime());
-        getController().onInsetsModified(mDisplayContent.getImeInputTarget().getWindowState());
+        getController().onImeControlTargetChanged(base);
+        base.setRequestedVisibleTypes(ime(), ime());
+        getController().onRequestedVisibleTypesChanged(base);
 
         // Send our spy window (app) into the system so that we can detect the invocation.
         final WindowState win = createWindow(null, TYPE_APPLICATION, "app");
@@ -485,7 +484,7 @@
         mDisplayContent.updateImeInputAndControlTarget(app);
 
         app.setRequestedVisibleTypes(ime(), ime());
-        getController().onInsetsModified(app);
+        getController().onRequestedVisibleTypesChanged(app);
         assertTrue(ime.getControllableInsetProvider().getSource().isVisible());
 
         getController().updateAboveInsetsState(true /* notifyInsetsChange */);
diff --git a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
index 7cb7c79d..2b19ad9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
@@ -107,6 +107,9 @@
                 InstrumentationRegistry.getInstrumentation().getContext().getCacheDir();
         mFolder = new File(cacheFolder, "launch_params_tests");
         deleteRecursively(mFolder);
+        mFolder.mkdir();
+        mUserFolderGetter.apply(TEST_USER_ID).mkdir();
+        mUserFolderGetter.apply(ALTERNATIVE_USER_ID).mkdir();
 
         mDisplayUniqueId = "test:" + sNextUniqueId++;
         mTestDisplay = new TestDisplayContent.Builder(mAtm, 1000, 1500)
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationDeviceConfigTests.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationDeviceConfigTests.java
deleted file mode 100644
index 2b7a06b..0000000
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationDeviceConfigTests.java
+++ /dev/null
@@ -1,109 +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.server.wm;
-
-import static com.android.server.wm.LetterboxConfigurationDeviceConfig.sKeyToDefaultValueMap;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import android.platform.test.annotations.Presubmit;
-import android.provider.DeviceConfig;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.modules.utils.testing.TestableDeviceConfig;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-import java.util.Map;
-
-/**
- * Test class for {@link LetterboxConfigurationDeviceConfig}.
- *
- * atest WmTests:LetterboxConfigurationDeviceConfigTests
- */
-@SmallTest
-@Presubmit
-public class LetterboxConfigurationDeviceConfigTests {
-
-    private LetterboxConfigurationDeviceConfig mDeviceConfig;
-
-    @Rule
-    public final TestableDeviceConfig.TestableDeviceConfigRule
-            mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule();
-
-    @Before
-    public void setUp() {
-        mDeviceConfig = new LetterboxConfigurationDeviceConfig(/* executor */ Runnable::run);
-    }
-
-    @Test
-    public void testGetFlag_flagIsActive_flagChanges() throws Throwable {
-        for (Map.Entry<String, Boolean> entry : sKeyToDefaultValueMap.entrySet()) {
-            testGetFlagForKey_flagIsActive_flagChanges(entry.getKey(), entry.getValue());
-        }
-    }
-
-    private void testGetFlagForKey_flagIsActive_flagChanges(final String key, boolean defaultValue)
-            throws InterruptedException {
-        mDeviceConfig.updateFlagActiveStatus(/* isActive */ true, key);
-
-        assertEquals("Unexpected default value for " + key,
-                mDeviceConfig.getFlag(key), defaultValue);
-
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, key,
-                /* value */ Boolean.TRUE.toString(), /* makeDefault */ false);
-
-        assertTrue("Flag " + key + "is not true after change", mDeviceConfig.getFlag(key));
-
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, key,
-                /* value */ Boolean.FALSE.toString(), /* makeDefault */ false);
-
-        assertFalse("Flag " + key + "is not false after change", mDeviceConfig.getFlag(key));
-    }
-
-    @Test
-    public void testGetFlag_flagIsNotActive_alwaysReturnDefaultValue() throws Throwable {
-        for (Map.Entry<String, Boolean> entry : sKeyToDefaultValueMap.entrySet()) {
-            testGetFlagForKey_flagIsNotActive_alwaysReturnDefaultValue(
-                    entry.getKey(), entry.getValue());
-        }
-    }
-
-    private void testGetFlagForKey_flagIsNotActive_alwaysReturnDefaultValue(final String key,
-            boolean defaultValue) throws InterruptedException {
-        assertEquals("Unexpected default value for " + key,
-                mDeviceConfig.getFlag(key), defaultValue);
-
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, key,
-                /* value */ Boolean.TRUE.toString(), /* makeDefault */ false);
-
-        assertEquals("Flag " + key + "is not set to default after change",
-                mDeviceConfig.getFlag(key), defaultValue);
-
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, key,
-                /* value */ Boolean.FALSE.toString(), /* makeDefault */ false);
-
-        assertEquals("Flag " + key + "is not set to default after change",
-                mDeviceConfig.getFlag(key), defaultValue);
-    }
-
-}
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 7d507e9..81a3794 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -38,6 +38,7 @@
 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.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
 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;
@@ -62,7 +63,6 @@
 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 static org.mockito.Mockito.verify;
 
@@ -146,7 +146,9 @@
     @Test
     @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
     public void testShouldIgnoreRequestedOrientation_cameraCompatTreatment_returnsTrue() {
-        doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled(anyBoolean());
+        doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabledAtBuildTime();
 
         // Recreate DisplayContent with DisplayRotationCompatPolicy
         mActivity = setUpActivityWithComponent();
@@ -305,7 +307,7 @@
     @Test
     public void testShouldRefreshActivityForCameraCompat_flagIsDisabled_returnsFalse() {
         doReturn(false).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+                .isCameraCompatTreatmentEnabled();
 
         assertFalse(mController.shouldRefreshActivityForCameraCompat());
     }
@@ -314,7 +316,7 @@
     @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
     public void testShouldRefreshActivityForCameraCompat_overrideEnabled_returnsFalse() {
         doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+                .isCameraCompatTreatmentEnabled();
 
         assertFalse(mController.shouldRefreshActivityForCameraCompat());
     }
@@ -324,7 +326,7 @@
     public void testShouldRefreshActivityForCameraCompat_propertyIsTrueAndOverride_returnsFalse()
             throws Exception {
         doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+                .isCameraCompatTreatmentEnabled();
         mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ true);
 
         mController = new LetterboxUiController(mWm, mActivity);
@@ -336,7 +338,7 @@
     public void testShouldRefreshActivityForCameraCompat_propertyIsFalse_returnsFalse()
             throws Exception {
         doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+                .isCameraCompatTreatmentEnabled();
         mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ false);
 
         mController = new LetterboxUiController(mWm, mActivity);
@@ -348,7 +350,7 @@
     public void testShouldRefreshActivityForCameraCompat_propertyIsTrue_returnsTrue()
             throws Exception {
         doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+                .isCameraCompatTreatmentEnabled();
         mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ true);
 
         mController = new LetterboxUiController(mWm, mActivity);
@@ -362,7 +364,7 @@
     @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE})
     public void testShouldRefreshActivityViaPauseForCameraCompat_flagIsDisabled_returnsFalse() {
         doReturn(false).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+                .isCameraCompatTreatmentEnabled();
 
         assertFalse(mController.shouldRefreshActivityViaPauseForCameraCompat());
     }
@@ -371,7 +373,7 @@
     @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE})
     public void testShouldRefreshActivityViaPauseForCameraCompat_overrideEnabled_returnsTrue() {
         doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+                .isCameraCompatTreatmentEnabled();
 
         assertTrue(mController.shouldRefreshActivityViaPauseForCameraCompat());
     }
@@ -381,7 +383,7 @@
     public void testShouldRefreshActivityViaPauseForCameraCompat_propertyIsFalseAndOverride_returnFalse()
             throws Exception {
         doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+                .isCameraCompatTreatmentEnabled();
         mockThatProperty(PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, /* value */ false);
 
         mController = new LetterboxUiController(mWm, mActivity);
@@ -393,7 +395,7 @@
     public void testShouldRefreshActivityViaPauseForCameraCompat_propertyIsTrue_returnsTrue()
             throws Exception {
         doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+                .isCameraCompatTreatmentEnabled();
         mockThatProperty(PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, /* value */ true);
 
         mController = new LetterboxUiController(mWm, mActivity);
@@ -406,7 +408,7 @@
     @Test
     public void testShouldForceRotateForCameraCompat_flagIsDisabled_returnsFalse() {
         doReturn(false).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+                .isCameraCompatTreatmentEnabled();
 
         assertFalse(mController.shouldForceRotateForCameraCompat());
     }
@@ -415,7 +417,7 @@
     @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION})
     public void testShouldForceRotateForCameraCompat_overrideEnabled_returnsFalse() {
         doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+                .isCameraCompatTreatmentEnabled();
 
         assertFalse(mController.shouldForceRotateForCameraCompat());
     }
@@ -425,7 +427,7 @@
     public void testShouldForceRotateForCameraCompat_propertyIsTrueAndOverride_returnsFalse()
             throws Exception {
         doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+                .isCameraCompatTreatmentEnabled();
         mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ true);
 
         mController = new LetterboxUiController(mWm, mActivity);
@@ -437,7 +439,7 @@
     public void testShouldForceRotateForCameraCompat_propertyIsFalse_returnsFalse()
             throws Exception {
         doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+                .isCameraCompatTreatmentEnabled();
         mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ false);
 
         mController = new LetterboxUiController(mWm, mActivity);
@@ -449,7 +451,7 @@
     public void testShouldForceRotateForCameraCompat_propertyIsTrue_returnsTrue()
             throws Exception {
         doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+                .isCameraCompatTreatmentEnabled();
         mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ true);
 
         mController = new LetterboxUiController(mWm, mActivity);
@@ -461,7 +463,7 @@
     public void testGetCropBoundsIfNeeded_handleCropForTransparentActivityBasedOnOpaqueBounds() {
         final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
                  WindowInsets.Type.navigationBars());
-        taskbar.setInsetsRoundedCornerFrame(true);
+        taskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
         final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
         final Rect opaqueBounds = new Rect(0, 0, 500, 300);
         doReturn(opaqueBounds).when(mActivity).getBounds();
@@ -505,7 +507,7 @@
     public void testGetCropBoundsIfNeeded_appliesCrop() {
         final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
                 WindowInsets.Type.navigationBars());
-        taskbar.setInsetsRoundedCornerFrame(true);
+        taskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
         final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
 
         // Apply crop if taskbar is expanded
@@ -528,7 +530,7 @@
     public void testGetCropBoundsIfNeeded_appliesCropWithSizeCompatScaling() {
         final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
                 WindowInsets.Type.navigationBars());
-        taskbar.setInsetsRoundedCornerFrame(true);
+        taskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
         final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
         final float scaling = 2.0f;
 
@@ -740,7 +742,9 @@
     @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
             OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
     public void testOverrideOrientationIfNeeded_whenCameraNotActive_returnsUnchanged() {
-        doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled(anyBoolean());
+        doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabledAtBuildTime();
 
         // Recreate DisplayContent with DisplayRotationCompatPolicy
         mActivity = setUpActivityWithComponent();
@@ -758,7 +762,9 @@
     @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
             OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
     public void testOverrideOrientationIfNeeded_whenCameraActive_returnsPortrait() {
-        doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled(anyBoolean());
+        doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabledAtBuildTime();
 
         // Recreate DisplayContent with DisplayRotationCompatPolicy
         mActivity = setUpActivityWithComponent();
@@ -1059,7 +1065,9 @@
     @Test
     public void testgetFixedOrientationLetterboxAspectRatio_splitScreenAspectEnabled() {
         doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabled(anyBoolean());
+                .isCameraCompatTreatmentEnabled();
+        doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabledAtBuildTime();
         doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration)
                 .isCameraCompatSplitScreenAspectRatioEnabled();
         doReturn(false).when(mActivity.mWmService.mLetterboxConfiguration)
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 0ccb0d0..b02b774 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -49,10 +49,12 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 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.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 
 import static java.lang.Integer.MAX_VALUE;
 
@@ -1139,6 +1141,40 @@
     }
 
     @Test
+    public void addTask_taskAlreadyInRecentsMovedToTop_callsTaskNotificationController() {
+        final Task firstTask = createTaskBuilder(".Task").build();
+        final Task secondTask = createTaskBuilder(".Task2").build();
+
+        mRecentTasks.add(firstTask);
+        mRecentTasks.add(secondTask);
+
+        TaskChangeNotificationController controller =
+                mAtm.getTaskChangeNotificationController();
+        clearInvocations(controller);
+
+        // Add firstTask back to top
+        mRecentTasks.add(firstTask);
+        verify(controller).notifyTaskListUpdated();
+    }
+
+    @Test
+    public void addTask_taskAlreadyInRecentsOnTop_doesNotNotify() {
+        final Task firstTask = createTaskBuilder(".Task").build();
+        final Task secondTask = createTaskBuilder(".Task2").build();
+
+        mRecentTasks.add(firstTask);
+        mRecentTasks.add(secondTask);
+
+        TaskChangeNotificationController controller =
+                mAtm.getTaskChangeNotificationController();
+        clearInvocations(controller);
+
+        // Add secondTask to top again
+        mRecentTasks.add(secondTask);
+        verifyZeroInteractions(controller);
+    }
+
+    @Test
     public void removeTask_callsTaskNotificationController() {
         final Task task = createTaskBuilder(".Task").build();
 
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 6a89b55..3908947 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -30,6 +30,7 @@
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.provider.DeviceConfig.NAMESPACE_CONSTRAIN_DISPLAY_APIS;
+import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
 import static android.view.Surface.ROTATION_0;
 import static android.view.Surface.ROTATION_180;
 import static android.view.Surface.ROTATION_270;
@@ -3548,7 +3549,7 @@
 
         final InsetsSource navSource = new InsetsSource(
                 InsetsSource.createId(null, 0, navigationBars()), navigationBars());
-        navSource.setInsetsRoundedCornerFrame(true);
+        navSource.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
         navSource.setFrame(new Rect(0, screenHeight - taskbarHeight, screenWidth, screenHeight));
 
         mActivity.mWmService.mLetterboxConfiguration.setLetterboxActivityCornersRadius(15);
@@ -3596,7 +3597,7 @@
 
         final InsetsSource navSource = new InsetsSource(
                 InsetsSource.createId(null, 0, navigationBars()), navigationBars());
-        navSource.setInsetsRoundedCornerFrame(true);
+        navSource.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
         // Immersive activity has transient navbar
         navSource.setVisible(!immersive);
         navSource.setFrame(new Rect(0, screenHeight - taskbarHeight, screenWidth, screenHeight));
diff --git a/services/tests/wmtests/src/com/android/server/wm/SynchedDeviceConfigTests.java b/services/tests/wmtests/src/com/android/server/wm/SynchedDeviceConfigTests.java
new file mode 100644
index 0000000..7d44e11
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/SynchedDeviceConfigTests.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+
+import android.app.ActivityThread;
+import android.platform.test.annotations.Presubmit;
+import android.provider.DeviceConfig;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.modules.utils.testing.TestableDeviceConfig;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test class for {@link SynchedDeviceConfig}.
+ *
+ * atest WmTests:SynchedDeviceConfigTests
+ */
+@SmallTest
+@Presubmit
+public class SynchedDeviceConfigTests {
+
+    private static final long WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS = 2000; // 2 sec
+    private static final String NAMESPACE_FOR_TEST = "TestingNameSpace";
+
+    private SynchedDeviceConfig mDeviceConfig;
+
+    private Executor mExecutor;
+
+    @Rule
+    public final TestableDeviceConfig.TestableDeviceConfigRule
+            mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule();
+
+    @Before
+    public void setUp() {
+        mExecutor = Objects.requireNonNull(ActivityThread.currentApplication()).getMainExecutor();
+        mDeviceConfig = SynchedDeviceConfig
+                .builder(/* nameSpace */ NAMESPACE_FOR_TEST, /* executor */ mExecutor)
+                .addDeviceConfigEntry(/* key */ "key1", /* default */ true, /* enabled */ true)
+                .addDeviceConfigEntry(/* key */ "key2", /* default */ false, /* enabled */ true)
+                .addDeviceConfigEntry(/* key */ "key3",  /* default */ true, /* enabled */ false)
+                .addDeviceConfigEntry(/* key */ "key4",  /* default */ false, /* enabled */ false)
+                .build();
+    }
+
+    @After
+    public void tearDown() {
+        DeviceConfig.removeOnPropertiesChangedListener(mDeviceConfig);
+    }
+
+    @Test
+    public void testWhenStarted_initialValuesAreDefaultOrFalseIfDisabled() {
+        assertFlagValue(/* key */ "key1", /* expected */ true); // enabled
+        assertFlagValue(/* key */ "key2", /* expected */ false); // enabled
+        assertFlagValue(/* key */ "key3", /* expected */ false); // disabled
+        assertFlagValue(/* key */ "key4", /* expected */ false); // disabled
+    }
+
+    @Test
+    public void testIsEnabled() {
+        assertFlagEnabled(/* key */ "key1", /* expected */ true);
+        assertFlagEnabled(/* key */ "key2", /* expected */ true);
+        assertFlagEnabled(/* key */ "key3", /* expected */ false);
+        assertFlagEnabled(/* key */ "key4", /* expected */ false);
+    }
+
+    @Test
+    public void testWhenUpdated_onlyEnabledChanges() {
+        final CountDownLatch countDownLatch = new CountDownLatch(4);
+        spyOn(mDeviceConfig);
+        doAnswer(invocation -> {
+            invocation.callRealMethod();
+            countDownLatch.countDown();
+            return null;
+        }).when(mDeviceConfig).onPropertiesChanged(any());
+        try {
+            // We update all the keys
+            updateProperty(/* key */ "key1", /* value */ false);
+            updateProperty(/* key */ "key2", /* value */ true);
+            updateProperty(/* key */ "key3", /* value */ false);
+            updateProperty(/* key */ "key4", /* value */ true);
+
+            assertThat(countDownLatch.await(
+                    WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue();
+
+            // We update all the flags but only the enabled ones change
+            assertFlagValue(/* key */ "key1", /* expected */ false); // changes
+            assertFlagValue(/* key */ "key2", /* expected */ true); // changes
+            assertFlagValue(/* key */ "key3", /* expected */ false); // disabled
+            assertFlagValue(/* key */ "key4", /* expected */ false); // disabled
+        } catch (InterruptedException e) {
+            Assert.fail(e.getMessage());
+        }
+    }
+
+    private void assertFlagValue(String key, boolean expectedValue) {
+        assertEquals(/* message */"Flag " + key + " value is not " + expectedValue, /* expected */
+                expectedValue, /* actual */ mDeviceConfig.getFlagValue(key));
+    }
+
+
+    private void assertFlagEnabled(String key, boolean expectedValue) {
+        assertEquals(/* message */
+                "Flag " + key + " enabled is not " + expectedValue, /* expected */
+                expectedValue, /* actual */ mDeviceConfig.isBuildTimeFlagEnabled(key));
+    }
+
+    private void updateProperty(String key, Boolean value) {
+        DeviceConfig.setProperty(NAMESPACE_FOR_TEST, key, /* value */
+                value.toString(), /* makeDefault */ false);
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 28241d3..f332b69 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -563,6 +563,36 @@
         assertEquals(freeformBounds, task.getBounds());
     }
 
+    @Test
+    public void testTopActivityEligibleForUserAspectRatioButton() {
+        DisplayContent display = mAtm.mRootWindowContainer.getDefaultDisplay();
+        final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
+                .setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build();
+        final Task task = rootTask.getBottomMostTask();
+        final ActivityRecord root = task.getTopNonFinishingActivity();
+        spyOn(mWm.mLetterboxConfiguration);
+
+        // When device config flag is disabled the button is not enabled
+        doReturn(false).when(mWm.mLetterboxConfiguration)
+                .isUserAppAspectRatioSettingsEnabled();
+        doReturn(false).when(mWm.mLetterboxConfiguration)
+                .isTranslucentLetterboxingEnabled();
+        assertFalse(task.getTaskInfo().topActivityEligibleForUserAspectRatioButton);
+
+        // The flag is enabled
+        doReturn(true).when(mWm.mLetterboxConfiguration)
+                .isUserAppAspectRatioSettingsEnabled();
+        spyOn(root);
+        doReturn(task).when(root).getOrganizedTask();
+        // When the flag is enabled and the top activity is not in size compat mode.
+        doReturn(false).when(root).inSizeCompatMode();
+        assertTrue(task.getTaskInfo().topActivityEligibleForUserAspectRatioButton);
+
+        // When in size compat mode the button is not enabled
+        doReturn(true).when(root).inSizeCompatMode();
+        assertFalse(task.getTaskInfo().topActivityEligibleForUserAspectRatioButton);
+    }
+
     /**
      * Tests that a task with forced orientation has orientation-consistent bounds within the
      * parent.
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 7e4a9de..45ecc3f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
@@ -30,30 +30,18 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.content.Context;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.graphics.Insets;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManagerGlobal;
-import android.util.DisplayMetrics;
-import android.util.TypedValue;
 import android.view.Display;
 import android.view.DisplayCutout;
 import android.view.DisplayInfo;
 
 import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry;
 
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
 class TestDisplayContent extends DisplayContent {
 
     public static final int DEFAULT_LOGICAL_DISPLAY_DENSITY = 300;
@@ -89,6 +77,10 @@
         spyOn(inputMonitor);
         doNothing().when(inputMonitor).resumeDispatchingLw(any());
 
+        final InsetsPolicy insetsPolicy = getInsetsPolicy();
+        WindowTestsBase.suppressInsetsAnimation(insetsPolicy.getPermanentControlTarget());
+        WindowTestsBase.suppressInsetsAnimation(insetsPolicy.getTransientControlTarget());
+
         // 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
@@ -105,13 +97,8 @@
         private boolean mSystemDecorations = false;
         private int mStatusBarHeight = 0;
         private SettingsEntry mOverrideSettings;
-        private DisplayMetrics mDisplayMetrics;
         @NonNull
         private DeviceStateController mDeviceStateController = mock(DeviceStateController.class);
-        @Mock
-        Context mMockContext;
-        @Mock
-        Resources mResources;
 
         Builder(ActivityTaskManagerService service, int width, int height) {
             mService = service;
@@ -124,8 +111,6 @@
             // Set unique ID so physical display overrides are not inheritted from
             // DisplayWindowSettings.
             mInfo.uniqueId = generateUniqueId();
-            mDisplayMetrics = new DisplayMetrics();
-            updateDisplayMetrics();
         }
         Builder(ActivityTaskManagerService service, DisplayInfo info) {
             mService = service;
@@ -195,31 +180,7 @@
             mInfo.logicalDensityDpi = dpi;
             return this;
         }
-        Builder updateDisplayMetrics() {
-            mInfo.getAppMetrics(mDisplayMetrics);
-            return this;
-        }
-        Builder setDefaultMinTaskSizeDp(int valueDp) {
-            MockitoAnnotations.initMocks(this);
-            doReturn(mMockContext).when(mService.mContext).createConfigurationContext(any());
-            doReturn(mResources).when(mMockContext).getResources();
-            doAnswer(
-                    new Answer() {
-                        @Override
-                        public Object answer(InvocationOnMock i) {
-                            Object[] args = i.getArguments();
-                            TypedValue v = (TypedValue) args[1];
-                            v.type = TypedValue.TYPE_DIMENSION;
-                            v.data = TypedValue.createComplexDimension(valueDp,
-                                    TypedValue.COMPLEX_UNIT_DIP);
-                            return null;
-                        }
-                    }
-            ).when(mResources).getValue(
-                    eq(com.android.internal.R.dimen.default_minimal_size_resizable_task),
-                    any(TypedValue.class), eq(true));
-            return this;
-        }
+
         Builder setDeviceStateController(@NonNull DeviceStateController deviceStateController) {
             mDeviceStateController = deviceStateController;
             return this;
diff --git a/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java b/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java
new file mode 100644
index 0000000..df11a44
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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;
+
+import static android.server.wm.ActivityManagerTestBase.createFullscreenActivityScenarioRule;
+import static android.server.wm.BuildUtils.HW_TIMEOUT_MULTIPLIER;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.platform.test.annotations.Presubmit;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.TrustedPresentationThresholds;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * TODO (b/287076178): Move these tests to
+ * {@link android.view.surfacecontrol.cts.TrustedPresentationCallbackTest} when API is made public
+ */
+@Presubmit
+public class TrustedPresentationCallbackTest {
+    private static final String TAG = "TrustedPresentationCallbackTest";
+    private static final int STABILITY_REQUIREMENT_MS = 500;
+    private static final long WAIT_TIME_MS = HW_TIMEOUT_MULTIPLIER * 2000L;
+
+    private static final float FRACTION_VISIBLE = 0.1f;
+
+    @Rule
+    public ActivityScenarioRule<TestActivity> mActivityRule = createFullscreenActivityScenarioRule(
+            TestActivity.class);
+
+    private TestActivity mActivity;
+
+    @Before
+    public void setup() {
+        mActivityRule.getScenario().onActivity(activity -> mActivity = activity);
+    }
+
+    @Test
+    public void testAddTrustedPresentationListenerOnWindow() throws InterruptedException {
+        boolean[] results = new boolean[1];
+        CountDownLatch receivedResults = new CountDownLatch(1);
+        TrustedPresentationThresholds thresholds = new TrustedPresentationThresholds(
+                1 /* minAlpha */, FRACTION_VISIBLE, STABILITY_REQUIREMENT_MS);
+        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        mActivity.getWindow().getRootSurfaceControl().addTrustedPresentationCallback(t, thresholds,
+                Runnable::run, inTrustedPresentationState -> {
+                    Log.d(TAG, "onTrustedPresentationChanged " + inTrustedPresentationState);
+                    results[0] = inTrustedPresentationState;
+                    receivedResults.countDown();
+                });
+        t.apply();
+
+        assertTrue("Timed out waiting for results",
+                receivedResults.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertTrue(results[0]);
+    }
+
+    @Test
+    public void testRemoveTrustedPresentationListenerOnWindow() throws InterruptedException {
+        final Object resultsLock = new Object();
+        boolean[] results = new boolean[1];
+        boolean[] receivedResults = new boolean[1];
+        TrustedPresentationThresholds thresholds = new TrustedPresentationThresholds(
+                1 /* minAlpha */, FRACTION_VISIBLE, STABILITY_REQUIREMENT_MS);
+        Consumer<Boolean> trustedPresentationCallback = inTrustedPresentationState -> {
+            synchronized (resultsLock) {
+                results[0] = inTrustedPresentationState;
+                receivedResults[0] = true;
+                resultsLock.notify();
+            }
+        };
+        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        mActivity.getWindow().getRootSurfaceControl().addTrustedPresentationCallback(t, thresholds,
+                Runnable::run, trustedPresentationCallback);
+        t.apply();
+
+        synchronized (resultsLock) {
+            if (!receivedResults[0]) {
+                resultsLock.wait(WAIT_TIME_MS);
+            }
+            // Make sure we received the results and not just timed out
+            assertTrue("Timed out waiting for results", receivedResults[0]);
+            assertTrue(results[0]);
+
+            // reset the state
+            receivedResults[0] = false;
+        }
+
+        mActivity.getWindow().getRootSurfaceControl().removeTrustedPresentationCallback(t,
+                trustedPresentationCallback);
+        t.apply();
+
+        synchronized (resultsLock) {
+            if (!receivedResults[0]) {
+                resultsLock.wait(WAIT_TIME_MS);
+            }
+            // Ensure we waited the full time and never received a notify on the result from the
+            // callback.
+            assertFalse("Should never have received a callback", receivedResults[0]);
+            // results shouldn't have changed.
+            assertTrue(results[0]);
+        }
+    }
+
+    public static class TestActivity extends Activity {
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index d3f6818..64330d8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -52,6 +52,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -80,6 +81,7 @@
 import android.platform.test.annotations.Presubmit;
 import android.util.DisplayMetrics;
 import android.util.MergedConfiguration;
+import android.view.ContentRecordingSession;
 import android.view.IWindow;
 import android.view.IWindowSessionCallback;
 import android.view.InputChannel;
@@ -99,6 +101,8 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.compatibility.common.util.AdoptShellPermissionsRule;
+import com.android.internal.os.IResultReceiver;
+import com.android.server.LocalServices;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -759,6 +763,63 @@
     }
 
     @Test
+    public void setContentRecordingSession_sessionNull_returnsTrue() {
+        WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class);
+
+        boolean result = wmInternal.setContentRecordingSession(/* incomingSession= */ null);
+
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void setContentRecordingSession_sessionContentDisplay_returnsTrue() {
+        WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class);
+        ContentRecordingSession session = ContentRecordingSession.createDisplaySession(
+                DEFAULT_DISPLAY);
+
+        boolean result = wmInternal.setContentRecordingSession(session);
+
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void setContentRecordingSession_sessionContentTask_noMatchingTask_returnsFalse() {
+        WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class);
+        IBinder launchCookie = new Binder();
+        ContentRecordingSession session = ContentRecordingSession.createTaskSession(launchCookie);
+
+        boolean result = wmInternal.setContentRecordingSession(session);
+
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void setContentRecordingSession_sessionContentTask_matchingTask_returnsTrue() {
+        WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class);
+        ActivityRecord activityRecord = createActivityRecord(createTask(mDefaultDisplay));
+        ContentRecordingSession session = ContentRecordingSession.createTaskSession(
+                activityRecord.mLaunchCookie);
+
+        boolean result = wmInternal.setContentRecordingSession(session);
+
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void setContentRecordingSession_matchingTask_mutatesSessionWithWindowContainerToken() {
+        WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class);
+        Task task = createTask(mDefaultDisplay);
+        ActivityRecord activityRecord = createActivityRecord(task);
+        ContentRecordingSession session = ContentRecordingSession.createTaskSession(
+                activityRecord.mLaunchCookie);
+
+        wmInternal.setContentRecordingSession(session);
+
+        assertThat(session.getTokenToRecord()).isEqualTo(
+                task.mRemoteToken.toWindowContainerToken().asBinder());
+    }
+
+    @Test
     public void testisLetterboxBackgroundMultiColored() {
         assertThat(setupLetterboxConfigurationWithBackgroundType(
                 LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING)).isTrue();
@@ -905,6 +966,56 @@
                 argThat(h -> (h.inputConfig & InputConfig.SPY) == InputConfig.SPY));
     }
 
+    @Test
+    public void testRequestKeyboardShortcuts_noWindow() {
+        doNothing().when(mWm.mContext).enforceCallingOrSelfPermission(anyString(), anyString());
+        doReturn(null).when(mWm).getFocusedWindowLocked();
+        doReturn(null).when(mWm.mRoot).getCurrentInputMethodWindow();
+
+        TestResultReceiver receiver = new TestResultReceiver();
+        mWm.requestAppKeyboardShortcuts(receiver, 0);
+        assertNotNull(receiver.resultData);
+        assertTrue(receiver.resultData.isEmpty());
+
+        receiver = new TestResultReceiver();
+        mWm.requestImeKeyboardShortcuts(receiver, 0);
+        assertNotNull(receiver.resultData);
+        assertTrue(receiver.resultData.isEmpty());
+    }
+
+    @Test
+    public void testRequestKeyboardShortcuts() throws RemoteException {
+        final IWindow window = mock(IWindow.class);
+        final IBinder binder = mock(IBinder.class);
+        doReturn(binder).when(window).asBinder();
+        final WindowState windowState =
+                createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "appWin", window);
+        doNothing().when(mWm.mContext).enforceCallingOrSelfPermission(anyString(), anyString());
+        doReturn(windowState).when(mWm).getFocusedWindowLocked();
+        doReturn(windowState).when(mWm.mRoot).getCurrentInputMethodWindow();
+
+        TestResultReceiver receiver = new TestResultReceiver();
+        mWm.requestAppKeyboardShortcuts(receiver, 0);
+        mWm.requestImeKeyboardShortcuts(receiver, 0);
+        verify(window, times(2)).requestAppKeyboardShortcuts(receiver, 0);
+    }
+
+    class TestResultReceiver implements IResultReceiver {
+        public android.os.Bundle resultData;
+        private final IBinder mBinder = mock(IBinder.class);
+
+        @Override
+        public void send(int resultCode, android.os.Bundle resultData)
+                throws android.os.RemoteException {
+            this.resultData = resultData;
+        }
+
+        @Override
+        public android.os.IBinder asBinder() {
+            return mBinder;
+        }
+    }
+
     private void setupActivityWithLaunchCookie(IBinder launchCookie, WindowContainerToken wct) {
         final WindowContainer.RemoteToken remoteToken = mock(WindowContainer.RemoteToken.class);
         when(remoteToken.toWindowContainerToken()).thenReturn(wct);
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 be8ee78..d5547ec 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -222,6 +222,10 @@
         displayPolicy.finishWindowsDrawn();
         displayPolicy.finishScreenTurningOn();
 
+        final InsetsPolicy insetsPolicy = mDefaultDisplay.getInsetsPolicy();
+        suppressInsetsAnimation(insetsPolicy.getTransientControlTarget());
+        suppressInsetsAnimation(insetsPolicy.getPermanentControlTarget());
+
         mTransaction = mSystemServicesTestRule.mTransaction;
         mMockSession = mock(Session.class);
 
@@ -278,6 +282,15 @@
         checkDeviceSpecificOverridesNotApplied();
     }
 
+    /**
+     * The test doesn't create real SurfaceControls, but mocked ones. This prevents the target from
+     * controlling them, or it will cause {@link NullPointerException}.
+     */
+    static void suppressInsetsAnimation(InsetsControlTarget target) {
+        spyOn(target);
+        Mockito.doNothing().when(target).notifyInsetsControlChanged();
+    }
+
     @After
     public void tearDown() throws Exception {
         if (mUseFakeSettingsProvider) {
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 13945a1..997015f 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -407,7 +407,9 @@
                 var eventLogger = new EventLogger(SESSION_MAX_EVENT_SIZE,
                         "SoundTriggerSessionLogs for package: "
                         + Objects.requireNonNull(originatorIdentity.packageName)
-                        + "#" + sessionId);
+                        + "#" + sessionId
+                        + " - " + originatorIdentity.uid
+                        + "|" + originatorIdentity.pid);
                 return new SoundTriggerSessionStub(client,
                         newSoundTriggerHelper(moduleProperties, eventLogger), eventLogger);
             }
@@ -428,7 +430,9 @@
                 var eventLogger = new EventLogger(SESSION_MAX_EVENT_SIZE,
                         "SoundTriggerSessionLogs for package: "
                         + Objects.requireNonNull(originatorIdentity.packageName) + "#"
-                        + sessionId);
+                        + sessionId
+                        + " - " + originatorIdentity.uid
+                        + "|" + originatorIdentity.pid);
                 return new SoundTriggerSessionStub(client,
                         newSoundTriggerHelper(moduleProperties, eventLogger), eventLogger);
             }
@@ -1801,7 +1805,9 @@
                         ServiceEvent.Type.ATTACH, identity.packageName + "#" + sessionId));
             var eventLogger = new EventLogger(SESSION_MAX_EVENT_SIZE,
                     "LocalSoundTriggerEventLogger for package: " +
-                    identity.packageName + "#" + sessionId);
+                    identity.packageName + "#" + sessionId
+                        + " - " + identity.uid
+                        + "|" + identity.pid);
 
             return new SessionImpl(newSoundTriggerHelper(underlyingModule, eventLogger, isTrusted),
                     client, eventLogger, identity);
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/DefaultHalFactory.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/DefaultHalFactory.java
index 2f2cb59..55cbf29 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/DefaultHalFactory.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/DefaultHalFactory.java
@@ -22,7 +22,7 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemProperties;
-import android.util.Log;
+import android.util.Slog;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -62,7 +62,7 @@
                         android.hardware.soundtrigger3.ISoundTriggerHw.class.getCanonicalName()
                                 + "/default";
                 if (ServiceManager.isDeclared(aidlServiceName)) {
-                    Log.i(TAG, "Connecting to default soundtrigger3.ISoundTriggerHw");
+                    Slog.i(TAG, "Connecting to default soundtrigger3.ISoundTriggerHw");
                     return new SoundTriggerHw3Compat(ServiceManager.waitForService(aidlServiceName),
                             () -> {
                                 // This property needs to be defined in an init.rc script and
@@ -72,7 +72,7 @@
                 }
 
                 // Fallback to soundtrigger-V2.x (HIDL).
-                Log.i(TAG, "Connecting to default soundtrigger-V2.x.ISoundTriggerHw");
+                Slog.i(TAG, "Connecting to default soundtrigger-V2.x.ISoundTriggerHw");
                 ISoundTriggerHw driver = ISoundTriggerHw.getService(true);
                 return SoundTriggerHw2Compat.create(driver, () -> {
                     // This property needs to be defined in an init.rc script and
@@ -81,7 +81,7 @@
                 }, mCaptureStateNotifier);
             } else if (mockHal == USE_MOCK_HAL_V2) {
                 // Use V2 mock.
-                Log.i(TAG, "Connecting to mock soundtrigger-V2.x.ISoundTriggerHw");
+                Slog.i(TAG, "Connecting to mock soundtrigger-V2.x.ISoundTriggerHw");
                 HwBinder.setTrebleTestingOverride(true);
                 try {
                     ISoundTriggerHw driver = ISoundTriggerHw.getService("mock", true);
@@ -89,7 +89,7 @@
                         try {
                             driver.debug(null, new ArrayList<>(Arrays.asList("reboot")));
                         } catch (Exception e) {
-                            Log.e(TAG, "Failed to reboot mock HAL", e);
+                            Slog.e(TAG, "Failed to reboot mock HAL", e);
                         }
                     }, mCaptureStateNotifier);
                 } finally {
@@ -100,14 +100,14 @@
                 final String aidlServiceName =
                         android.hardware.soundtrigger3.ISoundTriggerHw.class.getCanonicalName()
                                 + "/mock";
-                Log.i(TAG, "Connecting to mock soundtrigger3.ISoundTriggerHw");
+                Slog.i(TAG, "Connecting to mock soundtrigger3.ISoundTriggerHw");
                 return new SoundTriggerHw3Compat(ServiceManager.waitForService(aidlServiceName),
                         () -> {
                             try {
                                 ServiceManager.waitForService(aidlServiceName).shellCommand(null,
                                         null, null, new String[]{"reboot"}, null, null);
                             } catch (Exception e) {
-                                Log.e(TAG, "Failed to reboot mock HAL", e);
+                                Slog.e(TAG, "Failed to reboot mock HAL", e);
                             }
                         });
             } else {
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/ExternalCaptureStateTracker.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/ExternalCaptureStateTracker.java
index d195fbe..e3d64d4 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/ExternalCaptureStateTracker.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/ExternalCaptureStateTracker.java
@@ -17,7 +17,7 @@
 package com.android.server.soundtrigger_middleware;
 
 import android.annotation.NonNull;
-import android.util.Log;
+import android.util.Slog;
 
 import java.util.LinkedList;
 import java.util.List;
@@ -94,7 +94,7 @@
                 }
             }
         } catch (Exception e) {
-            Log.e(TAG, "Exception caught while setting capture state", e);
+            Slog.e(TAG, "Exception caught while setting capture state", e);
         }
     }
 
@@ -102,7 +102,7 @@
      * Called by native code when the remote service died.
      */
     private void binderDied() {
-        Log.w(TAG, "Audio policy service died");
+        Slog.w(TAG, "Audio policy service died");
         mNeedToConnect.release();
     }
 }
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalEnforcer.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalEnforcer.java
index c3e0a3c..0f63347 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalEnforcer.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalEnforcer.java
@@ -27,7 +27,7 @@
 import android.media.soundtrigger_middleware.RecognitionEventSys;
 import android.os.DeadObjectException;
 import android.os.IBinder;
-import android.util.Log;
+import android.util.Slog;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -227,10 +227,10 @@
         }
         if (e.getCause() instanceof DeadObjectException) {
             // Server is dead, no need to reboot.
-            Log.e(TAG, "HAL died");
+            Slog.e(TAG, "HAL died");
             throw new RecoverableException(Status.DEAD_OBJECT);
         }
-        Log.e(TAG, "Exception caught from HAL, rebooting HAL");
+        Slog.e(TAG, "Exception caught from HAL, rebooting HAL");
         reboot();
         throw e;
     }
@@ -257,14 +257,14 @@
             synchronized (mModelStates) {
                 ModelState state = mModelStates.get(model);
                 if (state == null) {
-                    Log.wtfStack(TAG, "Unexpected recognition event for model: " + model);
+                    Slog.wtfStack(TAG, "Unexpected recognition event for model: " + model);
                     reboot();
                     return;
                 }
                 if (event.recognitionEvent.recognitionStillActive
                         && event.recognitionEvent.status != RecognitionStatus.SUCCESS
                         && event.recognitionEvent.status != RecognitionStatus.FORCED) {
-                    Log.wtfStack(TAG,
+                    Slog.wtfStack(TAG,
                             "recognitionStillActive is only allowed when the recognition status "
                                     + "is SUCCESS");
                     reboot();
@@ -283,14 +283,14 @@
             synchronized (mModelStates) {
                 ModelState state = mModelStates.get(model);
                 if (state == null) {
-                    Log.wtfStack(TAG, "Unexpected recognition event for model: " + model);
+                    Slog.wtfStack(TAG, "Unexpected recognition event for model: " + model);
                     reboot();
                     return;
                 }
                 if (event.phraseRecognitionEvent.common.recognitionStillActive
                         && event.phraseRecognitionEvent.common.status != RecognitionStatus.SUCCESS
                         && event.phraseRecognitionEvent.common.status != RecognitionStatus.FORCED) {
-                    Log.wtfStack(TAG,
+                    Slog.wtfStack(TAG,
                             "recognitionStillActive is only allowed when the recognition status "
                                     + "is SUCCESS");
                     reboot();
@@ -309,13 +309,13 @@
             synchronized (mModelStates) {
                 ModelState state = mModelStates.get(modelHandle);
                 if (state == null) {
-                    Log.wtfStack(TAG, "Unexpected unload event for model: " + modelHandle);
+                    Slog.wtfStack(TAG, "Unexpected unload event for model: " + modelHandle);
                     reboot();
                     return;
                 }
 
                 if (state == ModelState.ACTIVE) {
-                    Log.wtfStack(TAG, "Trying to unload an active model: " + modelHandle);
+                    Slog.wtfStack(TAG, "Trying to unload an active model: " + modelHandle);
                     reboot();
                     return;
                 }
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalWatchdog.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalWatchdog.java
index 0390f03..5e525e0 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalWatchdog.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalWatchdog.java
@@ -23,7 +23,7 @@
 import android.media.soundtrigger.RecognitionConfig;
 import android.media.soundtrigger.SoundModel;
 import android.os.IBinder;
-import android.util.Log;
+import android.util.Slog;
 
 import java.util.Objects;
 
@@ -172,7 +172,7 @@
 
         Watchdog() {
             mTask = mTimer.createTask(() -> {
-                Log.e(TAG, "HAL deadline expired. Rebooting.", mException);
+                Slog.e(TAG, "HAL deadline expired. Rebooting.", mException);
                 reboot();
             }, TIMEOUT_MS);
         }
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java
index df2e9b4..730e92c 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java
@@ -32,7 +32,7 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.system.OsConstants;
-import android.util.Log;
+import android.util.Slog;
 
 import java.io.IOException;
 import java.util.HashMap;
@@ -240,7 +240,7 @@
                 try {
                     hidlModel.data.close();
                 } catch (IOException e) {
-                    Log.e(TAG, "Failed to close file", e);
+                    Slog.e(TAG, "Failed to close file", e);
                 }
             }
         }
@@ -276,7 +276,7 @@
                 try {
                     hidlModel.common.data.close();
                 } catch (IOException e) {
-                    Log.e(TAG, "Failed to close file", e);
+                    Slog.e(TAG, "Failed to close file", e);
                 }
             }
         }
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImpl.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImpl.java
index 3b800de..5a064da 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImpl.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImpl.java
@@ -20,7 +20,7 @@
 import android.media.soundtrigger_middleware.ISoundTriggerCallback;
 import android.media.soundtrigger_middleware.ISoundTriggerModule;
 import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
-import android.util.Log;
+import android.util.Slog;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -85,7 +85,7 @@
             try {
                 modules.add(new SoundTriggerModule(halFactory, audioSessionProvider));
             } catch (Exception e) {
-                Log.e(TAG, "Failed to add a SoundTriggerModule instance", e);
+                Slog.e(TAG, "Failed to add a SoundTriggerModule instance", e);
             }
         }
 
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java
index 7ec2d9f..0b9ed8c 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java
@@ -36,7 +36,7 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
-import android.util.Log;
+import android.util.Slog;
 import android.util.SparseArray;
 
 import com.android.internal.util.Preconditions;
@@ -150,7 +150,7 @@
                     e.getMessage());
         }
 
-        Log.wtf(TAG, "Unexpected exception", e);
+        Slog.wtf(TAG, "Unexpected exception", e);
         throw new ServiceSpecificException(Status.INTERNAL_ERROR, e.getMessage());
     }
 
@@ -701,7 +701,7 @@
                 try {
                     mCallback.onRecognition(modelHandle, event, captureSession);
                 } catch (Exception e) {
-                    Log.w(TAG, "Client callback exception.", e);
+                    Slog.w(TAG, "Client callback exception.", e);
                }
             }
 
@@ -719,7 +719,7 @@
                 try {
                     mCallback.onPhraseRecognition(modelHandle, event, captureSession);
                 } catch (Exception e) {
-                    Log.w(TAG, "Client callback exception.", e);
+                    Slog.w(TAG, "Client callback exception.", e);
                }
             }
 
@@ -734,7 +734,7 @@
                 try {
                     mCallback.onModelUnloaded(modelHandle);
                 } catch (Exception e) {
-                    Log.w(TAG, "Client callback exception.", e);
+                    Slog.w(TAG, "Client callback exception.", e);
                 }
             }
 
@@ -746,7 +746,7 @@
                 } catch (RemoteException e) {
                     // Dead client will be handled by binderDied() - no need to handle here.
                     // In any case, client callbacks are considered best effort.
-                    Log.e(TAG, "Client callback exception.", e);
+                    Slog.e(TAG, "Client callback exception.", e);
                 }
             }
 
@@ -761,7 +761,7 @@
                 } catch (RemoteException e) {
                     // Dead client will be handled by binderDied() - no need to handle here.
                     // In any case, client callbacks are considered best effort.
-                    Log.e(TAG, "Client callback exception.", e);
+                    Slog.e(TAG, "Client callback exception.", e);
                 }
             }
 
@@ -795,11 +795,11 @@
                    // Check if state updated unexpectedly to log race conditions.
                     for (Map.Entry<Integer, ModelState> entry : mLoadedModels.entrySet()) {
                         if (cachedMap.get(entry.getKey()) != entry.getValue().activityState) {
-                            Log.e(TAG, "Unexpected state update in binderDied. Race occurred!");
+                            Slog.e(TAG, "Unexpected state update in binderDied. Race occurred!");
                         }
                     }
                     if (mLoadedModels.size() != cachedMap.size()) {
-                        Log.e(TAG, "Unexpected state update in binderDied. Race occurred!");
+                        Slog.e(TAG, "Unexpected state update in binderDied. Race occurred!");
                     }
                     try {
                         // Detach
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java
index e793f31..45a7faf 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java
@@ -31,7 +31,7 @@
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.util.Log;
+import android.util.Slog;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -136,7 +136,7 @@
 
     @Override
     public void binderDied() {
-        Log.w(TAG, "Underlying HAL driver died.");
+        Slog.w(TAG, "Underlying HAL driver died.");
         List<ISoundTriggerCallback> callbacks;
         synchronized (this) {
             callbacks = new ArrayList<>(mActiveSessions.size());
@@ -270,7 +270,7 @@
                     try {
                         mAudioSessionProvider.releaseSession(audioSession.mSessionHandle);
                     } catch (Exception ee) {
-                        Log.e(TAG, "Failed to release session.", ee);
+                        Slog.e(TAG, "Failed to release session.", ee);
                     }
                     throw e;
                 }
@@ -286,7 +286,7 @@
                     checkValid();
                     Model loadedModel = new Model();
                     int result = loadedModel.load(model, audioSession);
-                    Log.d(TAG, String.format("loadPhraseModel()->%d", result));
+                    Slog.d(TAG, String.format("loadPhraseModel()->%d", result));
                     return result;
                 } catch (Exception e) {
                     // We must do this outside the lock, to avoid possible deadlocks with the remote
@@ -294,7 +294,7 @@
                     try {
                         mAudioSessionProvider.releaseSession(audioSession.mSessionHandle);
                     } catch (Exception ee) {
-                        Log.e(TAG, "Failed to release session.", ee);
+                        Slog.e(TAG, "Failed to release session.", ee);
                     }
                     throw e;
                 }
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 7193de7..3e79193 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -50,7 +50,6 @@
 import android.telephony.ims.RcsUceAdapter;
 import android.telephony.ims.feature.MmTelFeature;
 import android.telephony.ims.feature.RcsFeature;
-import android.telephony.ims.stub.ImsRegistrationImplBase;
 
 import com.android.internal.telephony.ICarrierConfigLoader;
 import com.android.telephony.Rlog;
@@ -5813,6 +5812,21 @@
          */
         public static final int NR_SA_DISABLE_POLICY_VOWIFI_REGISTERED = 3;
 
+        /**
+         * This specifies whether the carrier support the global number format or not.
+         * {@link SubscriptionManager#getPhoneNumber(int)},
+         * {@link SubscriptionManager#getPhoneNumber(int, int)} with
+         * {@link SubscriptionManager#PHONE_NUMBER_SOURCE_IMS}
+         * In order to provide the phone number to the APIs, the framework extracts the phone
+         * number from the message received from the carrier server. If the carrier does not use
+         * global number format, the framework could not provide phone number.
+         * <p>
+         * If not set or set to false value, the framework handle only global number format URI.
+         * @hide
+         */
+        public static final String KEY_ALLOW_NON_GLOBAL_PHONE_NUMBER_FORMAT_BOOL =
+                KEY_PREFIX + "allow_non_global_phone_number_format_bool";
+
         private Ims() {}
 
         private static PersistableBundle getDefaults() {
@@ -5925,6 +5939,8 @@
             defaults.putString(KEY_IMS_USER_AGENT_STRING,
                                "#MANUFACTURER#_#MODEL#_Android#AV#_#BUILD#");
 
+            defaults.putBoolean(KEY_ALLOW_NON_GLOBAL_PHONE_NUMBER_FORMAT_BOOL, false);
+
             return defaults;
         }
     }
@@ -9730,6 +9746,27 @@
     public static final String KEY_IWLAN_HANDOVER_POLICY_STRING_ARRAY =
             "iwlan_handover_policy_string_array";
 
+    /**
+     * Score table for {@link TelephonyManager#MOBILE_DATA_POLICY_AUTO_DATA_SWITCH}. The score is
+     * used in conjunction with a tolerance value defined in resource config
+     * {@code auto_data_switch_score_tolerance}, greater than which device will switch to the sub
+     * with higher score.
+     * Possible keys are network type name string(also see {@link #KEY_BANDWIDTH_STRING_ARRAY}).
+     * Value should be "score_level_0, score_level_1, score_level_2, score_level_3,score_level_4".
+     * Each network type must have 5 scores correspond to {@link CellSignalStrength}, where score is
+     * a non-negative integer. A score of 0 is treated the same as data out of service.
+     *
+     * For NR (5G), the following network names should be used:
+     * - NR_NSA: NR NSA, sub-6 frequencies
+     * - NR_NSA_MMWAVE: NR NSA, mmwave frequencies
+     * - NR_SA: NR SA, sub-6 frequencies
+     * - NR_SA_MMWAVE: NR SA, mmwave frequencies
+     *
+     * @hide
+     */
+    public static final String KEY_AUTO_DATA_SWITCH_RAT_SIGNAL_SCORE_BUNDLE =
+            "auto_data_switch_rat_signal_score_string_bundle";
+
     /** The default value for every variable. */
     private static final PersistableBundle sDefaults;
 
@@ -10404,6 +10441,51 @@
         sDefaults.putStringArray(KEY_IWLAN_HANDOVER_POLICY_STRING_ARRAY, new String[]{
                 "source=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, "
                         + "target=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, type=allowed"});
+        PersistableBundle auto_data_switch_rat_signal_score_string_bundle = new PersistableBundle();
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "NR_SA_MMWAVE", new int[]{10000, 13227, 16000, 18488, 20017});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "NR_NSA_MMWAVE", new int[]{8000, 10227, 12488, 15017, 15278});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "LTE", new int[]{3731, 5965, 8618, 11179, 13384});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "NR_SA", new int[]{5288, 6795, 6955, 7562, 9713});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "NR_NSA", new int[]{5463, 6827, 8029, 9007, 9428});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "UMTS", new int[]{100, 169, 183, 192, 300});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "eHRPD", new int[]{10, 400, 600, 800, 1000});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "TD_SCDMA", new int[]{1, 100, 500, 1000});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "iDEN", new int[]{1, 2, 10, 50, 100});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "EvDo_B", new int[]{1000, 1495, 2186, 2532, 2600});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "HSPA+", new int[]{1619, 2500, 3393, 4129, 4212});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "HSPA", new int[]{1000, 1495, 2186, 2532, 2544});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "HSUPA", new int[]{1500, 1919, 2132, 2362, 2704});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "HSDPA", new int[]{1500, 1732, 4000, 7000, 8000});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "EvDo_A", new int[]{600, 840, 1200, 1300, 1400});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "EvDo_0", new int[]{300, 600, 1000, 1500, 2000});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "1xRTT", new int[]{50, 60, 70, 80});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "EDGE", new int[]{154, 169, 183, 192, 267});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "GPRS", new int[]{15, 30, 40, 45, 50});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "CDMA", new int[]{1, 50, 100, 300, 2000});
+        auto_data_switch_rat_signal_score_string_bundle.putIntArray(
+                "GSM", new int[]{1, 2, 10, 50, 100});
+        sDefaults.putPersistableBundle(KEY_AUTO_DATA_SWITCH_RAT_SIGNAL_SCORE_BUNDLE,
+                auto_data_switch_rat_signal_score_string_bundle);
         sDefaults.putInt(KEY_CELLULAR_USAGE_SETTING_INT,
                 SubscriptionManager.USAGE_SETTING_UNKNOWN);
         // Default data stall recovery configurations.
diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp
index 4ba538e..37b529b 100644
--- a/tests/FlickerTests/Android.bp
+++ b/tests/FlickerTests/Android.bp
@@ -23,14 +23,55 @@
     default_applicable_licenses: ["frameworks_base_license"],
 }
 
-android_test {
-    name: "FlickerTests",
+filegroup {
+    name: "FlickerTestsBase-src",
+    srcs: ["src/com/android/server/wm/flicker/*.kt"],
+}
+
+filegroup {
+    name: "FlickerTestsAppClose-src",
+    srcs: ["src/**/close/*.kt"],
+}
+
+filegroup {
+    name: "FlickerTestsActivityEmbedding-src",
     srcs: [
-        "src/**/*.java",
-        "src/**/*.kt",
+        "src/**/activityembedding/*.kt",
+        "src/**/activityembedding/open/*.kt",
+        "src/**/activityembedding/close/*.kt",
+        "src/**/activityembedding/rotation/*.kt",
     ],
-    manifest: "AndroidManifest.xml",
-    test_config: "AndroidTest.xml",
+}
+
+filegroup {
+    name: "FlickerTestsIme-src",
+    srcs: ["src/**/ime/*.kt"],
+}
+
+filegroup {
+    name: "FlickerTestsAppLaunch-src",
+    srcs: ["src/**/launch/*.kt"],
+}
+
+filegroup {
+    name: "FlickerTestsNotification-src",
+    srcs: ["src/**/notification/*.kt"],
+}
+
+filegroup {
+    name: "FlickerTestsQuickswitch-src",
+    srcs: ["src/**/quickswitch/*.kt"],
+}
+
+filegroup {
+    name: "FlickerTestsRotation-src",
+    srcs: ["src/**/rotation/*.kt"],
+}
+
+java_defaults {
+    name: "FlickerTestsDefault",
+    manifest: "manifests/AndroidManifest.xml",
+    test_config_template: "AndroidTestTemplate.xml",
     platform_apis: true,
     certificate: "platform",
     optimize: {
@@ -42,16 +83,114 @@
         "androidx.test.ext.junit",
         "flickertestapplib",
         "flickerlib",
-        "flickerlib-apphelpers",
         "flickerlib-helpers",
-        "truth-prebuilt",
-        "launcher-helper-lib",
-        "launcher-aosp-tapl",
         "platform-test-annotations",
-        "wm-flicker-window-extensions",
+        "wm-flicker-common-app-helpers",
     ],
     data: [
         ":FlickerTestApp",
+        "trace_config/*",
+    ],
+}
+
+android_test {
+    name: "FlickerTestsOther",
+    defaults: ["FlickerTestsDefault"],
+    additional_manifests: ["manifests/AndroidManifestOther.xml"],
+    package_name: "com.android.server.wm.flicker",
+    instrumentation_target_package: "com.android.server.wm.flicker",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    exclude_srcs: [
+        ":FlickerTestsAppClose-src",
+        ":FlickerTestsIme-src",
+        ":FlickerTestsAppLaunch-src",
+        ":FlickerTestsQuickswitch-src",
+        ":FlickerTestsRotation-src",
+        ":FlickerTestsNotification-src",
+    ],
+}
+
+android_test {
+    name: "FlickerTestsAppClose",
+    defaults: ["FlickerTestsDefault"],
+    additional_manifests: ["manifests/AndroidManifestAppClose.xml"],
+    package_name: "com.android.server.wm.flicker.close",
+    instrumentation_target_package: "com.android.server.wm.flicker.close",
+    srcs: [
+        ":FlickerTestsBase-src",
+        ":FlickerTestsAppClose-src",
+    ],
+    exclude_srcs: [
+        ":FlickerTestsActivityEmbedding-src",
+    ],
+}
+
+android_test {
+    name: "FlickerTestsIme",
+    defaults: ["FlickerTestsDefault"],
+    additional_manifests: ["manifests/AndroidManifestIme.xml"],
+    package_name: "com.android.server.wm.flicker.ime",
+    instrumentation_target_package: "com.android.server.wm.flicker.ime",
+    srcs: [
+        ":FlickerTestsBase-src",
+        ":FlickerTestsIme-src",
+    ],
+}
+
+android_test {
+    name: "FlickerTestsAppLaunch",
+    defaults: ["FlickerTestsDefault"],
+    additional_manifests: ["manifests/AndroidManifestAppLaunch.xml"],
+    package_name: "com.android.server.wm.flicker.launch",
+    instrumentation_target_package: "com.android.server.wm.flicker.launch",
+    srcs: [
+        ":FlickerTestsBase-src",
+        ":FlickerTestsAppLaunch-src",
+    ],
+    exclude_srcs: [
+        ":FlickerTestsActivityEmbedding-src",
+    ],
+}
+
+android_test {
+    name: "FlickerTestsNotification",
+    defaults: ["FlickerTestsDefault"],
+    additional_manifests: ["manifests/AndroidManifestNotification.xml"],
+    package_name: "com.android.server.wm.flicker.notification",
+    instrumentation_target_package: "com.android.server.wm.flicker.notification",
+    srcs: [
+        ":FlickerTestsBase-src",
+        ":FlickerTestsNotification-src",
+    ],
+}
+
+android_test {
+    name: "FlickerTestsQuickswitch",
+    defaults: ["FlickerTestsDefault"],
+    additional_manifests: ["manifests/AndroidManifestQuickswitch.xml"],
+    package_name: "com.android.server.wm.flicker.quickswitch",
+    instrumentation_target_package: "com.android.server.wm.flicker.quickswitch",
+    srcs: [
+        ":FlickerTestsBase-src",
+        ":FlickerTestsQuickswitch-src",
+    ],
+}
+
+android_test {
+    name: "FlickerTestsRotation",
+    defaults: ["FlickerTestsDefault"],
+    additional_manifests: ["manifests/AndroidManifestRotation.xml"],
+    package_name: "com.android.server.wm.flicker.rotation",
+    instrumentation_target_package: "com.android.server.wm.flicker.rotation",
+    srcs: [
+        ":FlickerTestsBase-src",
+        ":FlickerTestsRotation-src",
+    ],
+    exclude_srcs: [
+        ":FlickerTestsActivityEmbedding-src",
     ],
 }
 
diff --git a/tests/FlickerTests/AndroidManifest.xml b/tests/FlickerTests/AndroidManifest.xml
deleted file mode 100644
index 462f91b..0000000
--- a/tests/FlickerTests/AndroidManifest.xml
+++ /dev/null
@@ -1,55 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.server.wm.flicker">
-
-    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
-    <!-- Read and write traces from external storage -->
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <!-- Allow the test to write directly to /sdcard/ -->
-    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
-    <!-- Write secure settings -->
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
-    <!-- Capture screen contents -->
-    <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" />
-    <!-- Enable / Disable tracing !-->
-    <uses-permission android:name="android.permission.DUMP" />
-    <!-- Force-stop test apps -->
-    <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/>
-    <!-- Run layers trace -->
-    <uses-permission android:name="android.permission.HARDWARE_TEST"/>
-    <!-- Capture screen recording -->
-    <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/>
-    <!-- Workaround grant runtime permission exception from b/152733071 -->
-    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
-    <uses-permission android:name="android.permission.READ_LOGS"/>
-    <!-- ATM.removeRootTasksWithActivityTypes() -->
-    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" />
-    <!-- ActivityOptions.makeCustomTaskAnimation() -->
-    <uses-permission android:name="android.permission.START_TASKS_FROM_RECENTS" />
-    <!-- Allow the test to write directly to /sdcard/ -->
-    <application android:requestLegacyExternalStorage="true" android:largeHeap="true">
-        <uses-library android:name="android.test.runner"/>
-        <uses-library android:name="androidx.window.extensions" android:required="false"/>
-    </application>
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.server.wm.flicker"
-                     android:label="WindowManager Flicker Tests">
-    </instrumentation>
-</manifest>
diff --git a/tests/FlickerTests/AndroidTest.xml b/tests/FlickerTests/AndroidTest.xml
deleted file mode 100644
index 32ff243..0000000
--- a/tests/FlickerTests/AndroidTest.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- * Copyright 2018 Google Inc. All Rights Reserved.
- -->
-<configuration description="Runs WindowManager Flicker Tests">
-    <option name="test-tag" value="FlickerTests" />
-    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
-        <!-- keeps the screen on during tests -->
-        <option name="screen-always-on" value="on" />
-        <!-- prevents the phone from restarting -->
-        <option name="force-skip-system-props" value="true" />
-        <!-- set WM tracing verbose level to all -->
-        <option name="run-command" value="cmd window tracing level all" />
-        <!-- set WM tracing to frame (avoid incomplete states) -->
-        <option name="run-command" value="cmd window tracing frame" />
-        <!-- ensure lock screen mode is swipe -->
-        <option name="run-command" value="locksettings set-disabled false" />
-        <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests -->
-        <option name="run-command" value="pm disable com.google.android.internal.betterbug" />
-        <!-- restart launcher to activate TAPL -->
-        <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" />
-        <!-- Ensure output directory is empty at the start -->
-        <option name="run-command" value="rm -rf /sdcard/flicker" />
-        <!-- Increase trace size: 20mb for WM and 80mb for SF -->
-        <option name="run-command" value="cmd window tracing size 20480" />
-        <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920" />
-    </target_preparer>
-    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
-        <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1" />
-        <option name="run-command" value="settings put system show_touches 1" />
-        <option name="run-command" value="settings put system pointer_location 1" />
-        <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard" />
-        <option name="teardown-command" value="settings delete system show_touches" />
-        <option name="teardown-command" value="settings delete system pointer_location" />
-        <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural" />
-    </target_preparer>
-    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="cleanup-apks" value="true"/>
-        <option name="test-file-name" value="FlickerTests.apk"/>
-        <option name="test-file-name" value="FlickerTestApp.apk" />
-    </target_preparer>
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
-        <option name="package" value="com.android.server.wm.flicker"/>
-        <option name="shell-timeout" value="6600s" />
-        <option name="test-timeout" value="6600s" />
-        <option name="hidden-api-checks" value="false" />
-    </test>
-    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
-        <option name="directory-keys" value="/sdcard/flicker" />
-        <option name="collect-on-run-ended-only" value="true" />
-        <option name="clean-up" value="true" />
-    </metrics_collector>
-</configuration>
diff --git a/tests/FlickerTests/AndroidTestTemplate.xml b/tests/FlickerTests/AndroidTestTemplate.xml
new file mode 100644
index 0000000..ed63ec0
--- /dev/null
+++ b/tests/FlickerTests/AndroidTestTemplate.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ -->
+<configuration description="Runs WindowManager {MODULE}">
+    <option name="test-tag" value="FlickerTests"/>
+    <!-- Needed for storing the perfetto trace files in the sdcard/test_results-->
+    <option name="isolated-storage" value="false"/>
+
+    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+        <!-- keeps the screen on during tests -->
+        <option name="screen-always-on" value="on"/>
+        <!-- prevents the phone from restarting -->
+        <option name="force-skip-system-props" value="true"/>
+        <!-- set WM tracing verbose level to all -->
+        <option name="run-command" value="cmd window tracing level all"/>
+        <!-- set WM tracing to frame (avoid incomplete states) -->
+        <option name="run-command" value="cmd window tracing frame"/>
+        <!-- ensure lock screen mode is swipe -->
+        <option name="run-command" value="locksettings set-disabled false"/>
+        <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests -->
+        <option name="run-command" value="pm disable com.google.android.internal.betterbug"/>
+        <!-- restart launcher to activate TAPL -->
+        <option name="run-command"
+                value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher"/>
+        <!-- Increase trace size: 20mb for WM and 80mb for SF -->
+        <option name="run-command" value="cmd window tracing size 20480"/>
+        <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920"/>
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="test-user-token" value="%TEST_USER%"/>
+        <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+        <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
+        <option name="run-command" value="settings put system show_touches 1"/>
+        <option name="run-command" value="settings put system pointer_location 1"/>
+        <option name="teardown-command"
+                value="settings delete secure show_ime_with_hard_keyboard"/>
+        <option name="teardown-command" value="settings delete system show_touches"/>
+        <option name="teardown-command" value="settings delete system pointer_location"/>
+        <option name="teardown-command"
+                value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/>
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="{MODULE}.apk"/>
+        <option name="test-file-name" value="FlickerTestApp.apk"/>
+    </target_preparer>
+    <!-- Needed for pushing the trace config file -->
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="push-file"
+                key="trace_config.textproto"
+                value="/data/misc/perfetto-traces/trace_config.textproto"
+        />
+        <!--Install the content provider automatically when we push some file in sdcard folder.-->
+        <!--Needed to avoid the installation during the test suite.-->
+        <option name="push-file" key="trace_config.textproto" value="/sdcard/sample.textproto"/>
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="{PACKAGE}"/>
+        <option name="shell-timeout" value="6600s"/>
+        <option name="test-timeout" value="6600s"/>
+        <option name="hidden-api-checks" value="false"/>
+        <option name="device-listeners" value="android.device.collectors.PerfettoListener"/>
+        <!-- PerfettoListener related arguments -->
+        <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true"/>
+        <option name="instrumentation-arg"
+                key="perfetto_config_file"
+                value="trace_config.textproto"
+        />
+        <option name="instrumentation-arg" key="per_run" value="true"/>
+    </test>
+    <!-- Needed for pulling the collected trace config on to the host -->
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="pull-pattern-keys" value="perfetto_file_path"/>
+        <option name="directory-keys"
+                value="/data/user/0/com.android.server.wm.flicker/files"/>
+        <option name="directory-keys"
+                value="/data/user/0/com.android.server.wm.flicker.close/files"/>
+        <option name="directory-keys"
+                value="/data/user/0/com.android.server.wm.flicker.ime/files"/>
+        <option name="directory-keys"
+                value="/data/user/0/com.android.server.wm.flicker.launch/files"/>
+        <option name="directory-keys"
+                value="/data/user/0/com.android.server.wm.flicker.quickswitch/files"/>
+        <option name="directory-keys"
+                value="/data/user/0/com.android.server.wm.flicker.rotation/files"/>
+        <option name="directory-keys"
+                value="/data/user/0/com.android.server.wm.flicker.notification/files"/>
+        <option name="collect-on-run-ended-only" value="true"/>
+        <option name="clean-up" value="true"/>
+    </metrics_collector>
+</configuration>
diff --git a/tests/FlickerTests/manifests/AndroidManifest.xml b/tests/FlickerTests/manifests/AndroidManifest.xml
new file mode 100644
index 0000000..1a34d9e
--- /dev/null
+++ b/tests/FlickerTests/manifests/AndroidManifest.xml
@@ -0,0 +1,58 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="com.android.server.wm.flicker">
+
+    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
+    <!-- Read and write traces from external storage -->
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <!-- Allow the test to write directly to /sdcard/ -->
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <!-- Write secure settings -->
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <!-- Capture screen contents -->
+    <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" />
+    <!-- Enable / Disable tracing !-->
+    <uses-permission android:name="android.permission.DUMP" />
+    <!-- Force-stop test apps -->
+    <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/>
+    <!-- Run layers trace -->
+    <uses-permission android:name="android.permission.HARDWARE_TEST"/>
+    <!-- Capture screen recording -->
+    <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/>
+    <!-- Workaround grant runtime permission exception from b/152733071 -->
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
+    <uses-permission android:name="android.permission.READ_LOGS"/>
+    <!-- ATM.removeRootTasksWithActivityTypes() -->
+    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" />
+    <!-- ActivityOptions.makeCustomTaskAnimation() -->
+    <uses-permission android:name="android.permission.START_TASKS_FROM_RECENTS" />
+    <!-- Allow the test to write directly to /sdcard/ -->
+    <application android:requestLegacyExternalStorage="true" android:largeHeap="true">
+        <uses-library android:name="android.test.runner"/>
+        <uses-library android:name="androidx.window.extensions" android:required="false"/>
+
+        <!-- (b/197936012) Remove startup provider due to test timeout issue -->
+        <provider
+            android:name="androidx.startup.InitializationProvider"
+            android:authorities="${applicationId}.androidx-startup"
+            tools:node="remove" />
+    </application>
+</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestAppClose.xml b/tests/FlickerTests/manifests/AndroidManifestAppClose.xml
new file mode 100644
index 0000000..4cdcb90
--- /dev/null
+++ b/tests/FlickerTests/manifests/AndroidManifestAppClose.xml
@@ -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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.wm.flicker.close">
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker.close"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
+</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestAppLaunch.xml b/tests/FlickerTests/manifests/AndroidManifestAppLaunch.xml
new file mode 100644
index 0000000..659a745
--- /dev/null
+++ b/tests/FlickerTests/manifests/AndroidManifestAppLaunch.xml
@@ -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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.wm.flicker.launch">
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker.launch"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
+</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestIme.xml b/tests/FlickerTests/manifests/AndroidManifestIme.xml
new file mode 100644
index 0000000..abd03af
--- /dev/null
+++ b/tests/FlickerTests/manifests/AndroidManifestIme.xml
@@ -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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.wm.flicker.ime">
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker.ime"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
+</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestNotification.xml b/tests/FlickerTests/manifests/AndroidManifestNotification.xml
new file mode 100644
index 0000000..ad33dee
--- /dev/null
+++ b/tests/FlickerTests/manifests/AndroidManifestNotification.xml
@@ -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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.wm.flicker.close">
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker.notification"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
+</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestOther.xml b/tests/FlickerTests/manifests/AndroidManifestOther.xml
new file mode 100644
index 0000000..47749b8
--- /dev/null
+++ b/tests/FlickerTests/manifests/AndroidManifestOther.xml
@@ -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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.wm.flicker">
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
+</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestQuickswitch.xml b/tests/FlickerTests/manifests/AndroidManifestQuickswitch.xml
new file mode 100644
index 0000000..203035d
--- /dev/null
+++ b/tests/FlickerTests/manifests/AndroidManifestQuickswitch.xml
@@ -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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.wm.flicker.quickswitch">
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker.quickswitch"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
+</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestRotation.xml b/tests/FlickerTests/manifests/AndroidManifestRotation.xml
new file mode 100644
index 0000000..2852cf2
--- /dev/null
+++ b/tests/FlickerTests/manifests/AndroidManifestRotation.xml
@@ -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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.wm.flicker.rotation">
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.server.wm.flicker.rotation"
+                     android:label="WindowManager Flicker Tests">
+    </instrumentation>
+</manifest>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
index efd9b00..7c9c05d 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
@@ -21,7 +21,7 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerBuilderProvider
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.tapl.LauncherInstrumentation
@@ -37,7 +37,7 @@
 abstract class BaseTest
 @JvmOverloads
 constructor(
-    protected val flicker: FlickerTest,
+    protected val flicker: LegacyFlickerTest,
     protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
     protected val tapl: LauncherInstrumentation = LauncherInstrumentation()
 ) {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
index ed9e14f..b18a1a8 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
@@ -23,14 +23,14 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.common.traces.component.IComponentNameMatcher
 import android.tools.common.traces.wm.WindowManagerTrace
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.helpers.WindowUtils
 
 /**
  * Checks that [ComponentNameMatcher.STATUS_BAR] window is visible and above the app windows in all
  * WM trace entries
  */
-fun FlickerTest.statusBarWindowIsAlwaysVisible() {
+fun LegacyFlickerTest.statusBarWindowIsAlwaysVisible() {
     assertWm { this.isAboveAppWindowVisible(ComponentNameMatcher.STATUS_BAR) }
 }
 
@@ -38,7 +38,7 @@
  * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows in all WM
  * trace entries
  */
-fun FlickerTest.navBarWindowIsAlwaysVisible() {
+fun LegacyFlickerTest.navBarWindowIsAlwaysVisible() {
     assertWm { this.isAboveAppWindowVisible(ComponentNameMatcher.NAV_BAR) }
 }
 
@@ -46,7 +46,7 @@
  * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at the
  * start and end of the WM trace
  */
-fun FlickerTest.navBarWindowIsVisibleAtStartAndEnd() {
+fun LegacyFlickerTest.navBarWindowIsVisibleAtStartAndEnd() {
     this.navBarWindowIsVisibleAtStart()
     this.navBarWindowIsVisibleAtEnd()
 }
@@ -55,7 +55,7 @@
  * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at the
  * start of the WM trace
  */
-fun FlickerTest.navBarWindowIsVisibleAtStart() {
+fun LegacyFlickerTest.navBarWindowIsVisibleAtStart() {
     assertWmStart { this.isAboveAppWindowVisible(ComponentNameMatcher.NAV_BAR) }
 }
 
@@ -63,7 +63,7 @@
  * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at the end
  * of the WM trace
  */
-fun FlickerTest.navBarWindowIsVisibleAtEnd() {
+fun LegacyFlickerTest.navBarWindowIsVisibleAtEnd() {
     assertWmEnd { this.isAboveAppWindowVisible(ComponentNameMatcher.NAV_BAR) }
 }
 
@@ -71,7 +71,7 @@
  * Checks that [ComponentNameMatcher.TASK_BAR] window is visible and above the app windows in all WM
  * trace entries
  */
-fun FlickerTest.taskBarWindowIsAlwaysVisible() {
+fun LegacyFlickerTest.taskBarWindowIsAlwaysVisible() {
     assertWm { this.isAboveAppWindowVisible(ComponentNameMatcher.TASK_BAR) }
 }
 
@@ -79,7 +79,7 @@
  * Checks that [ComponentNameMatcher.TASK_BAR] window is visible and above the app windows in all WM
  * trace entries
  */
-fun FlickerTest.taskBarWindowIsVisibleAtEnd() {
+fun LegacyFlickerTest.taskBarWindowIsVisibleAtEnd() {
     assertWmEnd { this.isAboveAppWindowVisible(ComponentNameMatcher.TASK_BAR) }
 }
 
@@ -93,43 +93,45 @@
  * @param allStates if all states should be checked, othersie, just initial and final
  */
 @JvmOverloads
-fun FlickerTest.entireScreenCovered(allStates: Boolean = true) {
+fun LegacyFlickerTest.entireScreenCovered(allStates: Boolean = true) {
     if (allStates) {
         assertLayers {
             this.invoke("entireScreenCovered") { entry ->
-                entry.entry.displays.filter { it.isOn }.forEach { display ->
-                    entry.visibleRegion().coversAtLeast(display.layerStackSpace)
-                }
+                entry.entry.displays
+                    .filter { it.isOn }
+                    .forEach { display ->
+                        entry.visibleRegion().coversAtLeast(display.layerStackSpace)
+                    }
             }
         }
     } else {
         assertLayersStart {
-            this.entry.displays.filter { it.isOn }.forEach { display ->
-                this.visibleRegion().coversAtLeast(display.layerStackSpace)
-            }
+            this.entry.displays
+                .filter { it.isOn }
+                .forEach { display -> this.visibleRegion().coversAtLeast(display.layerStackSpace) }
         }
         assertLayersEnd {
-            this.entry.displays.filter { it.isOn }.forEach { display ->
-                this.visibleRegion().coversAtLeast(display.layerStackSpace)
-            }
+            this.entry.displays
+                .filter { it.isOn }
+                .forEach { display -> this.visibleRegion().coversAtLeast(display.layerStackSpace) }
         }
     }
 }
 
 /** Checks that [ComponentNameMatcher.NAV_BAR] layer is visible at the start of the SF trace */
-fun FlickerTest.navBarLayerIsVisibleAtStart() {
+fun LegacyFlickerTest.navBarLayerIsVisibleAtStart() {
     assertLayersStart { this.isVisible(ComponentNameMatcher.NAV_BAR) }
 }
 
 /** Checks that [ComponentNameMatcher.NAV_BAR] layer is visible at the end of the SF trace */
-fun FlickerTest.navBarLayerIsVisibleAtEnd() {
+fun LegacyFlickerTest.navBarLayerIsVisibleAtEnd() {
     assertLayersEnd { this.isVisible(ComponentNameMatcher.NAV_BAR) }
 }
 
 /**
  * Checks that [ComponentNameMatcher.NAV_BAR] layer is visible at the start and end of the SF trace
  */
-fun FlickerTest.navBarLayerIsVisibleAtStartAndEnd() {
+fun LegacyFlickerTest.navBarLayerIsVisibleAtStartAndEnd() {
     this.navBarLayerIsVisibleAtStart()
     this.navBarLayerIsVisibleAtEnd()
 }
@@ -137,18 +139,18 @@
 /**
  * Checks that [ComponentNameMatcher.TASK_BAR] layer is visible at the start and end of the SF trace
  */
-fun FlickerTest.taskBarLayerIsVisibleAtStartAndEnd() {
+fun LegacyFlickerTest.taskBarLayerIsVisibleAtStartAndEnd() {
     this.taskBarLayerIsVisibleAtStart()
     this.taskBarLayerIsVisibleAtEnd()
 }
 
 /** Checks that [ComponentNameMatcher.TASK_BAR] layer is visible at the start of the SF trace */
-fun FlickerTest.taskBarLayerIsVisibleAtStart() {
+fun LegacyFlickerTest.taskBarLayerIsVisibleAtStart() {
     assertLayersStart { this.isVisible(ComponentNameMatcher.TASK_BAR) }
 }
 
 /** Checks that [ComponentNameMatcher.TASK_BAR] layer is visible at the end of the SF trace */
-fun FlickerTest.taskBarLayerIsVisibleAtEnd() {
+fun LegacyFlickerTest.taskBarLayerIsVisibleAtEnd() {
     assertLayersEnd { this.isVisible(ComponentNameMatcher.TASK_BAR) }
 }
 
@@ -156,7 +158,7 @@
  * Checks that [ComponentNameMatcher.STATUS_BAR] layer is visible at the start and end of the SF
  * trace
  */
-fun FlickerTest.statusBarLayerIsVisibleAtStartAndEnd() {
+fun LegacyFlickerTest.statusBarLayerIsVisibleAtStartAndEnd() {
     assertLayersStart { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
     assertLayersEnd { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
 }
@@ -165,7 +167,7 @@
  * Asserts that the [ComponentNameMatcher.NAV_BAR] layer is at the correct position at the start of
  * the SF trace
  */
-fun FlickerTest.navBarLayerPositionAtStart() {
+fun LegacyFlickerTest.navBarLayerPositionAtStart() {
     assertLayersStart {
         val display =
             this.entry.displays.firstOrNull { !it.isVirtual } ?: error("There is no display!")
@@ -180,7 +182,7 @@
  * Asserts that the [ComponentNameMatcher.NAV_BAR] layer is at the correct position at the end of
  * the SF trace
  */
-fun FlickerTest.navBarLayerPositionAtEnd() {
+fun LegacyFlickerTest.navBarLayerPositionAtEnd() {
     assertLayersEnd {
         val display =
             this.entry.displays.minByOrNull { it.id }
@@ -196,7 +198,7 @@
  * Asserts that the [ComponentNameMatcher.NAV_BAR] layer is at the correct position at the start and
  * end of the SF trace
  */
-fun FlickerTest.navBarLayerPositionAtStartAndEnd() {
+fun LegacyFlickerTest.navBarLayerPositionAtStartAndEnd() {
     navBarLayerPositionAtStart()
     navBarLayerPositionAtEnd()
 }
@@ -205,7 +207,7 @@
  * Asserts that the [ComponentNameMatcher.STATUS_BAR] layer is at the correct position at the start
  * of the SF trace
  */
-fun FlickerTest.statusBarLayerPositionAtStart(
+fun LegacyFlickerTest.statusBarLayerPositionAtStart(
     wmTrace: WindowManagerTrace? = this.reader.readWmTrace()
 ) {
     // collect navbar position for the equivalent WM state
@@ -221,7 +223,7 @@
  * Asserts that the [ComponentNameMatcher.STATUS_BAR] layer is at the correct position at the end of
  * the SF trace
  */
-fun FlickerTest.statusBarLayerPositionAtEnd(
+fun LegacyFlickerTest.statusBarLayerPositionAtEnd(
     wmTrace: WindowManagerTrace? = this.reader.readWmTrace()
 ) {
     // collect navbar position for the equivalent WM state
@@ -237,7 +239,7 @@
  * Asserts that the [ComponentNameMatcher.STATUS_BAR] layer is at the correct position at the start
  * and end of the SF trace
  */
-fun FlickerTest.statusBarLayerPositionAtStartAndEnd() {
+fun LegacyFlickerTest.statusBarLayerPositionAtStartAndEnd() {
     statusBarLayerPositionAtStart()
     statusBarLayerPositionAtEnd()
 }
@@ -246,25 +248,25 @@
  * Asserts that the visibleRegion of the [ComponentNameMatcher.SNAPSHOT] layer can cover the
  * visibleRegion of the given app component exactly
  */
-fun FlickerTest.snapshotStartingWindowLayerCoversExactlyOnApp(component: IComponentNameMatcher) {
+fun LegacyFlickerTest.snapshotStartingWindowLayerCoversExactlyOnApp(
+    component: IComponentNameMatcher
+) {
     assertLayers {
         invoke("snapshotStartingWindowLayerCoversExactlyOnApp") {
             val snapshotLayers =
                 it.subjects.filter { subject ->
-                    subject.name.contains(ComponentNameMatcher.SNAPSHOT.toLayerName()) &&
+                    ComponentNameMatcher.SNAPSHOT.layerMatchesAnyOf(subject.layer) &&
                         subject.isVisible
                 }
+            val visibleAreas =
+                snapshotLayers
+                    .mapNotNull { snapshotLayer -> snapshotLayer.layer.visibleRegion }
+                    .toTypedArray()
+            val snapshotRegion = RegionSubject(visibleAreas, timestamp)
             // Verify the size of snapshotRegion covers appVisibleRegion exactly in animation.
-            if (snapshotLayers.isNotEmpty()) {
-                val visibleAreas =
-                    snapshotLayers
-                        .mapNotNull { snapshotLayer -> snapshotLayer.layer.visibleRegion }
-                        .toTypedArray()
-                val snapshotRegion = RegionSubject(visibleAreas, timestamp)
+            if (snapshotRegion.region.isNotEmpty) {
                 val appVisibleRegion = it.visibleRegion(component)
-                if (snapshotRegion.region.isNotEmpty) {
-                    snapshotRegion.coversExactly(appVisibleRegion.region)
-                }
+                snapshotRegion.coversExactly(appVisibleRegion.region)
             }
         }
     }
@@ -307,7 +309,7 @@
  *      otherwise we won't and the layer must appear immediately.
  * ```
  */
-fun FlickerTest.replacesLayer(
+fun LegacyFlickerTest.replacesLayer(
     originalLayer: IComponentNameMatcher,
     newLayer: IComponentNameMatcher,
     ignoreEntriesWithRotationLayer: Boolean = false,
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
index 7ef4d93..45cd65d 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
@@ -16,12 +16,12 @@
 
 package com.android.server.wm.flicker.activityembedding
 
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
 import org.junit.Before
 
-abstract class ActivityEmbeddingTestBase(flicker: FlickerTest) : BaseTest(flicker) {
+abstract class ActivityEmbeddingTestBase(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     val testApp = ActivityEmbeddingAppHelper(instrumentation)
 
     @Before
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplitTest.kt
deleted file mode 100644
index ed17059..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplitTest.kt
+++ /dev/null
@@ -1,129 +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.server.wm.flicker.activityembedding
-
-import android.platform.test.annotations.Presubmit
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test opening an activity that will launch another activity as ActivityEmbedding placeholder in
- * split.
- *
- * To run this test: `atest FlickerTests:OpenActivityEmbeddingPlaceholderSplitTest`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenActivityEmbeddingPlaceholderSplitTest(flicker: FlickerTest) :
-    ActivityEmbeddingTestBase(flicker) {
-
-    /** {@inheritDoc} */
-    override val transition: FlickerBuilder.() -> Unit = {
-        setup {
-            tapl.setExpectedRotationCheckEnabled(false)
-            testApp.launchViaIntent(wmHelper)
-        }
-        transitions { testApp.launchPlaceholderSplit(wmHelper) }
-        teardown {
-            tapl.goHome()
-            testApp.exit(wmHelper)
-        }
-    }
-
-    /** Main activity should become invisible after launching the placeholder primary activity. */
-    @Presubmit
-    @Test
-    fun mainActivityWindowBecomesInvisible() {
-        flicker.assertWm {
-            isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
-                .then()
-                .isAppWindowInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
-        }
-    }
-
-    /** Main activity should become invisible after launching the placeholder primary activity. */
-    @Presubmit
-    @Test
-    fun mainActivityLayerBecomesInvisible() {
-        flicker.assertLayers {
-            isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
-                .then()
-                .isInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
-        }
-    }
-
-    /**
-     * Placeholder primary and secondary should become visible after launch. The windows are not
-     * necessarily to become visible at the same time.
-     */
-    @Presubmit
-    @Test
-    fun placeholderSplitWindowsBecomeVisible() {
-        flicker.assertWm {
-            notContains(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
-                .then()
-                .isAppWindowInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
-                .then()
-                .isAppWindowVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
-        }
-        flicker.assertWm {
-            notContains(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
-                .then()
-                .isAppWindowInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
-                .then()
-                .isAppWindowVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
-        }
-    }
-
-    /** Placeholder primary and secondary should become visible together after launch. */
-    @Presubmit
-    @Test
-    fun placeholderSplitLayersBecomeVisible() {
-        flicker.assertLayers {
-            isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
-            isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
-                .then()
-                .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
-                .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
-        }
-    }
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingSecondaryToSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingSecondaryToSplitTest.kt
deleted file mode 100644
index 8638288..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingSecondaryToSplitTest.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.activityembedding
-
-import android.platform.test.annotations.Presubmit
-import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test opening a secondary activity that will split with the main activity.
- *
- * To run this test: `atest FlickerTests:OpenActivityEmbeddingSecondaryToSplitTest`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenActivityEmbeddingSecondaryToSplitTest(flicker: FlickerTest) :
-    ActivityEmbeddingTestBase(flicker) {
-
-    /** {@inheritDoc} */
-    override val transition: FlickerBuilder.() -> Unit = {
-        setup {
-            tapl.setExpectedRotationCheckEnabled(false)
-            testApp.launchViaIntent(wmHelper)
-        }
-        transitions { testApp.launchSecondaryActivity(wmHelper) }
-        teardown {
-            tapl.goHome()
-            testApp.exit(wmHelper)
-        }
-    }
-
-    /** Main activity should remain visible when enter split from fullscreen. */
-    @Presubmit
-    @Test
-    fun mainActivityWindowIsAlwaysVisible() {
-        flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
-    }
-
-    /**
-     * Main activity surface is animated from fullscreen to ActivityEmbedding split. During the
-     * transition, there is a period of time that it is covered by a snapshot of itself.
-     */
-    @Presubmit
-    @Test
-    fun mainActivityLayerIsAlwaysVisible() {
-        flicker.assertLayers {
-            isVisible(
-                ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT.or(
-                    ComponentNameMatcher.TRANSITION_SNAPSHOT
-                )
-            )
-        }
-        flicker.assertLayersEnd {
-            isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
-                .isInvisible(ComponentNameMatcher.TRANSITION_SNAPSHOT)
-        }
-    }
-
-    /** Secondary activity should become visible after launching into split. */
-    @Presubmit
-    @Test
-    fun secondaryActivityWindowBecomesVisible() {
-        flicker.assertWm {
-            notContains(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
-                .then()
-                .isAppWindowInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
-                .then()
-                .isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
-        }
-    }
-
-    /** Secondary activity should become visible after launching into split. */
-    @Presubmit
-    @Test
-    fun secondaryActivityLayerBecomesVisible() {
-        flicker.assertLayers {
-            isInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
-                .then()
-                .isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
-        }
-    }
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/RTLStartSecondaryWithPlaceholderTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/RTLStartSecondaryWithPlaceholderTest.kt
new file mode 100644
index 0000000..4bc17ed
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/RTLStartSecondaryWithPlaceholderTest.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.activityembedding
+
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching a placeholder split over a normal split, both splits are configured in RTL.
+ *
+ * Setup: From A launch a split in RTL - resulting in B|A. Transitions: From A start
+ * PlaceholderPrimary, which is configured to launch with PlaceholderSecondary in RTL. Expect split
+ * PlaceholderSecondary|PlaceholderPrimary covering split B|A.
+ *
+ * To run this test: `atest FlickerTests:RTLStartSecondaryWithPlaceholderTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class RTLStartSecondaryWithPlaceholderTest(flicker: LegacyFlickerTest) :
+    ActivityEmbeddingTestBase(flicker) {
+
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit = {
+        setup {
+            tapl.setExpectedRotationCheckEnabled(false)
+            testApp.launchViaIntent(wmHelper)
+            testApp.launchSecondaryActivityRTL(wmHelper)
+        }
+        transitions { testApp.launchPlaceholderSplitRTL(wmHelper) }
+        teardown {
+            tapl.goHome()
+            testApp.exit(wmHelper)
+        }
+    }
+
+    /**
+     * Main activity and Secondary activity will become invisible because they are covered by
+     * PlaceholderPrimary activity and PlaceholderSecondary activity.
+     */
+    @Presubmit
+    @Test
+    fun assertWindowVisibilities() {
+        flicker.assertWm {
+            isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                .then()
+                .isAppWindowInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+        }
+        flicker.assertWm {
+            isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .then()
+                .isAppWindowInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+        }
+        flicker.assertWm {
+            isAppWindowInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+                .then()
+                .isAppWindowVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+        }
+        flicker.assertWm {
+            isAppWindowInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+                .then()
+                .isAppWindowVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+        }
+    }
+
+    /**
+     * Main activity and Secondary activity will become invisible because they are covered by
+     * PlaceholderPrimary activity and PlaceholderSecondary activity.
+     */
+    @Presubmit
+    @Test
+    fun assertLayerVisibilities() {
+        flicker.assertLayers {
+            this.isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                .then()
+                .isInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+        }
+        flicker.assertLayers {
+            this.isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .then()
+                .isInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+        }
+        flicker.assertLayers {
+            isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+                .then()
+                .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+        }
+        flicker.assertLayers {
+            isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+                .then()
+                .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+        }
+    }
+
+    /** Main activity and Secondary activity split is in right-to-left layout direction. */
+    @Presubmit
+    @Test
+    fun assertWMRTLBeforeTransition() {
+        flicker.assertWmStart {
+            val mainActivityRegion =
+                this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+            val secondaryActivityRegion =
+                this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+            mainActivityRegion.notOverlaps(secondaryActivityRegion.region)
+            // secondary activity is on the left, main activity is on the right.
+            check { "isRTLBeforeTransition" }
+                .that(mainActivityRegion.region.bounds.left)
+                .isEqual(secondaryActivityRegion.region.bounds.right)
+        }
+    }
+
+    /** Main activity and Secondary activity split is in right-to-left layout direction. */
+    @Presubmit
+    @Test
+    fun assertLayerRTLBeforeTransition() {
+        flicker.assertLayersStart {
+            val mainActivityRegion =
+                this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+            val secondaryActivityRegion =
+                this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+            mainActivityRegion.notOverlaps(secondaryActivityRegion.region)
+            // secondary activity is on the left, main activity is on the right.
+            check { "isRTLBeforeTransition" }
+                .that(mainActivityRegion.region.bounds.left)
+                .isEqual(secondaryActivityRegion.region.bounds.right)
+        }
+    }
+
+    /**
+     * PlaceholderPrimary activity and PlaceholderSecondary activity split are in right-to-left
+     * layout direction.
+     */
+    @Presubmit
+    @Test
+    fun assertWMRTLAfterTransition() {
+        flicker.assertWmEnd {
+            val mainActivityRegion =
+                this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+            val secondaryActivityRegion =
+                this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+            mainActivityRegion.notOverlaps(secondaryActivityRegion.region)
+            // secondary activity is on the left, main activity is on the right.
+            check { "isRTLBeforeTransition" }
+                .that(mainActivityRegion.region.bounds.left)
+                .isEqual(secondaryActivityRegion.region.bounds.right)
+        }
+    }
+
+    /**
+     * PlaceholderPrimary activity and PlaceholderSecondary activity split are in right-to-left
+     * layout direction.
+     */
+    @Presubmit
+    @Test
+    fun assertLayerRTLAfterTransition() {
+        flicker.assertLayersEnd {
+            val mainActivityRegion =
+                this.visibleRegion(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+            val secondaryActivityRegion =
+                this.visibleRegion(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+            mainActivityRegion.notOverlaps(secondaryActivityRegion.region)
+            // Placeholder secondary activity is on the left, placeholder primary activity is on the
+            // right.
+            check { "isRTLAfterTransition" }
+                .that(mainActivityRegion.region.bounds.left)
+                .isEqual(secondaryActivityRegion.region.bounds.right)
+        }
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt
new file mode 100644
index 0000000..4530ef3
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.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.server.wm.flicker.activityembedding.close
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.datatypes.Rect
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test closing a secondary activity in a split.
+ *
+ * Setup: Launch A|B in split with B being the secondary activity. Transitions: Finish B and expect
+ * A to become fullscreen.
+ *
+ * To run this test: `atest FlickerTests:CloseSecondaryActivityInSplitTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class CloseSecondaryActivityInSplitTest(flicker: LegacyFlickerTest) :
+    ActivityEmbeddingTestBase(flicker) {
+
+    override val transition: FlickerBuilder.() -> Unit = {
+        setup {
+            tapl.setExpectedRotationCheckEnabled(false)
+            // Launches fullscreen A.
+            testApp.launchViaIntent(wmHelper)
+            // Launches a split A|B and waits for both activities to show.
+            testApp.launchSecondaryActivity(wmHelper)
+            // Get fullscreen bounds
+            startDisplayBounds =
+                wmHelper.currentState.layerState.physicalDisplayBounds
+                    ?: error("Can't get display bounds")
+        }
+        transitions {
+            // Finish secondary activity B.
+            testApp.finishSecondaryActivity(wmHelper)
+            // Expect the main activity A to expand into fullscreen.
+            wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
+        }
+        teardown {
+            tapl.goHome()
+            testApp.exit(wmHelper)
+        }
+    }
+
+    /** Main activity is always visible and becomes fullscreen in the end. */
+    @Presubmit
+    @Test
+    fun mainActivityWindowBecomesFullScreen() {
+        flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
+        flicker.assertWmEnd {
+            this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                .coversExactly(startDisplayBounds)
+        }
+    }
+
+    /** Main activity surface is animated from split to fullscreen. */
+    @Presubmit
+    @Test
+    fun mainActivityLayerIsAlwaysVisible() {
+        flicker.assertLayers {
+            isVisible(
+                ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT.or(
+                    ComponentNameMatcher.TRANSITION_SNAPSHOT
+                )
+            )
+        }
+        flicker.assertLayersEnd {
+            isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                .isInvisible(ComponentNameMatcher.TRANSITION_SNAPSHOT)
+        }
+    }
+
+    /** Secondary activity should destroy and become invisible. */
+    @Presubmit
+    @Test
+    fun secondaryActivityWindowFinishes() {
+        flicker.assertWm {
+            contains(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .then()
+                .notContains(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+        }
+    }
+
+    @Presubmit
+    @Test
+    fun secondaryActivityLayerFinishes() {
+        flicker.assertLayers {
+            isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .then()
+                .isInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+        }
+    }
+
+    companion object {
+        /** {@inheritDoc} */
+        private var startDisplayBounds = Rect.EMPTY
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt
new file mode 100644
index 0000000..0f406fd
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.activityembedding.open
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.datatypes.Rect
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import org.junit.FixMethodOrder
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching an activity with AlwaysExpand rule.
+ *
+ * Setup: Launch A|B in split with B being the secondary activity. Transitions: A start C with
+ * alwaysExpand=true, expect C to launch in fullscreen and cover split A|B.
+ *
+ * To run this test: `atest FlickerTests:MainActivityStartsSecondaryWithAlwaysExpandTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest) :
+    ActivityEmbeddingTestBase(flicker) {
+
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit = {
+        setup {
+            tapl.setExpectedRotationCheckEnabled(false)
+            // Launch a split
+            testApp.launchViaIntent(wmHelper)
+            testApp.launchSecondaryActivity(wmHelper)
+            startDisplayBounds =
+                wmHelper.currentState.layerState.physicalDisplayBounds ?: error("Display not found")
+        }
+        transitions {
+            // Launch C with alwaysExpand
+            testApp.launchAlwaysExpandActivity(wmHelper)
+        }
+        teardown {
+            tapl.goHome()
+            testApp.exit(wmHelper)
+        }
+    }
+
+    @Ignore("Not applicable to this CUJ.") override fun navBarWindowIsVisibleAtStartAndEnd() {}
+
+    @Ignore("Not applicable to this CUJ.") override fun statusBarWindowIsAlwaysVisible() {}
+
+    @Ignore("Not applicable to this CUJ.") override fun statusBarLayerPositionAtStartAndEnd() {}
+
+    /** Transition begins with a split. */
+    @FlakyTest(bugId = 286952194)
+    @Test
+    fun startsWithSplit() {
+        flicker.assertWmStart {
+            this.isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+        }
+        flicker.assertWmStart {
+            this.isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+        }
+    }
+
+    /** Main activity should become invisible after being covered by always expand activity. */
+    @FlakyTest(bugId = 286952194)
+    @Test
+    fun mainActivityLayerBecomesInvisible() {
+        flicker.assertLayers {
+            isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                .then()
+                .isInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+        }
+    }
+
+    /** Secondary activity should become invisible after being covered by always expand activity. */
+    @FlakyTest(bugId = 286952194)
+    @Test
+    fun secondaryActivityLayerBecomesInvisible() {
+        flicker.assertLayers {
+            isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .then()
+                .isInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+        }
+    }
+
+    /** At the end of transition always expand activity is in fullscreen. */
+    @FlakyTest(bugId = 286952194)
+    @Test
+    fun endsWithAlwaysExpandActivityCoveringFullScreen() {
+        flicker.assertWmEnd {
+            this.visibleRegion(ActivityEmbeddingAppHelper.ALWAYS_EXPAND_ACTIVITY_COMPONENT)
+                .coversExactly(startDisplayBounds)
+        }
+    }
+
+    /** Always expand activity is on top of the split. */
+    @FlakyTest(bugId = 286952194)
+    @Presubmit
+    @Test
+    fun endsWithAlwaysExpandActivityOnTop() {
+        flicker.assertWmEnd {
+            this.isAppWindowOnTop(ActivityEmbeddingAppHelper.ALWAYS_EXPAND_ACTIVITY_COMPONENT)
+        }
+    }
+
+    companion object {
+        /** {@inheritDoc} */
+        private var startDisplayBounds = Rect.EMPTY
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingPlaceholderSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingPlaceholderSplitTest.kt
new file mode 100644
index 0000000..8a997dd
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingPlaceholderSplitTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.activityembedding.open
+
+import android.platform.test.annotations.Presubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test opening an activity that will launch another activity as ActivityEmbedding placeholder in
+ * split.
+ *
+ * To run this test: `atest FlickerTests:OpenActivityEmbeddingPlaceholderSplitTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenActivityEmbeddingPlaceholderSplitTest(flicker: LegacyFlickerTest) :
+    ActivityEmbeddingTestBase(flicker) {
+
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit = {
+        setup {
+            tapl.setExpectedRotationCheckEnabled(false)
+            testApp.launchViaIntent(wmHelper)
+        }
+        transitions { testApp.launchPlaceholderSplit(wmHelper) }
+        teardown {
+            tapl.goHome()
+            testApp.exit(wmHelper)
+        }
+    }
+
+    /** Main activity should become invisible after launching the placeholder primary activity. */
+    @Presubmit
+    @Test
+    fun mainActivityWindowBecomesInvisible() {
+        flicker.assertWm {
+            isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                .then()
+                .isAppWindowInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+        }
+    }
+
+    /** Main activity should become invisible after launching the placeholder primary activity. */
+    @Presubmit
+    @Test
+    fun mainActivityLayerBecomesInvisible() {
+        flicker.assertLayers {
+            isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                .then()
+                .isInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+        }
+    }
+
+    /**
+     * Placeholder primary and secondary should become visible after launch. The windows are not
+     * necessarily to become visible at the same time.
+     */
+    @Presubmit
+    @Test
+    fun placeholderSplitWindowsBecomeVisible() {
+        flicker.assertWm {
+            notContains(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+                .then()
+                .isAppWindowInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+                .then()
+                .isAppWindowVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+        }
+        flicker.assertWm {
+            notContains(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+                .then()
+                .isAppWindowInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+                .then()
+                .isAppWindowVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+        }
+    }
+
+    /** Placeholder primary and secondary should become visible together after launch. */
+    @Presubmit
+    @Test
+    fun placeholderSplitLayersBecomeVisible() {
+        flicker.assertLayers {
+            isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+            isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+                .then()
+                .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+                .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+        }
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingSecondaryToSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingSecondaryToSplitTest.kt
new file mode 100644
index 0000000..49aa84b
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingSecondaryToSplitTest.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.server.wm.flicker.activityembedding.open
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test opening a secondary activity that will split with the main activity.
+ *
+ * To run this test: `atest FlickerTests:OpenActivityEmbeddingSecondaryToSplitTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenActivityEmbeddingSecondaryToSplitTest(flicker: LegacyFlickerTest) :
+    ActivityEmbeddingTestBase(flicker) {
+
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit = {
+        setup {
+            tapl.setExpectedRotationCheckEnabled(false)
+            testApp.launchViaIntent(wmHelper)
+        }
+        transitions { testApp.launchSecondaryActivity(wmHelper) }
+        teardown {
+            tapl.goHome()
+            testApp.exit(wmHelper)
+        }
+    }
+
+    /** Main activity should remain visible when enter split from fullscreen. */
+    @Presubmit
+    @Test
+    fun mainActivityWindowIsAlwaysVisible() {
+        flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
+    }
+
+    /**
+     * Main activity surface is animated from fullscreen to ActivityEmbedding split. During the
+     * transition, there is a period of time that it is covered by a snapshot of itself.
+     */
+    @Presubmit
+    @Test
+    fun mainActivityLayerIsAlwaysVisible() {
+        flicker.assertLayers {
+            isVisible(
+                ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT.or(
+                    ComponentNameMatcher.TRANSITION_SNAPSHOT
+                )
+            )
+        }
+        flicker.assertLayersEnd {
+            isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                .isInvisible(ComponentNameMatcher.TRANSITION_SNAPSHOT)
+        }
+    }
+
+    /** Secondary activity should become visible after launching into split. */
+    @Presubmit
+    @Test
+    fun secondaryActivityWindowBecomesVisible() {
+        flicker.assertWm {
+            notContains(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .then()
+                .isAppWindowInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .then()
+                .isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+        }
+    }
+
+    /** Secondary activity should become visible after launching into split. */
+    @Presubmit
+    @Test
+    fun secondaryActivityLayerBecomesVisible() {
+        flicker.assertLayers {
+            isInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .then()
+                .isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+        }
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt
new file mode 100644
index 0000000..27de12e
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.activityembedding.open
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.datatypes.Rect
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching a secondary activity over an existing split. By default the new secondary activity
+ * will stack over the previous secondary activity.
+ *
+ * Setup: From Activity A launch a split A|B.
+ *
+ * Transitions: Let B start C, expect C to cover B and end up in split A|C.
+ *
+ * To run this test: `atest FlickerTests:OpenThirdActivityOverSplitTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenThirdActivityOverSplitTest(flicker: LegacyFlickerTest) :
+    ActivityEmbeddingTestBase(flicker) {
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit = {
+        setup {
+            tapl.setExpectedRotationCheckEnabled(false)
+            // Launch a split.
+            testApp.launchViaIntent(wmHelper)
+            testApp.launchSecondaryActivity(wmHelper)
+
+            startDisplayBounds =
+                wmHelper.currentState.layerState.physicalDisplayBounds
+                    ?: error("Can't get display bounds")
+        }
+        transitions { testApp.launchThirdActivity(wmHelper) }
+        teardown {
+            tapl.goHome()
+            testApp.exit(wmHelper)
+        }
+    }
+
+    /** Main activity remains visible throughout the transition. */
+    @Presubmit
+    @Test
+    fun mainActivityWindowAlwaysVisible() {
+        flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
+    }
+
+    /** Main activity remains visible throughout the transition and takes up half of the screen. */
+    @Presubmit
+    @Test
+    fun mainActivityLayersAlwaysVisible() {
+        flicker.assertLayers { isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
+
+        flicker.assertLayersStart {
+            val display =
+                this.entry.displays.firstOrNull { it.isOn && !it.isVirtual }
+                    ?: error("No non-virtual and on display found")
+            val mainActivityRegion =
+                this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+            val secondaryActivityRegion =
+                this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT).region
+            mainActivityRegion.plus(secondaryActivityRegion).coversExactly(display.layerStackSpace)
+        }
+
+        flicker.assertLayersEnd {
+            val display =
+                this.entry.displays.firstOrNull { it.isOn && !it.isVirtual }
+                    ?: error("No non-virtual and on display found")
+            val mainActivityRegion =
+                this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+            val secondaryActivityRegion =
+                this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+            secondaryActivityRegion.isEmpty()
+            val thirdActivityRegion =
+                this.visibleRegion(ActivityEmbeddingAppHelper.THIRD_ACTIVITY_COMPONENT)
+            mainActivityRegion
+                .plus(thirdActivityRegion.region)
+                .coversExactly(display.layerStackSpace)
+        }
+    }
+
+    /** Third activity launches during the transition and covers up secondary activity. */
+    @Presubmit
+    @Test
+    fun thirdActivityWindowLaunchesIntoSplit() {
+        flicker.assertWm {
+            isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .isAppWindowInvisible(ActivityEmbeddingAppHelper.THIRD_ACTIVITY_COMPONENT)
+                .then()
+                .isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .isAppWindowVisible(ActivityEmbeddingAppHelper.THIRD_ACTIVITY_COMPONENT)
+                .then()
+                .isAppWindowVisible(ActivityEmbeddingAppHelper.THIRD_ACTIVITY_COMPONENT)
+                .isAppWindowInvisible(
+                    ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT
+                ) // expectation
+        }
+    }
+
+    /** Third activity launches during the transition and covers up secondary activity. */
+    @Presubmit
+    @Test
+    fun thirdActivityLayerLaunchesIntoSplit() {
+        flicker.assertLayers {
+            isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .isInvisible(ActivityEmbeddingAppHelper.THIRD_ACTIVITY_COMPONENT)
+                .then()
+                .isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .isVisible(ActivityEmbeddingAppHelper.THIRD_ACTIVITY_COMPONENT)
+                .then()
+                .isVisible(ActivityEmbeddingAppHelper.THIRD_ACTIVITY_COMPONENT)
+                .isInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+        }
+    }
+
+    /** Assert the background animation layer is never visible during transition. */
+    @Presubmit
+    @Test
+    fun backgroundLayerNeverVisible() {
+        val backgroundColorLayer = ComponentNameMatcher("", "Animation Background")
+        flicker.assertLayers { isInvisible(backgroundColorLayer) }
+    }
+
+    companion object {
+        /** {@inheritDoc} */
+        private var startDisplayBounds = Rect.EMPTY
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt
new file mode 100644
index 0000000..da56500
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.activityembedding.rotation
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import com.android.server.wm.flicker.rotation.RotationTransition
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Tests rotating two activities in an Activity Embedding split.
+ *
+ * Setup: Launch A|B in split with B being the secondary activity. Transitions: Rotate display, and
+ * expect A and B to split evenly in new rotation.
+ *
+ * To run this test: `atest FlickerTests:RotateSplitNoChangeTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class RotateSplitNoChangeTest(flicker: LegacyFlickerTest) : RotationTransition(flicker) {
+
+    override val testApp = ActivityEmbeddingAppHelper(instrumentation)
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            super.transition(this)
+            setup {
+                testApp.launchViaIntent(wmHelper)
+                testApp.launchSecondaryActivity(wmHelper)
+            }
+        }
+
+    /**
+     * Checks that the [ComponentNameMatcher.ROTATION] layer appears during the transition, doesn't
+     * flicker, and disappears before the transition is complete
+     */
+    @Presubmit
+    @Test
+    fun rotationLayerAppearsAndVanishes() {
+        flicker.assertLayers {
+            this.isVisible(testApp)
+                .then()
+                .isVisible(ComponentNameMatcher.ROTATION)
+                .then()
+                .isVisible(testApp)
+                .isInvisible(ComponentNameMatcher.ROTATION)
+        }
+    }
+
+    /**
+     * Overrides inherited assertion because in AE Split, the main and secondary activity are
+     * separate layers, each covering up exactly half of the display.
+     */
+    @Presubmit
+    @Test
+    override fun appLayerRotates_StartingPos() {
+        flicker.assertLayersStart {
+            this.entry.displays.map { display ->
+                val leftLayerRegion =
+                    this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                val rightLayerRegion =
+                    this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                // Compare dimensions of two splits, given we're using default split attributes,
+                // both activities take up the same visible size on the display.
+                check { "height" }
+                    .that(leftLayerRegion.region.height)
+                    .isEqual(rightLayerRegion.region.height)
+                check { "width" }
+                    .that(leftLayerRegion.region.width)
+                    .isEqual(rightLayerRegion.region.width)
+                leftLayerRegion.notOverlaps(rightLayerRegion.region)
+                // Layers of two activities sum to be fullscreen size on display.
+                leftLayerRegion.plus(rightLayerRegion.region).coversExactly(display.layerStackSpace)
+            }
+        }
+    }
+
+    /** Verifies dimensions of both split activities hold their invariance after transition too. */
+    @Presubmit
+    @Test
+    override fun appLayerRotates_EndingPos() {
+        flicker.assertLayersEnd {
+            this.entry.displays.map { display ->
+                val leftLayerRegion =
+                    this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                val rightLayerRegion =
+                    this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                check { "height" }
+                    .that(leftLayerRegion.region.height)
+                    .isEqual(rightLayerRegion.region.height)
+                check { "width" }
+                    .that(leftLayerRegion.region.width)
+                    .isEqual(rightLayerRegion.region.width)
+                leftLayerRegion.notOverlaps(rightLayerRegion.region)
+                leftLayerRegion.plus(rightLayerRegion.region).coversExactly(display.layerStackSpace)
+            }
+        }
+    }
+
+    /** Both activities in split should remain visible during rotation. */
+    @Presubmit
+    @Test
+    fun bothActivitiesAreAlwaysVisible() {
+        flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
+        flicker.assertWm {
+            isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+        }
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.rotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt
index 10b71ff..dbbc771 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt
@@ -17,11 +17,11 @@
 package com.android.server.wm.flicker.close
 
 import android.platform.test.annotations.FlakyTest
-import android.tools.device.flicker.annotation.FlickerServiceCompatible
+import android.tools.common.flicker.annotation.FlickerServiceCompatible
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -76,7 +76,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseAppBackButtonTest(flicker: FlickerTest) : CloseAppTransition(flicker) {
+open class CloseAppBackButtonTest(flicker: LegacyFlickerTest) : CloseAppTransition(flicker) {
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -96,13 +96,11 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTestCfArm.kt
index 9fa84019..566f393 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTestCfArm.kt
@@ -16,10 +16,10 @@
 
 package com.android.server.wm.flicker.close
 
-import android.tools.device.flicker.annotation.FlickerServiceCompatible
+import android.tools.common.flicker.annotation.FlickerServiceCompatible
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -29,18 +29,16 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseAppBackButtonTestCfArm(flicker: FlickerTest) : CloseAppBackButtonTest(flicker) {
+class CloseAppBackButtonTestCfArm(flicker: LegacyFlickerTest) : CloseAppBackButtonTest(flicker) {
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): List<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt
index e5bd350..ed930fc 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt
@@ -17,11 +17,11 @@
 package com.android.server.wm.flicker.close
 
 import android.platform.test.annotations.FlakyTest
-import android.tools.device.flicker.annotation.FlickerServiceCompatible
+import android.tools.common.flicker.annotation.FlickerServiceCompatible
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -76,7 +76,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseAppHomeButtonTest(flicker: FlickerTest) : CloseAppTransition(flicker) {
+open class CloseAppHomeButtonTest(flicker: LegacyFlickerTest) : CloseAppTransition(flicker) {
     /** {@inheritDoc} */
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -101,8 +101,6 @@
         /** Creates the test configurations. */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTestCfArm.kt
index 136995a..49ed183 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTestCfArm.kt
@@ -16,10 +16,10 @@
 
 package com.android.server.wm.flicker.close
 
-import android.tools.device.flicker.annotation.FlickerServiceCompatible
+import android.tools.common.flicker.annotation.FlickerServiceCompatible
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -29,13 +29,11 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseAppHomeButtonTestCfArm(flicker: FlickerTest) : CloseAppHomeButtonTest(flicker) {
+class CloseAppHomeButtonTestCfArm(flicker: LegacyFlickerTest) : CloseAppHomeButtonTest(flicker) {
     companion object {
         /** Creates the test configurations. */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
index 4570fa2..8737edb 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
@@ -16,11 +16,14 @@
 
 package com.android.server.wm.flicker.close
 
+import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
+import android.tools.common.flicker.subject.layers.LayersTraceSubject.Companion.VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS
+import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.common.traces.component.ComponentNameMatcher.Companion.LAUNCHER
 import android.tools.device.apphelpers.StandardAppHelper
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.setRotation
@@ -28,7 +31,7 @@
 import org.junit.Test
 
 /** Base test class for transitions that close an app back to the launcher screen */
-abstract class CloseAppTransition(flicker: FlickerTest) : BaseTest(flicker) {
+abstract class CloseAppTransition(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     protected open val testApp: StandardAppHelper = SimpleAppHelper(instrumentation)
 
     /** {@inheritDoc} */
@@ -71,4 +74,21 @@
             ignoreEntriesWithRotationLayer = flicker.scenario.isLandscapeOrSeascapeAtStart
         )
     }
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+        flicker.assertLayers {
+            this.visibleLayersShownMoreThanOneConsecutiveEntry(
+                VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS + listOf(ComponentNameMatcher.NAV_BAR)
+            )
+        }
+    }
+
+    @FlakyTest(bugId = 288369951)
+    @Test
+    fun visibleLayersShownMoreThanOneConsecutiveEntryWithNavBar() {
+        flicker.assertLayers { this.visibleLayersShownMoreThanOneConsecutiveEntry() }
+    }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
index daecfe7..ced7a1e 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
@@ -59,6 +59,83 @@
             .waitForAndVerify()
     }
 
+    /** Clicks the button to launch a third activity over a secondary activity. */
+    fun launchThirdActivity(wmHelper: WindowManagerStateHelper) {
+        val launchButton =
+            uiDevice.wait(
+                Until.findObject(By.res(getPackage(), "launch_third_activity_button")),
+                FIND_TIMEOUT
+            )
+        require(launchButton != null) { "Can't find launch third activity button on screen." }
+        launchButton.click()
+        wmHelper
+            .StateSyncBuilder()
+            .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
+            .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_STOPPED)
+            .withActivityState(THIRD_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
+            .waitForAndVerify()
+    }
+
+    /**
+     * Clicks the button to finishes the secondary activity launched through
+     * [launchSecondaryActivity], waits for the main activity to resume.
+     */
+    fun finishSecondaryActivity(wmHelper: WindowManagerStateHelper) {
+        val finishButton =
+            uiDevice.wait(
+                Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")),
+                FIND_TIMEOUT
+            )
+        require(finishButton != null) { "Can't find finish secondary activity button on screen." }
+        finishButton.click()
+        wmHelper
+            .StateSyncBuilder()
+            .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT)
+            .waitForAndVerify()
+    }
+
+    /**
+     * Clicks the button to launch a secondary activity with alwaysExpand enabled, which will launch
+     * a fullscreen window on top of the visible region.
+     */
+    fun launchAlwaysExpandActivity(wmHelper: WindowManagerStateHelper) {
+        val launchButton =
+            uiDevice.wait(
+                Until.findObject(By.res(getPackage(), "launch_always_expand_activity_button")),
+                FIND_TIMEOUT
+            )
+        require(launchButton != null) {
+            "Can't find launch always expand activity button on screen."
+        }
+        launchButton.click()
+        wmHelper
+            .StateSyncBuilder()
+            .withActivityState(ALWAYS_EXPAND_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
+            .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_PAUSED)
+            .waitForAndVerify()
+    }
+
+    /**
+     * Clicks the button to launch the secondary activity in RTL, which should split with the main
+     * activity based on the split pair rule.
+     */
+    fun launchSecondaryActivityRTL(wmHelper: WindowManagerStateHelper) {
+        val launchButton =
+            uiDevice.wait(
+                Until.findObject(By.res(getPackage(), "launch_secondary_activity_rtl_button")),
+                FIND_TIMEOUT
+            )
+        require(launchButton != null) {
+            "Can't find launch secondary activity rtl button on screen."
+        }
+        launchButton.click()
+        wmHelper
+            .StateSyncBuilder()
+            .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
+            .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
+            .waitForAndVerify()
+    }
+
     /**
      * Clicks the button to launch the placeholder primary activity, which should launch the
      * placeholder secondary activity based on the placeholder rule.
@@ -78,6 +155,25 @@
             .waitForAndVerify()
     }
 
+    /**
+     * Clicks the button to launch the placeholder primary activity in RTL, which should launch the
+     * placeholder secondary activity based on the placeholder rule.
+     */
+    fun launchPlaceholderSplitRTL(wmHelper: WindowManagerStateHelper) {
+        val launchButton =
+            uiDevice.wait(
+                Until.findObject(By.res(getPackage(), "launch_placeholder_split_rtl_button")),
+                FIND_TIMEOUT
+            )
+        require(launchButton != null) { "Can't find launch placeholder split button on screen." }
+        launchButton.click()
+        wmHelper
+            .StateSyncBuilder()
+            .withActivityState(PLACEHOLDER_PRIMARY_COMPONENT, PlatformConsts.STATE_RESUMED)
+            .withActivityState(PLACEHOLDER_SECONDARY_COMPONENT, PlatformConsts.STATE_RESUMED)
+            .waitForAndVerify()
+    }
+
     companion object {
         private const val TAG = "ActivityEmbeddingAppHelper"
 
@@ -87,6 +183,12 @@
         val SECONDARY_ACTIVITY_COMPONENT =
             ActivityOptions.ActivityEmbedding.SecondaryActivity.COMPONENT.toFlickerComponent()
 
+        val THIRD_ACTIVITY_COMPONENT =
+            ActivityOptions.ActivityEmbedding.ThirdActivity.COMPONENT.toFlickerComponent()
+
+        val ALWAYS_EXPAND_ACTIVITY_COMPONENT =
+            ActivityOptions.ActivityEmbedding.AlwaysExpandActivity.COMPONENT.toFlickerComponent()
+
         val PLACEHOLDER_PRIMARY_COMPONENT =
             ActivityOptions.ActivityEmbedding.PlaceholderPrimaryActivity.COMPONENT
                 .toFlickerComponent()
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt
index 9227e07..5c8cbe4 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt
@@ -19,7 +19,7 @@
 package com.android.server.wm.flicker.helpers
 
 import android.tools.common.Rotation
-import android.tools.device.flicker.legacy.IFlickerTestData
+import android.tools.device.flicker.legacy.FlickerTestData
 import android.tools.device.flicker.rules.ChangeDisplayOrientationRule
 
 /**
@@ -27,7 +27,7 @@
  *
  * @param rotation New device rotation
  */
-fun IFlickerTestData.setRotation(rotation: Rotation) =
+fun FlickerTestData.setRotation(rotation: Rotation) =
     ChangeDisplayOrientationRule.setRotation(
         rotation,
         instrumentation,
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java
index a8f1b3d..eeee7b4 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java
@@ -62,6 +62,33 @@
     }
 
     /**
+     * Injects a series of {@link MotionEvent}s to simulate tapping.
+     *
+     * @param point coordinates of pointer to tap
+     * @param times the number of times to tap
+     */
+    public boolean tap(@NonNull Tuple point, int times) throws InterruptedException {
+        PointerProperties ptrProp = getPointerProp(0, MotionEvent.TOOL_TYPE_FINGER);
+        PointerCoords ptrCoord = getPointerCoord(point.x, point.y, 1, 1);
+
+        for (int i = 0; i <= times; i++) {
+            // If already tapped, inject delay in between movements
+            if (times > 0) {
+                SystemClock.sleep(50L);
+            }
+            if (!primaryPointerDown(ptrProp, ptrCoord, SystemClock.uptimeMillis())) {
+                return false;
+            }
+            // Delay before releasing tap
+            SystemClock.sleep(100L);
+            if (!primaryPointerUp(ptrProp, ptrCoord, SystemClock.uptimeMillis())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
      * Injects a series of {@link MotionEvent}s to simulate a drag gesture without pointer release.
      *
      * Simulates a drag gesture without releasing the primary pointer. The primary pointer info
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt
index a670d68..d83b6d3 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt
@@ -17,6 +17,8 @@
 package com.android.server.wm.flicker.helpers
 
 import android.app.Instrumentation
+import android.tools.common.datatypes.Rect
+import android.tools.common.datatypes.Region
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.apphelpers.StandardAppHelper
 import android.tools.device.helpers.FIND_TIMEOUT
@@ -36,6 +38,8 @@
         ActivityOptions.NonResizeablePortraitActivity.COMPONENT.toFlickerComponent()
 ) : StandardAppHelper(instr, launcherName, component) {
 
+    private val gestureHelper: GestureHelper = GestureHelper(mInstrumentation)
+
     fun clickRestart(wmHelper: WindowManagerStateHelper) {
         val restartButton =
             uiDevice.wait(
@@ -56,4 +60,74 @@
             ?: error("Restart dialog button not found")
         wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
     }
+
+    fun repositionHorizontally(displayBounds: Rect, right: Boolean) {
+        val x = if (right) displayBounds.right - BOUNDS_OFFSET else BOUNDS_OFFSET
+        reposition(x.toFloat(), displayBounds.centerY().toFloat())
+    }
+
+    fun repositionVertically(displayBounds: Rect, bottom: Boolean) {
+        val y = if (bottom) displayBounds.bottom - BOUNDS_OFFSET else BOUNDS_OFFSET
+        reposition(displayBounds.centerX().toFloat(), y.toFloat())
+    }
+
+    private fun reposition(x: Float, y: Float) {
+        val coords = GestureHelper.Tuple(x, y)
+        require(gestureHelper.tap(coords, 2)) { "Failed to reposition letterbox app" }
+    }
+
+    fun waitForAppToMoveHorizontallyTo(
+        wmHelper: WindowManagerStateHelper,
+        displayBounds: Rect,
+        right: Boolean
+    ) {
+        wmHelper
+            .StateSyncBuilder()
+            .add("letterboxAppRepositioned") {
+                val letterboxAppWindow = getWindowRegion(wmHelper)
+                val appRegionBounds = letterboxAppWindow.bounds
+                val appWidth = appRegionBounds.width
+                return@add if (right)
+                    appRegionBounds.left == displayBounds.right - appWidth &&
+                        appRegionBounds.right == displayBounds.right
+                else
+                    appRegionBounds.left == displayBounds.left &&
+                        appRegionBounds.right == displayBounds.left + appWidth
+            }
+            .waitForAndVerify()
+    }
+
+    fun waitForAppToMoveVerticallyTo(
+        wmHelper: WindowManagerStateHelper,
+        displayBounds: Rect,
+        navBarHeight: Int,
+        bottom: Boolean
+    ) {
+        wmHelper
+            .StateSyncBuilder()
+            .add("letterboxAppRepositioned") {
+                val letterboxAppWindow = getWindowRegion(wmHelper)
+                val appRegionBounds = letterboxAppWindow.bounds
+                val appHeight = appRegionBounds.height
+                return@add if (bottom)
+                    appRegionBounds.bottom == displayBounds.bottom &&
+                        appRegionBounds.top == (displayBounds.bottom - appHeight + navBarHeight)
+                else
+                    appRegionBounds.top == displayBounds.top &&
+                        appRegionBounds.bottom == displayBounds.top + appHeight
+            }
+            .waitForAndVerify()
+    }
+
+    private fun getWindowRegion(wmHelper: WindowManagerStateHelper): Region {
+        val windowRegion = wmHelper.getWindowRegion(this)
+        require(!windowRegion.isEmpty) {
+            "Unable to find letterbox app window in the current state"
+        }
+        return windowRegion
+    }
+
+    companion object {
+        private const val BOUNDS_OFFSET: Int = 100
+    }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
index 24e231c..c975a50 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
@@ -250,10 +250,13 @@
             waitConditions = arrayOf(ConditionsFactory.hasPipWindow())
         )
 
+        val windowRegion = wmHelper.getWindowRegion(this)
+
         wmHelper
             .StateSyncBuilder()
             .withWindowSurfaceAppeared(this)
             .withPipShown()
+            .withSurfaceVisibleRegion(this, windowRegion)
             .waitForAndVerify()
     }
 
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt
index 7e0632d..98446c1 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt
@@ -22,8 +22,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeEditorPopupDialogAppHelper
@@ -37,7 +37,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseImeOnDismissPopupDialogTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class CloseImeOnDismissPopupDialogTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     private val imeTestApp = ImeEditorPopupDialogAppHelper(instrumentation)
 
     /** {@inheritDoc} */
@@ -101,10 +101,9 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTestCfArm.kt
index c355e27..d87a1da 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTestCfArm.kt
@@ -18,8 +18,8 @@
 
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -28,15 +28,14 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeOnDismissPopupDialogTestCfArm(flicker: FlickerTest) :
+class CloseImeOnDismissPopupDialogTestCfArm(flicker: LegacyFlickerTest) :
     CloseImeOnDismissPopupDialogTest(flicker) {
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
index 537238b..b995d3df 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
@@ -22,8 +22,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeAppHelper
@@ -41,7 +41,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseImeOnGoHomeTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class CloseImeOnGoHomeTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     private val testApp = ImeAppHelper(instrumentation)
 
     /** {@inheritDoc} */
@@ -115,10 +115,9 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTestCfArm.kt
index 0fe52df..3b5bfa9 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTestCfArm.kt
@@ -17,7 +17,7 @@
 package com.android.server.wm.flicker.ime
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -26,4 +26,4 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeOnGoHomeTestCfArm(flicker: FlickerTest) : CloseImeOnGoHomeTest(flicker)
+class CloseImeOnGoHomeTestCfArm(flicker: LegacyFlickerTest) : CloseImeOnGoHomeTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt
index cbe03dc..765bb4c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt
@@ -21,8 +21,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
@@ -49,7 +49,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseImeShownOnAppStartOnGoHomeTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class CloseImeShownOnAppStartOnGoHomeTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
 
     /** {@inheritDoc} */
@@ -94,11 +94,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // b/190352379 (IME doesn't show on app launch in 90 degrees)
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTestCfArm.kt
index 5aacb30..58411cc 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTestCfArm.kt
@@ -17,7 +17,7 @@
 package com.android.server.wm.flicker.ime
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -26,5 +26,5 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseImeShownOnAppStartOnGoHomeTestCfArm(flicker: FlickerTest) :
+open class CloseImeShownOnAppStartOnGoHomeTestCfArm(flicker: LegacyFlickerTest) :
     CloseImeShownOnAppStartOnGoHomeTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
index 82c390b..c87217b 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
@@ -21,8 +21,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
@@ -49,7 +49,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseImeShownOnAppStartToAppOnPressBackTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class CloseImeShownOnAppStartToAppOnPressBackTest(flicker: LegacyFlickerTest) :
+    BaseTest(flicker) {
     private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
 
     /** {@inheritDoc} */
@@ -88,11 +89,10 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 // b/190352379 (IME doesn't show on app launch in 90 degrees)
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTestCfArm.kt
index eb81aed..41b06c0 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTestCfArm.kt
@@ -17,7 +17,7 @@
 package com.android.server.wm.flicker.ime
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -26,5 +26,5 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeShownOnAppStartToAppOnPressBackTestCfArm(flicker: FlickerTest) :
+class CloseImeShownOnAppStartToAppOnPressBackTestCfArm(flicker: LegacyFlickerTest) :
     CloseImeShownOnAppStartToAppOnPressBackTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
index 8d80759..c74870b 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
@@ -21,8 +21,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeAppHelper
@@ -42,7 +42,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseImeToAppOnPressBackTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class CloseImeToAppOnPressBackTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     private val testApp = ImeAppHelper(instrumentation)
 
     /** {@inheritDoc} */
@@ -115,8 +115,6 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTestCfArm.kt
index db1440b..104af22 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTestCfArm.kt
@@ -17,7 +17,7 @@
 package com.android.server.wm.flicker.ime
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -26,5 +26,5 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeToAppOnPressBackTestCfArm(flicker: FlickerTest) :
+class CloseImeToAppOnPressBackTestCfArm(flicker: LegacyFlickerTest) :
     CloseImeToAppOnPressBackTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
index 1526295..21fd590 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
@@ -22,8 +22,8 @@
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeAppHelper
@@ -44,7 +44,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseImeToHomeOnFinishActivityTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class CloseImeToHomeOnFinishActivityTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     private val simpleApp = SimpleAppHelper(instrumentation)
     private val testApp = ImeAppHelper(instrumentation)
 
@@ -90,10 +90,9 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTestCfArm.kt
index 405ab6b..0e18385 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTestCfArm.kt
@@ -17,7 +17,7 @@
 package com.android.server.wm.flicker.ime
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -26,5 +26,5 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class CloseImeToHomeOnFinishActivityTestCfArm(flicker: FlickerTest) :
+open class CloseImeToHomeOnFinishActivityTestCfArm(flicker: LegacyFlickerTest) :
     CloseImeToHomeOnFinishActivityTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CommonAssertions.kt
index 8e33719..777231e 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CommonAssertions.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CommonAssertions.kt
@@ -19,21 +19,21 @@
 package com.android.server.wm.flicker.ime
 
 import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 
-fun FlickerTest.imeLayerBecomesVisible() {
+fun LegacyFlickerTest.imeLayerBecomesVisible() {
     assertLayers {
         this.isInvisible(ComponentNameMatcher.IME).then().isVisible(ComponentNameMatcher.IME)
     }
 }
 
-fun FlickerTest.imeLayerBecomesInvisible() {
+fun LegacyFlickerTest.imeLayerBecomesInvisible() {
     assertLayers {
         this.isVisible(ComponentNameMatcher.IME).then().isInvisible(ComponentNameMatcher.IME)
     }
 }
 
-fun FlickerTest.imeWindowIsAlwaysVisible(rotatesScreen: Boolean = false) {
+fun LegacyFlickerTest.imeWindowIsAlwaysVisible(rotatesScreen: Boolean = false) {
     if (rotatesScreen) {
         assertWm {
             this.isNonAppWindowVisible(ComponentNameMatcher.IME)
@@ -47,7 +47,7 @@
     }
 }
 
-fun FlickerTest.imeWindowBecomesVisible() {
+fun LegacyFlickerTest.imeWindowBecomesVisible() {
     assertWm {
         this.isNonAppWindowInvisible(ComponentNameMatcher.IME)
             .then()
@@ -55,7 +55,7 @@
     }
 }
 
-fun FlickerTest.imeWindowBecomesInvisible() {
+fun LegacyFlickerTest.imeWindowBecomesInvisible() {
     assertWm {
         this.isNonAppWindowVisible(ComponentNameMatcher.IME)
             .then()
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
index 19bbf0c..976ac82 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
@@ -22,8 +22,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.helpers.WindowUtils
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
@@ -42,7 +42,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenImeWindowToFixedPortraitAppTest(flicker: FlickerTest) : BaseTest(flicker) {
+class OpenImeWindowToFixedPortraitAppTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
 
     /** {@inheritDoc} */
@@ -99,19 +99,18 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations =
                     listOf(
                         Rotation.ROTATION_90,
                     ),
                 supportedNavigationModes = listOf(NavBar.MODE_3BUTTON, NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppCfArmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppCfArmTest.kt
index 03f21f9..db80001 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppCfArmTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppCfArmTest.kt
@@ -17,7 +17,7 @@
 package com.android.server.wm.flicker.ime
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -26,5 +26,5 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ShowImeOnAppStartWhenLaunchingAppCfArmTest(flicker: FlickerTest) :
+class ShowImeOnAppStartWhenLaunchingAppCfArmTest(flicker: LegacyFlickerTest) :
     ShowImeOnAppStartWhenLaunchingAppTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
index 496165a..44bd8c8 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
@@ -16,17 +16,17 @@
 
 package com.android.server.wm.flicker.ime
 
+import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import android.platform.test.annotations.Postsubmit
 import android.tools.common.Timestamp
-import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.common.flicker.subject.exceptions.ExceptionMessageBuilder
 import android.tools.common.flicker.subject.exceptions.InvalidPropertyException
+import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
@@ -47,7 +47,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker: FlickerTest) :
+open class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker: LegacyFlickerTest) :
     BaseTest(flicker) {
     private val imeTestApp =
         ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
@@ -93,33 +93,35 @@
 
         layerTraceEntries.zipWithNext { prev, next ->
             val prevSnapshotLayerVisible =
-                    ComponentNameMatcher.SNAPSHOT.layerMatchesAnyOf(prev.visibleLayers)
+                ComponentNameMatcher.SNAPSHOT.layerMatchesAnyOf(prev.visibleLayers)
             val nextSnapshotLayerVisible =
-                    ComponentNameMatcher.SNAPSHOT.layerMatchesAnyOf(next.visibleLayers)
+                ComponentNameMatcher.SNAPSHOT.layerMatchesAnyOf(next.visibleLayers)
 
-            if (imeSnapshotRemovedTimestamp == null &&
-                    (prevSnapshotLayerVisible && !nextSnapshotLayerVisible)) {
+            if (
+                imeSnapshotRemovedTimestamp == null &&
+                    (prevSnapshotLayerVisible && !nextSnapshotLayerVisible)
+            ) {
                 imeSnapshotRemovedTimestamp = next.timestamp
             }
         }
 
         // if so, make an assertion
         imeSnapshotRemovedTimestamp?.let { timestamp ->
-            val stateAfterSnapshot = layerTrace?.getEntryAt(timestamp)
-                    ?: error("State not found for $timestamp")
+            val stateAfterSnapshot =
+                layerTrace?.getEntryAt(timestamp) ?: error("State not found for $timestamp")
 
-            val imeLayers = ComponentNameMatcher.IME
-                    .filterLayers(stateAfterSnapshot.visibleLayers.toList())
+            val imeLayers =
+                ComponentNameMatcher.IME.filterLayers(stateAfterSnapshot.visibleLayers.toList())
 
             require(imeLayers.isNotEmpty()) { "IME layer not found" }
             if (imeLayers.any { it.color.a != 1.0f }) {
-                val errorMsgBuilder = ExceptionMessageBuilder()
+                val errorMsgBuilder =
+                    ExceptionMessageBuilder()
                         .setTimestamp(timestamp)
                         .forInvalidProperty("IME layer alpha")
                         .setExpected("is 1.0")
                         .setActual("not 1.0")
-                        .addExtraDescription("Filter",
-                                ComponentNameMatcher.IME.toLayerIdentifier())
+                        .addExtraDescription("Filter", ComponentNameMatcher.IME.toLayerIdentifier())
                 throw InvalidPropertyException(errorMsgBuilder)
             }
         }
@@ -129,15 +131,14 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_90)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTestCfArm.kt
index 3aca2a0..6d9ea22 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTestCfArm.kt
@@ -17,7 +17,7 @@
 package com.android.server.wm.flicker.ime
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -26,5 +26,5 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTestCfArm(flicker: FlickerTest) :
+class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTestCfArm(flicker: LegacyFlickerTest) :
     ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt
index f64ad53..ae05e37 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt
@@ -21,8 +21,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.helpers.reopenAppFromOverview
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
@@ -41,7 +41,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ShowImeOnAppStartWhenLaunchingAppFromOverviewTest(flicker: FlickerTest) :
+open class ShowImeOnAppStartWhenLaunchingAppFromOverviewTest(flicker: LegacyFlickerTest) :
     BaseTest(flicker) {
     private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
 
@@ -140,10 +140,9 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTestCfArm.kt
index e1aa418..92b3968 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTestCfArm.kt
@@ -18,8 +18,8 @@
 
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -28,16 +28,15 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ShowImeOnAppStartWhenLaunchingAppFromOverviewTestCfArm(flicker: FlickerTest) :
+class ShowImeOnAppStartWhenLaunchingAppFromOverviewTestCfArm(flicker: LegacyFlickerTest) :
     ShowImeOnAppStartWhenLaunchingAppFromOverviewTest(flicker) {
 
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
index 11bc7b9..c991651 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
@@ -22,8 +22,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
@@ -44,7 +44,7 @@
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Presubmit
-open class ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest(flicker: FlickerTest) :
+open class ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest(flicker: LegacyFlickerTest) :
     BaseTest(flicker) {
     private val testApp = SimpleAppHelper(instrumentation)
     private val imeTestApp =
@@ -121,12 +121,11 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL),
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
 
         private const val TAG_IME_VISIBLE = "imeVisible"
         private const val TAG_IME_INVISIBLE = "imeInVisible"
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestCfArm.kt
index 0f57467..09bfacc 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestCfArm.kt
@@ -18,7 +18,7 @@
 
 import android.platform.test.annotations.Presubmit
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -28,5 +28,5 @@
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Presubmit
-open class ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestCfArm(flicker: FlickerTest) :
+open class ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestCfArm(flicker: LegacyFlickerTest) :
     ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
index 46967db..9763521 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
@@ -21,8 +21,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
@@ -79,7 +79,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ShowImeOnAppStartWhenLaunchingAppTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class ShowImeOnAppStartWhenLaunchingAppTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
     private val initializeApp = ImeStateInitializeHelper(instrumentation)
 
@@ -123,15 +123,14 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
index 827110d..7bd5825 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
@@ -21,8 +21,8 @@
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeAppHelper
@@ -37,7 +37,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ShowImeWhenFocusingOnInputFieldTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class ShowImeWhenFocusingOnInputFieldTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     private val testApp = ImeAppHelper(instrumentation)
 
     /** {@inheritDoc} */
@@ -79,10 +79,9 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTestCfArm.kt
index f5b2294..f8c8149 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTestCfArm.kt
@@ -17,7 +17,7 @@
 package com.android.server.wm.flicker.ime
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -26,5 +26,5 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ShowImeWhenFocusingOnInputFieldTestCfArm(flicker: FlickerTest) :
+class ShowImeWhenFocusingOnInputFieldTestCfArm(flicker: LegacyFlickerTest) :
     ShowImeWhenFocusingOnInputFieldTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
index 277b9155..0b168ba 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
@@ -21,8 +21,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.view.WindowInsets.Type.ime
 import android.view.WindowInsets.Type.navigationBars
 import android.view.WindowInsets.Type.statusBars
@@ -45,7 +45,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ShowImeWhileDismissingThemedPopupDialogTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class ShowImeWhileDismissingThemedPopupDialogTest(flicker: LegacyFlickerTest) :
+    BaseTest(flicker) {
     private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
 
     /** {@inheritDoc} */
@@ -84,15 +85,14 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTestCfArm.kt
index 8891d26..ad41b81 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTestCfArm.kt
@@ -18,8 +18,8 @@
 
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -28,21 +28,20 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ShowImeWhileDismissingThemedPopupDialogTestCfArm(flicker: FlickerTest) :
+class ShowImeWhileDismissingThemedPopupDialogTestCfArm(flicker: LegacyFlickerTest) :
     ShowImeWhileDismissingThemedPopupDialogTest(flicker) {
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt
index 9275d6a..7722c93 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt
@@ -21,8 +21,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.traces.parsers.WindowManagerStateHelper
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
@@ -45,7 +45,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ShowImeWhileEnteringOverviewTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class ShowImeWhileEnteringOverviewTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     private val imeTestApp =
         ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation)
 
@@ -205,13 +205,11 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTestCfArm.kt
index fc39713..90fbbf9 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTestCfArm.kt
@@ -17,7 +17,7 @@
 package com.android.server.wm.flicker.ime
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -26,5 +26,5 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ShowImeWhileEnteringOverviewTestCfArm(flicker: FlickerTest) :
+class ShowImeWhileEnteringOverviewTestCfArm(flicker: LegacyFlickerTest) :
     ShowImeWhileEnteringOverviewTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTest.kt
deleted file mode 100644
index e6594c9..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTest.kt
+++ /dev/null
@@ -1,131 +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.server.wm.flicker.launch
-
-import android.platform.test.annotations.Presubmit
-import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import android.tools.device.traces.parsers.toFlickerComponent
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.BaseTest
-import com.android.server.wm.flicker.helpers.TwoActivitiesAppHelper
-import com.android.server.wm.flicker.testapp.ActivityOptions
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test the back and forward transition between 2 activities.
- *
- * To run this test: `atest FlickerTests:ActivitiesTransitionTest`
- *
- * Actions:
- * ```
- *     Launch an app
- *     Launch a secondary activity within the app
- *     Close the secondary activity back to the initial one
- * ```
- *
- * Notes:
- * ```
- *     1. Part of the test setup occurs automatically via
- *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
- *        including configuring navigation mode, initial orientation and ensuring no
- *        apps are running before setup
- * ```
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ActivitiesTransitionTest(flicker: FlickerTest) : BaseTest(flicker) {
-    private val testApp: TwoActivitiesAppHelper = TwoActivitiesAppHelper(instrumentation)
-
-    /** {@inheritDoc} */
-    override val transition: FlickerBuilder.() -> Unit = {
-        setup {
-            tapl.setExpectedRotation(flicker.scenario.startRotation.value)
-            testApp.launchViaIntent(wmHelper)
-        }
-        teardown { testApp.exit(wmHelper) }
-        transitions {
-            testApp.openSecondActivity(device, wmHelper)
-            tapl.pressBack()
-            wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
-        }
-    }
-
-    /**
-     * Checks that the [ActivityOptions.LaunchNewActivity] activity is visible at the start of the
-     * transition, that [ActivityOptions.SimpleActivity] becomes visible during the transition, and
-     * that [ActivityOptions.LaunchNewActivity] is again visible at the end
-     */
-    @Presubmit
-    @Test
-    fun finishSubActivity() {
-        val buttonActivityComponent =
-            ActivityOptions.LaunchNewActivity.COMPONENT.toFlickerComponent()
-        val imeAutoFocusActivityComponent =
-            ActivityOptions.SimpleActivity.COMPONENT.toFlickerComponent()
-        flicker.assertWm {
-            this.isAppWindowOnTop(buttonActivityComponent)
-                .then()
-                .isAppWindowOnTop(imeAutoFocusActivityComponent)
-                .then()
-                .isAppWindowOnTop(buttonActivityComponent)
-        }
-    }
-
-    /**
-     * Checks that the [ComponentNameMatcher.LAUNCHER] window is not on top. The launcher cannot be
-     * asserted with `isAppWindowVisible` because it contains 2 windows with the exact same name,
-     * and both are never simultaneously visible
-     */
-    @Presubmit
-    @Test
-    fun launcherWindowNotOnTop() {
-        flicker.assertWm { this.isAppWindowNotOnTop(ComponentNameMatcher.LAUNCHER) }
-    }
-
-    /**
-     * Checks that the [ComponentNameMatcher.LAUNCHER] layer is never visible during the transition
-     */
-    @Presubmit
-    @Test
-    fun launcherLayerNotVisible() {
-        flicker.assertLayers { this.isInvisible(ComponentNameMatcher.LAUNCHER) }
-    }
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTestCfArm.kt
deleted file mode 100644
index 8b89a8b..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivitiesTransitionTestCfArm.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.launch
-
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import org.junit.FixMethodOrder
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ActivitiesTransitionTestCfArm(flicker: FlickerTest) : ActivitiesTransitionTest(flicker) {
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt
new file mode 100644
index 0000000..3461907
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.server.wm.flicker.launch
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.device.traces.parsers.toFlickerComponent
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.BaseTest
+import com.android.server.wm.flicker.helpers.TwoActivitiesAppHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test the back and forward transition between 2 activities.
+ *
+ * To run this test: `atest FlickerTests:ActivitiesTransitionTest`
+ *
+ * Actions:
+ * ```
+ *     Launch an app
+ *     Launch a secondary activity within the app
+ *     Close the secondary activity back to the initial one
+ * ```
+ *
+ * Notes:
+ * ```
+ *     1. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class ActivityTransitionTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
+    private val testApp: TwoActivitiesAppHelper = TwoActivitiesAppHelper(instrumentation)
+
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit = {
+        setup {
+            tapl.setExpectedRotation(flicker.scenario.startRotation.value)
+            testApp.launchViaIntent(wmHelper)
+        }
+        teardown { testApp.exit(wmHelper) }
+        transitions {
+            testApp.openSecondActivity(device, wmHelper)
+            tapl.pressBack()
+            wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
+        }
+    }
+
+    /**
+     * Checks that the [ActivityOptions.LaunchNewActivity] activity is visible at the start of the
+     * transition, that [ActivityOptions.SimpleActivity] becomes visible during the transition, and
+     * that [ActivityOptions.LaunchNewActivity] is again visible at the end
+     */
+    @Presubmit
+    @Test
+    fun finishSubActivity() {
+        val buttonActivityComponent =
+            ActivityOptions.LaunchNewActivity.COMPONENT.toFlickerComponent()
+        val imeAutoFocusActivityComponent =
+            ActivityOptions.SimpleActivity.COMPONENT.toFlickerComponent()
+        flicker.assertWm {
+            this.isAppWindowOnTop(buttonActivityComponent)
+                .then()
+                .isAppWindowOnTop(imeAutoFocusActivityComponent)
+                .then()
+                .isAppWindowOnTop(buttonActivityComponent)
+        }
+    }
+
+    /**
+     * Checks that the [ComponentNameMatcher.LAUNCHER] window is not on top. The launcher cannot be
+     * asserted with `isAppWindowVisible` because it contains 2 windows with the exact same name,
+     * and both are never simultaneously visible
+     */
+    @Presubmit
+    @Test
+    fun launcherWindowNotOnTop() {
+        flicker.assertWm { this.isAppWindowNotOnTop(ComponentNameMatcher.LAUNCHER) }
+    }
+
+    /**
+     * Checks that the [ComponentNameMatcher.LAUNCHER] layer is never visible during the transition
+     */
+    @Presubmit
+    @Test
+    fun launcherLayerNotVisible() {
+        flicker.assertLayers { this.isInvisible(ComponentNameMatcher.LAUNCHER) }
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTestCfArm.kt
new file mode 100644
index 0000000..6bbf40e
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTestCfArm.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.server.wm.flicker.launch
+
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ActivityTransitionTestCfArm(flicker: LegacyFlickerTest) : ActivityTransitionTest(flicker) {
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest.kt
deleted file mode 100644
index 549183f..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest.kt
+++ /dev/null
@@ -1,73 +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.server.wm.flicker.launch
-
-import android.tools.device.apphelpers.CameraAppHelper
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import androidx.test.filters.RequiresDevice
-import org.junit.FixMethodOrder
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test launching an app after cold opening camera
- *
- * To run this test: `atest FlickerTests:OpenAppAfterCameraTest`
- *
- * Notes: Some default assertions are inherited [OpenAppTransition]
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenAppAfterCameraTest(flicker: FlickerTest) : OpenAppFromLauncherTransition(flicker) {
-    private val cameraApp = CameraAppHelper(instrumentation)
-    /** {@inheritDoc} */
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            super.transition(this)
-            setup {
-                tapl.setExpectedRotationCheckEnabled(false)
-                // 1. Open camera - cold -> close it first
-                cameraApp.exit(wmHelper)
-                cameraApp.launchViaIntent(wmHelper)
-                // Can't use TAPL due to Recents not showing in 3 Button Nav in full screen mode
-                device.pressHome()
-                tapl.getWorkspace()
-            }
-            teardown { testApp.exit(wmHelper) }
-            transitions { testApp.launchViaIntent(wmHelper) }
-        }
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTestCfArm.kt
deleted file mode 100644
index ac05c76..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTestCfArm.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.launch
-
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import org.junit.FixMethodOrder
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenAppAfterCameraTestCfArm(flicker: FlickerTest) : OpenAppAfterCameraTest(flicker) {
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt
deleted file mode 100644
index 3a80c66..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt
+++ /dev/null
@@ -1,219 +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.server.wm.flicker.launch
-
-import android.platform.test.annotations.FlakyTest
-import android.tools.common.Rotation
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule
-import androidx.test.filters.RequiresDevice
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test cold launching an app from launcher
- *
- * To run this test: `atest FlickerTests:OpenAppColdFromIcon`
- *
- * Actions:
- * ```
- *     Make sure no apps are running on the device
- *     Launch an app [testApp] by clicking it's icon on all apps and wait animation to complete
- * ```
- *
- * Notes:
- * ```
- *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
- *        are inherited [OpenAppTransition]
- *     2. Part of the test setup occurs automatically via
- *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
- *        including configuring navigation mode, initial orientation and ensuring no
- *        apps are running before setup
- * ```
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenAppColdFromIcon(flicker: FlickerTest) : OpenAppFromLauncherTransition(flicker) {
-    /** {@inheritDoc} */
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            super.transition(this)
-            setup {
-                if (flicker.scenario.isTablet) {
-                    tapl.setExpectedRotation(flicker.scenario.startRotation.value)
-                } else {
-                    tapl.setExpectedRotation(Rotation.ROTATION_0.value)
-                }
-                RemoveAllTasksButHomeRule.removeAllTasksButHome()
-            }
-            transitions {
-                tapl
-                    .goHome()
-                    .switchToAllApps()
-                    .getAppIcon(testApp.launcherName)
-                    .launch(testApp.`package`)
-            }
-            teardown { testApp.exit(wmHelper) }
-        }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun focusChanges() {
-        super.focusChanges()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun appWindowReplacesLauncherAsTopWindow() {
-        super.appWindowReplacesLauncherAsTopWindow()
-    }
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun appWindowAsTopWindowAtEnd() {
-        super.appWindowAsTopWindowAtEnd()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun appWindowBecomesTopWindow() {
-        super.appWindowBecomesTopWindow()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun appWindowBecomesVisible() {
-        super.appWindowBecomesVisible()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun appWindowIsTopWindowAtEnd() {
-        super.appWindowIsTopWindowAtEnd()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun appLayerBecomesVisible() {
-        super.appLayerBecomesVisible()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun appLayerReplacesLauncher() {
-        super.appLayerReplacesLauncher()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun cujCompleted() {
-        super.cujCompleted()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun entireScreenCovered() {
-        super.entireScreenCovered()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() {
-        super.navBarLayerIsVisibleAtStartAndEnd()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun navBarLayerPositionAtStartAndEnd() {
-        super.navBarLayerPositionAtStartAndEnd()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun navBarWindowIsAlwaysVisible() {
-        super.navBarWindowIsAlwaysVisible()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun navBarWindowIsVisibleAtStartAndEnd() {
-        super.navBarWindowIsVisibleAtStartAndEnd()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun statusBarLayerIsVisibleAtStartAndEnd() {
-        super.statusBarLayerIsVisibleAtStartAndEnd()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun statusBarLayerPositionAtStartAndEnd() {
-        super.statusBarLayerPositionAtStartAndEnd()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun statusBarWindowIsAlwaysVisible() {
-        super.statusBarWindowIsAlwaysVisible()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() {
-        super.taskBarLayerIsVisibleAtStartAndEnd()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun taskBarWindowIsAlwaysVisible() {
-        super.taskBarWindowIsAlwaysVisible()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
-    }
-
-    @FlakyTest(bugId = 240916028)
-    @Test
-    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
-        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-    }
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIconCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIconCfArm.kt
deleted file mode 100644
index d33a272..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIconCfArm.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.launch
-
-import android.platform.test.annotations.FlakyTest
-import android.tools.device.flicker.annotation.FlickerServiceCompatible
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/** Some assertions will fail because of b/264415996 */
-@FlickerServiceCompatible
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenAppColdFromIconCfArm(flicker: FlickerTest) : OpenAppColdFromIcon(flicker) {
-    @Test
-    @FlakyTest
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
-    }
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt
deleted file mode 100644
index 26f88d2..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt
+++ /dev/null
@@ -1,94 +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.server.wm.flicker.launch
-
-import android.platform.test.annotations.Postsubmit
-import android.platform.test.annotations.Presubmit
-import android.tools.device.flicker.annotation.FlickerServiceCompatible
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.helpers.setRotation
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test cold launching an app from launcher
- *
- * To run this test: `atest FlickerTests:OpenAppColdTest`
- *
- * Actions:
- * ```
- *     Make sure no apps are running on the device
- *     Launch an app [testApp] and wait animation to complete
- * ```
- *
- * Notes:
- * ```
- *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
- *        are inherited [OpenAppTransition]
- *     2. Part of the test setup occurs automatically via
- *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
- *        including configuring navigation mode, initial orientation and ensuring no
- *        apps are running before setup
- * ```
- */
-@RequiresDevice
-@FlickerServiceCompatible
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenAppColdTest(flicker: FlickerTest) : OpenAppFromLauncherTransition(flicker) {
-    /** {@inheritDoc} */
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            super.transition(this)
-            setup {
-                removeAllTasksButHome()
-                this.setRotation(flicker.scenario.startRotation)
-            }
-            teardown { testApp.exit(wmHelper) }
-            transitions { testApp.launchViaIntent(wmHelper) }
-        }
-
-    /** {@inheritDoc} */
-    @Presubmit @Test override fun appLayerReplacesLauncher() = super.appLayerReplacesLauncher()
-
-    @Postsubmit
-    @Test
-    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTestCfArm.kt
deleted file mode 100644
index d9a99da..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTestCfArm.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.launch
-
-import android.platform.test.annotations.FlakyTest
-import android.tools.device.flicker.annotation.FlickerServiceCompatible
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-@FlickerServiceCompatible
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenAppColdTestCfArm(flicker: FlickerTest) : OpenAppColdTest(flicker) {
-    @FlakyTest(bugId = 273696733)
-    @Test
-    override fun appLayerReplacesLauncher() = super.appLayerReplacesLauncher()
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt
new file mode 100644
index 0000000..ae1f78a
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt
@@ -0,0 +1,218 @@
+/*
+ * 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.wm.flicker.launch
+
+import android.platform.test.annotations.FlakyTest
+import android.tools.common.Rotation
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule
+import androidx.test.filters.RequiresDevice
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test cold launching an app from launcher
+ *
+ * To run this test: `atest FlickerTests:OpenAppColdFromIcon`
+ *
+ * Actions:
+ * ```
+ *     Make sure no apps are running on the device
+ *     Launch an app [testApp] by clicking it's icon on all apps and wait animation to complete
+ * ```
+ *
+ * Notes:
+ * ```
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [OpenAppTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class OpenAppFromIconColdTest(flicker: LegacyFlickerTest) :
+    OpenAppFromLauncherTransition(flicker) {
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            super.transition(this)
+            setup {
+                if (flicker.scenario.isTablet) {
+                    tapl.setExpectedRotation(flicker.scenario.startRotation.value)
+                } else {
+                    tapl.setExpectedRotation(Rotation.ROTATION_0.value)
+                }
+                RemoveAllTasksButHomeRule.removeAllTasksButHome()
+            }
+            transitions {
+                tapl
+                    .goHome()
+                    .switchToAllApps()
+                    .getAppIcon(testApp.launcherName)
+                    .launch(testApp.`package`)
+            }
+            teardown { testApp.exit(wmHelper) }
+        }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun focusChanges() {
+        super.focusChanges()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun appWindowReplacesLauncherAsTopWindow() {
+        super.appWindowReplacesLauncherAsTopWindow()
+    }
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun appWindowAsTopWindowAtEnd() {
+        super.appWindowAsTopWindowAtEnd()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun appWindowBecomesTopWindow() {
+        super.appWindowBecomesTopWindow()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun appWindowBecomesVisible() {
+        super.appWindowBecomesVisible()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun appWindowIsTopWindowAtEnd() {
+        super.appWindowIsTopWindowAtEnd()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun appLayerBecomesVisible() {
+        super.appLayerBecomesVisible()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun appLayerReplacesLauncher() {
+        super.appLayerReplacesLauncher()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun cujCompleted() {
+        super.cujCompleted()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun entireScreenCovered() {
+        super.entireScreenCovered()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun navBarLayerIsVisibleAtStartAndEnd() {
+        super.navBarLayerIsVisibleAtStartAndEnd()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun navBarLayerPositionAtStartAndEnd() {
+        super.navBarLayerPositionAtStartAndEnd()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun navBarWindowIsAlwaysVisible() {
+        super.navBarWindowIsAlwaysVisible()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun navBarWindowIsVisibleAtStartAndEnd() {
+        super.navBarWindowIsVisibleAtStartAndEnd()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun statusBarLayerIsVisibleAtStartAndEnd() {
+        super.statusBarLayerIsVisibleAtStartAndEnd()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun statusBarLayerPositionAtStartAndEnd() {
+        super.statusBarLayerPositionAtStartAndEnd()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun statusBarWindowIsAlwaysVisible() {
+        super.statusBarWindowIsAlwaysVisible()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun taskBarLayerIsVisibleAtStartAndEnd() {
+        super.taskBarLayerIsVisibleAtStartAndEnd()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun taskBarWindowIsAlwaysVisible() {
+        super.taskBarWindowIsAlwaysVisible()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+    }
+
+    @FlakyTest(bugId = 240916028)
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
+        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTestCfArm.kt
new file mode 100644
index 0000000..4fd4a61
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTestCfArm.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.server.wm.flicker.launch
+
+import android.platform.test.annotations.FlakyTest
+import android.tools.common.flicker.annotation.FlickerServiceCompatible
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/** Some assertions will fail because of b/264415996 */
+@FlickerServiceCompatible
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenAppFromIconColdTestCfArm(flicker: LegacyFlickerTest) : OpenAppFromIconColdTest(flicker) {
+    @Test
+    @FlakyTest
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt
new file mode 100644
index 0000000..c95c548
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.wm.flicker.launch
+
+import android.tools.device.apphelpers.CameraAppHelper
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching an app after cold opening camera
+ *
+ * To run this test: `atest FlickerTests:OpenAppAfterCameraTest`
+ *
+ * Notes: Some default assertions are inherited [OpenAppTransition]
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class OpenAppFromIntentColdAfterCameraTest(flicker: LegacyFlickerTest) :
+    OpenAppFromLauncherTransition(flicker) {
+    private val cameraApp = CameraAppHelper(instrumentation)
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            super.transition(this)
+            setup {
+                tapl.setExpectedRotationCheckEnabled(false)
+                // 1. Open camera - cold -> close it first
+                cameraApp.exit(wmHelper)
+                cameraApp.launchViaIntent(wmHelper)
+                // Can't use TAPL due to Recents not showing in 3 Button Nav in full screen mode
+                device.pressHome()
+                tapl.getWorkspace()
+            }
+            teardown { testApp.exit(wmHelper) }
+            transitions { testApp.launchViaIntent(wmHelper) }
+        }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTestCfArm.kt
new file mode 100644
index 0000000..117cff2
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTestCfArm.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.server.wm.flicker.launch
+
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenAppFromIntentColdAfterCameraTestCfArm(flicker: LegacyFlickerTest) :
+    OpenAppFromIntentColdAfterCameraTest(flicker) {
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt
new file mode 100644
index 0000000..e39a578
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.server.wm.flicker.launch
+
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
+import android.tools.common.flicker.annotation.FlickerServiceCompatible
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.setRotation
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test cold launching an app from launcher
+ *
+ * To run this test: `atest FlickerTests:OpenAppColdTest`
+ *
+ * Actions:
+ * ```
+ *     Make sure no apps are running on the device
+ *     Launch an app [testApp] and wait animation to complete
+ * ```
+ *
+ * Notes:
+ * ```
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [OpenAppTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@FlickerServiceCompatible
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class OpenAppFromIntentColdTest(flicker: LegacyFlickerTest) :
+    OpenAppFromLauncherTransition(flicker) {
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            super.transition(this)
+            setup {
+                removeAllTasksButHome()
+                this.setRotation(flicker.scenario.startRotation)
+            }
+            teardown { testApp.exit(wmHelper) }
+            transitions { testApp.launchViaIntent(wmHelper) }
+        }
+
+    /** {@inheritDoc} */
+    @Presubmit @Test override fun appLayerReplacesLauncher() = super.appLayerReplacesLauncher()
+
+    @Postsubmit
+    @Test
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTestCfArm.kt
new file mode 100644
index 0000000..6d0b6f4
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTestCfArm.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.server.wm.flicker.launch
+
+import android.platform.test.annotations.FlakyTest
+import android.tools.common.flicker.annotation.FlickerServiceCompatible
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@FlickerServiceCompatible
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenAppFromIntentColdTestCfArm(flicker: LegacyFlickerTest) :
+    OpenAppFromIntentColdTest(flicker) {
+    @FlakyTest(bugId = 273696733)
+    @Test
+    override fun appLayerReplacesLauncher() = super.appLayerReplacesLauncher()
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt
new file mode 100644
index 0000000..d2c3807
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.server.wm.flicker.launch
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.flicker.annotation.FlickerServiceCompatible
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.setRotation
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test warm launching an app from launcher
+ *
+ * To run this test: `atest FlickerTests:OpenAppWarmTest`
+ *
+ * Actions:
+ * ```
+ *     Launch [testApp]
+ *     Press home
+ *     Relaunch an app [testApp] and wait animation to complete (only this action is traced)
+ * ```
+ *
+ * Notes:
+ * ```
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [OpenAppTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@FlickerServiceCompatible
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class OpenAppFromIntentWarmTest(flicker: LegacyFlickerTest) :
+    OpenAppFromLauncherTransition(flicker) {
+    /** Defines the transition used to run the test */
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            super.transition(this)
+            setup {
+                tapl.setExpectedRotationCheckEnabled(false)
+                testApp.launchViaIntent(wmHelper)
+                tapl.goHome()
+                wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
+                this.setRotation(flicker.scenario.startRotation)
+            }
+            teardown { testApp.exit(wmHelper) }
+            transitions { testApp.launchViaIntent(wmHelper) }
+        }
+
+    /** {@inheritDoc} */
+    @FlakyTest(bugId = 206753786)
+    @Test
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun appLayerBecomesVisible() = super.appLayerBecomesVisible_warmStart()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun appWindowBecomesVisible() = super.appWindowBecomesVisible_warmStart()
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTestCfArm.kt
new file mode 100644
index 0000000..3e0958a
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTestCfArm.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.server.wm.flicker.launch
+
+import android.tools.common.flicker.annotation.FlickerServiceCompatible
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@FlickerServiceCompatible
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenAppFromIntentWarmTestCfArm(flicker: LegacyFlickerTest) :
+    OpenAppFromIntentWarmTest(flicker) {
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt
index 3d5a8e3..62fb570 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt
@@ -18,12 +18,13 @@
 
 import android.platform.test.annotations.Presubmit
 import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import com.android.server.wm.flicker.replacesLayer
 import org.junit.Test
 
 /** Base class for app launch tests */
-abstract class OpenAppFromLauncherTransition(flicker: FlickerTest) : OpenAppTransition(flicker) {
+abstract class OpenAppFromLauncherTransition(flicker: LegacyFlickerTest) :
+    OpenAppTransition(flicker) {
 
     /** Checks that the focus changes from the [ComponentNameMatcher.LAUNCHER] to [testApp] */
     @Presubmit
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
deleted file mode 100644
index b21777b..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt
+++ /dev/null
@@ -1,135 +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.server.wm.flicker.launch
-
-import android.platform.test.annotations.Postsubmit
-import android.platform.test.rule.SettingOverrideRule
-import android.provider.Settings
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import androidx.test.filters.RequiresDevice
-import org.junit.ClassRule
-import org.junit.FixMethodOrder
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test cold launching an app from a notification from the lock screen.
- *
- * This test assumes the device doesn't have AOD enabled
- *
- * To run this test: `atest FlickerTests:OpenAppFromLockNotificationCold`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Postsubmit
-open class OpenAppFromLockNotificationCold(flicker: FlickerTest) :
-    OpenAppFromNotificationCold(flicker) {
-
-    override val openingNotificationsFromLockScreen = true
-
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            // Needs to run at start of transition,
-            // so before the transition defined in super.transition
-            transitions { device.wakeUp() }
-
-            super.transition(this)
-
-            // Needs to run at the end of the setup, so after the setup defined in super.transition
-            setup {
-                device.sleep()
-                wmHelper.StateSyncBuilder().withoutTopVisibleAppWindows().waitForAndVerify()
-            }
-        }
-
-    /** {@inheritDoc} */
-    @Test @Ignore("Display is off at the start") override fun navBarLayerPositionAtStartAndEnd() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Display is off at the start")
-    override fun statusBarLayerPositionAtStartAndEnd() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Display is off at the start")
-    override fun taskBarLayerIsVisibleAtStartAndEnd() {}
-
-    /** {@inheritDoc} */
-    @Test @Ignore("Display is off at the start") override fun taskBarWindowIsAlwaysVisible() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Display is off at the start")
-    override fun statusBarLayerIsVisibleAtStartAndEnd() =
-        super.statusBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
-    override fun navBarWindowIsVisibleAtStartAndEnd() = super.navBarWindowIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
-    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
-    override fun navBarWindowIsAlwaysVisible() {}
-
-    /** {@inheritDoc} */
-    @Postsubmit
-    @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-
-        /**
-         * 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",
-            )
-    }
-}
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
deleted file mode 100644
index ec92ca6..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt
+++ /dev/null
@@ -1,166 +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.server.wm.flicker.launch
-
-import android.platform.test.annotations.FlakyTest
-import android.platform.test.annotations.Presubmit
-import android.platform.test.rule.SettingOverrideRule
-import android.provider.Settings
-import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.statusBarLayerPositionAtEnd
-import org.junit.ClassRule
-import org.junit.FixMethodOrder
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test warm launching an app from a notification from the lock screen.
- *
- * This test assumes the device doesn't have AOD enabled
- *
- * To run this test: `atest FlickerTests:OpenAppFromLockNotificationWarm`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenAppFromLockNotificationWarm(flicker: FlickerTest) : OpenAppFromNotificationWarm(flicker) {
-
-    override val openingNotificationsFromLockScreen = true
-
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            // Needs to run at start of transition,
-            // so before the transition defined in super.transition
-            transitions { device.wakeUp() }
-
-            super.transition(this)
-
-            // Needs to run at the end of the setup, so after the setup defined in super.transition
-            setup {
-                device.sleep()
-                wmHelper.StateSyncBuilder().withoutTopVisibleAppWindows().waitForAndVerify()
-            }
-        }
-
-    /**
-     * Checks that we start of with no top windows and then [testApp] becomes the first and only top
-     * window of the transition, with snapshot or splash screen windows optionally showing first.
-     */
-    @Test
-    @Presubmit
-    fun appWindowBecomesFirstAndOnlyTopWindow() {
-        flicker.assertWm {
-            this.hasNoVisibleAppWindow()
-                .then()
-                .isAppWindowOnTop(ComponentNameMatcher.SNAPSHOT, isOptional = true)
-                .then()
-                .isAppWindowOnTop(ComponentNameMatcher.SPLASH_SCREEN, isOptional = true)
-                .then()
-                .isAppWindowOnTop(testApp)
-        }
-    }
-
-    /** Checks that the screen is locked at the start of the transition */
-    @Test
-    @Presubmit
-    fun screenLockedStart() {
-        flicker.assertWmStart { isKeyguardShowing() }
-    }
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
-    override fun navBarLayerPositionAtStartAndEnd() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
-    override fun taskBarWindowIsAlwaysVisible() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun statusBarLayerPositionAtStartAndEnd() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun statusBarLayerIsVisibleAtStartAndEnd() =
-        super.statusBarLayerIsVisibleAtStartAndEnd()
-
-    /**
-     * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the
-     * transition
-     */
-    @Presubmit @Test fun statusBarLayerPositionAtEnd() = flicker.statusBarLayerPositionAtEnd()
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
-    override fun navBarWindowIsVisibleAtStartAndEnd() = super.navBarWindowIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
-    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
-    override fun navBarWindowIsAlwaysVisible() {}
-
-    /** {@inheritDoc} */
-    @FlakyTest(bugId = 246284526)
-    @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-
-        /**
-         * 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",
-            )
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt
deleted file mode 100644
index 009d617..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.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.server.wm.flicker.launch
-
-import android.platform.test.annotations.FlakyTest
-import android.platform.test.annotations.Postsubmit
-import android.platform.test.annotations.Presubmit
-import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import android.tools.device.helpers.wakeUpAndGoToHomeScreen
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.helpers.ShowWhenLockedAppHelper
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test cold launching an app from a notification from the lock screen when there is an app overlaid
- * on the lock screen.
- *
- * To run this test: `atest FlickerTests:OpenAppFromLockNotificationWithLockOverlayApp`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Postsubmit
-class OpenAppFromLockNotificationWithLockOverlayApp(flicker: FlickerTest) :
-    OpenAppFromLockNotificationCold(flicker) {
-    private val showWhenLockedApp = ShowWhenLockedAppHelper(instrumentation)
-
-    // Although we are technically still locked here, the overlay app means we should open the
-    // notification shade as if we were unlocked.
-    override val openingNotificationsFromLockScreen = false
-
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            super.transition(this)
-
-            setup {
-                device.wakeUpAndGoToHomeScreen()
-
-                // Launch an activity that is shown when the device is locked
-                showWhenLockedApp.launchViaIntent(wmHelper)
-                wmHelper.StateSyncBuilder().withFullScreenApp(showWhenLockedApp).waitForAndVerify()
-
-                device.sleep()
-                wmHelper.StateSyncBuilder().withoutTopVisibleAppWindows().waitForAndVerify()
-            }
-
-            teardown { showWhenLockedApp.exit(wmHelper) }
-        }
-
-    @Test
-    @FlakyTest(bugId = 227143265)
-    fun showWhenLockedAppWindowBecomesVisible() {
-        flicker.assertWm {
-            this.hasNoVisibleAppWindow()
-                .then()
-                .isAppWindowOnTop(ComponentNameMatcher.SNAPSHOT, isOptional = true)
-                .then()
-                .isAppWindowOnTop(showWhenLockedApp)
-        }
-    }
-
-    @Test
-    @FlakyTest(bugId = 227143265)
-    fun showWhenLockedAppLayerBecomesVisible() {
-        flicker.assertLayers {
-            this.isInvisible(showWhenLockedApp)
-                .then()
-                .isVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
-                .then()
-                .isVisible(showWhenLockedApp)
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Presubmit @Test override fun appLayerBecomesVisible() = super.appLayerBecomesVisible()
-
-    /** {@inheritDoc} */
-    @FlakyTest(bugId = 227143265)
-    @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
-    /** {@inheritDoc} */
-    @FlakyTest(bugId = 209599395)
-    @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
-    @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
-
-    @FlakyTest(bugId = 278227468)
-    @Test
-    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
-        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockTransition.kt
deleted file mode 100644
index eae9ca1..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockTransition.kt
+++ /dev/null
@@ -1,127 +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.server.wm.flicker.launch
-
-import android.platform.test.annotations.FlakyTest
-import android.platform.test.annotations.Presubmit
-import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import com.android.server.wm.flicker.navBarLayerPositionAtEnd
-import com.android.server.wm.flicker.statusBarLayerPositionAtEnd
-import org.junit.Assume
-import org.junit.Ignore
-import org.junit.Test
-
-/** Base class for app launch tests from lock screen */
-abstract class OpenAppFromLockTransition(flicker: FlickerTest) : OpenAppTransition(flicker) {
-
-    /** Defines the transition used to run the test */
-    override val transition: FlickerBuilder.() -> Unit = {
-        super.transition(this)
-        setup {
-            device.sleep()
-            wmHelper.StateSyncBuilder().withoutTopVisibleAppWindows().waitForAndVerify()
-        }
-        teardown { testApp.exit(wmHelper) }
-        transitions { testApp.launchViaIntent(wmHelper) }
-    }
-
-    /** Check that we go from no focus to focus on the [testApp] */
-    @Presubmit
-    @Test
-    open fun focusChanges() {
-        flicker.assertEventLog { this.focusChanges("", testApp.`package`) }
-    }
-
-    /**
-     * Checks that we start of with no top windows and then [testApp] becomes the first and only top
-     * window of the transition, with snapshot or splash screen windows optionally showing first.
-     */
-    @FlakyTest(bugId = 203538234)
-    @Test
-    open fun appWindowBecomesFirstAndOnlyTopWindow() {
-        flicker.assertWm {
-            this.hasNoVisibleAppWindow()
-                .then()
-                .isAppWindowOnTop(ComponentNameMatcher.SNAPSHOT, isOptional = true)
-                .then()
-                .isAppWindowOnTop(ComponentNameMatcher.SPLASH_SCREEN, isOptional = true)
-                .then()
-                .isAppWindowOnTop(testApp)
-        }
-    }
-
-    /** Checks that the screen is locked at the start of the transition */
-    @Presubmit
-    @Test
-    fun screenLockedStart() {
-        flicker.assertLayersStart { isEmpty() }
-    }
-
-    /** {@inheritDoc} */
-    @FlakyTest(bugId = 203538234)
-    @Test
-    override fun appWindowBecomesVisible() = super.appWindowBecomesVisible()
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun navBarLayerPositionAtStartAndEnd() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun statusBarLayerPositionAtStartAndEnd() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun taskBarLayerIsVisibleAtStartAndEnd() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun taskBarWindowIsAlwaysVisible() {}
-
-    /** Checks the position of the [ComponentNameMatcher.NAV_BAR] at the end of the transition */
-    @Presubmit
-    @Test
-    open fun navBarLayerPositionAtEnd() {
-        Assume.assumeFalse(flicker.scenario.isTablet)
-        flicker.navBarLayerPositionAtEnd()
-    }
-
-    /** Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the end of the transition */
-    @Presubmit @Test fun statusBarLayerPositionAtEnd() = flicker.statusBarLayerPositionAtEnd()
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun statusBarLayerIsVisibleAtStartAndEnd() {}
-
-    /**
-     * Checks that the [ComponentNameMatcher.STATUS_BAR] layer is visible at the end of the trace
-     *
-     * It is not possible to check at the start because the screen is off
-     */
-    @Presubmit
-    @Test
-    fun statusBarLayerIsVisibleAtEnd() {
-        flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenTransition.kt
new file mode 100644
index 0000000..687bc19
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenTransition.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.server.wm.flicker.launch
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import com.android.server.wm.flicker.navBarLayerPositionAtEnd
+import com.android.server.wm.flicker.statusBarLayerPositionAtEnd
+import org.junit.Assume
+import org.junit.Ignore
+import org.junit.Test
+
+/** Base class for app launch tests from lock screen */
+abstract class OpenAppFromLockscreenTransition(flicker: LegacyFlickerTest) :
+    OpenAppTransition(flicker) {
+
+    /** Defines the transition used to run the test */
+    override val transition: FlickerBuilder.() -> Unit = {
+        super.transition(this)
+        setup {
+            device.sleep()
+            wmHelper.StateSyncBuilder().withoutTopVisibleAppWindows().waitForAndVerify()
+        }
+        teardown { testApp.exit(wmHelper) }
+        transitions { testApp.launchViaIntent(wmHelper) }
+    }
+
+    /** Check that we go from no focus to focus on the [testApp] */
+    @Presubmit
+    @Test
+    open fun focusChanges() {
+        flicker.assertEventLog { this.focusChanges("", testApp.`package`) }
+    }
+
+    /**
+     * Checks that we start of with no top windows and then [testApp] becomes the first and only top
+     * window of the transition, with snapshot or splash screen windows optionally showing first.
+     */
+    @FlakyTest(bugId = 203538234)
+    @Test
+    open fun appWindowBecomesFirstAndOnlyTopWindow() {
+        flicker.assertWm {
+            this.hasNoVisibleAppWindow()
+                .then()
+                .isAppWindowOnTop(ComponentNameMatcher.SNAPSHOT, isOptional = true)
+                .then()
+                .isAppWindowOnTop(ComponentNameMatcher.SPLASH_SCREEN, isOptional = true)
+                .then()
+                .isAppWindowOnTop(testApp)
+        }
+    }
+
+    /** Checks that the screen is locked at the start of the transition */
+    @Presubmit
+    @Test
+    fun screenLockedStart() {
+        flicker.assertLayersStart { isEmpty() }
+    }
+
+    /** {@inheritDoc} */
+    @FlakyTest(bugId = 203538234)
+    @Test
+    override fun appWindowBecomesVisible() = super.appWindowBecomesVisible()
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun navBarLayerPositionAtStartAndEnd() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun statusBarLayerPositionAtStartAndEnd() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun taskBarLayerIsVisibleAtStartAndEnd() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun taskBarWindowIsAlwaysVisible() {}
+
+    /** Checks the position of the [ComponentNameMatcher.NAV_BAR] at the end of the transition */
+    @Presubmit
+    @Test
+    open fun navBarLayerPositionAtEnd() {
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarLayerPositionAtEnd()
+    }
+
+    /** Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the end of the transition */
+    @Presubmit @Test fun statusBarLayerPositionAtEnd() = flicker.statusBarLayerPositionAtEnd()
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun statusBarLayerIsVisibleAtStartAndEnd() {}
+
+    /**
+     * Checks that the [ComponentNameMatcher.STATUS_BAR] layer is visible at the end of the trace
+     *
+     * It is not possible to check at the start because the screen is off
+     */
+    @Presubmit
+    @Test
+    fun statusBarLayerIsVisibleAtEnd() {
+        flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt
new file mode 100644
index 0000000..7a16060
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt
@@ -0,0 +1,224 @@
+/*
+ * 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.server.wm.flicker.launch
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.common.flicker.annotation.FlickerServiceCompatible
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
+import org.junit.Assume
+import org.junit.FixMethodOrder
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching an app while the device is locked
+ *
+ * This test assumes the device doesn't have AOD enabled
+ *
+ * To run this test: `atest FlickerTests:OpenAppNonResizeableTest`
+ *
+ * Actions:
+ * ```
+ *     Lock the device.
+ *     Launch an app on top of the lock screen [testApp] and wait animation to complete
+ * ```
+ *
+ * Notes:
+ * ```
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [OpenAppTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@FlickerServiceCompatible
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class OpenAppFromLockscreenViaIntentTest(flicker: LegacyFlickerTest) :
+    OpenAppFromLockscreenTransition(flicker) {
+    override val testApp = NonResizeableAppHelper(instrumentation)
+
+    /**
+     * Checks that the [ComponentNameMatcher.NAV_BAR] layer starts invisible, becomes visible during
+     * unlocking animation and remains visible at the end
+     */
+    @Presubmit
+    @Test
+    fun navBarLayerVisibilityChanges() {
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.assertLayers {
+            this.isInvisible(ComponentNameMatcher.NAV_BAR)
+                .then()
+                .isVisible(ComponentNameMatcher.NAV_BAR)
+        }
+    }
+
+    /** Checks if [testApp] is visible at the end of the transition */
+    @Presubmit
+    @Test
+    fun appWindowBecomesVisibleAtEnd() {
+        flicker.assertWmEnd { this.isAppWindowVisible(testApp) }
+    }
+
+    /**
+     * Checks that the [ComponentNameMatcher.NAV_BAR] starts the transition invisible, then becomes
+     * visible during the unlocking animation and remains visible at the end of the transition
+     */
+    @Presubmit
+    @Test
+    fun navBarWindowsVisibilityChanges() {
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.assertWm {
+            this.isNonAppWindowInvisible(ComponentNameMatcher.NAV_BAR)
+                .then()
+                .isAboveAppWindowVisible(ComponentNameMatcher.NAV_BAR)
+        }
+    }
+
+    /**
+     * Checks that the [ComponentNameMatcher.TASK_BAR] starts the transition invisible, then becomes
+     * visible during the unlocking animation and remains visible at the end of the transition
+     */
+    @Presubmit
+    @Test
+    fun taskBarLayerIsVisibleAtEnd() {
+        Assume.assumeTrue(flicker.scenario.isTablet)
+        flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.TASK_BAR) }
+    }
+
+    /**
+     * Checks that the [ComponentNameMatcher.STATUS_BAR] layer is visible at the end of the trace
+     *
+     * It is not possible to check at the start because the screen is off
+     */
+    @Presubmit
+    @Test
+    override fun statusBarLayerIsVisibleAtStartAndEnd() {
+        flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
+    }
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun taskBarLayerIsVisibleAtStartAndEnd() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun navBarLayerIsVisibleAtStartAndEnd() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun taskBarWindowIsAlwaysVisible() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun navBarWindowIsAlwaysVisible() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun navBarWindowIsVisibleAtStartAndEnd() = super.navBarWindowIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun statusBarWindowIsAlwaysVisible() {}
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun appWindowBecomesFirstAndOnlyTopWindow() =
+        super.appWindowBecomesFirstAndOnlyTopWindow()
+
+    /** {@inheritDoc} */
+    @Presubmit @Test override fun appWindowBecomesVisible() = super.appWindowBecomesVisible()
+
+    /** Checks the [ComponentNameMatcher.NAV_BAR] is visible at the end of the transition */
+    @Presubmit
+    @Test
+    fun navBarLayerIsVisibleAtEnd() {
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.NAV_BAR) }
+    }
+
+    /** {@inheritDoc} */
+    @FlakyTest
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun appLayerBecomesVisible() {
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        super.appLayerBecomesVisible()
+    }
+
+    /** {@inheritDoc} */
+    @FlakyTest(bugId = 227143265)
+    @Test
+    fun appLayerBecomesVisibleTablet() {
+        Assume.assumeTrue(flicker.scenario.isTablet)
+        super.appLayerBecomesVisible()
+    }
+
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+    @FlakyTest(bugId = 251217585)
+    @Test
+    override fun focusChanges() {
+        super.focusChanges()
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
+                supportedNavigationModes = listOf(NavBar.MODE_GESTURAL),
+                supportedRotations = listOf(Rotation.ROTATION_0)
+            )
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt
deleted file mode 100644
index 7bcb910..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt
+++ /dev/null
@@ -1,113 +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.server.wm.flicker.launch
-
-import android.platform.test.annotations.Postsubmit
-import android.platform.test.annotations.Presubmit
-import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.statusBarLayerPositionAtEnd
-import org.junit.FixMethodOrder
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test cold launching an app from a notification.
- *
- * This test assumes the device doesn't have AOD enabled
- *
- * To run this test: `atest FlickerTests:OpenAppFromNotificationCold`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Postsubmit
-open class OpenAppFromNotificationCold(flicker: FlickerTest) :
-    OpenAppFromNotificationWarm(flicker) {
-    /** {@inheritDoc} */
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            super.transition(this)
-
-            setup {
-                // Close the app that posted the notification to trigger a cold start next time
-                // it is open - can't just kill it because that would remove the notification.
-                tapl.setExpectedRotationCheckEnabled(false)
-                tapl.goHome()
-                tapl.workspace.switchToOverview()
-                tapl.overview.dismissAllTasks()
-            }
-        }
-
-    @Presubmit @Test override fun appWindowBecomesVisible() = appWindowBecomesVisible_coldStart()
-
-    @Presubmit @Test override fun appLayerBecomesVisible() = appLayerBecomesVisible_coldStart()
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
-    override fun navBarLayerPositionAtStartAndEnd() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun statusBarLayerPositionAtStartAndEnd() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun statusBarLayerIsVisibleAtStartAndEnd() =
-        super.statusBarLayerIsVisibleAtStartAndEnd()
-
-    /**
-     * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the
-     * transition
-     */
-    @Presubmit @Test open fun statusBarLayerPositionAtEnd() = flicker.statusBarLayerPositionAtEnd()
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
-    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
-    override fun navBarWindowIsAlwaysVisible() {}
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationColdCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationColdCfArm.kt
deleted file mode 100644
index 8b4a613..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationColdCfArm.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.launch
-
-import android.platform.test.annotations.Postsubmit
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import org.junit.FixMethodOrder
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Postsubmit
-class OpenAppFromNotificationColdCfArm(flicker: FlickerTest) :
-    OpenAppFromNotificationCold(flicker) {
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt
deleted file mode 100644
index 425e674d..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt
+++ /dev/null
@@ -1,207 +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.server.wm.flicker.launch
-
-import android.platform.test.annotations.Postsubmit
-import android.platform.test.annotations.Presubmit
-import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import android.tools.device.helpers.wakeUpAndGoToHomeScreen
-import android.view.WindowInsets
-import android.view.WindowManager
-import androidx.test.filters.RequiresDevice
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.Until
-import com.android.server.wm.flicker.helpers.NotificationAppHelper
-import com.android.server.wm.flicker.helpers.setRotation
-import com.android.server.wm.flicker.navBarLayerIsVisibleAtEnd
-import com.android.server.wm.flicker.navBarLayerPositionAtEnd
-import com.android.server.wm.flicker.navBarWindowIsVisibleAtEnd
-import com.android.server.wm.flicker.taskBarLayerIsVisibleAtEnd
-import com.android.server.wm.flicker.taskBarWindowIsVisibleAtEnd
-import org.junit.Assume
-import org.junit.FixMethodOrder
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test cold launching an app from a notification.
- *
- * This test assumes the device doesn't have AOD enabled
- *
- * To run this test: `atest FlickerTests:OpenAppFromNotificationWarm`
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenAppFromNotificationWarm(flicker: FlickerTest) : OpenAppTransition(flicker) {
-    override val testApp: NotificationAppHelper = NotificationAppHelper(instrumentation)
-
-    open val openingNotificationsFromLockScreen = false
-
-    /** {@inheritDoc} */
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            setup {
-                device.wakeUpAndGoToHomeScreen()
-                this.setRotation(flicker.scenario.startRotation)
-                testApp.launchViaIntent(wmHelper)
-                wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
-                testApp.postNotification(wmHelper)
-                device.pressHome()
-                wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
-            }
-
-            transitions {
-                var startY = 10
-                var endY = 3 * device.displayHeight / 4
-                var steps = 25
-                if (openingNotificationsFromLockScreen) {
-                    val wm: WindowManager =
-                        instrumentation.context.getSystemService(WindowManager::class.java)
-                            ?: error("Unable to connect to WindowManager service")
-                    val metricInsets = wm.currentWindowMetrics.windowInsets
-                    val insets =
-                        metricInsets.getInsetsIgnoringVisibility(
-                            WindowInsets.Type.statusBars() or WindowInsets.Type.displayCutout()
-                        )
-
-                    startY = insets.top + 100
-                    endY = device.displayHeight / 2
-                    steps = 4
-                }
-
-                // Swipe down to show the notification shade
-                val x = device.displayWidth / 2
-                device.swipe(x, startY, x, endY, steps)
-                device.waitForIdle(2000)
-                instrumentation.uiAutomation.syncInputTransactions()
-
-                // Launch the activity by clicking the notification
-                val notification =
-                    device.wait(Until.findObject(By.text("Flicker Test Notification")), 2000L)
-                notification?.click() ?: error("Notification not found")
-                instrumentation.uiAutomation.syncInputTransactions()
-
-                // Wait for the app to launch
-                wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
-            }
-
-            teardown { testApp.exit(wmHelper) }
-        }
-
-    @Presubmit @Test override fun appWindowBecomesVisible() = appWindowBecomesVisible_warmStart()
-
-    @Presubmit @Test override fun appLayerBecomesVisible() = appLayerBecomesVisible_warmStart()
-
-    @Presubmit
-    @Test
-    open fun notificationAppWindowVisibleAtEnd() {
-        flicker.assertWmEnd { this.isAppWindowVisible(testApp) }
-    }
-
-    @Presubmit
-    @Test
-    open fun notificationAppWindowOnTopAtEnd() {
-        flicker.assertWmEnd { this.isAppWindowOnTop(testApp) }
-    }
-
-    @Presubmit
-    @Test
-    open fun notificationAppLayerVisibleAtEnd() {
-        flicker.assertLayersEnd { this.isVisible(testApp) }
-    }
-
-    /**
-     * Checks that the [ComponentNameMatcher.TASK_BAR] window is visible at the end of the
-     * transition
-     *
-     * Note: Large screen only
-     */
-    @Presubmit
-    @Test
-    open fun taskBarWindowIsVisibleAtEnd() {
-        Assume.assumeTrue(flicker.scenario.isTablet)
-        flicker.taskBarWindowIsVisibleAtEnd()
-    }
-
-    /**
-     * Checks that the [ComponentNameMatcher.TASK_BAR] layer is visible at the end of the transition
-     *
-     * Note: Large screen only
-     */
-    @Presubmit
-    @Test
-    open fun taskBarLayerIsVisibleAtEnd() {
-        Assume.assumeTrue(flicker.scenario.isTablet)
-        flicker.taskBarLayerIsVisibleAtEnd()
-    }
-
-    /** Checks the position of the [ComponentNameMatcher.NAV_BAR] at the end of the transition */
-    @Presubmit
-    @Test
-    open fun navBarLayerPositionAtEnd() {
-        Assume.assumeFalse(flicker.scenario.isTablet)
-        flicker.navBarLayerPositionAtEnd()
-    }
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    open fun navBarLayerIsVisibleAtEnd() {
-        Assume.assumeFalse(flicker.scenario.isTablet)
-        flicker.navBarLayerIsVisibleAtEnd()
-    }
-
-    @Presubmit
-    @Test
-    open fun navBarWindowIsVisibleAtEnd() {
-        Assume.assumeFalse(flicker.scenario.isTablet)
-        flicker.navBarWindowIsVisibleAtEnd()
-    }
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Display is off at the start")
-    override fun taskBarLayerIsVisibleAtStartAndEnd() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Postsubmit
-    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarmCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarmCfArm.kt
deleted file mode 100644
index 43d28fa..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarmCfArm.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.launch
-
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import org.junit.FixMethodOrder
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenAppFromNotificationWarmCfArm(flicker: FlickerTest) :
-    OpenAppFromNotificationWarm(flicker) {
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
index 8e1b059..eec6bfd 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
@@ -19,11 +19,11 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.Rotation
-import android.tools.device.flicker.annotation.FlickerServiceCompatible
+import android.tools.common.flicker.annotation.FlickerServiceCompatible
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.helpers.setRotation
 import org.junit.FixMethodOrder
@@ -60,7 +60,8 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenAppFromOverviewTest(flicker: FlickerTest) : OpenAppFromLauncherTransition(flicker) {
+open class OpenAppFromOverviewTest(flicker: LegacyFlickerTest) :
+    OpenAppFromLauncherTransition(flicker) {
 
     /** Defines the transition used to run the test */
     override val transition: FlickerBuilder.() -> Unit
@@ -106,13 +107,11 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTestCfArm.kt
index ff24190..ab6a1ea 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTestCfArm.kt
@@ -16,10 +16,10 @@
 
 package com.android.server.wm.flicker.launch
 
-import android.tools.device.flicker.annotation.FlickerServiceCompatible
+import android.tools.common.flicker.annotation.FlickerServiceCompatible
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -30,18 +30,17 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenAppFromOverviewTestCfArm(flicker: FlickerTest) : OpenAppFromOverviewTest(flicker) {
+open class OpenAppFromOverviewTestCfArm(flicker: LegacyFlickerTest) :
+    OpenAppFromOverviewTest(flicker) {
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
          * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
deleted file mode 100644
index 1383ae3..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
+++ /dev/null
@@ -1,224 +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.server.wm.flicker.launch
-
-import android.platform.test.annotations.FlakyTest
-import android.platform.test.annotations.Presubmit
-import android.tools.common.NavBar
-import android.tools.common.Rotation
-import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.device.flicker.annotation.FlickerServiceCompatible
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
-import org.junit.Assume
-import org.junit.FixMethodOrder
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test launching an app while the device is locked
- *
- * This test assumes the device doesn't have AOD enabled
- *
- * To run this test: `atest FlickerTests:OpenAppNonResizeableTest`
- *
- * Actions:
- * ```
- *     Lock the device.
- *     Launch an app on top of the lock screen [testApp] and wait animation to complete
- * ```
- *
- * Notes:
- * ```
- *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
- *        are inherited [OpenAppTransition]
- *     2. Part of the test setup occurs automatically via
- *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
- *        including configuring navigation mode, initial orientation and ensuring no
- *        apps are running before setup
- * ```
- */
-@RequiresDevice
-@FlickerServiceCompatible
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenAppNonResizeableTest(flicker: FlickerTest) : OpenAppFromLockTransition(flicker) {
-    override val testApp = NonResizeableAppHelper(instrumentation)
-
-    /**
-     * Checks that the [ComponentNameMatcher.NAV_BAR] layer starts invisible, becomes visible during
-     * unlocking animation and remains visible at the end
-     */
-    @Presubmit
-    @Test
-    fun navBarLayerVisibilityChanges() {
-        Assume.assumeFalse(flicker.scenario.isTablet)
-        flicker.assertLayers {
-            this.isInvisible(ComponentNameMatcher.NAV_BAR)
-                .then()
-                .isVisible(ComponentNameMatcher.NAV_BAR)
-        }
-    }
-
-    /** Checks if [testApp] is visible at the end of the transition */
-    @Presubmit
-    @Test
-    fun appWindowBecomesVisibleAtEnd() {
-        flicker.assertWmEnd { this.isAppWindowVisible(testApp) }
-    }
-
-    /**
-     * Checks that the [ComponentNameMatcher.NAV_BAR] starts the transition invisible, then becomes
-     * visible during the unlocking animation and remains visible at the end of the transition
-     */
-    @Presubmit
-    @Test
-    fun navBarWindowsVisibilityChanges() {
-        Assume.assumeFalse(flicker.scenario.isTablet)
-        flicker.assertWm {
-            this.isNonAppWindowInvisible(ComponentNameMatcher.NAV_BAR)
-                .then()
-                .isAboveAppWindowVisible(ComponentNameMatcher.NAV_BAR)
-        }
-    }
-
-    /**
-     * Checks that the [ComponentNameMatcher.TASK_BAR] starts the transition invisible, then becomes
-     * visible during the unlocking animation and remains visible at the end of the transition
-     */
-    @Presubmit
-    @Test
-    fun taskBarLayerIsVisibleAtEnd() {
-        Assume.assumeTrue(flicker.scenario.isTablet)
-        flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.TASK_BAR) }
-    }
-
-    /**
-     * Checks that the [ComponentNameMatcher.STATUS_BAR] layer is visible at the end of the trace
-     *
-     * It is not possible to check at the start because the screen is off
-     */
-    @Presubmit
-    @Test
-    override fun statusBarLayerIsVisibleAtStartAndEnd() {
-        flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
-    }
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun taskBarLayerIsVisibleAtStartAndEnd() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun navBarLayerIsVisibleAtStartAndEnd() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun taskBarWindowIsAlwaysVisible() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun navBarWindowIsAlwaysVisible() {}
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
-    override fun navBarWindowIsVisibleAtStartAndEnd() = super.navBarWindowIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Test
-    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
-    override fun statusBarWindowIsAlwaysVisible() {}
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun appWindowBecomesFirstAndOnlyTopWindow() =
-        super.appWindowBecomesFirstAndOnlyTopWindow()
-
-    /** {@inheritDoc} */
-    @Presubmit @Test override fun appWindowBecomesVisible() = super.appWindowBecomesVisible()
-
-    /** Checks the [ComponentNameMatcher.NAV_BAR] is visible at the end of the transition */
-    @Presubmit
-    @Test
-    fun navBarLayerIsVisibleAtEnd() {
-        Assume.assumeFalse(flicker.scenario.isTablet)
-        flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.NAV_BAR) }
-    }
-
-    /** {@inheritDoc} */
-    @FlakyTest
-    @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun appLayerBecomesVisible() {
-        Assume.assumeFalse(flicker.scenario.isTablet)
-        super.appLayerBecomesVisible()
-    }
-
-    /** {@inheritDoc} */
-    @FlakyTest(bugId = 227143265)
-    @Test
-    fun appLayerBecomesVisibleTablet() {
-        Assume.assumeTrue(flicker.scenario.isTablet)
-        super.appLayerBecomesVisible()
-    }
-
-    @Presubmit
-    @Test
-    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
-        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
-    @FlakyTest(bugId = 251217585)
-    @Test
-    override fun focusChanges() {
-        super.focusChanges()
-    }
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
-                supportedNavigationModes = listOf(NavBar.MODE_GESTURAL),
-                supportedRotations = listOf(Rotation.ROTATION_0)
-            )
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt
index 87a14c6..bb11be5 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt
@@ -20,7 +20,7 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.apphelpers.StandardAppHelper
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.helpers.wakeUpAndGoToHomeScreen
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
@@ -28,7 +28,7 @@
 import org.junit.Test
 
 /** Base class for app launch tests */
-abstract class OpenAppTransition(flicker: FlickerTest) : BaseTest(flicker) {
+abstract class OpenAppTransition(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     protected open val testApp: StandardAppHelper = SimpleAppHelper(instrumentation)
 
     /** {@inheritDoc} */
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt
deleted file mode 100644
index 3385830..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt
+++ /dev/null
@@ -1,105 +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.server.wm.flicker.launch
-
-import android.platform.test.annotations.FlakyTest
-import android.platform.test.annotations.Presubmit
-import android.tools.device.flicker.annotation.FlickerServiceCompatible
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.helpers.setRotation
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test warm launching an app from launcher
- *
- * To run this test: `atest FlickerTests:OpenAppWarmTest`
- *
- * Actions:
- * ```
- *     Launch [testApp]
- *     Press home
- *     Relaunch an app [testApp] and wait animation to complete (only this action is traced)
- * ```
- *
- * Notes:
- * ```
- *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
- *        are inherited [OpenAppTransition]
- *     2. Part of the test setup occurs automatically via
- *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
- *        including configuring navigation mode, initial orientation and ensuring no
- *        apps are running before setup
- * ```
- */
-@RequiresDevice
-@FlickerServiceCompatible
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class OpenAppWarmTest(flicker: FlickerTest) : OpenAppFromLauncherTransition(flicker) {
-    /** Defines the transition used to run the test */
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            super.transition(this)
-            setup {
-                tapl.setExpectedRotationCheckEnabled(false)
-                testApp.launchViaIntent(wmHelper)
-                tapl.goHome()
-                wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
-                this.setRotation(flicker.scenario.startRotation)
-            }
-            teardown { testApp.exit(wmHelper) }
-            transitions { testApp.launchViaIntent(wmHelper) }
-        }
-
-    /** {@inheritDoc} */
-    @FlakyTest(bugId = 206753786)
-    @Test
-    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun appLayerBecomesVisible() = super.appLayerBecomesVisible_warmStart()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun appWindowBecomesVisible() = super.appWindowBecomesVisible_warmStart()
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTestCfArm.kt
deleted file mode 100644
index d8b38b3..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTestCfArm.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS 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.launch
-
-import android.tools.device.flicker.annotation.FlickerServiceCompatible
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import org.junit.FixMethodOrder
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-@FlickerServiceCompatible
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenAppWarmTestCfArm(flicker: FlickerTest) : OpenAppWarmTest(flicker) {
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt
new file mode 100644
index 0000000..1bdb6e71
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.wm.flicker.launch
+
+import android.os.SystemClock
+import android.platform.test.annotations.Postsubmit
+import android.tools.common.flicker.subject.layers.LayersTraceSubject
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.apphelpers.CameraAppHelper
+import android.tools.device.apphelpers.StandardAppHelper
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule
+import android.view.KeyEvent
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.setRotation
+import org.junit.FixMethodOrder
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test cold launching camera from launcher by double pressing power button
+ *
+ * To run this test: `atest FlickerTests:OpenCameraOnDoubleClickPowerButton`
+ *
+ * Actions:
+ * ```
+ *     Make sure no apps are running on the device
+ *     Launch an app [testApp] and wait animation to complete
+ * ```
+ *
+ * Notes:
+ * ```
+ *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ *        are inherited [OpenAppTransition]
+ *     2. Part of the test setup occurs automatically via
+ *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
+ *        including configuring navigation mode, initial orientation and ensuring no
+ *        apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenCameraFromHomeOnDoubleClickPowerButtonTest(flicker: LegacyFlickerTest) :
+    OpenAppFromLauncherTransition(flicker) {
+    private val cameraApp = CameraAppHelper(instrumentation)
+    override val testApp: StandardAppHelper
+        get() = cameraApp
+
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            setup {
+                RemoveAllTasksButHomeRule.removeAllTasksButHome()
+                this.setRotation(flicker.scenario.startRotation)
+            }
+            transitions {
+                device.pressKeyCode(KeyEvent.KEYCODE_POWER)
+                SystemClock.sleep(100)
+                device.pressKeyCode(KeyEvent.KEYCODE_POWER)
+                wmHelper.StateSyncBuilder().withWindowSurfaceAppeared(cameraApp).waitForAndVerify()
+            }
+            teardown { RemoveAllTasksButHomeRule.removeAllTasksButHome() }
+        }
+
+    @Postsubmit @Test override fun appLayerBecomesVisible() = super.appLayerBecomesVisible()
+
+    @Postsubmit @Test override fun appWindowAsTopWindowAtEnd() = super.appWindowAsTopWindowAtEnd()
+
+    @Postsubmit @Test override fun appWindowBecomesTopWindow() = super.appWindowBecomesTopWindow()
+
+    @Postsubmit @Test override fun appWindowBecomesVisible() = super.appWindowBecomesVisible()
+
+    @Postsubmit @Test override fun appLayerReplacesLauncher() = super.appLayerReplacesLauncher()
+
+    @Postsubmit @Test override fun appWindowIsTopWindowAtEnd() = super.appWindowIsTopWindowAtEnd()
+
+    @Postsubmit
+    @Test
+    override fun appWindowReplacesLauncherAsTopWindow() =
+        super.appWindowReplacesLauncherAsTopWindow()
+
+    @Postsubmit @Test override fun focusChanges() = super.focusChanges()
+
+    @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+
+    @Ignore("Not applicable to this CUJ. App is full screen at the end")
+    @Test
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+
+    @Postsubmit
+    @Test
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+
+    @Postsubmit
+    @Test
+    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+
+    @Ignore("Status bar visibility depends on whether the permission dialog is displayed or not")
+    @Test
+    override fun statusBarLayerIsVisibleAtStartAndEnd() =
+        super.statusBarLayerIsVisibleAtStartAndEnd()
+
+    @Ignore("Status bar visibility depends on whether the permission dialog is displayed or not")
+    @Test
+    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
+
+    @Ignore("Status bar visibility depends on whether the permission dialog is displayed or not")
+    @Test
+    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
+
+    @Ignore("Not applicable to this CUJ. App is full screen at the end")
+    @Test
+    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
+
+    @Ignore("Not applicable to this CUJ. App is full screen at the end")
+    @Test
+    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
+
+    @Postsubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+        flicker.assertLayers {
+            this.visibleLayersShownMoreThanOneConsecutiveEntry(
+                LayersTraceSubject.VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS +
+                    listOf(CAMERA_BACKGROUND)
+            )
+        }
+    }
+
+    @Postsubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+    @Ignore("Not applicable to this CUJ. App is full screen at the end")
+    @Test
+    override fun navBarWindowIsVisibleAtStartAndEnd() {
+        super.navBarWindowIsVisibleAtStartAndEnd()
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+
+        private val CAMERA_BACKGROUND =
+            ComponentNameMatcher(
+                "Background for SurfaceView" +
+                    "[com.google.android.GoogleCamera/" +
+                    "com.google.android.apps.camera.legacy.app.activity.main.CameraActivity]"
+            )
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenCameraOnDoubleClickPowerButton.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenCameraOnDoubleClickPowerButton.kt
deleted file mode 100644
index ae9ca80..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenCameraOnDoubleClickPowerButton.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.server.wm.flicker.launch
-
-import android.os.SystemClock
-import android.platform.test.annotations.Postsubmit
-import android.tools.device.apphelpers.CameraAppHelper
-import android.tools.device.apphelpers.StandardAppHelper
-import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
-import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule
-import android.view.KeyEvent
-import androidx.test.filters.RequiresDevice
-import com.android.server.wm.flicker.helpers.setRotation
-import org.junit.FixMethodOrder
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test cold launching camera from launcher by double pressing power button
- *
- * To run this test: `atest FlickerTests:OpenCameraOnDoubleClickPowerButton`
- *
- * Actions:
- * ```
- *     Make sure no apps are running on the device
- *     Launch an app [testApp] and wait animation to complete
- * ```
- *
- * Notes:
- * ```
- *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
- *        are inherited [OpenAppTransition]
- *     2. Part of the test setup occurs automatically via
- *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
- *        including configuring navigation mode, initial orientation and ensuring no
- *        apps are running before setup
- * ```
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenCameraOnDoubleClickPowerButton(flicker: FlickerTest) :
-    OpenAppFromLauncherTransition(flicker) {
-    private val cameraApp = CameraAppHelper(instrumentation)
-    override val testApp: StandardAppHelper
-        get() = cameraApp
-
-    override val transition: FlickerBuilder.() -> Unit
-        get() = {
-            setup {
-                RemoveAllTasksButHomeRule.removeAllTasksButHome()
-                this.setRotation(flicker.scenario.startRotation)
-            }
-            transitions {
-                device.pressKeyCode(KeyEvent.KEYCODE_POWER)
-                SystemClock.sleep(100)
-                device.pressKeyCode(KeyEvent.KEYCODE_POWER)
-                wmHelper.StateSyncBuilder().withWindowSurfaceAppeared(cameraApp).waitForAndVerify()
-            }
-            teardown { RemoveAllTasksButHomeRule.removeAllTasksButHome() }
-        }
-
-    @Postsubmit @Test override fun appLayerBecomesVisible() = super.appLayerBecomesVisible()
-
-    @Postsubmit @Test override fun appWindowAsTopWindowAtEnd() = super.appWindowAsTopWindowAtEnd()
-
-    @Postsubmit @Test override fun appWindowBecomesTopWindow() = super.appWindowBecomesTopWindow()
-
-    @Postsubmit @Test override fun appWindowBecomesVisible() = super.appWindowBecomesVisible()
-
-    @Postsubmit @Test override fun appLayerReplacesLauncher() = super.appLayerReplacesLauncher()
-
-    @Postsubmit @Test override fun appWindowIsTopWindowAtEnd() = super.appWindowIsTopWindowAtEnd()
-
-    @Postsubmit
-    @Test
-    override fun appWindowReplacesLauncherAsTopWindow() =
-        super.appWindowReplacesLauncherAsTopWindow()
-
-    @Postsubmit @Test override fun focusChanges() = super.focusChanges()
-
-    @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
-
-    @Ignore("Not applicable to this CUJ. App is full screen at the end")
-    @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
-    @Postsubmit
-    @Test
-    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
-    @Postsubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
-    @Ignore("Status bar visibility depends on whether the permission dialog is displayed or not")
-    @Test
-    override fun statusBarLayerIsVisibleAtStartAndEnd() =
-        super.statusBarLayerIsVisibleAtStartAndEnd()
-
-    @Ignore("Status bar visibility depends on whether the permission dialog is displayed or not")
-    @Test
-    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
-
-    @Ignore("Status bar visibility depends on whether the permission dialog is displayed or not")
-    @Test
-    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
-    @Ignore("Not applicable to this CUJ. App is full screen at the end")
-    @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
-
-    @Ignore("Not applicable to this CUJ. App is full screen at the end")
-    @Test
-    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
-
-    @Postsubmit
-    @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
-    @Postsubmit
-    @Test
-    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
-        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
-    @Ignore("Not applicable to this CUJ. App is full screen at the end")
-    @Test
-    override fun navBarWindowIsVisibleAtStartAndEnd() {
-        super.navBarWindowIsVisibleAtStartAndEnd()
-    }
-
-    companion object {
-        /**
-         * Creates the test configurations.
-         *
-         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
-         * navigation modes.
-         */
-        @Parameterized.Parameters(name = "{0}")
-        @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
-    }
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
index b48611e..98e3646 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
@@ -25,8 +25,8 @@
 import android.tools.device.flicker.junit.FlickerBuilderProvider
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule
 import android.tools.device.helpers.wakeUpAndGoToHomeScreen
 import androidx.test.filters.RequiresDevice
@@ -54,7 +54,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OverrideTaskTransitionTest(val flicker: FlickerTest) {
+class OverrideTaskTransitionTest(val flicker: LegacyFlickerTest) {
 
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
     private val testApp = SimpleAppHelper(instrumentation)
@@ -121,8 +121,6 @@
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
index d0fd732..39c8ca0 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt
@@ -28,8 +28,8 @@
 import android.tools.common.traces.component.IComponentMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.device.helpers.WindowUtils
 import android.tools.device.traces.parsers.toFlickerComponent
 import androidx.test.filters.RequiresDevice
@@ -58,7 +58,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) {
+class TaskTransitionTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     private val launchNewTaskApp = NewTasksAppHelper(instrumentation)
     private val simpleApp = SimpleAppHelper(instrumentation)
     private val wallpaper by lazy { getWallpaperPackage(instrumentation) }
@@ -214,8 +214,6 @@
 
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt
new file mode 100644
index 0000000..6f5daeb
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.notification
+
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.rule.SettingOverrideRule
+import android.provider.Settings
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import org.junit.ClassRule
+import org.junit.FixMethodOrder
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test cold launching an app from a notification from the lock screen.
+ *
+ * This test assumes the device doesn't have AOD enabled
+ *
+ * To run this test: `atest FlickerTests:OpenAppFromLockNotificationCold`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Postsubmit
+open class OpenAppFromLockscreenNotificationColdTest(flicker: LegacyFlickerTest) :
+    OpenAppFromNotificationColdTest(flicker) {
+
+    override val openingNotificationsFromLockScreen = true
+
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            // Needs to run at start of transition,
+            // so before the transition defined in super.transition
+            transitions { device.wakeUp() }
+
+            super.transition(this)
+
+            // Needs to run at the end of the setup, so after the setup defined in super.transition
+            setup {
+                device.sleep()
+                wmHelper.StateSyncBuilder().withoutTopVisibleAppWindows().waitForAndVerify()
+            }
+        }
+
+    /** {@inheritDoc} */
+    @Test @Ignore("Display is off at the start") override fun navBarLayerPositionAtStartAndEnd() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Display is off at the start")
+    override fun statusBarLayerPositionAtStartAndEnd() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Display is off at the start")
+    override fun taskBarLayerIsVisibleAtStartAndEnd() {}
+
+    /** {@inheritDoc} */
+    @Test @Ignore("Display is off at the start") override fun taskBarWindowIsAlwaysVisible() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Display is off at the start")
+    override fun statusBarLayerIsVisibleAtStartAndEnd() =
+        super.statusBarLayerIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun navBarWindowIsVisibleAtStartAndEnd() = super.navBarWindowIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun navBarWindowIsAlwaysVisible() {}
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+
+        /**
+         * 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",
+            )
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt
new file mode 100644
index 0000000..483caa7
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.notification
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Presubmit
+import android.platform.test.rule.SettingOverrideRule
+import android.provider.Settings
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.statusBarLayerPositionAtEnd
+import org.junit.ClassRule
+import org.junit.FixMethodOrder
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test warm launching an app from a notification from the lock screen.
+ *
+ * This test assumes the device doesn't have AOD enabled
+ *
+ * To run this test: `atest FlickerTests:OpenAppFromLockNotificationWarm`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenAppFromLockscreenNotificationWarmTest(flicker: LegacyFlickerTest) :
+    OpenAppFromNotificationWarmTest(flicker) {
+
+    override val openingNotificationsFromLockScreen = true
+
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            // Needs to run at start of transition,
+            // so before the transition defined in super.transition
+            transitions { device.wakeUp() }
+
+            super.transition(this)
+
+            // Needs to run at the end of the setup, so after the setup defined in super.transition
+            setup {
+                device.sleep()
+                wmHelper.StateSyncBuilder().withoutTopVisibleAppWindows().waitForAndVerify()
+            }
+        }
+
+    /**
+     * Checks that we start of with no top windows and then [testApp] becomes the first and only top
+     * window of the transition, with snapshot or splash screen windows optionally showing first.
+     */
+    @Test
+    @Presubmit
+    fun appWindowBecomesFirstAndOnlyTopWindow() {
+        flicker.assertWm {
+            this.hasNoVisibleAppWindow()
+                .then()
+                .isAppWindowOnTop(ComponentNameMatcher.SNAPSHOT, isOptional = true)
+                .then()
+                .isAppWindowOnTop(ComponentNameMatcher.SPLASH_SCREEN, isOptional = true)
+                .then()
+                .isAppWindowOnTop(testApp)
+        }
+    }
+
+    /** Checks that the screen is locked at the start of the transition */
+    @Test
+    @Presubmit
+    fun screenLockedStart() {
+        flicker.assertWmStart { isKeyguardShowing() }
+    }
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun navBarLayerPositionAtStartAndEnd() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun taskBarWindowIsAlwaysVisible() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun statusBarLayerPositionAtStartAndEnd() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun statusBarLayerIsVisibleAtStartAndEnd() =
+        super.statusBarLayerIsVisibleAtStartAndEnd()
+
+    /**
+     * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the
+     * transition
+     */
+    @Presubmit @Test fun statusBarLayerPositionAtEnd() = flicker.statusBarLayerPositionAtEnd()
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun navBarWindowIsVisibleAtStartAndEnd() = super.navBarWindowIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun navBarWindowIsAlwaysVisible() {}
+
+    /** {@inheritDoc} */
+    @FlakyTest(bugId = 246284526)
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+
+        /**
+         * 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",
+            )
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt
new file mode 100644
index 0000000..4e8a697
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.notification
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.device.helpers.wakeUpAndGoToHomeScreen
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.ShowWhenLockedAppHelper
+import org.junit.FixMethodOrder
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test cold launching an app from a notification from the lock screen when there is an app overlaid
+ * on the lock screen.
+ *
+ * This test assumes the device doesn't have AOD enabled
+ *
+ * To run this test: `atest FlickerTests:OpenAppFromLockNotificationWithLockOverlayApp`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Postsubmit
+class OpenAppFromLockscreenNotificationWithOverlayAppTest(flicker: LegacyFlickerTest) :
+    OpenAppFromLockscreenNotificationColdTest(flicker) {
+    private val showWhenLockedApp = ShowWhenLockedAppHelper(instrumentation)
+
+    // Although we are technically still locked here, the overlay app means we should open the
+    // notification shade as if we were unlocked.
+    override val openingNotificationsFromLockScreen = false
+
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            super.transition(this)
+
+            setup {
+                device.wakeUpAndGoToHomeScreen()
+
+                // Launch an activity that is shown when the device is locked
+                showWhenLockedApp.launchViaIntent(wmHelper)
+                wmHelper.StateSyncBuilder().withFullScreenApp(showWhenLockedApp).waitForAndVerify()
+
+                device.sleep()
+                wmHelper.StateSyncBuilder().withoutTopVisibleAppWindows().waitForAndVerify()
+            }
+
+            teardown { showWhenLockedApp.exit(wmHelper) }
+        }
+
+    @Test
+    @FlakyTest(bugId = 227143265)
+    fun showWhenLockedAppWindowBecomesVisible() {
+        flicker.assertWm {
+            this.hasNoVisibleAppWindow()
+                .then()
+                .isAppWindowOnTop(ComponentNameMatcher.SNAPSHOT, isOptional = true)
+                .then()
+                .isAppWindowOnTop(showWhenLockedApp)
+        }
+    }
+
+    @Test
+    @FlakyTest(bugId = 227143265)
+    fun showWhenLockedAppLayerBecomesVisible() {
+        flicker.assertLayers {
+            this.isInvisible(showWhenLockedApp)
+                .then()
+                .isVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
+                .then()
+                .isVisible(showWhenLockedApp)
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Presubmit @Test override fun appLayerBecomesVisible() = super.appLayerBecomesVisible()
+
+    /** {@inheritDoc} */
+    @FlakyTest(bugId = 227143265)
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun navBarLayerIsVisibleAtStartAndEnd() {}
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+
+    @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
+
+    @FlakyTest(bugId = 278227468)
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.kt
new file mode 100644
index 0000000..50dec3b
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.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.server.wm.flicker.notification
+
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.statusBarLayerPositionAtEnd
+import org.junit.FixMethodOrder
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test cold launching an app from a notification.
+ *
+ * To run this test: `atest FlickerTests:OpenAppFromNotificationCold`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Postsubmit
+open class OpenAppFromNotificationColdTest(flicker: LegacyFlickerTest) :
+    OpenAppFromNotificationWarmTest(flicker) {
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            super.transition(this)
+
+            setup {
+                // Close the app that posted the notification to trigger a cold start next time
+                // it is open - can't just kill it because that would remove the notification.
+                tapl.setExpectedRotationCheckEnabled(false)
+                tapl.goHome()
+                tapl.workspace.switchToOverview()
+                tapl.overview.dismissAllTasks()
+            }
+        }
+
+    @Presubmit @Test override fun appWindowBecomesVisible() = appWindowBecomesVisible_coldStart()
+
+    @Presubmit @Test override fun appLayerBecomesVisible() = appLayerBecomesVisible_coldStart()
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun navBarLayerPositionAtStartAndEnd() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun statusBarLayerPositionAtStartAndEnd() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
+    override fun statusBarLayerIsVisibleAtStartAndEnd() =
+        super.statusBarLayerIsVisibleAtStartAndEnd()
+
+    /**
+     * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the
+     * transition
+     */
+    @Presubmit @Test open fun statusBarLayerPositionAtEnd() = flicker.statusBarLayerPositionAtEnd()
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+    override fun navBarWindowIsAlwaysVisible() {}
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTestCfArm.kt
new file mode 100644
index 0000000..a147171
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTestCfArm.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.server.wm.flicker.notification
+
+import android.platform.test.annotations.Postsubmit
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Postsubmit
+class OpenAppFromNotificationColdTestCfArm(flicker: LegacyFlickerTest) :
+    OpenAppFromNotificationColdTest(flicker) {
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt
new file mode 100644
index 0000000..19a070b
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.notification
+
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.device.helpers.wakeUpAndGoToHomeScreen
+import android.view.WindowInsets
+import android.view.WindowManager
+import androidx.test.filters.RequiresDevice
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.helpers.NotificationAppHelper
+import com.android.server.wm.flicker.helpers.setRotation
+import com.android.server.wm.flicker.navBarLayerIsVisibleAtEnd
+import com.android.server.wm.flicker.navBarLayerPositionAtEnd
+import com.android.server.wm.flicker.navBarWindowIsVisibleAtEnd
+import com.android.server.wm.flicker.taskBarLayerIsVisibleAtEnd
+import com.android.server.wm.flicker.taskBarWindowIsVisibleAtEnd
+import org.junit.Assume
+import org.junit.FixMethodOrder
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test cold launching an app from a notification.
+ *
+ * To run this test: `atest FlickerTests:OpenAppFromNotificationWarm`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) :
+    OpenAppTransition(flicker) {
+    override val testApp: NotificationAppHelper = NotificationAppHelper(instrumentation)
+
+    open val openingNotificationsFromLockScreen = false
+
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            setup {
+                device.wakeUpAndGoToHomeScreen()
+                this.setRotation(flicker.scenario.startRotation)
+                testApp.launchViaIntent(wmHelper)
+                wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
+                testApp.postNotification(wmHelper)
+                device.pressHome()
+                wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
+            }
+
+            transitions {
+                var startY = 10
+                var endY = 3 * device.displayHeight / 4
+                var steps = 25
+                if (openingNotificationsFromLockScreen) {
+                    val wm: WindowManager =
+                        instrumentation.context.getSystemService(WindowManager::class.java)
+                            ?: error("Unable to connect to WindowManager service")
+                    val metricInsets = wm.currentWindowMetrics.windowInsets
+                    val insets =
+                        metricInsets.getInsetsIgnoringVisibility(
+                            WindowInsets.Type.statusBars() or WindowInsets.Type.displayCutout()
+                        )
+
+                    startY = insets.top + 100
+                    endY = device.displayHeight / 2
+                    steps = 4
+                }
+
+                // Swipe down to show the notification shade
+                val x = device.displayWidth / 2
+                device.swipe(x, startY, x, endY, steps)
+                device.waitForIdle(2000)
+                instrumentation.uiAutomation.syncInputTransactions()
+
+                // Launch the activity by clicking the notification
+                val notification =
+                    device.wait(Until.findObject(By.text("Flicker Test Notification")), 2000L)
+                notification?.click() ?: error("Notification not found")
+                instrumentation.uiAutomation.syncInputTransactions()
+
+                // Wait for the app to launch
+                wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
+            }
+
+            teardown { testApp.exit(wmHelper) }
+        }
+
+    @Presubmit @Test override fun appWindowBecomesVisible() = appWindowBecomesVisible_warmStart()
+
+    @Presubmit @Test override fun appLayerBecomesVisible() = appLayerBecomesVisible_warmStart()
+
+    @Presubmit
+    @Test
+    open fun notificationAppWindowVisibleAtEnd() {
+        flicker.assertWmEnd { this.isAppWindowVisible(testApp) }
+    }
+
+    @Presubmit
+    @Test
+    open fun notificationAppWindowOnTopAtEnd() {
+        flicker.assertWmEnd { this.isAppWindowOnTop(testApp) }
+    }
+
+    @Presubmit
+    @Test
+    open fun notificationAppLayerVisibleAtEnd() {
+        flicker.assertLayersEnd { this.isVisible(testApp) }
+    }
+
+    /**
+     * Checks that the [ComponentNameMatcher.TASK_BAR] window is visible at the end of the
+     * transition
+     *
+     * Note: Large screen only
+     */
+    @Presubmit
+    @Test
+    open fun taskBarWindowIsVisibleAtEnd() {
+        Assume.assumeTrue(flicker.scenario.isTablet)
+        flicker.taskBarWindowIsVisibleAtEnd()
+    }
+
+    /**
+     * Checks that the [ComponentNameMatcher.TASK_BAR] layer is visible at the end of the transition
+     *
+     * Note: Large screen only
+     */
+    @Presubmit
+    @Test
+    open fun taskBarLayerIsVisibleAtEnd() {
+        Assume.assumeTrue(flicker.scenario.isTablet)
+        flicker.taskBarLayerIsVisibleAtEnd()
+    }
+
+    /** Checks the position of the [ComponentNameMatcher.NAV_BAR] at the end of the transition */
+    @Presubmit
+    @Test
+    open fun navBarLayerPositionAtEnd() {
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarLayerPositionAtEnd()
+    }
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    open fun navBarLayerIsVisibleAtEnd() {
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarLayerIsVisibleAtEnd()
+    }
+
+    @Presubmit
+    @Test
+    open fun navBarWindowIsVisibleAtEnd() {
+        Assume.assumeFalse(flicker.scenario.isTablet)
+        flicker.navBarWindowIsVisibleAtEnd()
+    }
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Display is off at the start")
+    override fun taskBarLayerIsVisibleAtStartAndEnd() {}
+
+    /** {@inheritDoc} */
+    @Test
+    @Postsubmit
+    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTestCfArm.kt
new file mode 100644
index 0000000..98356d7
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTestCfArm.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.server.wm.flicker.notification
+
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenAppFromNotificationWarmTestCfArm(flicker: LegacyFlickerTest) :
+    OpenAppFromNotificationWarmTest(flicker) {
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppTransition.kt
new file mode 100644
index 0000000..684b4b7
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppTransition.kt
@@ -0,0 +1,136 @@
+/*
+ * 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.server.wm.flicker.notification
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.apphelpers.StandardAppHelper
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.helpers.wakeUpAndGoToHomeScreen
+import com.android.server.wm.flicker.BaseTest
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.helpers.setRotation
+import org.junit.Test
+
+/** Base class for app launch tests */
+abstract class OpenAppTransition(flicker: LegacyFlickerTest) : BaseTest(flicker) {
+    protected open val testApp: StandardAppHelper = SimpleAppHelper(instrumentation)
+
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit = {
+        setup {
+            tapl.setExpectedRotation(flicker.scenario.startRotation.value)
+            device.wakeUpAndGoToHomeScreen()
+            this.setRotation(flicker.scenario.startRotation)
+        }
+        teardown { testApp.exit(wmHelper) }
+    }
+
+    /**
+     * Checks that the [testApp] layer doesn't exist or is invisible at the start of the transition,
+     * but is created and/or becomes visible during the transition.
+     */
+    @Presubmit
+    @Test
+    open fun appLayerBecomesVisible() {
+        appLayerBecomesVisible_coldStart()
+    }
+
+    protected fun appLayerBecomesVisible_coldStart() {
+        flicker.assertLayers {
+            this.notContains(testApp)
+                .then()
+                .isInvisible(testApp, isOptional = true)
+                .then()
+                .isVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
+                .then()
+                .isVisible(ComponentNameMatcher.SPLASH_SCREEN, isOptional = true)
+                .then()
+                .isVisible(testApp)
+        }
+    }
+
+    protected fun appLayerBecomesVisible_warmStart() {
+        flicker.assertLayers {
+            this.isInvisible(testApp)
+                .then()
+                .isVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
+                .then()
+                .isVisible(ComponentNameMatcher.SPLASH_SCREEN, isOptional = true)
+                .then()
+                .isVisible(testApp)
+        }
+    }
+
+    /**
+     * Checks that the [testApp] window doesn't exist at the start of the transition, that it is
+     * created (invisible - optional) and becomes visible during the transition
+     *
+     * The `isAppWindowInvisible` step is optional because we log once per frame, upon logging, the
+     * window may be visible or not depending on what was processed until that moment.
+     */
+    @Presubmit @Test open fun appWindowBecomesVisible() = appWindowBecomesVisible_coldStart()
+
+    protected fun appWindowBecomesVisible_coldStart() {
+        flicker.assertWm {
+            this.notContains(testApp)
+                .then()
+                .isAppWindowInvisible(testApp, isOptional = true)
+                .then()
+                .isAppWindowVisible(testApp)
+        }
+    }
+
+    protected fun appWindowBecomesVisible_warmStart() {
+        flicker.assertWm {
+            this.isAppWindowInvisible(testApp)
+                .then()
+                .isAppWindowVisible(ComponentNameMatcher.SNAPSHOT, isOptional = true)
+                .then()
+                .isAppWindowVisible(ComponentNameMatcher.SPLASH_SCREEN, isOptional = true)
+                .then()
+                .isAppWindowVisible(testApp)
+        }
+    }
+
+    /**
+     * Checks that [testApp] window is not on top at the start of the transition, and then becomes
+     * the top visible window until the end of the transition.
+     */
+    @Presubmit
+    @Test
+    open fun appWindowBecomesTopWindow() {
+        flicker.assertWm {
+            this.isAppWindowNotOnTop(testApp)
+                .then()
+                .isAppWindowOnTop(
+                    testApp.or(ComponentNameMatcher.SNAPSHOT).or(ComponentNameMatcher.SPLASH_SCREEN)
+                )
+        }
+    }
+
+    /**
+     * Checks that [testApp] window is not on top at the start of the transition, and then becomes
+     * the top visible window until the end of the transition.
+     */
+    @Presubmit
+    @Test
+    open fun appWindowIsTopWindowAtEnd() {
+        flicker.assertWmEnd { this.isAppWindowOnTop(testApp) }
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
index a8b80ad..7883910 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
@@ -23,8 +23,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
@@ -52,7 +52,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class QuickSwitchBetweenTwoAppsBackTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class QuickSwitchBetweenTwoAppsBackTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     private val testApp1 = SimpleAppHelper(instrumentation)
     private val testApp2 = NonResizeableAppHelper(instrumentation)
 
@@ -243,10 +243,9 @@
 
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTestCfArm.kt
index f970a79..f68cd5c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTestCfArm.kt
@@ -18,8 +18,8 @@
 
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -28,15 +28,14 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class QuickSwitchBetweenTwoAppsBackTestCfArm(flicker: FlickerTest) :
+class QuickSwitchBetweenTwoAppsBackTestCfArm(flicker: LegacyFlickerTest) :
     QuickSwitchBetweenTwoAppsBackTest(flicker) {
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
index 96cd8ff..1c4c7cd 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
@@ -23,8 +23,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
@@ -53,7 +53,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class QuickSwitchBetweenTwoAppsForwardTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class QuickSwitchBetweenTwoAppsForwardTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     private val testApp1 = SimpleAppHelper(instrumentation)
     private val testApp2 = NonResizeableAppHelper(instrumentation)
 
@@ -261,10 +261,9 @@
 
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTestCfArm.kt
index 9f48cda..3de58ac 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTestCfArm.kt
@@ -18,8 +18,8 @@
 
 import android.tools.common.NavBar
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -28,15 +28,14 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class QuickSwitchBetweenTwoAppsForwardTestCfArm(flicker: FlickerTest) :
+class QuickSwitchBetweenTwoAppsForwardTestCfArm(flicker: LegacyFlickerTest) :
     QuickSwitchBetweenTwoAppsForwardTest(flicker) {
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
index 7e935f0..6417456 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
@@ -24,8 +24,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
@@ -52,7 +52,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class QuickSwitchFromLauncherTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class QuickSwitchFromLauncherTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     private val testApp = SimpleAppHelper(instrumentation)
 
     /** {@inheritDoc} */
@@ -272,12 +272,11 @@
 
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL),
                 // TODO: Test with 90 rotation
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTestCfArm.kt
index af671df..84fc754 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTestCfArm.kt
@@ -19,8 +19,8 @@
 import android.tools.common.NavBar
 import android.tools.common.Rotation
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -29,17 +29,16 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class QuickSwitchFromLauncherTestCfArm(flicker: FlickerTest) :
+open class QuickSwitchFromLauncherTestCfArm(flicker: LegacyFlickerTest) :
     QuickSwitchFromLauncherTest(flicker) {
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.nonRotationTests(
+        fun getParams() =
+            LegacyFlickerTestFactory.nonRotationTests(
                 supportedNavigationModes = listOf(NavBar.MODE_GESTURAL),
                 // TODO: Test with 90 rotation
                 supportedRotations = listOf(Rotation.ROTATION_0)
             )
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
index ef75c61..842ece3 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
@@ -21,8 +21,8 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import org.junit.FixMethodOrder
@@ -34,7 +34,7 @@
 /**
  * Test opening an app and cycling through app rotations
  *
- * Currently runs:
+ * Currently, runs:
  * ```
  *      0 -> 90 degrees
  *      90 -> 0 degrees
@@ -85,7 +85,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class ChangeAppRotationTest(flicker: FlickerTest) : RotationTransition(flicker) {
+open class ChangeAppRotationTest(flicker: LegacyFlickerTest) : RotationTransition(flicker) {
     override val testApp = SimpleAppHelper(instrumentation)
     override val transition: FlickerBuilder.() -> Unit
         get() = {
@@ -140,13 +140,11 @@
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.rotationTests] for configuring screen orientation and navigation
-         * modes.
+         * See [LegacyFlickerTestFactory.rotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.rotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.rotationTests()
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTestCfArm.kt
index 0e6b20f..1ab5c5a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTestCfArm.kt
@@ -17,8 +17,8 @@
 package com.android.server.wm.flicker.rotation
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -27,18 +27,16 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class ChangeAppRotationTestCfArm(flicker: FlickerTest) : ChangeAppRotationTest(flicker) {
+class ChangeAppRotationTestCfArm(flicker: LegacyFlickerTest) : ChangeAppRotationTest(flicker) {
     companion object {
         /**
          * Creates the test configurations.
          *
-         * See [FlickerTestFactory.rotationTests] for configuring screen orientation and navigation
-         * modes.
+         * See [LegacyFlickerTestFactory.rotationTests] for configuring screen orientation and
+         * navigation modes.
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.rotationTests()
-        }
+        fun getParams() = LegacyFlickerTestFactory.rotationTests()
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
index fe9da33..b0ca4d2 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/RotationTransition.kt
@@ -20,13 +20,13 @@
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.apphelpers.StandardAppHelper
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTest
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.setRotation
 import org.junit.Test
 
 /** Base class for app rotation tests */
-abstract class RotationTransition(flicker: FlickerTest) : BaseTest(flicker) {
+abstract class RotationTransition(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     protected abstract val testApp: StandardAppHelper
 
     /** {@inheritDoc} */
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
index f654bdd4..b6ad3cc7 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
@@ -19,11 +19,12 @@
 import android.platform.test.annotations.PlatinumTest
 import android.platform.test.annotations.Presubmit
 import android.tools.common.ScenarioBuilder
+import android.tools.common.ScenarioImpl
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.device.flicker.legacy.FlickerBuilder
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import android.view.WindowManager
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.helpers.SeamlessRotationAppHelper
@@ -91,7 +92,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class SeamlessAppRotationTest(flicker: FlickerTest) : RotationTransition(flicker) {
+open class SeamlessAppRotationTest(flicker: LegacyFlickerTest) : RotationTransition(flicker) {
     override val testApp = SeamlessRotationAppHelper(instrumentation)
 
     /** {@inheritDoc} */
@@ -236,43 +237,50 @@
     }
 
     companion object {
-        private val FlickerTest.starveUiThread
+        private val LegacyFlickerTest.starveUiThread
             get() =
-                getConfigValue<Boolean>(ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD)
+                scenario.getConfigValue<Boolean>(
+                    ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD
+                )
                     ?: false
 
         @JvmStatic
         protected fun createConfig(
-            sourceConfig: FlickerTest,
+            sourceConfig: LegacyFlickerTest,
             starveUiThread: Boolean
-        ): FlickerTest {
-            val originalScenario = sourceConfig.initialize("createConfig")
+        ): LegacyFlickerTest {
+            val originalScenario = sourceConfig.initialize("createConfig") as ScenarioImpl
             val nameExt = if (starveUiThread) "_BUSY_UI_THREAD" else ""
             val newConfig =
                 ScenarioBuilder()
-                    .fromScenario(originalScenario)
+                    .forClass(originalScenario.testClass)
+                    .withStartRotation(originalScenario.startRotation)
+                    .withEndRotation(originalScenario.endRotation)
+                    .withNavBarMode(originalScenario.navBarMode)
+                    .withExtraConfigs(originalScenario.extraConfig)
+                    .withDescriptionOverride(originalScenario.description)
                     .withExtraConfig(
                         ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD,
                         starveUiThread
                     )
                     .withDescriptionOverride("${originalScenario.description}$nameExt")
-            return FlickerTest(newConfig)
+            return LegacyFlickerTest(newConfig)
         }
 
         /**
          * Creates the test configurations for seamless rotation based on the default rotation tests
-         * from [FlickerTestFactory.rotationTests], but adding a flag (
+         * from [LegacyFlickerTestFactory.rotationTests], but adding a flag (
          * [ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD]) to indicate if the app should
          * starve the UI thread of not
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.rotationTests().flatMap { sourceConfig ->
-                val defaultRun = createConfig(sourceConfig, starveUiThread = false)
-                val busyUiRun = createConfig(sourceConfig, starveUiThread = true)
+        fun getParams() =
+            LegacyFlickerTestFactory.rotationTests().flatMap { sourceCfg ->
+                val legacyCfg = sourceCfg as LegacyFlickerTest
+                val defaultRun = createConfig(legacyCfg, starveUiThread = false)
+                val busyUiRun = createConfig(legacyCfg, starveUiThread = true)
                 listOf(defaultRun, busyUiRun)
             }
-        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTestCfArm.kt
index b236d87..592be05 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTestCfArm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTestCfArm.kt
@@ -17,8 +17,8 @@
 package com.android.server.wm.flicker.rotation
 
 import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
-import android.tools.device.flicker.legacy.FlickerTest
-import android.tools.device.flicker.legacy.FlickerTestFactory
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
@@ -29,22 +29,23 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-open class SeamlessAppRotationTestCfArm(flicker: FlickerTest) : SeamlessAppRotationTest(flicker) {
+open class SeamlessAppRotationTestCfArm(flicker: LegacyFlickerTest) :
+    SeamlessAppRotationTest(flicker) {
     companion object {
         /**
          * Creates the test configurations for seamless rotation based on the default rotation tests
-         * from [FlickerTestFactory.rotationTests], but adding a flag (
+         * from [LegacyFlickerTestFactory.rotationTests], but adding a flag (
          * [ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD]) to indicate if the app should
          * starve the UI thread of not
          */
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
-        fun getParams(): Collection<FlickerTest> {
-            return FlickerTestFactory.rotationTests().flatMap { sourceConfig ->
-                val defaultRun = createConfig(sourceConfig, starveUiThread = false)
-                val busyUiRun = createConfig(sourceConfig, starveUiThread = true)
+        fun getParams() =
+            LegacyFlickerTestFactory.rotationTests().flatMap { sourceCfg ->
+                val legacyCfg = sourceCfg as LegacyFlickerTest
+                val defaultRun = createConfig(legacyCfg, starveUiThread = false)
+                val busyUiRun = createConfig(legacyCfg, starveUiThread = true)
                 listOf(defaultRun, busyUiRun)
             }
-        }
     }
 }
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index 1ec9ec9..7a2e74b 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -22,7 +22,9 @@
     <application android:allowBackup="false"
                  android:supportsRtl="true">
         <uses-library android:name="androidx.window.extensions" android:required="false"/>
-
+        <property
+            android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
+            android:value="true" />
         <activity android:name=".SimpleActivity"
                   android:taskAffinity="com.android.server.wm.flicker.testapp.SimpleActivity"
                   android:theme="@style/CutoutShortEdges"
@@ -198,6 +200,20 @@
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
             android:exported="false"/>
         <activity
+            android:name=".ActivityEmbeddingThirdActivity"
+            android:label="ActivityEmbedding Third"
+            android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding"
+            android:theme="@style/CutoutShortEdges"
+            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+            android:exported="false"/>
+        <activity
+            android:name=".ActivityEmbeddingAlwaysExpandActivity"
+            android:label="ActivityEmbedding AlwaysExpand"
+            android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding"
+            android:theme="@style/CutoutShortEdges"
+            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+            android:exported="false"/>
+        <activity
             android:name=".ActivityEmbeddingPlaceholderPrimaryActivity"
             android:label="ActivityEmbedding Placeholder Primary"
             android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding"
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml
index 3a02cad..f0dfdfc 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml
@@ -20,5 +20,4 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
-
 </LinearLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
index d78b9a8..b9d789b 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
@@ -25,16 +25,39 @@
         android:id="@+id/launch_secondary_activity_button"
         android:layout_width="wrap_content"
         android:layout_height="48dp"
-        android:layout_centerHorizontal="true"
         android:onClick="launchSecondaryActivity"
+        android:tag="LEFT_TO_RIGHT"
         android:text="Launch Secondary Activity" />
 
     <Button
+        android:id="@+id/launch_secondary_activity_rtl_button"
+        android:layout_width="wrap_content"
+        android:layout_height="48dp"
+        android:onClick="launchSecondaryActivity"
+        android:tag="RIGHT_TO_LEFT"
+        android:text="Launch Secondary Activity in RTL" />
+
+    <Button
         android:id="@+id/launch_placeholder_split_button"
         android:layout_width="wrap_content"
         android:layout_height="48dp"
-        android:layout_centerHorizontal="true"
         android:onClick="launchPlaceholderSplit"
+        android:tag="LEFT_TO_RIGHT"
         android:text="Launch Placeholder Split" />
 
+    <Button
+        android:id="@+id/launch_always_expand_activity_button"
+        android:layout_width="wrap_content"
+        android:layout_height="48dp"
+        android:onClick="launchAlwaysExpandActivity"
+        android:text="Launch Always Expand Activity" />
+
+    <Button
+        android:id="@+id/launch_placeholder_split_rtl_button"
+        android:layout_width="wrap_content"
+        android:layout_height="48dp"
+        android:onClick="launchPlaceholderSplit"
+        android:tag="RIGHT_TO_LEFT"
+        android:text="Launch Placeholder Split in RTL" />
+
 </LinearLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
new file mode 100644
index 0000000..6731446
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
@@ -0,0 +1,38 @@
+<?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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/secondary_activity_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+  <Button
+      android:id="@+id/finish_secondary_activity_button"
+      android:layout_width="wrap_content"
+      android:layout_height="48dp"
+      android:text="Finish" />
+
+  <Button
+      android:id="@+id/launch_third_activity_button"
+      android:layout_width="wrap_content"
+      android:layout_height="48dp"
+      android:layout_centerHorizontal="true"
+      android:onClick="launchThirdActivity"
+      android:text="Launch a third activity" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingAlwaysExpandActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingAlwaysExpandActivity.java
new file mode 100644
index 0000000..d9b24ed
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingAlwaysExpandActivity.java
@@ -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.server.wm.flicker.testapp;
+
+import android.app.Activity;
+import android.graphics.Color;
+import android.os.Bundle;
+
+/**
+ * Activity with alwaysExpand=true (launched via R.id.launch_always_expand_activity_button)
+ */
+public class ActivityEmbeddingAlwaysExpandActivity extends Activity {
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.activity_embedding_base_layout);
+    findViewById(R.id.root_activity_layout).setBackgroundColor(Color.GREEN);
+  }
+
+}
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
index 6a7a2cc..817c79c 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
@@ -16,87 +16,138 @@
 
 package com.android.server.wm.flicker.testapp;
 
+
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
-import android.util.ArraySet;
-import android.util.Log;
 import android.view.View;
 
-import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
-import androidx.window.extensions.embedding.EmbeddingRule;
-import androidx.window.extensions.embedding.SplitPairRule;
-import androidx.window.extensions.embedding.SplitPlaceholderRule;
-
 import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper;
+import androidx.annotation.NonNull;
+import androidx.window.embedding.ActivityFilter;
+import androidx.window.embedding.ActivityRule;
+import androidx.window.embedding.EmbeddingAspectRatio;
+import androidx.window.embedding.RuleController;
+import androidx.window.embedding.SplitAttributes;
+import androidx.window.embedding.SplitAttributes.LayoutDirection;
+import androidx.window.embedding.SplitController;
+import androidx.window.embedding.SplitPairFilter;
+import androidx.window.embedding.SplitPairRule;
+import androidx.window.embedding.SplitPlaceholderRule;
+import androidx.window.embedding.SplitRule;
 
+import java.util.HashSet;
 import java.util.Set;
 
 /** Main activity of the ActivityEmbedding test app to launch other embedding activities. */
 public class ActivityEmbeddingMainActivity extends Activity {
     private static final String TAG = "ActivityEmbeddingMainActivity";
     private static final float DEFAULT_SPLIT_RATIO = 0.5f;
+    private RuleController mRuleController;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_embedding_main_layout);
+        final SplitController.SplitSupportStatus status = SplitController.getInstance(
+                this).getSplitSupportStatus();
+        if (status != SplitController.SplitSupportStatus.SPLIT_AVAILABLE) {
+            throw new RuntimeException(
+                    "Unable to initiate SplitController in ActivityEmbeddingMainActivity, "
+                            + "splitSupportStatus = " + status);
+        }
+        mRuleController = RuleController.getInstance(this);
     }
 
     /** R.id.launch_secondary_activity_button onClick */
     public void launchSecondaryActivity(View view) {
-        initializeSplitRules(createSplitPairRules());
+        final String layoutDirection = view.getTag().toString();
+        mRuleController.clearRules();
+        mRuleController.addRule(createSplitPairRules(layoutDirection));
         startActivity(new Intent().setComponent(
                 ActivityOptions.ActivityEmbedding.SecondaryActivity.COMPONENT));
     }
 
+    /** R.id.launch_always_expand_activity_button onClick */
+    public void launchAlwaysExpandActivity(View view) {
+        final Set<ActivityFilter> activityFilters = new HashSet<>();
+        activityFilters.add(
+                new ActivityFilter(ActivityOptions.ActivityEmbedding.AlwaysExpandActivity.COMPONENT,
+                        null));
+        final ActivityRule activityRule = new ActivityRule.Builder(activityFilters)
+                .setAlwaysExpand(true)
+                .build();
+
+        RuleController rc = RuleController.getInstance(this);
+
+        rc.addRule(activityRule);
+        startActivity(new Intent().setComponent(
+                ActivityOptions.ActivityEmbedding.AlwaysExpandActivity.COMPONENT));
+    }
+
     /** R.id.launch_placeholder_split_button onClick */
     public void launchPlaceholderSplit(View view) {
-        initializeSplitRules(createSplitPlaceholderRules());
+        final String layoutDirection = view.getTag().toString();
+        mRuleController.clearRules();
+        mRuleController.addRule(createSplitPlaceholderRules(layoutDirection));
         startActivity(new Intent().setComponent(
                 ActivityOptions.ActivityEmbedding.PlaceholderPrimaryActivity.COMPONENT));
     }
 
-    private void initializeSplitRules(Set<EmbeddingRule> rules) {
-        ActivityEmbeddingComponent embeddingComponent =
-                ActivityEmbeddingAppHelper.getActivityEmbeddingComponent();
-        if (embeddingComponent == null) {
-            // Embedding not supported
-            Log.d(TAG, "ActivityEmbedding is not supported on this device");
-            finish();
-            return;
+    private static SplitPairRule createSplitPairRules(@NonNull String layoutDirection) {
+        final Set<SplitPairFilter> pairFilters = new HashSet<>();
+        final SplitPairFilter activitiesPair = new SplitPairFilter(
+                ActivityOptions.ActivityEmbedding.MainActivity.COMPONENT,
+                ActivityOptions.ActivityEmbedding.SecondaryActivity.COMPONENT,
+                null /* secondaryActivityIntentAction */);
+        pairFilters.add(activitiesPair);
+        final SplitAttributes splitAttributes = new SplitAttributes.Builder()
+                .setSplitType(SplitAttributes.SplitType.SPLIT_TYPE_EQUAL)
+                .setLayoutDirection(parseLayoutDirection(layoutDirection))
+                .build();
+        // Setting thresholds to ALWAYS_ALLOW values to make it easy for running on all devices.
+        final SplitPairRule rule = new SplitPairRule.Builder(pairFilters)
+                .setDefaultSplitAttributes(splitAttributes)
+                .setMinWidthDp(SplitRule.SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+                .setMinHeightDp(SplitRule.SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+                .setMinSmallestWidthDp(SplitRule.SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+                .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ALWAYS_ALLOW)
+                .setMaxAspectRatioInLandscape(EmbeddingAspectRatio.ALWAYS_ALLOW)
+                .build();
+        return rule;
+    }
+
+    private static SplitPlaceholderRule createSplitPlaceholderRules(
+            @NonNull String layoutDirection) {
+        final Set<ActivityFilter> activityFilters = new HashSet<>();
+        activityFilters.add(new ActivityFilter(
+                ActivityOptions.ActivityEmbedding.PlaceholderPrimaryActivity.COMPONENT,
+                null /* intentAction */));
+        final Intent intent = new Intent();
+        intent.setComponent(
+                ActivityOptions.ActivityEmbedding.PlaceholderSecondaryActivity.COMPONENT);
+        final SplitAttributes splitAttributes = new SplitAttributes.Builder()
+                .setSplitType(SplitAttributes.SplitType.SPLIT_TYPE_EQUAL)
+                .setLayoutDirection(parseLayoutDirection(layoutDirection))
+                .build();
+        final SplitPlaceholderRule rule = new SplitPlaceholderRule.Builder(activityFilters, intent)
+                .setDefaultSplitAttributes(splitAttributes)
+                .setMinWidthDp(SplitRule.SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+                .setMinHeightDp(SplitRule.SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+                .setMinSmallestWidthDp(SplitRule.SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+                .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ALWAYS_ALLOW)
+                .setMaxAspectRatioInLandscape(EmbeddingAspectRatio.ALWAYS_ALLOW)
+                .build();
+        return rule;
+    }
+
+    private static LayoutDirection parseLayoutDirection(@NonNull String layoutDirectionStr) {
+        if (layoutDirectionStr.equals(LayoutDirection.LEFT_TO_RIGHT.toString())) {
+            return LayoutDirection.LEFT_TO_RIGHT;
         }
-        embeddingComponent.setEmbeddingRules(rules);
-    }
-
-    private Set<EmbeddingRule> createSplitPairRules() {
-        final Set<EmbeddingRule> rules = new ArraySet<>();
-        final SplitPairRule rule = new SplitPairRule.Builder(
-                activitiesPair -> activitiesPair.first instanceof ActivityEmbeddingMainActivity
-                        && activitiesPair.second instanceof ActivityEmbeddingSecondaryActivity,
-                activityIntentPair ->
-                        activityIntentPair.first instanceof ActivityEmbeddingMainActivity
-                                && activityIntentPair.second.getComponent().equals(ActivityOptions
-                                .ActivityEmbedding.SecondaryActivity.COMPONENT),
-                windowMetrics -> true)
-                .setSplitRatio(DEFAULT_SPLIT_RATIO)
-                .build();
-        rules.add(rule);
-        return rules;
-    }
-
-    private Set<EmbeddingRule> createSplitPlaceholderRules() {
-        final Set<EmbeddingRule> rules = new ArraySet<>();
-        final SplitPlaceholderRule rule = new SplitPlaceholderRule.Builder(
-                new Intent().setComponent(
-                        ActivityOptions.ActivityEmbedding.PlaceholderSecondaryActivity.COMPONENT),
-                activity -> activity instanceof ActivityEmbeddingPlaceholderPrimaryActivity,
-                intent -> intent.getComponent().equals(
-                        ActivityOptions.ActivityEmbedding.PlaceholderPrimaryActivity.COMPONENT),
-                windowMetrics -> true)
-                .setSplitRatio(DEFAULT_SPLIT_RATIO)
-                .build();
-        rules.add(rule);
-        return rules;
+        if (layoutDirectionStr.equals(LayoutDirection.RIGHT_TO_LEFT.toString())) {
+            return LayoutDirection.RIGHT_TO_LEFT;
+        }
+        return LayoutDirection.LOCALE;
     }
 }
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
index 00f4c25..dc21027 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
@@ -16,15 +16,34 @@
 
 package com.android.server.wm.flicker.testapp;
 
+import android.app.Activity;
+import android.content.Intent;
 import android.graphics.Color;
+import android.os.Bundle;
+import android.view.View;
 
 /**
  * Activity to be used as the secondary activity to split with
  * {@link ActivityEmbeddingMainActivity}.
  */
-public class ActivityEmbeddingSecondaryActivity extends ActivityEmbeddingBaseActivity {
+public class ActivityEmbeddingSecondaryActivity extends Activity {
+
     @Override
-    int getBackgroundColor() {
-        return Color.YELLOW;
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_embedding_secondary_activity_layout);
+        findViewById(R.id.secondary_activity_layout).setBackgroundColor(Color.YELLOW);
+        findViewById(R.id.finish_secondary_activity_button).setOnClickListener(
+              new View.OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        finish();
+                    }
+            });
+    }
+
+    public void launchThirdActivity(View view) {
+        startActivity(new Intent().setComponent(
+                ActivityOptions.ActivityEmbedding.ThirdActivity.COMPONENT));
     }
 }
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingThirdActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingThirdActivity.java
new file mode 100644
index 0000000..3bd7281
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingThirdActivity.java
@@ -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.server.wm.flicker.testapp;
+
+import android.app.Activity;
+import android.graphics.Color;
+import android.os.Bundle;
+
+/**
+ * Activity to be used also as a secondary activity to split with
+ * {@link ActivityEmbeddingMainActivity}.
+ */
+public class ActivityEmbeddingThirdActivity extends Activity {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_embedding_base_layout);
+        findViewById(R.id.root_activity_layout).setBackgroundColor(Color.RED);
+    }
+}
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
index 9c3226b..d84ac42 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
@@ -99,6 +99,18 @@
                     FLICKER_APP_PACKAGE + ".ActivityEmbeddingSecondaryActivity");
         }
 
+        public static class ThirdActivity {
+            public static final String LABEL = "ActivityEmbeddingThirdActivity";
+            public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
+                    FLICKER_APP_PACKAGE + ".ActivityEmbeddingThirdActivity");
+        }
+
+        public static class AlwaysExpandActivity {
+            public static final String LABEL = "ActivityEmbeddingAlwaysExpandActivity";
+            public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
+                FLICKER_APP_PACKAGE + ".ActivityEmbeddingAlwaysExpandActivity");
+        }
+
         public static class PlaceholderPrimaryActivity {
             public static final String LABEL = "ActivityEmbeddingPlaceholderPrimaryActivity";
             public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
@@ -180,6 +192,8 @@
             public static final String LABEL = "SplitScreenPrimaryActivity";
             public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
                     FLICKER_APP_PACKAGE + ".SplitScreenActivity");
+
+            public static final String EXTRA_LAUNCH_ADJACENT = "launch_adjacent";
         }
 
         public static class Secondary {
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SplitScreenActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SplitScreenActivity.java
index 70196ae..8a27252 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SplitScreenActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SplitScreenActivity.java
@@ -16,9 +16,14 @@
 
 package com.android.server.wm.flicker.testapp;
 
+import static com.android.server.wm.flicker.testapp.ActivityOptions.SplitScreen.Primary.EXTRA_LAUNCH_ADJACENT;
+
 import android.app.Activity;
+import android.content.Intent;
 import android.os.Bundle;
 
+import androidx.annotation.Nullable;
+
 public class SplitScreenActivity extends Activity {
 
     @Override
@@ -26,4 +31,17 @@
         super.onCreate(icicle);
         setContentView(R.layout.activity_splitscreen);
     }
+
+    @Override
+    protected void onPostCreate(@Nullable Bundle savedInstanceState) {
+        super.onPostCreate(savedInstanceState);
+        final boolean startSecondaryActivity = getIntent().hasExtra(EXTRA_LAUNCH_ADJACENT);
+        if (startSecondaryActivity) {
+            final Intent intent = new Intent(this, SplitScreenSecondaryActivity.class);
+            intent.addCategory(Intent.CATEGORY_LAUNCHER);
+            intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT | Intent.FLAG_ACTIVITY_NEW_TASK
+                    | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+            startActivity(intent);
+        }
+    }
 }
diff --git a/tests/FlickerTests/trace_config/trace_config.textproto b/tests/FlickerTests/trace_config/trace_config.textproto
new file mode 100644
index 0000000..c9a35ac
--- /dev/null
+++ b/tests/FlickerTests/trace_config/trace_config.textproto
@@ -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 this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# proto-message: TraceConfig
+
+# Enable periodic flushing of the trace buffer into the output file.
+write_into_file: true
+
+# Writes the userspace buffer into the file every 1s.
+file_write_period_ms: 2500
+
+# See b/126487238 - we need to guarantee ordering of events.
+flush_period_ms: 30000
+
+# The trace buffers needs to be big enough to hold |file_write_period_ms| of
+# trace data. The trace buffer sizing depends on the number of trace categories
+# enabled and the device activity.
+
+# RSS events
+buffers: {
+  size_kb: 63488
+  fill_policy: RING_BUFFER
+}
+
+data_sources {
+  config {
+    name: "linux.process_stats"
+    target_buffer: 0
+    # polled per-process memory counters and process/thread names.
+    # If you don't want the polled counters, remove the "process_stats_config"
+    # section, but keep the data source itself as it still provides on-demand
+    # thread/process naming for ftrace data below.
+    process_stats_config {
+      scan_all_processes_on_start: true
+    }
+  }
+}
+
+data_sources: {
+  config {
+    name: "linux.ftrace"
+    ftrace_config {
+      ftrace_events: "ftrace/print"
+      ftrace_events: "task/task_newtask"
+      ftrace_events: "task/task_rename"
+      atrace_categories: "ss"
+      atrace_categories: "wm"
+      atrace_categories: "am"
+      atrace_categories: "aidl"
+      atrace_categories: "input"
+      atrace_categories: "binder_driver"
+      atrace_categories: "sched_process_exit"
+      atrace_apps: "com.android.server.wm.flicker"
+      atrace_apps: "com.android.server.wm.flicker.other"
+      atrace_apps: "com.android.server.wm.flicker.close"
+      atrace_apps: "com.android.server.wm.flicker.ime"
+      atrace_apps: "com.android.server.wm.flicker.launch"
+      atrace_apps: "com.android.server.wm.flicker.quickswitch"
+      atrace_apps: "com.android.server.wm.flicker.rotation"
+      atrace_apps: "com.android.server.wm.flicker.testapp"
+      atrace_apps: "com.android.systemui"
+      atrace_apps: "com.google.android.apps.nexuslauncher"
+    }
+  }
+}
+
diff --git a/tools/protologtool/Android.bp b/tools/protologtool/Android.bp
index 039bb4e..46745e9 100644
--- a/tools/protologtool/Android.bp
+++ b/tools/protologtool/Android.bp
@@ -11,7 +11,7 @@
     name: "protologtool-lib",
     srcs: [
         "src/com/android/protolog/tool/**/*.kt",
-        ":protolog-common-src",
+        ":protolog-common-no-android-src",
     ],
     static_libs: [
         "javaparser",