Merge "[Thread] rename package name for Thread platform flags" into main
diff --git a/LSE_APP_COMPAT_OWNERS b/LSE_APP_COMPAT_OWNERS
new file mode 100644
index 0000000..3db0cd4
--- /dev/null
+++ b/LSE_APP_COMPAT_OWNERS
@@ -0,0 +1,6 @@
+# Owners for the App Compat flags (large_screen_experiences_app_compat)
+mcarli@google.com
+eevlachavas@google.com
+gracielawputri@google.com
+minagranic@google.com
+mariiasand@google.com
diff --git a/core/api/current.txt b/core/api/current.txt
index 4a2abf6..bdfe388 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -7024,6 +7024,7 @@
     method public void deleteNotificationChannelGroup(String);
     method public android.service.notification.StatusBarNotification[] getActiveNotifications();
     method public android.app.AutomaticZenRule getAutomaticZenRule(String);
+    method @FlaggedApi("android.app.modes_api") public int getAutomaticZenRuleState(@NonNull String);
     method public java.util.Map<java.lang.String,android.app.AutomaticZenRule> getAutomaticZenRules();
     method public int getBubblePreference();
     method @NonNull public android.app.NotificationManager.Policy getConsolidatedNotificationPolicy();
@@ -10088,7 +10089,7 @@
     method public CharSequence coerceToText(android.content.Context);
     method public String getHtmlText();
     method public android.content.Intent getIntent();
-    method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @Nullable public android.app.PendingIntent getPendingIntent();
+    method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @Nullable public android.content.IntentSender getIntentSender();
     method public CharSequence getText();
     method @Nullable public android.view.textclassifier.TextLinks getTextLinks();
     method public android.net.Uri getUri();
@@ -10099,7 +10100,7 @@
     method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item build();
     method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setHtmlText(@Nullable String);
     method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setIntent(@Nullable android.content.Intent);
-    method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setPendingIntent(@Nullable android.app.PendingIntent);
+    method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setIntentSender(@Nullable android.content.IntentSender);
     method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setText(@Nullable CharSequence);
     method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setUri(@Nullable android.net.Uri);
   }
@@ -53136,7 +53137,7 @@
     field public static final int DRAG_FLAG_GLOBAL_URI_READ = 1; // 0x1
     field public static final int DRAG_FLAG_GLOBAL_URI_WRITE = 2; // 0x2
     field public static final int DRAG_FLAG_OPAQUE = 512; // 0x200
-    field @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final int DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG = 8192; // 0x2000
+    field @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final int DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG = 8192; // 0x2000
     field @Deprecated public static final int DRAWING_CACHE_QUALITY_AUTO = 0; // 0x0
     field @Deprecated public static final int DRAWING_CACHE_QUALITY_HIGH = 1048576; // 0x100000
     field @Deprecated public static final int DRAWING_CACHE_QUALITY_LOW = 524288; // 0x80000
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 48dcbe5..038ea5e 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3063,6 +3063,14 @@
 
 }
 
+package android.service.chooser {
+
+  @FlaggedApi("android.service.chooser.enable_chooser_result") public final class ChooserResult implements android.os.Parcelable {
+    ctor public ChooserResult(int, @Nullable android.content.ComponentName, boolean);
+  }
+
+}
+
 package android.service.dreams {
 
   public abstract class DreamOverlayService extends android.app.Service {
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index b5e3556..8f81ae2 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -225,6 +225,7 @@
     boolean removeAutomaticZenRule(String id, boolean fromUser);
     boolean removeAutomaticZenRules(String packageName, boolean fromUser);
     int getRuleInstanceCount(in ComponentName owner);
+    int getAutomaticZenRuleState(String id);
     void setAutomaticZenRuleState(String id, in Condition condition);
 
     byte[] getBackupPayload(int user);
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 9dfb5b0..d49a254 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1406,6 +1406,26 @@
     }
 
     /**
+     * Returns the current activation state of an {@link AutomaticZenRule}.
+     *
+     * <p>Returns {@link Condition#STATE_UNKNOWN} if the rule does not exist or the calling
+     * package doesn't have access to it.
+     *
+     * @param id The id of the rule
+     * @return the state of the rule.
+     */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    @Condition.State
+    public int getAutomaticZenRuleState(@NonNull String id) {
+        INotificationManager service = getService();
+        try {
+            return service.getAutomaticZenRuleState(id);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Informs the notification manager that the state of an {@link AutomaticZenRule} has changed.
      * Use this method to put the system into Do Not Disturb mode or request that it exits Do Not
      * Disturb mode. The calling app must own the provided {@link android.app.AutomaticZenRule}.
diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java
index eb357fe..728c350 100644
--- a/core/java/android/content/ClipData.java
+++ b/core/java/android/content/ClipData.java
@@ -26,8 +26,6 @@
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SuppressLint;
-import android.app.PendingIntent;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.pm.ActivityInfo;
 import android.content.res.AssetFileDescriptor;
@@ -213,7 +211,7 @@
         final CharSequence mText;
         final String mHtmlText;
         final Intent mIntent;
-        final PendingIntent mPendingIntent;
+        final IntentSender mIntentSender;
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
         Uri mUri;
         private TextLinks mTextLinks;
@@ -225,12 +223,11 @@
          * A builder for a ClipData Item.
          */
         @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
-        @SuppressLint("PackageLayering")
         public static final class Builder {
             private CharSequence mText;
             private String mHtmlText;
             private Intent mIntent;
-            private PendingIntent mPendingIntent;
+            private IntentSender mIntentSender;
             private Uri mUri;
 
             /**
@@ -264,18 +261,20 @@
             }
 
             /**
-             * Sets the PendingIntent for the item to be constructed. To prevent receiving apps from
-             * improperly manipulating the intent to launch another activity as this caller, the
-             * provided PendingIntent must be immutable (see {@link PendingIntent#FLAG_IMMUTABLE}).
-             * The system will clean up the PendingIntent when it is no longer used.
+             * Sets the {@link IntentSender} for the item to be constructed. To prevent receiving
+             * apps from improperly manipulating the intent to launch another activity as this
+             * caller, the provided IntentSender must be immutable.
+             *
+             * If there is a fixed lifetime for this ClipData (ie. for drag and drop), the system
+             * will cancel the IntentSender when it is no longer used.
              */
             @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
             @NonNull
-            public Builder setPendingIntent(@Nullable PendingIntent pendingIntent) {
-                if (pendingIntent != null && !pendingIntent.isImmutable()) {
-                    throw new IllegalArgumentException("Expected pending intent to be immutable");
+            public Builder setIntentSender(@Nullable IntentSender intentSender) {
+                if (intentSender != null && !intentSender.isImmutable()) {
+                    throw new IllegalArgumentException("Expected intent sender to be immutable");
                 }
-                mPendingIntent = pendingIntent;
+                mIntentSender = intentSender;
                 return this;
             }
 
@@ -295,7 +294,7 @@
             @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
             @NonNull
             public Item build() {
-                return new Item(mText, mHtmlText, mIntent, mPendingIntent, mUri);
+                return new Item(mText, mHtmlText, mIntent, mIntentSender, mUri);
             }
         }
 
@@ -305,7 +304,7 @@
             mText = other.mText;
             mHtmlText = other.mHtmlText;
             mIntent = other.mIntent;
-            mPendingIntent = other.mPendingIntent;
+            mIntentSender = other.mIntentSender;
             mUri = other.mUri;
             mActivityInfo = other.mActivityInfo;
             mTextLinks = other.mTextLinks;
@@ -366,7 +365,7 @@
         /**
          * Builder ctor.
          */
-        private Item(CharSequence text, String htmlText, Intent intent, PendingIntent pendingIntent,
+        private Item(CharSequence text, String htmlText, Intent intent, IntentSender intentSender,
                 Uri uri) {
             if (htmlText != null && text == null) {
                 throw new IllegalArgumentException(
@@ -375,7 +374,7 @@
             mText = text;
             mHtmlText = htmlText;
             mIntent = intent;
-            mPendingIntent = pendingIntent;
+            mIntentSender = intentSender;
             mUri = uri;
         }
 
@@ -401,12 +400,12 @@
         }
 
         /**
-         * Returns the pending intent in this Item.
+         * Returns the {@link IntentSender} in this Item.
          */
         @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
         @Nullable
-        public PendingIntent getPendingIntent() {
-            return mPendingIntent;
+        public IntentSender getIntentSender() {
+            return mIntentSender;
         }
 
         /**
@@ -1131,35 +1130,6 @@
     }
 
     /**
-     * Checks if this clip data has a pending intent that is an activity type.
-     * @hide
-     */
-    public boolean hasActivityPendingIntents() {
-        final int size = mItems.size();
-        for (int i = 0; i < size; i++) {
-            final Item item = mItems.get(i);
-            if (item.mPendingIntent != null && item.mPendingIntent.isActivity()) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Cleans up all pending intents in the ClipData.
-     * @hide
-     */
-    public void cleanUpPendingIntents() {
-        final int size = mItems.size();
-        for (int i = 0; i < size; i++) {
-            final Item item = mItems.get(i);
-            if (item.mPendingIntent != null) {
-                item.mPendingIntent.cancel();
-            }
-        }
-    }
-
-    /**
      * Prepare this {@link ClipData} to leave an app process.
      *
      * @hide
@@ -1361,7 +1331,7 @@
             TextUtils.writeToParcel(item.mText, dest, flags);
             dest.writeString8(item.mHtmlText);
             dest.writeTypedObject(item.mIntent, flags);
-            dest.writeTypedObject(item.mPendingIntent, flags);
+            dest.writeTypedObject(item.mIntentSender, flags);
             dest.writeTypedObject(item.mUri, flags);
             dest.writeTypedObject(mParcelItemActivityInfos ? item.mActivityInfo : null, flags);
             dest.writeTypedObject(item.mTextLinks, flags);
@@ -1381,11 +1351,11 @@
             CharSequence text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
             String htmlText = in.readString8();
             Intent intent = in.readTypedObject(Intent.CREATOR);
-            PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR);
+            IntentSender intentSender = in.readTypedObject(IntentSender.CREATOR);
             Uri uri = in.readTypedObject(Uri.CREATOR);
             ActivityInfo info = in.readTypedObject(ActivityInfo.CREATOR);
             TextLinks textLinks = in.readTypedObject(TextLinks.CREATOR);
-            Item item = new Item(text, htmlText, intent, pendingIntent, uri);
+            Item item = new Item(text, htmlText, intent, intentSender, uri);
             item.setActivityInfo(info);
             item.setTextLinks(textLinks);
             mItems.add(item);
diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl
index 62db65f..cec49c7 100644
--- a/core/java/android/content/pm/ILauncherApps.aidl
+++ b/core/java/android/content/pm/ILauncherApps.aidl
@@ -130,4 +130,6 @@
     void unRegisterDumpCallback(IDumpCallback cb);
 
     void setArchiveCompatibilityOptions(boolean enableIconOverlay, boolean enableUnarchivalConfirmation);
+
+    List<UserHandle> getUserProfiles();
 }
diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java
index 9c859c4..e437925 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -695,12 +695,23 @@
      * Otherwise it'll return the same list as {@link UserManager#getUserProfiles()} would.
      */
     public List<UserHandle> getProfiles() {
-        if (mUserManager.isManagedProfile()) {
-            // If it's a managed profile, only return the current profile.
-            final List result =  new ArrayList(1);
+        if (mUserManager.isManagedProfile()
+                || (android.multiuser.Flags.enableLauncherAppsHiddenProfileChecks()
+                        && android.os.Flags.allowPrivateProfile()
+                        && mUserManager.isPrivateProfile())) {
+            // If it's a managed or private profile, only return the current profile.
+            final List result = new ArrayList(1);
             result.add(android.os.Process.myUserHandle());
             return result;
         } else {
+            if (android.multiuser.Flags.enableLauncherAppsHiddenProfileChecks()) {
+                try {
+                    return mService.getUserProfiles();
+                } catch (RemoteException re) {
+                    throw re.rethrowFromSystemServer();
+                }
+            }
+
             return mUserManager.getUserProfiles();
         }
     }
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 2add77e..57b437f 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -558,8 +558,11 @@
      * on a particular SessionConfiguration.</p>
      *
      * @return List of CameraCharacteristic keys containing characterisitics specific to a session
-     * configuration. For Android 15, this list only contains CONTROL_ZOOM_RATIO_RANGE and
-     * SCALER_AVAILABLE_MAX_DIGITAL_ZOOM.
+     * configuration. If {@link #INFO_SESSION_CONFIGURATION_QUERY_VERSION} is
+     * {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, then this list will only contain
+     * CONTROL_ZOOM_RATIO_RANGE and SCALER_AVAILABLE_MAX_DIGITAL_ZOOM
+     *
+     * @see INFO_SESSION_CONFIGURATION_QUERY_VERSION
      */
     @NonNull
     @FlaggedApi(Flags.FLAG_FEATURE_COMBINATION_QUERY)
diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java
index 1a0074f..991bade 100644
--- a/core/java/android/hardware/camera2/CameraDevice.java
+++ b/core/java/android/hardware/camera2/CameraDevice.java
@@ -1718,7 +1718,7 @@
          * <p>Other than that, the characteristics returned here can be used in the same way as
          * those returned from {@link CameraManager#getCameraCharacteristics}.</p>
          *
-         * @param sessionConfig : The session configuration for which characteristics are fetched.
+         * @param sessionConfig The session configuration for which characteristics are fetched.
          * @return CameraCharacteristics specific to a given session configuration.
          *
          * @throws IllegalArgumentException      if the session configuration is invalid
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index 4b170f3..b1c24a7 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -87,6 +87,7 @@
 
 # PerformanceHintManager
 per-file PerformanceHintManager.java = file:/ADPF_OWNERS
+per-file WorkDuration.java = file:/ADPF_OWNERS
 
 # IThermal interfaces
 per-file IThermal* = file:/THERMAL_OWNERS
diff --git a/core/java/android/service/chooser/ChooserResult.java b/core/java/android/service/chooser/ChooserResult.java
index 4603be1..2d56ec7 100644
--- a/core/java/android/service/chooser/ChooserResult.java
+++ b/core/java/android/service/chooser/ChooserResult.java
@@ -20,6 +20,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.compat.annotation.Overridable;
@@ -91,6 +92,7 @@
     }
 
     /** @hide */
+    @TestApi
     public ChooserResult(@ResultType int type, @Nullable ComponentName componentName,
             boolean isShortcut) {
         mType = type;
diff --git a/core/java/android/view/KeyboardShortcutGroup.java b/core/java/android/view/KeyboardShortcutGroup.java
index 763ca26..c4c87ef 100644
--- a/core/java/android/view/KeyboardShortcutGroup.java
+++ b/core/java/android/view/KeyboardShortcutGroup.java
@@ -35,6 +35,8 @@
     private final List<KeyboardShortcutInfo> mItems;
     // The system group looks different UI wise.
     private boolean mSystemGroup;
+    // The package name for the shortcut
+    private CharSequence mPackageName;
 
     /**
      * @param label The title to be used for this group, or null if there is none.
@@ -82,6 +84,7 @@
         mLabel = source.readCharSequence();
         source.readTypedList(mItems, KeyboardShortcutInfo.CREATOR);
         mSystemGroup = source.readInt() == 1;
+        mPackageName = source.readCharSequence();
     }
 
     /**
@@ -105,6 +108,22 @@
     }
 
     /**
+     * @param packageName the name of the package associated with this shortcut.
+     * @hide
+     */
+    public void setPackageName(CharSequence packageName) {
+        mPackageName = packageName;
+    }
+
+    /**
+     * Return the package name of the app associated with this shortcut.
+     * @hide
+     */
+    public CharSequence getPackageName() {
+        return mPackageName;
+    }
+
+    /**
      * Adds an item to the existing list.
      *
      * @param item The item to be added.
@@ -123,6 +142,7 @@
         dest.writeCharSequence(mLabel);
         dest.writeTypedList(mItems);
         dest.writeInt(mSystemGroup ? 1 : 0);
+        dest.writeCharSequence(mPackageName);
     }
 
     public static final @android.annotation.NonNull Creator<KeyboardShortcutGroup> CREATOR =
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 8cbfdcb..cd6d79c 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -78,6 +78,7 @@
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
+import android.content.IntentSender;
 import android.content.res.ColorStateList;
 import android.content.res.CompatibilityInfo;
 import android.content.res.Configuration;
@@ -5357,16 +5358,16 @@
     /**
      * Flag indicating that an unhandled drag should be delegated to the system to be started if no
      * visible window wishes to handle the drop. When using this flag, the caller must provide
-     * ClipData with an Item that contains an immutable PendingIntent to an activity to be launched
+     * ClipData with an Item that contains an immutable IntentSender to an activity to be launched
      * (not a broadcast, service, etc).  See
-     * {@link ClipData.Item.Builder#setPendingIntent(PendingIntent)}.
+     * {@link ClipData.Item.Builder#setIntentSender(IntentSender)}.
      *
      * The system can decide to launch the intent or not based on factors like the current screen
      * size or windowing mode. If the system does not launch the intent, it will be canceled via the
      * normal drag and drop flow.
      */
     @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
-    public static final int DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG = 1 << 13;
+    public static final int DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG = 1 << 13;
 
     /**
      * Vertical scroll factor cached by {@link #getVerticalScrollFactor}.
@@ -28665,10 +28666,10 @@
             if (com.android.window.flags.Flags.delegateUnhandledDrags()) {
                 data.prepareToLeaveProcess(
                         (flags & (DRAG_FLAG_GLOBAL_SAME_APPLICATION | DRAG_FLAG_GLOBAL)) != 0);
-                if ((flags & DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG) != 0) {
-                    if (!data.hasActivityPendingIntents()) {
+                if ((flags & DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG) != 0) {
+                    if (!hasActivityPendingIntents(data)) {
                         // Reset the flag if there is no launchable activity intent
-                        flags &= ~DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG;
+                        flags &= ~DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG;
                         Log.w(VIEW_LOG_TAG, "startDragAndDrop called with "
                                 + "DRAG_FLAG_START_INTENT_ON_UNHANDLED_DRAG but the clip data "
                                 + "contains non-activity PendingIntents");
@@ -28781,7 +28782,7 @@
                     mAttachInfo.mDragSurface.release();
                 }
                 if (mAttachInfo.mDragData != null) {
-                    mAttachInfo.mDragData.cleanUpPendingIntents();
+                    View.cleanUpPendingIntents(mAttachInfo.mDragData);
                 }
                 mAttachInfo.mDragSurface = surface;
                 mAttachInfo.mDragToken = token;
@@ -28806,6 +28807,39 @@
         }
     }
 
+     /**
+     * Checks if this clip data has a pending intent that is an activity type.
+     * @hide
+     */
+    static boolean hasActivityPendingIntents(ClipData data) {
+        final int size = data.getItemCount();
+        for (int i = 0; i < size; i++) {
+            final ClipData.Item item = data.getItemAt(i);
+            if (item.getIntentSender() != null) {
+                final PendingIntent pi = new PendingIntent(item.getIntentSender().getTarget());
+                if (pi.isActivity()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Cleans up all pending intents in the ClipData.
+     * @hide
+     */
+    static void cleanUpPendingIntents(ClipData data) {
+        final int size = data.getItemCount();
+        for (int i = 0; i < size; i++) {
+            final ClipData.Item item = data.getItemAt(i);
+            if (item.getIntentSender() != null) {
+                final PendingIntent pi = new PendingIntent(item.getIntentSender().getTarget());
+                pi.cancel();
+            }
+        }
+    }
+
     void setAccessibilityDragStarted(boolean started) {
         int pflags4 = mPrivateFlags4;
         if (started) {
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 75deceb..1e79786 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -8626,7 +8626,7 @@
                         mAttachInfo.mDragSurface = null;
                     }
                     if (mAttachInfo.mDragData != null) {
-                        mAttachInfo.mDragData.cleanUpPendingIntents();
+                        View.cleanUpPendingIntents(mAttachInfo.mDragData);
                         mAttachInfo.mDragData = null;
                     }
                 }
@@ -8652,6 +8652,12 @@
         if (mView != null) {
             mView.requestKeyboardShortcuts(list, deviceId);
         }
+        int numGroups = list.size();
+        for (int i = 0; i < numGroups; ++i) {
+            final KeyboardShortcutGroup group = list.get(i);
+            group.setPackageName(mBasePackageName);
+
+        }
         data.putParcelableArrayList(WindowManager.PARCEL_KEY_SHORTCUTS_ARRAY, list);
         try {
             receiver.send(0, data);
diff --git a/core/java/android/view/textclassifier/TextClassificationConstants.java b/core/java/android/view/textclassifier/TextClassificationConstants.java
index d0ed8ee..7dd7719 100644
--- a/core/java/android/view/textclassifier/TextClassificationConstants.java
+++ b/core/java/android/view/textclassifier/TextClassificationConstants.java
@@ -137,10 +137,6 @@
                     properties.getBoolean(
                             LOCAL_TEXT_CLASSIFIER_ENABLED,
                             LOCAL_TEXT_CLASSIFIER_ENABLED_DEFAULT);
-            sSystemTextClassifierEnabled =
-                    properties.getBoolean(
-                            SYSTEM_TEXT_CLASSIFIER_ENABLED,
-                            SYSTEM_TEXT_CLASSIFIER_ENABLED_DEFAULT);
             sModelDarkLaunchEnabled =
                     properties.getBoolean(
                             MODEL_DARK_LAUNCH_ENABLED,
@@ -199,8 +195,11 @@
     }
 
     public boolean isSystemTextClassifierEnabled() {
-        ensureMemoizedValues();
-        return sSystemTextClassifierEnabled;
+        // Don't memoize this value because we want to be able to receive config
+        // updates at runtime.
+        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
+                SYSTEM_TEXT_CLASSIFIER_ENABLED,
+                SYSTEM_TEXT_CLASSIFIER_ENABLED_DEFAULT);
     }
 
     public boolean isModelDarkLaunchEnabled() {
diff --git a/core/java/android/window/flags/OWNERS b/core/java/android/window/flags/OWNERS
index fa81ee3..3fa3760 100644
--- a/core/java/android/window/flags/OWNERS
+++ b/core/java/android/window/flags/OWNERS
@@ -1 +1,2 @@
-per-file responsible_apis.aconfig = file:/BAL_OWNERS
\ No newline at end of file
+per-file responsible_apis.aconfig = file:/BAL_OWNERS
+per-file large_screen_experiences_app_compat.aconfig = file:/LSE_APP_COMPAT_OWNERS
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
index a7260bb..c34730f 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
@@ -49,8 +49,13 @@
         this.mRemoteComposeState = remoteComposeState;
     }
 
-    public void reset() {
-        mBuffer.reset();
+    /**
+     * Reset the internal buffers
+     *
+     * @param expectedSize provided hint for the main buffer size
+     */
+    public void reset(int expectedSize) {
+        mBuffer.reset(expectedSize);
         mRemoteComposeState.reset();
     }
 
@@ -288,8 +293,7 @@
     public static void read(InputStream fd, RemoteComposeBuffer buffer) {
         try {
             byte[] bytes = readAllBytes(fd);
-            buffer.reset();
-            buffer.mBuffer.resize(bytes.length);
+            buffer.reset(bytes.length);
             System.arraycopy(bytes, 0, buffer.mBuffer.mBuffer, 0, bytes.length);
             buffer.mBuffer.mSize = bytes.length;
         } catch (Exception e) {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
index 4518d94..b7cb392 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
@@ -83,10 +83,18 @@
         mIndex = currentIndex;
     }
 
-    public void reset() {
+    /**
+     * Reset the internal buffer
+     *
+     * @param expectedSize provided hint for the buffer size
+     */
+    public void reset(int expectedSize) {
         mIndex = 0;
         mStartingIndex = 0;
         mSize = 0;
+        if (expectedSize > mMaxSize) {
+            resize(expectedSize);
+        }
     }
 
     public int size() {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java
index 4bfdc59..76b7144 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java
@@ -33,7 +33,7 @@
     int mImageWidth;
     int mImageHeight;
     byte[] mBitmap;
-    public static final int MAX_IMAGE_DIMENSION = 6000;
+    public static final int MAX_IMAGE_DIMENSION = 8000;
 
     public static final Companion COMPANION = new Companion();
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 0b3a065..710b5f8 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7182,10 +7182,10 @@
     <permission android:name="android.permission.MANAGE_SPEECH_RECOGNITION"
         android:protectionLevel="signature" />
 
-    <!-- @SystemApi Allows an application to manage the content suggestions service.
+    <!-- @SystemApi Allows an application to interact with the content suggestions service.
          @hide  <p>Not for use by third-party applications.</p> -->
     <permission android:name="android.permission.MANAGE_CONTENT_SUGGESTIONS"
-        android:protectionLevel="signature" />
+        android:protectionLevel="signature|role" />
 
     <!-- @SystemApi Allows an application to manage the app predictions service.
          @hide  <p>Not for use by third-party applications.</p> -->
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassificationConstantsTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassificationConstantsTest.java
index a567b4b..20a8768 100644
--- a/core/tests/coretests/src/android/view/textclassifier/TextClassificationConstantsTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/TextClassificationConstantsTest.java
@@ -28,6 +28,7 @@
 import org.junit.runner.RunWith;
 
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -74,6 +75,13 @@
                         .isEqualTo(1));
     }
 
+    @Test
+    public void runtimeMutableSettings() {
+        assertOverride(
+                TextClassificationConstants.SYSTEM_TEXT_CLASSIFIER_ENABLED,
+                settings -> settings.isSystemTextClassifierEnabled());
+    }
+
     private static void assertSettings(
             String key, String value, Consumer<TextClassificationConstants> settingsConsumer) {
         final String originalValue =
@@ -87,6 +95,21 @@
         }
     }
 
+    private static void assertOverride(
+            String key, Predicate<TextClassificationConstants> settingsPredicate) {
+        final String originalValue =
+                DeviceConfig.getProperty(DeviceConfig.NAMESPACE_TEXTCLASSIFIER, key);
+        TextClassificationConstants settings = new TextClassificationConstants();
+        try {
+            setDeviceConfig(key, "true");
+            assertThat(settingsPredicate.test(settings)).isTrue();
+            setDeviceConfig(key, "false");
+            assertThat(settingsPredicate.test(settings)).isFalse();
+        } finally {
+            setDeviceConfig(key, originalValue);
+        }
+    }
+
     private static void setDeviceConfig(String key, String value) {
         DeviceConfig.setProperty(
                 DeviceConfig.NAMESPACE_TEXTCLASSIFIER, key, value, /* makeDefault */ false);
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 310300d..d66c925 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -206,6 +206,8 @@
     srcs: [
         "multivalentTests/src/**/*.kt",
     ],
+    // TODO(b/323188766): Include BubbleStackViewTest once the robolectric issue is fixed.
+    exclude_srcs: ["multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt"],
     static_libs: [
         "junit",
         "androidx.test.runner",
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
index 5825bbf..9cd14fca 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
@@ -466,6 +466,26 @@
             .isEqualTo(expectedExpandedViewY)
     }
 
+    @Test
+    fun testGetTaskViewContentWidth_onLeft() {
+        positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
+        val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */)
+        val paddings = positioner.getExpandedViewContainerPadding(true /* onLeft */,
+                false /* isOverflow */)
+        assertThat(taskViewWidth).isEqualTo(
+                positioner.screenRect.width() - paddings[0] - paddings[2])
+    }
+
+    @Test
+    fun testGetTaskViewContentWidth_onRight() {
+        positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
+        val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */)
+        val paddings = positioner.getExpandedViewContainerPadding(false /* onLeft */,
+                false /* isOverflow */)
+        assertThat(taskViewWidth).isEqualTo(
+                positioner.screenRect.width() - paddings[0] - paddings[2])
+    }
+
     private val defaultYPosition: Float
         /**
          * Calculates the Y position bubbles should be placed based on the config. Based on the
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
new file mode 100644
index 0000000..8989fc5
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.graphics.drawable.Icon
+import android.os.UserHandle
+import android.view.IWindowManager
+import android.view.WindowManager
+import android.view.WindowManagerGlobal
+import androidx.test.annotation.UiThreadTest
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.internal.protolog.common.ProtoLog
+import com.android.launcher3.icons.BubbleIconFactory
+import com.android.wm.shell.R
+import com.android.wm.shell.bubbles.Bubbles.SysuiProxy
+import com.android.wm.shell.common.FloatingContentCoordinator
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.taskview.TaskView
+import com.android.wm.shell.taskview.TaskViewTaskController
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+import java.util.function.Consumer
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+/** Unit tests for [BubbleStackView]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleStackViewTest {
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var positioner: BubblePositioner
+    private lateinit var iconFactory: BubbleIconFactory
+    private lateinit var expandedViewManager: FakeBubbleExpandedViewManager
+    private lateinit var bubbleStackView: BubbleStackView
+    private lateinit var shellExecutor: ShellExecutor
+    private lateinit var windowManager: IWindowManager
+    private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory
+    private lateinit var bubbleData: BubbleData
+
+    @Before
+    fun setUp() {
+        // Disable protolog tool when running the tests from studio
+        ProtoLog.REQUIRE_PROTOLOGTOOL = false
+        windowManager = WindowManagerGlobal.getWindowManagerService()!!
+        shellExecutor = TestShellExecutor()
+        val windowManager = context.getSystemService(WindowManager::class.java)
+        iconFactory =
+            BubbleIconFactory(
+                context,
+                context.resources.getDimensionPixelSize(R.dimen.bubble_size),
+                context.resources.getDimensionPixelSize(R.dimen.bubble_badge_size),
+                Color.BLACK,
+                context.resources.getDimensionPixelSize(
+                    com.android.internal.R.dimen.importance_ring_stroke_width
+                )
+            )
+        positioner = BubblePositioner(context, windowManager)
+        val bubbleStackViewManager = FakeBubbleStackViewManager()
+        bubbleData =
+            BubbleData(
+                context,
+                BubbleLogger(UiEventLoggerFake()),
+                positioner,
+                BubbleEducationController(context),
+                shellExecutor
+            )
+
+        val sysuiProxy = mock<SysuiProxy>()
+        expandedViewManager = FakeBubbleExpandedViewManager()
+        bubbleTaskViewFactory = FakeBubbleTaskViewFactory()
+        bubbleStackView =
+            BubbleStackView(
+                context,
+                bubbleStackViewManager,
+                positioner,
+                bubbleData,
+                null,
+                FloatingContentCoordinator(),
+                { sysuiProxy },
+                shellExecutor
+            )
+    }
+
+    @UiThreadTest
+    @Test
+    fun addBubble() {
+        val bubble = createAndInflateBubble()
+        bubbleStackView.addBubble(bubble)
+        assertThat(bubbleStackView.bubbleCount).isEqualTo(1)
+    }
+
+    @UiThreadTest
+    @Test
+    fun tapBubbleToExpand() {
+        val bubble = createAndInflateBubble()
+        bubbleStackView.addBubble(bubble)
+        assertThat(bubbleStackView.bubbleCount).isEqualTo(1)
+
+        bubble.iconView!!.performClick()
+        // we're checking the expanded state in BubbleData because that's the source of truth. This
+        // will eventually propagate an update back to the stack view, but setting the entire
+        // pipeline is outside the scope of a unit test.
+        assertThat(bubbleData.isExpanded).isTrue()
+    }
+
+    private fun createAndInflateBubble(): Bubble {
+        val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
+        val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button)
+        val bubble = Bubble.createAppBubble(intent, UserHandle(1), icon, directExecutor())
+        bubble.setInflateSynchronously(true)
+        bubbleData.notificationEntryUpdated(bubble, true, false)
+
+        val semaphore = Semaphore(0)
+        val callback: BubbleViewInfoTask.Callback =
+            BubbleViewInfoTask.Callback { semaphore.release() }
+        bubble.inflate(
+            callback,
+            context,
+            expandedViewManager,
+            bubbleTaskViewFactory,
+            positioner,
+            bubbleStackView,
+            null,
+            iconFactory,
+            false
+        )
+
+        assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+        assertThat(bubble.isInflated).isTrue()
+        return bubble
+    }
+
+    private class FakeBubbleStackViewManager : BubbleStackViewManager {
+
+        override fun onAllBubblesAnimatedOut() {}
+
+        override fun updateWindowFlagsForBackpress(interceptBack: Boolean) {}
+
+        override fun checkNotificationPanelExpandedState(callback: Consumer<Boolean>) {}
+
+        override fun hideCurrentInputMethod() {}
+    }
+
+    private class TestShellExecutor : ShellExecutor {
+
+        override fun execute(runnable: Runnable) {
+            runnable.run()
+        }
+
+        override fun executeDelayed(r: Runnable, delayMillis: Long) {
+            r.run()
+        }
+
+        override fun removeCallbacks(r: Runnable) {}
+
+        override fun hasCallback(r: Runnable): Boolean = false
+    }
+
+    private inner class FakeBubbleTaskViewFactory : BubbleTaskViewFactory {
+        override fun create(): BubbleTaskView {
+            val taskViewTaskController = mock<TaskViewTaskController>()
+            val taskView = TaskView(context, taskViewTaskController)
+            return BubbleTaskView(taskView, shellExecutor)
+        }
+    }
+
+    private inner class FakeBubbleExpandedViewManager : BubbleExpandedViewManager {
+
+        override val overflowBubbles: List<Bubble>
+            get() = emptyList()
+
+        override fun setOverflowListener(listener: BubbleData.Listener) {}
+
+        override fun collapseStack() {}
+
+        override fun updateWindowFlagsForBackpress(intercept: Boolean) {}
+
+        override fun promoteBubbleFromOverflow(bubble: Bubble) {}
+
+        override fun removeBubble(key: String, reason: Int) {}
+
+        override fun dismissBubble(bubble: Bubble, reason: Int) {}
+
+        override fun setAppBubbleTaskId(key: String, taskId: Int) {}
+
+        override fun isStackExpanded(): Boolean = false
+
+        override fun isShowingAsBubbleBar(): Boolean = false
+    }
+}
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 cda29c9..a5853d6 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
@@ -395,7 +395,7 @@
     public int getTaskViewContentWidth(boolean onLeft) {
         int[] paddings = getExpandedViewContainerPadding(onLeft, /* isOverflow = */ false);
         int pointerOffset = showBubblesVertically() ? getPointerSize() : 0;
-        return mPositionRect.width() - paddings[0] - paddings[2] - pointerOffset;
+        return mScreenRect.width() - paddings[0] - paddings[2] - pointerOffset;
     }
 
     /** Gets the y position of the expanded view if it was top-aligned. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 8eecf1c..458ea05 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -74,13 +74,18 @@
             ShellController shellController,
             DisplayController displayController,
             DisplayInsetsController displayInsetsController,
-            PipDisplayLayoutState pipDisplayLayoutState) {
+            PipBoundsState pipBoundsState,
+            PipBoundsAlgorithm pipBoundsAlgorithm,
+            PipDisplayLayoutState pipDisplayLayoutState,
+            PipScheduler pipScheduler,
+            @ShellMainThread ShellExecutor mainExecutor) {
         if (!PipUtils.isPip2ExperimentEnabled()) {
             return Optional.empty();
         } else {
             return Optional.ofNullable(PipController.create(
                     context, shellInit, shellController, displayController, displayInsetsController,
-                    pipDisplayLayoutState));
+                    pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler,
+                    mainExecutor));
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index 186cb61..e73a850 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -18,14 +18,31 @@
 
 import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
 
+import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP;
+
+import android.app.PictureInPictureParams;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
+import android.graphics.Rect;
 import android.view.InsetsState;
+import android.view.SurfaceControl;
+
+import androidx.annotation.BinderThread;
 
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.ExternalInterfaceBinder;
+import com.android.wm.shell.common.RemoteCallable;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.pip.IPip;
+import com.android.wm.shell.common.pip.IPipAnimationListener;
+import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
+import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -37,32 +54,54 @@
  * Manages the picture-in-picture (PIP) UI and states for Phones.
  */
 public class PipController implements ConfigurationChangeListener,
-        DisplayController.OnDisplaysChangedListener {
+        DisplayController.OnDisplaysChangedListener, RemoteCallable<PipController> {
     private static final String TAG = PipController.class.getSimpleName();
 
     private Context mContext;
     private ShellController mShellController;
     private DisplayController mDisplayController;
     private DisplayInsetsController mDisplayInsetsController;
+    private PipBoundsState mPipBoundsState;
+    private PipBoundsAlgorithm mPipBoundsAlgorithm;
     private PipDisplayLayoutState mPipDisplayLayoutState;
+    private PipScheduler mPipScheduler;
+    private ShellExecutor mMainExecutor;
 
     private PipController(Context context,
             ShellInit shellInit,
             ShellController shellController,
             DisplayController displayController,
             DisplayInsetsController displayInsetsController,
-            PipDisplayLayoutState pipDisplayLayoutState) {
+            PipBoundsState pipBoundsState,
+            PipBoundsAlgorithm pipBoundsAlgorithm,
+            PipDisplayLayoutState pipDisplayLayoutState,
+            PipScheduler pipScheduler,
+            ShellExecutor mainExecutor) {
         mContext = context;
         mShellController = shellController;
         mDisplayController = displayController;
         mDisplayInsetsController = displayInsetsController;
+        mPipBoundsState = pipBoundsState;
+        mPipBoundsAlgorithm = pipBoundsAlgorithm;
         mPipDisplayLayoutState = pipDisplayLayoutState;
+        mPipScheduler = pipScheduler;
+        mMainExecutor = mainExecutor;
 
         if (PipUtils.isPip2ExperimentEnabled()) {
             shellInit.addInitCallback(this::onInit, this);
         }
     }
 
+    @Override
+    public Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    public ShellExecutor getRemoteCallExecutor() {
+        return mMainExecutor;
+    }
+
     private void onInit() {
         // Ensure that we have the display info in case we get calls to update the bounds before the
         // listener calls back
@@ -80,6 +119,10 @@
                                         .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()));
                     }
                 });
+
+        // Allow other outside processes to bind to PiP controller using the key below.
+        mShellController.addExternalInterface(KEY_EXTRA_SHELL_PIP,
+                this::createExternalInterface, this);
     }
 
     /**
@@ -90,16 +133,24 @@
             ShellController shellController,
             DisplayController displayController,
             DisplayInsetsController displayInsetsController,
-            PipDisplayLayoutState pipDisplayLayoutState) {
+            PipBoundsState pipBoundsState,
+            PipBoundsAlgorithm pipBoundsAlgorithm,
+            PipDisplayLayoutState pipDisplayLayoutState,
+            PipScheduler pipScheduler,
+            ShellExecutor mainExecutor) {
         if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {
             ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: Device doesn't support Pip feature", TAG);
             return null;
         }
         return new PipController(context, shellInit, shellController, displayController,
-                displayInsetsController, pipDisplayLayoutState);
+                displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState,
+                pipScheduler, mainExecutor);
     }
 
+    private ExternalInterfaceBinder createExternalInterface() {
+        return new IPipImpl(this);
+    }
 
     @Override
     public void onConfigurationChanged(Configuration newConfiguration) {
@@ -130,4 +181,86 @@
     private void onDisplayChanged(DisplayLayout layout) {
         mPipDisplayLayoutState.setDisplayLayout(layout);
     }
+
+    private Rect getSwipePipToHomeBounds(ComponentName componentName, ActivityInfo activityInfo,
+            PictureInPictureParams pictureInPictureParams,
+            int launcherRotation, Rect hotseatKeepClearArea) {
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "getSwipePipToHomeBounds: %s", componentName);
+        mPipBoundsState.setBoundsStateForEntry(componentName, activityInfo, pictureInPictureParams,
+                mPipBoundsAlgorithm);
+        return mPipBoundsAlgorithm.getEntryDestinationBounds();
+    }
+
+    private void onSwipePipToHomeAnimationStart(int taskId, ComponentName componentName,
+            Rect destinationBounds, SurfaceControl overlay, Rect appBounds) {
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "onSwipePipToHomeAnimationStart: %s", componentName);
+        mPipScheduler.setInSwipePipToHomeTransition(true);
+        // TODO: cache the overlay if provided for reparenting later.
+    }
+
+    /**
+     * The interface for calls from outside the host process.
+     */
+    @BinderThread
+    private static class IPipImpl extends IPip.Stub implements ExternalInterfaceBinder {
+        private PipController mController;
+
+        IPipImpl(PipController controller) {
+            mController = controller;
+        }
+
+        /**
+         * Invalidates this instance, preventing future calls from updating the controller.
+         */
+        @Override
+        public void invalidate() {
+            mController = null;
+        }
+
+        @Override
+        public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
+                PictureInPictureParams pictureInPictureParams, int launcherRotation,
+                Rect keepClearArea) {
+            Rect[] result = new Rect[1];
+            executeRemoteCallWithTaskPermission(mController, "startSwipePipToHome",
+                    (controller) -> {
+                        result[0] = controller.getSwipePipToHomeBounds(componentName, activityInfo,
+                                pictureInPictureParams, launcherRotation, keepClearArea);
+                    }, true /* blocking */);
+            return result[0];
+        }
+
+        @Override
+        public void stopSwipePipToHome(int taskId, ComponentName componentName,
+                Rect destinationBounds, SurfaceControl overlay, Rect appBounds) {
+            if (overlay != null) {
+                overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome");
+            }
+            executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome",
+                    (controller) -> controller.onSwipePipToHomeAnimationStart(
+                            taskId, componentName, destinationBounds, overlay, appBounds));
+        }
+
+        @Override
+        public void abortSwipePipToHome(int taskId, ComponentName componentName) {}
+
+        @Override
+        public void setShelfHeight(boolean visible, int height) {}
+
+        @Override
+        public void setLauncherKeepClearAreaHeight(boolean visible, int height) {}
+
+        @Override
+        public void setLauncherAppIconSize(int iconSizePx) {}
+
+        @Override
+        public void setPipAnimationListener(IPipAnimationListener listener) {
+            // TODO: set a proper animation listener to update the Launcher state as needed.
+        }
+
+        @Override
+        public void setPipAnimationTypeToAlpha() {}
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
index 57b73b3..895c793 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
@@ -63,6 +63,9 @@
     @Nullable
     private SurfaceControl mPinnedTaskLeash;
 
+    // true if Launcher has started swipe PiP to home animation
+    private boolean mInSwipePipToHomeTransition;
+
     /**
      * Temporary PiP CUJ codes to schedule PiP related transitions directly from Shell.
      * This is used for a broadcast receiver to resolve intents. This should be removed once
@@ -168,6 +171,14 @@
         mPipTransitionController.startResizeTransition(wct, onFinishResizeCallback);
     }
 
+    void setInSwipePipToHomeTransition(boolean inSwipePipToHome) {
+        mInSwipePipToHomeTransition = true;
+    }
+
+    boolean isInSwipePipToHomeTransition() {
+        return mInSwipePipToHomeTransition;
+    }
+
     void onExitPip() {
         mPipTaskToken = null;
         mPinnedTaskLeash = null;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index fbf4d13..dfb0475 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -152,6 +152,12 @@
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
         if (transition == mEnterTransition) {
             mEnterTransition = null;
+            if (mPipScheduler.isInSwipePipToHomeTransition()) {
+                // If this is the second transition as a part of swipe PiP to home cuj,
+                // handle this transition as a special case with no-op animation.
+                return handleSwipePipToHomeTransition(info, startTransaction, finishTransaction,
+                        finishCallback);
+            }
             if (isLegacyEnter(info)) {
                 // If this is a legacy-enter-pip (auto-enter is off and PiP activity went to pause),
                 // then we should run an ALPHA type (cross-fade) animation.
@@ -207,6 +213,25 @@
         return true;
     }
 
+    private boolean handleSwipePipToHomeTransition(@NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        TransitionInfo.Change pipChange = getPipChange(info);
+        if (pipChange == null) {
+            return false;
+        }
+        mPipScheduler.setInSwipePipToHomeTransition(false);
+        mPipTaskToken = pipChange.getContainer();
+
+        // cache the PiP task token and leash
+        mPipScheduler.setPipTaskToken(mPipTaskToken);
+
+        startTransaction.apply();
+        finishCallback.onTransitionFinished(null);
+        return true;
+    }
+
     private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
diff --git a/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml
index 47a116b..2ef425c 100644
--- a/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml
@@ -21,6 +21,7 @@
 
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
     <uses-permission android:name="android.permission.VIBRATE"/>
+    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/>
 
     <application android:debuggable="true" android:largeHeap="true">
         <uses-library android:name="android.test.mock" />
diff --git a/libs/hwui/CanvasTransform.cpp b/libs/hwui/CanvasTransform.cpp
index b667daf..30e7a62 100644
--- a/libs/hwui/CanvasTransform.cpp
+++ b/libs/hwui/CanvasTransform.cpp
@@ -137,9 +137,10 @@
         return palette;
     }
 
-    SkColor color = palette == BitmapPalette::Light ? SK_ColorWHITE : SK_ColorBLACK;
-    color = paint->getColorFilter()->filterColor(color);
-    return paletteForColorHSV(color);
+    SkColor4f color = palette == BitmapPalette::Light ? SkColors::kWhite : SkColors::kBlack;
+    sk_sp<SkColorSpace> srgb = SkColorSpace::MakeSRGB();
+    color = paint->getColorFilter()->filterColor4f(color, srgb.get(), srgb.get());
+    return paletteForColorHSV(color.toSkColor());
 }
 
 bool transformPaint(ColorTransform transform, SkPaint* paint) {
diff --git a/libs/hwui/DamageAccumulator.cpp b/libs/hwui/DamageAccumulator.cpp
index fd27641..28d85bd 100644
--- a/libs/hwui/DamageAccumulator.cpp
+++ b/libs/hwui/DamageAccumulator.cpp
@@ -218,7 +218,7 @@
     }
 
     // Perform clipping
-    if (props.getClipDamageToBounds() && !frame->pendingDirty.isEmpty()) {
+    if (props.getClipDamageToBounds()) {
         if (!frame->pendingDirty.intersect(SkRect::MakeIWH(props.getWidth(), props.getHeight()))) {
             frame->pendingDirty.setEmpty();
         }
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 9c7f7cc..1d03301 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -1067,6 +1067,7 @@
 
     if (dirty->isEmpty()) {
         dirty->setIWH(frame.width(), frame.height());
+        return *dirty;
     }
 
     // At this point dirty is the area of the window to update. However,
diff --git a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
index b5cf011..ce8fb65 100644
--- a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
+++ b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
@@ -100,13 +100,15 @@
     public static final int FAILURE_REASON_EXPLICIT_HEALTH_CHECK = 2;
     public static final int FAILURE_REASON_APP_CRASH = 3;
     public static final int FAILURE_REASON_APP_NOT_RESPONDING = 4;
+    public static final int FAILURE_REASON_BOOT_LOOP = 5;
 
     @IntDef(prefix = { "FAILURE_REASON_" }, value = {
             FAILURE_REASON_UNKNOWN,
             FAILURE_REASON_NATIVE_CRASH,
             FAILURE_REASON_EXPLICIT_HEALTH_CHECK,
             FAILURE_REASON_APP_CRASH,
-            FAILURE_REASON_APP_NOT_RESPONDING
+            FAILURE_REASON_APP_NOT_RESPONDING,
+            FAILURE_REASON_BOOT_LOOP
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface FailureReasons {}
@@ -542,7 +544,7 @@
         mNumberOfNativeCrashPollsRemaining--;
         // Check if native watchdog reported a crash
         if ("1".equals(SystemProperties.get("sys.init.updatable_crashing"))) {
-            // We rollback everything available when crash is unattributable
+            // We rollback all available low impact rollbacks when crash is unattributable
             onPackageFailure(Collections.EMPTY_LIST, FAILURE_REASON_NATIVE_CRASH);
             // we stop polling after an attempt to execute rollback, regardless of whether the
             // attempt succeeds or not
@@ -572,6 +574,7 @@
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_30,
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_50,
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_70,
+                     PackageHealthObserverImpact.USER_IMPACT_LEVEL_90,
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_100})
     public @interface PackageHealthObserverImpact {
         /** No action to take. */
@@ -582,6 +585,7 @@
         int USER_IMPACT_LEVEL_30 = 30;
         int USER_IMPACT_LEVEL_50 = 50;
         int USER_IMPACT_LEVEL_70 = 70;
+        int USER_IMPACT_LEVEL_90 = 90;
         /* Action has high user impact, a last resort, user of a device will be very frustrated. */
         int USER_IMPACT_LEVEL_100 = 100;
     }
diff --git a/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java
index dd74a2a..5fb47dd 100644
--- a/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java
+++ b/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java
@@ -28,6 +28,7 @@
 import android.content.rollback.PackageRollbackInfo;
 import android.content.rollback.RollbackInfo;
 import android.content.rollback.RollbackManager;
+import android.crashrecovery.flags.Flags;
 import android.os.Environment;
 import android.os.FileUtils;
 import android.os.Handler;
@@ -45,7 +46,6 @@
 import com.android.server.PackageWatchdog.FailureReasons;
 import com.android.server.PackageWatchdog.PackageHealthObserver;
 import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
-import com.android.server.SystemConfig;
 import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog;
 import com.android.server.pm.ApexManager;
 
@@ -57,6 +57,7 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -84,7 +85,8 @@
     // True if needing to roll back only rebootless apexes when native crash happens
     private boolean mTwoPhaseRollbackEnabled;
 
-    RollbackPackageHealthObserver(Context context) {
+    @VisibleForTesting
+    RollbackPackageHealthObserver(Context context, ApexManager apexManager) {
         mContext = context;
         HandlerThread handlerThread = new HandlerThread("RollbackPackageHealthObserver");
         handlerThread.start();
@@ -94,7 +96,7 @@
         mLastStagedRollbackIdsFile = new File(dataDir, "last-staged-rollback-ids");
         mTwoPhaseRollbackEnabledFile = new File(dataDir, "two-phase-rollback-enabled");
         PackageWatchdog.getInstance(mContext).registerHealthObserver(this);
-        mApexManager = ApexManager.getInstance();
+        mApexManager = apexManager;
 
         if (SystemProperties.getBoolean("sys.boot_completed", false)) {
             // Load the value from the file if system server has crashed and restarted
@@ -107,24 +109,46 @@
         }
     }
 
+    RollbackPackageHealthObserver(Context context) {
+        this(context, ApexManager.getInstance());
+    }
+
     @Override
     public int onHealthCheckFailed(@Nullable VersionedPackage failedPackage,
             @FailureReasons int failureReason, int mitigationCount) {
-        boolean anyRollbackAvailable = !mContext.getSystemService(RollbackManager.class)
-                .getAvailableRollbacks().isEmpty();
         int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+        if (Flags.recoverabilityDetection()) {
+            List<RollbackInfo> availableRollbacks = getAvailableRollbacks();
+            List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel(
+                    availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_LOW);
+            if (!lowImpactRollbacks.isEmpty()) {
+                if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) {
+                    // For native crashes, we will directly roll back any available rollbacks at low
+                    // impact level
+                    impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
+                } else if (getRollbackForPackage(failedPackage, lowImpactRollbacks) != null) {
+                    // Rollback is available for crashing low impact package
+                    impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
+                } else {
+                    impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70;
+                }
+            }
+        } else {
+            boolean anyRollbackAvailable = !mContext.getSystemService(RollbackManager.class)
+                    .getAvailableRollbacks().isEmpty();
 
-        if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH
-                && anyRollbackAvailable) {
-            // For native crashes, we will directly roll back any available rollbacks
-            // Note: For non-native crashes the rollback-all step has higher impact
-            impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
-        } else if (getAvailableRollback(failedPackage) != null) {
-            // Rollback is available, we may get a callback into #execute
-            impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
-        } else if (anyRollbackAvailable) {
-            // If any rollbacks are available, we will commit them
-            impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70;
+            if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH
+                    && anyRollbackAvailable) {
+                // For native crashes, we will directly roll back any available rollbacks
+                // Note: For non-native crashes the rollback-all step has higher impact
+                impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
+            } else if (getAvailableRollback(failedPackage) != null) {
+                // Rollback is available, we may get a callback into #execute
+                impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
+            } else if (anyRollbackAvailable) {
+                // If any rollbacks are available, we will commit them
+                impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70;
+            }
         }
 
         return impact;
@@ -133,16 +157,34 @@
     @Override
     public boolean execute(@Nullable VersionedPackage failedPackage,
             @FailureReasons int rollbackReason, int mitigationCount) {
-        if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) {
-            mHandler.post(() -> rollbackAll(rollbackReason));
-            return true;
-        }
+        if (Flags.recoverabilityDetection()) {
+            List<RollbackInfo> availableRollbacks = getAvailableRollbacks();
+            if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) {
+                mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason));
+                return true;
+            }
 
-        RollbackInfo rollback = getAvailableRollback(failedPackage);
-        if (rollback != null) {
-            mHandler.post(() -> rollbackPackage(rollback, failedPackage, rollbackReason));
+            List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel(
+                    availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_LOW);
+            RollbackInfo rollback = getRollbackForPackage(failedPackage, lowImpactRollbacks);
+            if (rollback != null) {
+                mHandler.post(() -> rollbackPackage(rollback, failedPackage, rollbackReason));
+            } else if (!lowImpactRollbacks.isEmpty()) {
+                // Apply all available low impact rollbacks.
+                mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason));
+            }
         } else {
-            mHandler.post(() -> rollbackAll(rollbackReason));
+            if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) {
+                mHandler.post(() -> rollbackAll(rollbackReason));
+                return true;
+            }
+
+            RollbackInfo rollback = getAvailableRollback(failedPackage);
+            if (rollback != null) {
+                mHandler.post(() -> rollbackPackage(rollback, failedPackage, rollbackReason));
+            } else {
+                mHandler.post(() -> rollbackAll(rollbackReason));
+            }
         }
 
         // Assume rollbacks executed successfully
@@ -150,6 +192,31 @@
     }
 
     @Override
+    public int onBootLoop(int mitigationCount) {
+        int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+        if (Flags.recoverabilityDetection()) {
+            List<RollbackInfo> availableRollbacks = getAvailableRollbacks();
+            if (!availableRollbacks.isEmpty()) {
+                impact = getUserImpactBasedOnRollbackImpactLevel(availableRollbacks);
+            }
+        }
+        return impact;
+    }
+
+    @Override
+    public boolean executeBootLoopMitigation(int mitigationCount) {
+        if (Flags.recoverabilityDetection()) {
+            List<RollbackInfo> availableRollbacks = getAvailableRollbacks();
+
+            triggerLeastImpactLevelRollback(availableRollbacks,
+                    PackageWatchdog.FAILURE_REASON_BOOT_LOOP);
+            return true;
+        }
+        return false;
+    }
+
+
+    @Override
     public String getName() {
         return NAME;
     }
@@ -161,13 +228,16 @@
 
     @Override
     public boolean mayObservePackage(String packageName) {
-        if (mContext.getSystemService(RollbackManager.class)
-                .getAvailableRollbacks().isEmpty()) {
+        if (getAvailableRollbacks().isEmpty()) {
             return false;
         }
         return isPersistentSystemApp(packageName);
     }
 
+    private List<RollbackInfo> getAvailableRollbacks() {
+        return mContext.getSystemService(RollbackManager.class).getAvailableRollbacks();
+    }
+
     private boolean isPersistentSystemApp(@NonNull String packageName) {
         PackageManager pm = mContext.getPackageManager();
         try {
@@ -272,6 +342,40 @@
         return null;
     }
 
+    @AnyThread
+    private RollbackInfo getRollbackForPackage(@Nullable VersionedPackage failedPackage,
+            List<RollbackInfo> availableRollbacks) {
+        if (failedPackage == null) {
+            return null;
+        }
+
+        for (RollbackInfo rollback : availableRollbacks) {
+            for (PackageRollbackInfo packageRollback : rollback.getPackages()) {
+                if (packageRollback.getVersionRolledBackFrom().equals(failedPackage)) {
+                    return rollback;
+                }
+                // TODO(b/147666157): Extract version number of apk-in-apex so that we don't have
+                //  to rely on complicated reasoning as below
+
+                // Due to b/147666157, for apk in apex, we do not know the version we are rolling
+                // back from. But if a package X is embedded in apex A exclusively (not embedded in
+                // any other apex), which is not guaranteed, then it is sufficient to check only
+                // package names here, as the version of failedPackage and the PackageRollbackInfo
+                // can't be different. If failedPackage has a higher version, then it must have
+                // been updated somehow. There are two ways: it was updated by an update of apex A
+                // or updated directly as apk. In both cases, this rollback would have gotten
+                // expired when onPackageReplaced() was called. Since the rollback exists, it has
+                // same version as failedPackage.
+                if (packageRollback.isApkInApex()
+                        && packageRollback.getVersionRolledBackFrom().getPackageName()
+                        .equals(failedPackage.getPackageName())) {
+                    return rollback;
+                }
+            }
+        }
+        return null;
+    }
+
     /**
      * Returns {@code true} if staged session associated with {@code rollbackId} was marked
      * as handled, {@code false} if already handled.
@@ -396,12 +500,6 @@
             @FailureReasons int rollbackReason) {
         assertInWorkerThread();
 
-        if (isAutomaticRollbackDenied(SystemConfig.getInstance(), failedPackage)) {
-            Slog.d(TAG, "Automatic rollback not allowed for package "
-                    + failedPackage.getPackageName());
-            return;
-        }
-
         final RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class);
         int reasonToLog = WatchdogRollbackLogger.mapFailureReasonToMetric(rollbackReason);
         final String failedPackageToLog;
@@ -465,17 +563,6 @@
     }
 
     /**
-     * Returns true if this package is not eligible for automatic rollback.
-     */
-    @VisibleForTesting
-    @AnyThread
-    public static boolean isAutomaticRollbackDenied(SystemConfig systemConfig,
-            VersionedPackage versionedPackage) {
-        return systemConfig.getAutomaticRollbackDenylistedPackages()
-            .contains(versionedPackage.getPackageName());
-    }
-
-    /**
      * Two-phase rollback:
      * 1. roll back rebootless apexes first
      * 2. roll back all remaining rollbacks if native crash doesn't stop after (1) is done
@@ -495,14 +582,62 @@
         boolean found = false;
         for (RollbackInfo rollback : rollbacks) {
             if (isRebootlessApex(rollback)) {
-                VersionedPackage sample = rollback.getPackages().get(0).getVersionRolledBackFrom();
-                rollbackPackage(rollback, sample, PackageWatchdog.FAILURE_REASON_NATIVE_CRASH);
+                VersionedPackage firstRollback =
+                        rollback.getPackages().get(0).getVersionRolledBackFrom();
+                rollbackPackage(rollback, firstRollback,
+                        PackageWatchdog.FAILURE_REASON_NATIVE_CRASH);
                 found = true;
             }
         }
         return found;
     }
 
+    /**
+     * Rollback the package that has minimum rollback impact level.
+     * @param availableRollbacks all available rollbacks
+     * @param rollbackReason reason to rollback
+     */
+    private void triggerLeastImpactLevelRollback(List<RollbackInfo> availableRollbacks,
+            @FailureReasons int rollbackReason) {
+        int minRollbackImpactLevel = getMinRollbackImpactLevel(availableRollbacks);
+
+        if (minRollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_LOW) {
+            // Apply all available low impact rollbacks.
+            mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason));
+        } else if (minRollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_HIGH) {
+            // Rollback one package at a time. If that doesn't resolve the issue, rollback
+            // next with same impact level.
+            mHandler.post(() -> rollbackHighImpact(availableRollbacks, rollbackReason));
+        }
+    }
+
+    /**
+     * sort the available high impact rollbacks by first package name to have a deterministic order.
+     * Apply the first available rollback.
+     * @param availableRollbacks all available rollbacks
+     * @param rollbackReason reason to rollback
+     */
+    @WorkerThread
+    private void rollbackHighImpact(List<RollbackInfo> availableRollbacks,
+            @FailureReasons int rollbackReason) {
+        assertInWorkerThread();
+        List<RollbackInfo> highImpactRollbacks =
+                getRollbacksAvailableForImpactLevel(
+                        availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_HIGH);
+
+        // sort rollbacks based on package name of the first package. This is to have a
+        // deterministic order of rollbacks.
+        List<RollbackInfo> sortedHighImpactRollbacks = highImpactRollbacks.stream().sorted(
+                Comparator.comparing(a -> a.getPackages().get(0).getPackageName())).toList();
+        VersionedPackage firstRollback =
+                sortedHighImpactRollbacks
+                        .get(0)
+                        .getPackages()
+                        .get(0)
+                        .getVersionRolledBackFrom();
+        rollbackPackage(sortedHighImpactRollbacks.get(0), firstRollback, rollbackReason);
+    }
+
     @WorkerThread
     private void rollbackAll(@FailureReasons int rollbackReason) {
         assertInWorkerThread();
@@ -522,8 +657,77 @@
         }
 
         for (RollbackInfo rollback : rollbacks) {
-            VersionedPackage sample = rollback.getPackages().get(0).getVersionRolledBackFrom();
-            rollbackPackage(rollback, sample, rollbackReason);
+            VersionedPackage firstRollback =
+                    rollback.getPackages().get(0).getVersionRolledBackFrom();
+            rollbackPackage(rollback, firstRollback, rollbackReason);
         }
     }
+
+    /**
+     * Rollback all available low impact rollbacks
+     * @param availableRollbacks all available rollbacks
+     * @param rollbackReason reason to rollbacks
+     */
+    @WorkerThread
+    private void rollbackAllLowImpact(
+            List<RollbackInfo> availableRollbacks, @FailureReasons int rollbackReason) {
+        assertInWorkerThread();
+
+        List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel(
+                availableRollbacks,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        if (useTwoPhaseRollback(lowImpactRollbacks)) {
+            return;
+        }
+
+        Slog.i(TAG, "Rolling back all available low impact rollbacks");
+        // Add all rollback ids to mPendingStagedRollbackIds, so that we do not reboot before all
+        // pending staged rollbacks are handled.
+        for (RollbackInfo rollback : lowImpactRollbacks) {
+            if (rollback.isStaged()) {
+                mPendingStagedRollbackIds.add(rollback.getRollbackId());
+            }
+        }
+
+        for (RollbackInfo rollback : lowImpactRollbacks) {
+            VersionedPackage firstRollback =
+                    rollback.getPackages().get(0).getVersionRolledBackFrom();
+            rollbackPackage(rollback, firstRollback, rollbackReason);
+        }
+    }
+
+    private List<RollbackInfo> getRollbacksAvailableForImpactLevel(
+            List<RollbackInfo> availableRollbacks, int impactLevel) {
+        return availableRollbacks.stream()
+                .filter(rollbackInfo -> rollbackInfo.getRollbackImpactLevel() == impactLevel)
+                .toList();
+    }
+
+    private int getMinRollbackImpactLevel(List<RollbackInfo> availableRollbacks) {
+        return availableRollbacks.stream()
+                .mapToInt(RollbackInfo::getRollbackImpactLevel)
+                .min()
+                .orElse(-1);
+    }
+
+    private int getUserImpactBasedOnRollbackImpactLevel(List<RollbackInfo> availableRollbacks) {
+        int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+        int minImpact = getMinRollbackImpactLevel(availableRollbacks);
+        switch (minImpact) {
+            case PackageManager.ROLLBACK_USER_IMPACT_LOW:
+                impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70;
+                break;
+            case PackageManager.ROLLBACK_USER_IMPACT_HIGH:
+                impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_90;
+                break;
+            default:
+                impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+        }
+        return impact;
+    }
+
+    @VisibleForTesting
+    Handler getHandler() {
+        return mHandler;
+    }
 }
diff --git a/packages/CrashRecovery/services/java/com/android/server/rollback/WatchdogRollbackLogger.java b/packages/CrashRecovery/services/java/com/android/server/rollback/WatchdogRollbackLogger.java
index 898c543..519c0ed 100644
--- a/packages/CrashRecovery/services/java/com/android/server/rollback/WatchdogRollbackLogger.java
+++ b/packages/CrashRecovery/services/java/com/android/server/rollback/WatchdogRollbackLogger.java
@@ -18,6 +18,7 @@
 
 import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH;
 import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING;
+import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_BOOT_LOOPING;
 import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK;
 import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH;
 import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH_DURING_BOOT;
@@ -258,6 +259,8 @@
                 return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH;
             case PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING:
                 return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING;
+            case PackageWatchdog.FAILURE_REASON_BOOT_LOOP:
+                return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_BOOT_LOOPING;
             default:
                 return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN;
         }
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/client/CredentialManagerClient.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/client/CredentialManagerClient.kt
index 3fbff37..6f70cfb 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/client/CredentialManagerClient.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/client/CredentialManagerClient.kt
@@ -19,6 +19,7 @@
 import android.content.Intent
 import android.credentials.selection.BaseDialogResult
 import android.credentials.selection.UserSelectionDialogResult
+import com.android.credentialmanager.model.EntryInfo
 import com.android.credentialmanager.model.Request
 import kotlinx.coroutines.flow.StateFlow
 
@@ -54,4 +55,20 @@
      * @throws [IllegalStateException] if [requests] is not [Request.Get].
      */
     fun sendResult(result: UserSelectionDialogResult)
+
+    /**
+     * Sends a response to the system service with a selected [EntryInfo].
+     *
+     * @return if the current [Request.Get] flow can be ended peacefully.
+     * if not, App has to keep reacting to the further update from [requests] until [Request.Cancel]
+     * or [Request.Close] is received.
+     *
+     * @throws [IllegalStateException] if [requests] is not [Request.Get].
+     */
+    fun sendEntrySelectionResult(
+        entryInfo: EntryInfo,
+        resultCode: Int? = null,
+        resultData: Intent? = null,
+        isAutoSelected: Boolean = false,
+    ): Boolean
 }
\ No newline at end of file
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/client/impl/CredentialManagerClientImpl.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/client/impl/CredentialManagerClientImpl.kt
index ec1f052..7f59ba3 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/client/impl/CredentialManagerClientImpl.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/client/impl/CredentialManagerClientImpl.kt
@@ -16,16 +16,24 @@
 
 package com.android.credentialmanager.client.impl
 
+import android.app.Activity
 import android.content.Context
 import android.content.Intent
 import android.credentials.selection.BaseDialogResult
+import android.credentials.selection.BaseDialogResult.RESULT_CODE_DIALOG_USER_CANCELED
+import android.credentials.selection.Constants
+import android.credentials.selection.ProviderPendingIntentResponse
 import android.credentials.selection.UserSelectionDialogResult
 import android.os.Bundle
+import android.os.IBinder
+import android.os.ResultReceiver
 import android.util.Log
 import com.android.credentialmanager.TAG
 import com.android.credentialmanager.model.Request
 import com.android.credentialmanager.parse
 import com.android.credentialmanager.client.CredentialManagerClient
+import com.android.credentialmanager.model.EntryInfo
+
 import dagger.hilt.android.qualifiers.ApplicationContext
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -69,4 +77,58 @@
             )
         }
     }
+
+    override fun sendEntrySelectionResult(
+        entryInfo: EntryInfo,
+        resultCode: Int?,
+        resultData: Intent?,
+        isAutoSelected: Boolean,
+    ): Boolean {
+        Log.d(TAG, "sendEntrySelectionResult, resultCode: $resultCode, resultData: $resultData," +
+                " entryInfo: $entryInfo")
+        val currentRequest = requests.value
+        check(currentRequest is Request.Get) { "current request is not get." }
+        if (resultCode == Activity.RESULT_CANCELED) {
+            if (isAutoSelected) {
+                currentRequest.sendCancellationCode(RESULT_CODE_DIALOG_USER_CANCELED)
+            }
+            return isAutoSelected
+        }
+        val userSelectionDialogResult = UserSelectionDialogResult(
+            currentRequest.token,
+            entryInfo.providerId,
+            entryInfo.entryKey,
+            entryInfo.entrySubkey,
+            if (resultCode != null) ProviderPendingIntentResponse(
+                resultCode,
+                resultData
+            ) else null
+        )
+        sendResult(userSelectionDialogResult)
+        return entryInfo.shouldTerminateUiUponSuccessfulProviderResult
+    }
+
+    private fun Request.Get.sendCancellationCode(cancelCode: Int) {
+        sendCancellationCode(
+            cancelCode = cancelCode,
+            requestToken = token,
+            resultReceiver = resultReceiver,
+            finalResponseReceiver = finalResponseReceiver
+        )
+    }
+
+    private fun sendCancellationCode(
+        cancelCode: Int,
+        requestToken: IBinder?,
+        resultReceiver: ResultReceiver?,
+        finalResponseReceiver: ResultReceiver?
+    ) {
+        if (requestToken != null && resultReceiver != null) {
+            val resultData = Bundle().apply {
+                putParcelable(Constants.EXTRA_FINAL_RESPONSE_RECEIVER, finalResponseReceiver)
+            }
+            BaseDialogResult.addToBundle(BaseDialogResult(requestToken), resultData)
+            resultReceiver.send(cancelCode, resultData)
+        }
+    }
 }
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt
index 9242141..786c441 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt
@@ -54,3 +54,9 @@
         Constants.EXTRA_RESULT_RECEIVER,
         ResultReceiver::class.java
     )
+
+val Intent.finalResponseReceiver: ResultReceiver?
+    get() = this.getParcelableExtra(
+        Constants.EXTRA_FINAL_RESPONSE_RECEIVER,
+        ResultReceiver::class.java
+    )
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestGetMapper.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestGetMapper.kt
index f1f1f7c..1683cc4 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestGetMapper.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestGetMapper.kt
@@ -20,6 +20,7 @@
 import android.content.Intent
 import com.android.credentialmanager.ktx.getCredentialProviderDataList
 import com.android.credentialmanager.ktx.requestInfo
+import com.android.credentialmanager.ktx.finalResponseReceiver
 import com.android.credentialmanager.ktx.resultReceiver
 import com.android.credentialmanager.ktx.toProviderList
 import com.android.credentialmanager.model.Request
@@ -28,6 +29,7 @@
     return Request.Get(
         token = requestInfo?.token,
         resultReceiver = resultReceiver,
+        finalResponseReceiver = finalResponseReceiver,
         providerInfos = getCredentialProviderDataList.toProviderList(context)
     )
 }
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Request.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Request.kt
index 7636462..4a1c260 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Request.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Request.kt
@@ -49,6 +49,7 @@
     data class Get(
         override val token: IBinder?,
         val resultReceiver: ResultReceiver?,
+        val finalResponseReceiver: ResultReceiver?,
         val providerInfos: List<ProviderInfo>,
     ) : Request(token)
     /**
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index 0df40d7..283dc7d 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -25,7 +25,6 @@
 import com.android.credentialmanager.ui.WearApp
 import com.google.android.horologist.annotations.ExperimentalHorologistApi
 import dagger.hilt.android.AndroidEntryPoint
-import kotlin.system.exitProcess
 
 @AndroidEntryPoint(ComponentActivity::class)
 class CredentialSelectorActivity : Hilt_CredentialSelectorActivity() {
@@ -40,7 +39,7 @@
             MaterialTheme {
                 WearApp(
                     viewModel = viewModel,
-                    onCloseApp = { exitProcess(0) },
+                    onCloseApp = { finish() },
                 )
             }
         }
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
index 2fc98e2..366e3a7 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
@@ -19,11 +19,14 @@
 import android.content.Intent
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
+import com.android.credentialmanager.CredentialSelectorUiState.Get
 import com.android.credentialmanager.model.Request
 import com.android.credentialmanager.client.CredentialManagerClient
+import com.android.credentialmanager.model.EntryInfo
 import com.android.credentialmanager.model.get.ActionEntryInfo
 import com.android.credentialmanager.model.get.CredentialEntryInfo
 import com.android.credentialmanager.ui.mappers.toGet
+import android.util.Log
 import dagger.hilt.android.lifecycle.HiltViewModel
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
@@ -35,10 +38,20 @@
 @HiltViewModel
 class CredentialSelectorViewModel @Inject constructor(
     private val credentialManagerClient: CredentialManagerClient,
-) : ViewModel() {
-    private val isPrimaryScreen = MutableStateFlow(false)
-    val uiState: StateFlow<CredentialSelectorUiState> = credentialManagerClient.requests
-        .combine(isPrimaryScreen) { request, isPrimary ->
+) : FlowEngine, ViewModel() {
+    private val isPrimaryScreen = MutableStateFlow(true)
+    private val shouldClose = MutableStateFlow(false)
+    val uiState: StateFlow<CredentialSelectorUiState> =
+        combine(
+            credentialManagerClient.requests,
+            isPrimaryScreen,
+            shouldClose
+        ) { request, isPrimary, shouldClose ->
+            if (shouldClose) {
+                Log.d(TAG, "Request finished, closing ")
+                return@combine CredentialSelectorUiState.Close
+            }
+
             when (request) {
                 null -> CredentialSelectorUiState.Idle
                 is Request.Cancel -> CredentialSelectorUiState.Cancel(request.appName)
@@ -56,6 +69,41 @@
     fun updateRequest(intent: Intent) {
             credentialManagerClient.updateRequest(intent = intent)
     }
+
+    override fun back() {
+        Log.d(TAG, "OnBackPressed")
+        when (uiState.value) {
+            is Get.MultipleEntry -> isPrimaryScreen.value = true
+            else -> {
+                shouldClose.value = true
+                // TODO("b/300422310 - [Wear] Implement UI for cancellation request with message")
+            }
+        }
+    }
+
+    override fun cancel() {
+        shouldClose.value = true
+        // TODO("b/300422310 - [Wear] Implement UI for cancellation request with message")
+    }
+
+    override fun openSecondaryScreen() {
+        isPrimaryScreen.value = false
+    }
+
+    override fun sendSelectionResult(
+        entryInfo: EntryInfo,
+        resultCode: Int?,
+        resultData: Intent?,
+        isAutoSelected: Boolean,
+    ) {
+        val result = credentialManagerClient.sendEntrySelectionResult(
+            entryInfo = entryInfo,
+            resultCode = resultCode,
+            resultData = resultData,
+            isAutoSelected = isAutoSelected
+        )
+        shouldClose.value = result
+    }
 }
 
 sealed class CredentialSelectorUiState {
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt
new file mode 100644
index 0000000..e421644
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager
+
+import android.content.Intent
+import com.android.credentialmanager.model.EntryInfo
+
+/** Engine of the credential selecting flow. */
+interface FlowEngine {
+    /** Back from previous stage. */
+    fun back()
+    /** Cancels the selection flow. */
+    fun cancel()
+    /** Opens secondary screen. */
+    fun openSecondaryScreen()
+    /**
+     * Sends [entryInfo] as long as result after launching [EntryInfo.pendingIntent] with
+     * [EntryInfo.fillInIntent].
+     *
+     * @param entryInfo: selected entry.
+     * @param resultCode: result code received after launch.
+     * @param resultData: data received after launch
+     * @param isAutoSelected: whether the entry is auto selected or by user.
+     */
+    fun sendSelectionResult(
+        entryInfo: EntryInfo,
+        resultCode: Int? = null,
+        resultData: Intent? = null,
+        isAutoSelected: Boolean = false,
+    )
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
index f7158e8..332b816 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
@@ -18,8 +18,11 @@
 
 package com.android.credentialmanager.ui
 
+import android.util.Log
+import androidx.activity.compose.BackHandler
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.navigation.NavController
 import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
@@ -29,6 +32,8 @@
 import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry
 import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry
 import com.android.credentialmanager.CredentialSelectorViewModel
+import com.android.credentialmanager.FlowEngine
+import com.android.credentialmanager.TAG
 import com.android.credentialmanager.ui.screens.LoadingScreen
 import com.android.credentialmanager.ui.screens.single.passkey.SinglePasskeyScreen
 import com.android.credentialmanager.ui.screens.single.password.SinglePasswordScreen
@@ -44,6 +49,7 @@
 @Composable
 fun WearApp(
     viewModel: CredentialSelectorViewModel,
+    flowEngine: FlowEngine = viewModel,
     onCloseApp: () -> Unit,
 ) {
     val navController = rememberSwipeDismissableNavController()
@@ -52,7 +58,6 @@
         rememberSwipeDismissableNavHostState(swipeToDismissBoxState = swipeToDismissBoxState)
 
     val uiState by viewModel.uiState.collectAsStateWithLifecycle()
-
     WearNavScaffold(
         startDestination = Screen.Loading.route,
         navController = navController,
@@ -61,11 +66,11 @@
         composable(Screen.Loading.route) {
             LoadingScreen()
         }
-
         scrollable(Screen.SinglePasswordScreen.route) {
             SinglePasswordScreen(
-                credentialSelectorUiState = viewModel.uiState.value as SingleEntry,
+                entry = (remember { uiState } as SingleEntry).entry,
                 columnState = it.columnState,
+                flowEngine = flowEngine,
             )
         }
 
@@ -88,10 +93,13 @@
                 credentialSelectorUiState = viewModel.uiState.value as MultipleEntry,
                 screenIcon = null,
                 columnState = it.columnState,
-                )
+            )
         }
     }
-
+    BackHandler(true) {
+        viewModel.back()
+    }
+    Log.d(TAG, "uiState change, state: $uiState")
     when (val state = uiState) {
         CredentialSelectorUiState.Idle -> {
             if (navController.currentDestination?.route != Screen.Loading.route) {
@@ -142,7 +150,7 @@
             }
         }
 
-        is CredentialSelectorUiState.Get.MultipleEntry -> {
+        is MultipleEntry -> {
             navController.navigateToMultipleCredentialsFoldScreen()
         }
 
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
index 4c7f583..1f1a296 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
@@ -18,22 +18,19 @@
 
 package com.android.credentialmanager.ui.screens.single.password
 
+import android.util.Log
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.rememberNavController
-import com.android.credentialmanager.CredentialSelectorUiState
+import com.android.credentialmanager.FlowEngine
 import com.android.credentialmanager.R
+import com.android.credentialmanager.TAG
 import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract
+import com.android.credentialmanager.ktx.getIntentSenderRequest
 import com.android.credentialmanager.ui.components.PasswordRow
 import com.android.credentialmanager.ui.components.ContinueChip
 import com.android.credentialmanager.ui.components.DismissChip
@@ -41,71 +38,30 @@
 import com.android.credentialmanager.ui.components.SignInOptionsChip
 import com.android.credentialmanager.ui.screens.single.SingleAccountScreen
 import com.android.credentialmanager.model.get.CredentialEntryInfo
-import com.android.credentialmanager.ui.screens.single.UiState
 import com.google.android.horologist.annotations.ExperimentalHorologistApi
 import com.google.android.horologist.compose.layout.ScalingLazyColumnState
 
 /**
  * Screen that shows sign in with provider credential.
  *
- * @param credentialSelectorUiState The app bar view model.
+ * @param entry The password entry.
  * @param columnState ScalingLazyColumn configuration to be be applied to SingleAccountScreen
  * @param modifier styling for composable
- * @param viewModel ViewModel that updates ui state for this screen
- * @param navController handles navigation events from this screen
+ * @param flowEngine [FlowEngine] that updates ui state for this screen
  */
 @OptIn(ExperimentalHorologistApi::class)
 @Composable
 fun SinglePasswordScreen(
-    credentialSelectorUiState: CredentialSelectorUiState.Get.SingleEntry,
-    columnState: ScalingLazyColumnState,
-    modifier: Modifier = Modifier,
-    viewModel: SinglePasswordScreenViewModel = hiltViewModel(),
-    navController: NavHostController = rememberNavController(),
-) {
-    viewModel.initialize(credentialSelectorUiState.entry)
-
-    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
-
-    when (val state = uiState) {
-        UiState.CredentialScreen -> {
-            SinglePasswordScreen(
-                credentialSelectorUiState.entry,
-                columnState,
-                modifier,
-                viewModel
-            )
-        }
-
-        is UiState.CredentialSelected -> {
-            val launcher = rememberLauncherForActivityResult(
-                StartBalIntentSenderForResultContract()
-            ) {
-                viewModel.onPasswordInfoRetrieved(it.resultCode, null)
-            }
-
-            SideEffect {
-                state.intentSenderRequest?.let {
-                    launcher.launch(it)
-                }
-            }
-        }
-
-        UiState.Cancel -> {
-            // TODO(b/322797032) add valid navigation path here for going back
-            navController.popBackStack()
-        }
-    }
-}
-
-@OptIn(ExperimentalHorologistApi::class)
-@Composable
-private fun SinglePasswordScreen(
     entry: CredentialEntryInfo,
     columnState: ScalingLazyColumnState,
     modifier: Modifier = Modifier,
-    viewModel: SinglePasswordScreenViewModel,
+    flowEngine: FlowEngine,
 ) {
+    val launcher = rememberLauncherForActivityResult(
+        StartBalIntentSenderForResultContract()
+    ) {
+        flowEngine.sendSelectionResult(entry, it.resultCode, it.data)
+    }
     SingleAccountScreen(
         headerContent = {
             SignInHeader(
@@ -124,9 +80,13 @@
     ) {
         item {
             Column {
-                ContinueChip(viewModel::onContinueClick)
-                SignInOptionsChip(viewModel::onSignInOptionsClick)
-                DismissChip(viewModel::onDismissClick)
+                ContinueChip {
+                    entry.getIntentSenderRequest()?.let {
+                        launcher.launch(it)
+                    } ?: Log.w(TAG, "Cannot parse IntentSenderRequest")
+                }
+                SignInOptionsChip{ flowEngine.openSecondaryScreen() }
+                DismissChip { flowEngine.cancel() }
             }
         }
     }
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt
deleted file mode 100644
index 8debecb..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0N
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.screens.single.password
-
-import android.content.Intent
-import android.credentials.selection.UserSelectionDialogResult
-import android.credentials.selection.ProviderPendingIntentResponse
-import androidx.annotation.MainThread
-import androidx.lifecycle.ViewModel
-import com.android.credentialmanager.ktx.getIntentSenderRequest
-import com.android.credentialmanager.model.Request
-import com.android.credentialmanager.client.CredentialManagerClient
-import com.android.credentialmanager.model.get.CredentialEntryInfo
-import com.android.credentialmanager.ui.screens.single.UiState
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import javax.inject.Inject
-
-@HiltViewModel
-class SinglePasswordScreenViewModel @Inject constructor(
-    private val credentialManagerClient: CredentialManagerClient,
-) : ViewModel() {
-
-    private lateinit var requestGet: Request.Get
-    private lateinit var entryInfo: CredentialEntryInfo
-
-    private val _uiState =
-        MutableStateFlow<UiState>(UiState.CredentialScreen)
-    val uiState: StateFlow<UiState> = _uiState
-
-    @MainThread
-    fun initialize(entryInfo: CredentialEntryInfo) {
-        this.entryInfo = entryInfo
-    }
-
-    fun onDismissClick() {
-        _uiState.value = UiState.Cancel
-    }
-
-    fun onContinueClick() {
-        _uiState.value = UiState.CredentialSelected(
-            intentSenderRequest = entryInfo.getIntentSenderRequest()
-        )
-    }
-
-    fun onSignInOptionsClick() {
-    }
-
-    fun onPasswordInfoRetrieved(
-        resultCode: Int? = null,
-        resultData: Intent? = null,
-    ) {
-        val userSelectionDialogResult = UserSelectionDialogResult(
-            requestGet.token,
-            entryInfo.providerId,
-            entryInfo.entryKey,
-            entryInfo.entrySubkey,
-            if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
-        )
-        credentialManagerClient.sendResult(userSelectionDialogResult)
-    }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
index cf2f85e..634e067 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
@@ -20,6 +20,7 @@
 
 import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_STAGED_SESSION_ID;
 
+import android.Manifest;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
@@ -27,10 +28,10 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.pm.Flags;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.res.AssetFileDescriptor;
-import android.Manifest;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
@@ -200,7 +201,7 @@
         params.setPermissionState(Manifest.permission.USE_FULL_SCREEN_INTENT,
                 PackageInstaller.SessionParams.PERMISSION_STATE_DENIED);
 
-        if (pfd != null) {
+        if (pfd != null && Flags.readInstallInfo()) {
             try {
                 final PackageInstaller.InstallInfo result = installer.readInstallInfo(pfd,
                         debugPathName, 0);
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index 7240fb9..904e184 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -31,6 +31,7 @@
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.Flags;
 import android.content.pm.InstallSourceInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageInstaller;
@@ -397,7 +398,10 @@
             final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID,
                     -1 /* defaultValue */);
             final SessionInfo info = mInstaller.getSessionInfo(sessionId);
-            String resolvedPath = info != null ? info.getResolvedBaseApkPath() : null;
+            String resolvedPath = null;
+            if (info != null && Flags.getResolvedApkPath()) {
+                resolvedPath = info.getResolvedBaseApkPath();
+            }
             if (info == null || !info.isSealed() || resolvedPath == null) {
                 Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
                 finish();
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
index aeabbd5..22caabd 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
@@ -25,6 +25,7 @@
 import android.content.Context
 import android.content.Intent
 import android.content.pm.ApplicationInfo
+import android.content.pm.Flags
 import android.content.pm.PackageInfo
 import android.content.pm.PackageInstaller
 import android.content.pm.PackageInstaller.SessionInfo
@@ -362,7 +363,7 @@
         params.setPermissionState(
             Manifest.permission.USE_FULL_SCREEN_INTENT, SessionParams.PERMISSION_STATE_DENIED
         )
-        if (pfd != null) {
+        if (pfd != null && Flags.readInstallInfo()) {
             try {
                 val installInfo = packageInstaller.readInstallInfo(pfd, debugPathName, 0)
                 params.setAppPackageName(installInfo.packageName)
@@ -425,7 +426,8 @@
 
         if (PackageInstaller.ACTION_CONFIRM_INSTALL == intent.action) {
             val info = packageInstaller.getSessionInfo(sessionId)
-            val resolvedPath = info?.resolvedBaseApkPath
+            val resolvedPath =
+                    if (Flags.getResolvedApkPath()) info?.resolvedBaseApkPath else null
             if (info == null || !info.isSealed || resolvedPath == null) {
                 Log.w(LOG_TAG, "Session $sessionId in funky state; ignoring")
                 return InstallAborted(ABORT_REASON_INTERNAL_ERROR)
diff --git a/packages/SettingsLib/Spa/OWNERS b/packages/SettingsLib/Spa/OWNERS
index 464328e..67386d1 100644
--- a/packages/SettingsLib/Spa/OWNERS
+++ b/packages/SettingsLib/Spa/OWNERS
@@ -1,4 +1,4 @@
-set noparent
+include platform/frameworks/base:/packages/SettingsLib/OWNERS
 
 chaohuiw@google.com
 hanxu@google.com
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt
index 2ce3c66..bdc6a68 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
@@ -26,6 +27,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsTheme
@@ -37,7 +39,8 @@
     errorMessage: String? = null,
     singleLine: Boolean = true,
     enabled: Boolean = true,
-    onTextChange: (String) -> Unit,
+    shape: Shape = OutlinedTextFieldDefaults.shape,
+    onTextChange: (String) -> Unit
 ) {
     OutlinedTextField(
         modifier = Modifier
@@ -55,7 +58,8 @@
             if (errorMessage != null) {
                 Text(text = errorMessage)
             }
-        }
+        },
+        shape = shape
     )
 }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothBroadcastUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothBroadcastUtils.java
index 1ff2bef..5e3bd9a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothBroadcastUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothBroadcastUtils.java
@@ -43,5 +43,5 @@
     /**
      * Bluetooth scheme.
      */
-    public static final String SCHEME_BT_BROADCAST_METADATA = "BT:";
+    public static final String SCHEME_BT_BROADCAST_METADATA = "BLUETOOTH:UUID:184F;";
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
index 9bb11f8..da1fd55 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
@@ -31,38 +31,34 @@
 object BluetoothLeBroadcastMetadataExt {
     private const val TAG = "BtLeBroadcastMetadataExt"
 
-    // BluetoothLeBroadcastMetadata
-    private const val KEY_BT_QR_VER = "R"
-    private const val KEY_BT_ADDRESS_TYPE = "T"
-    private const val KEY_BT_DEVICE = "D"
-    private const val KEY_BT_ADVERTISING_SID = "AS"
-    private const val KEY_BT_BROADCAST_ID = "B"
+    // Data Elements for directing Broadcast Assistants
     private const val KEY_BT_BROADCAST_NAME = "BN"
-    private const val KEY_BT_PUBLIC_BROADCAST_DATA = "PM"
-    private const val KEY_BT_SYNC_INTERVAL = "SI"
-    private const val KEY_BT_BROADCAST_CODE = "C"
-    private const val KEY_BT_SUBGROUPS = "SG"
-    private const val KEY_BT_VENDOR_SPECIFIC = "V"
-    private const val KEY_BT_ANDROID_VERSION = "VN"
+    private const val KEY_BT_ADVERTISER_ADDRESS_TYPE = "AT"
+    private const val KEY_BT_ADVERTISER_ADDRESS = "AD"
+    private const val KEY_BT_BROADCAST_ID = "BI"
+    private const val KEY_BT_BROADCAST_CODE = "BC"
+    private const val KEY_BT_STREAM_METADATA = "MD"
+    private const val KEY_BT_STANDARD_QUALITY = "SQ"
+    private const val KEY_BT_HIGH_QUALITY = "HQ"
 
-    // Subgroup data
+    // Extended Bluetooth URI Data Elements
+    private const val KEY_BT_ADVERTISING_SID = "AS"
+    private const val KEY_BT_PA_INTERVAL = "PI"
+    private const val KEY_BT_NUM_SUBGROUPS = "NS"
+
+    // Subgroup data elements
     private const val KEY_BTSG_BIS_SYNC = "BS"
-    private const val KEY_BTSG_BIS_MASK = "BM"
-    private const val KEY_BTSG_AUDIO_CONTENT = "AC"
+    private const val KEY_BTSG_NUM_BISES = "NB"
+    private const val KEY_BTSG_METADATA = "SM"
 
-    // Vendor specific data
-    private const val KEY_BTVSD_COMPANY_ID = "VI"
-    private const val KEY_BTVSD_VENDOR_DATA = "VD"
+    // Vendor specific data, not being used
+    private const val KEY_BTVSD_VENDOR_DATA = "VS"
 
     private const val DELIMITER_KEY_VALUE = ":"
-    private const val DELIMITER_BT_LEVEL_1 = ";"
-    private const val DELIMITER_BT_LEVEL_2 = ","
+    private const val DELIMITER_ELEMENT = ";"
 
     private const val SUFFIX_QR_CODE = ";;"
 
-    private const val ANDROID_VER = "U"
-    private const val QR_CODE_VER = 0x010000
-
     // BT constants
     private const val BIS_SYNC_MAX_CHANNEL = 32
     private const val BIS_SYNC_NO_PREFERENCE = 0xFFFFFFFFu
@@ -71,33 +67,55 @@
     /**
      * Converts [BluetoothLeBroadcastMetadata] to QR code string.
      *
-     * QR code string will prefix with "BT:".
+     * QR code string will prefix with "BLUETOOTH:UUID:184F".
      */
     fun BluetoothLeBroadcastMetadata.toQrCodeString(): String {
         val entries = mutableListOf<Pair<String, String>>()
-        entries.add(Pair(KEY_BT_QR_VER, QR_CODE_VER.toString()))
-        entries.add(Pair(KEY_BT_ADDRESS_TYPE, this.sourceAddressType.toString()))
-        entries.add(Pair(KEY_BT_DEVICE, this.sourceDevice.address.replace(":", "-")))
-        entries.add(Pair(KEY_BT_ADVERTISING_SID, this.sourceAdvertisingSid.toString()))
-        entries.add(Pair(KEY_BT_BROADCAST_ID, this.broadcastId.toString()))
-        if (this.broadcastName != null) {
-            entries.add(Pair(KEY_BT_BROADCAST_NAME, Base64.encodeToString(
-                this.broadcastName?.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)))
-        }
-        if (this.publicBroadcastMetadata != null) {
-            entries.add(Pair(KEY_BT_PUBLIC_BROADCAST_DATA, Base64.encodeToString(
-                this.publicBroadcastMetadata?.rawMetadata, Base64.NO_WRAP)))
-        }
-        entries.add(Pair(KEY_BT_SYNC_INTERVAL, this.paSyncInterval.toString()))
+        // Generate data elements for directing Broadcast Assistants
+        require(this.broadcastName != null) { "Broadcast name is mandatory for QR code" }
+        entries.add(Pair(KEY_BT_BROADCAST_NAME, Base64.encodeToString(
+            this.broadcastName?.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)))
+        entries.add(Pair(KEY_BT_ADVERTISER_ADDRESS_TYPE, this.sourceAddressType.toString()))
+        entries.add(Pair(KEY_BT_ADVERTISER_ADDRESS, this.sourceDevice.address.replace(":", "")))
+        entries.add(Pair(KEY_BT_BROADCAST_ID, String.format("%X", this.broadcastId.toLong())))
         if (this.broadcastCode != null) {
             entries.add(Pair(KEY_BT_BROADCAST_CODE,
                 Base64.encodeToString(this.broadcastCode, Base64.NO_WRAP)))
         }
+        if (this.publicBroadcastMetadata != null &&
+                this.publicBroadcastMetadata?.rawMetadata?.size != 0) {
+            entries.add(Pair(KEY_BT_STREAM_METADATA, Base64.encodeToString(
+                this.publicBroadcastMetadata?.rawMetadata, Base64.NO_WRAP)))
+        }
+        if ((this.audioConfigQuality and
+                BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_STANDARD) != 0) {
+            entries.add(Pair(KEY_BT_STANDARD_QUALITY, "1"))
+        }
+        if ((this.audioConfigQuality and
+                BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_HIGH) != 0) {
+            entries.add(Pair(KEY_BT_HIGH_QUALITY, "1"))
+        }
+
+        // Generate extended Bluetooth URI data elements
+        entries.add(Pair(KEY_BT_ADVERTISING_SID,
+                String.format("%X", this.sourceAdvertisingSid.toLong())))
+        entries.add(Pair(KEY_BT_PA_INTERVAL, String.format("%X", this.paSyncInterval.toLong())))
+        entries.add(Pair(KEY_BT_NUM_SUBGROUPS, String.format("%X", this.subgroups.size.toLong())))
+
         this.subgroups.forEach {
-                subgroup -> entries.add(Pair(KEY_BT_SUBGROUPS, subgroup.toQrCodeString())) }
-        entries.add(Pair(KEY_BT_ANDROID_VERSION, ANDROID_VER))
+            val (bisSync, bisCount) = getBisSyncFromChannels(it.channels)
+            entries.add(Pair(KEY_BTSG_BIS_SYNC, String.format("%X", bisSync.toLong())))
+            if (bisCount > 0u) {
+                entries.add(Pair(KEY_BTSG_NUM_BISES, String.format("%X", bisCount.toLong())))
+            }
+            if (it.contentMetadata.rawMetadata.size != 0) {
+                entries.add(Pair(KEY_BTSG_METADATA,
+                    Base64.encodeToString(it.contentMetadata.rawMetadata, Base64.NO_WRAP)))
+            }
+        }
+
         val qrCodeString = SCHEME_BT_BROADCAST_METADATA +
-                entries.toQrCodeString(DELIMITER_BT_LEVEL_1) + SUFFIX_QR_CODE
+                entries.toQrCodeString(DELIMITER_ELEMENT) + SUFFIX_QR_CODE
         Log.d(TAG, "Generated QR string : $qrCodeString")
         return qrCodeString
     }
@@ -105,7 +123,7 @@
     /**
      * Converts QR code string to [BluetoothLeBroadcastMetadata].
      *
-     * QR code string should prefix with "BT:BluetoothLeBroadcastMetadata:".
+     * QR code string should prefix with "BLUETOOTH:UUID:184F".
      */
     fun convertToBroadcastMetadata(qrCodeString: String): BluetoothLeBroadcastMetadata? {
         if (!qrCodeString.startsWith(SCHEME_BT_BROADCAST_METADATA)) {
@@ -126,15 +144,6 @@
         }
     }
 
-    private fun BluetoothLeBroadcastSubgroup.toQrCodeString(): String {
-        val entries = mutableListOf<Pair<String, String>>()
-        entries.add(Pair(KEY_BTSG_BIS_SYNC, getBisSyncFromChannels(this.channels).toString()))
-        entries.add(Pair(KEY_BTSG_BIS_MASK, getBisMaskFromChannels(this.channels).toString()))
-        entries.add(Pair(KEY_BTSG_AUDIO_CONTENT,
-            Base64.encodeToString(this.contentMetadata.rawMetadata, Base64.NO_WRAP)))
-        return entries.toQrCodeString(DELIMITER_BT_LEVEL_2)
-    }
-
     private fun List<Pair<String, String>>.toQrCodeString(delimiter: String): String {
         val entryStrings = this.map{ it.first + DELIMITER_KEY_VALUE + it.second }
         return entryStrings.joinToString(separator = delimiter)
@@ -143,23 +152,29 @@
     @TargetApi(Build.VERSION_CODES.TIRAMISU)
     private fun parseQrCodeToMetadata(input: String): BluetoothLeBroadcastMetadata {
         // Split into a list of list
-        val level1Fields = input.split(DELIMITER_BT_LEVEL_1)
+        val elementFields = input.split(DELIMITER_ELEMENT)
             .map{it.split(DELIMITER_KEY_VALUE, limit = 2)}
-        var qrCodeVersion = -1
+
         var sourceAddrType = BluetoothDevice.ADDRESS_TYPE_UNKNOWN
         var sourceAddrString: String? = null
         var sourceAdvertiserSid = -1
         var broadcastId = -1
         var broadcastName: String? = null
-        var publicBroadcastMetadata: BluetoothLeAudioContentMetadata? = null
+        var streamMetadata: BluetoothLeAudioContentMetadata? = null
         var paSyncInterval = -1
         var broadcastCode: ByteArray? = null
-        // List of VendorID -> Data Pairs
-        var vendorDataList = mutableListOf<Pair<Int, ByteArray?>>()
-        var androidVersion: String? = null
+        var audioConfigQualityStandard = -1
+        var audioConfigQualityHigh = -1
+        var numSubgroups = -1
+
+        // List of subgroup data
+        var subgroupBisSyncList = mutableListOf<UInt>()
+        var subgroupNumOfBisesList = mutableListOf<UInt>()
+        var subgroupMetadataList = mutableListOf<ByteArray?>()
+
         val builder = BluetoothLeBroadcastMetadata.Builder()
 
-        for (field: List<String> in level1Fields) {
+        for (field: List<String> in elementFields) {
             if (field.isEmpty()) {
                 continue
             }
@@ -167,190 +182,200 @@
             // Ignore 3rd value and after
             val value = if (field.size > 1) field[1] else ""
             when (key) {
-                KEY_BT_QR_VER -> {
-                    require(qrCodeVersion == -1) { "Duplicate qrCodeVersion: $input" }
-                    qrCodeVersion = value.toInt()
+                // Parse data elements for directing Broadcast Assistants
+                KEY_BT_BROADCAST_NAME -> {
+                    require(broadcastName == null) { "Duplicate broadcastName: $input" }
+                    broadcastName = String(Base64.decode(value, Base64.NO_WRAP))
                 }
-                KEY_BT_ADDRESS_TYPE -> {
+                KEY_BT_ADVERTISER_ADDRESS_TYPE -> {
                     require(sourceAddrType == BluetoothDevice.ADDRESS_TYPE_UNKNOWN) {
                         "Duplicate sourceAddrType: $input"
                     }
                     sourceAddrType = value.toInt()
                 }
-                KEY_BT_DEVICE -> {
+                KEY_BT_ADVERTISER_ADDRESS -> {
                     require(sourceAddrString == null) { "Duplicate sourceAddr: $input" }
-                    sourceAddrString = value.replace("-", ":")
-                }
-                KEY_BT_ADVERTISING_SID -> {
-                    require(sourceAdvertiserSid == -1) { "Duplicate sourceAdvertiserSid: $input" }
-                    sourceAdvertiserSid = value.toInt()
+                    sourceAddrString = value.chunked(2).joinToString(":")
                 }
                 KEY_BT_BROADCAST_ID -> {
                     require(broadcastId == -1) { "Duplicate broadcastId: $input" }
-                    broadcastId = value.toInt()
-                }
-                KEY_BT_BROADCAST_NAME -> {
-                    require(broadcastName == null) { "Duplicate broadcastName: $input" }
-                    broadcastName = String(Base64.decode(value, Base64.NO_WRAP))
-                }
-                KEY_BT_PUBLIC_BROADCAST_DATA -> {
-                    require(publicBroadcastMetadata == null) {
-                        "Duplicate publicBroadcastMetadata $input"
-                    }
-                    publicBroadcastMetadata = BluetoothLeAudioContentMetadata
-                        .fromRawBytes(Base64.decode(value, Base64.NO_WRAP))
-                }
-                KEY_BT_SYNC_INTERVAL -> {
-                    require(paSyncInterval == -1) { "Duplicate paSyncInterval: $input" }
-                    paSyncInterval = value.toInt()
+                    broadcastId = value.toInt(16)
                 }
                 KEY_BT_BROADCAST_CODE -> {
                     require(broadcastCode == null) { "Duplicate broadcastCode: $input" }
-                    broadcastCode = Base64.decode(value, Base64.NO_WRAP)
+
+                    broadcastCode = Base64.decode(value.dropLastWhile { it.equals(0.toByte()) }
+                            .toByteArray(), Base64.NO_WRAP)
                 }
-                KEY_BT_ANDROID_VERSION -> {
-                    require(androidVersion == null) { "Duplicate androidVersion: $input" }
-                    androidVersion = value
-                    Log.i(TAG, "QR code Android version: $androidVersion")
+                KEY_BT_STREAM_METADATA -> {
+                    require(streamMetadata == null) {
+                        "Duplicate streamMetadata $input"
+                    }
+                    streamMetadata = BluetoothLeAudioContentMetadata
+                        .fromRawBytes(Base64.decode(value, Base64.NO_WRAP))
                 }
-                // Repeatable
-                KEY_BT_SUBGROUPS -> {
-                    builder.addSubgroup(parseSubgroupData(value))
+                KEY_BT_STANDARD_QUALITY -> {
+                    require(audioConfigQualityStandard == -1) {
+                        "Duplicate audioConfigQualityStandard: $input"
+                    }
+                    audioConfigQualityStandard = value.toInt()
                 }
-                // Repeatable
-                KEY_BT_VENDOR_SPECIFIC -> {
-                    vendorDataList.add(parseVendorData(value))
+                KEY_BT_HIGH_QUALITY -> {
+                    require(audioConfigQualityHigh == -1) {
+                        "Duplicate audioConfigQualityHigh: $input"
+                    }
+                    audioConfigQualityHigh = value.toInt()
+                }
+
+                // Parse extended Bluetooth URI data elements
+                KEY_BT_ADVERTISING_SID -> {
+                    require(sourceAdvertiserSid == -1) { "Duplicate sourceAdvertiserSid: $input" }
+                    sourceAdvertiserSid = value.toInt(16)
+                }
+                KEY_BT_PA_INTERVAL -> {
+                    require(paSyncInterval == -1) { "Duplicate paSyncInterval: $input" }
+                    paSyncInterval = value.toInt(16)
+                }
+                KEY_BT_NUM_SUBGROUPS -> {
+                    require(numSubgroups == -1) { "Duplicate numSubgroups: $input" }
+                    numSubgroups = value.toInt(16)
+                }
+
+                // Repeatable subgroup elements
+                KEY_BTSG_BIS_SYNC -> {
+                    subgroupBisSyncList.add(value.toUInt(16))
+                }
+                KEY_BTSG_NUM_BISES -> {
+                    subgroupNumOfBisesList.add(value.toUInt(16))
+                }
+                KEY_BTSG_METADATA -> {
+                    subgroupMetadataList.add(Base64.decode(value, Base64.NO_WRAP))
                 }
             }
         }
-        Log.d(TAG, "parseQrCodeToMetadata: sourceAddrType=$sourceAddrType, " +
+        Log.d(TAG, "parseQrCodeToMetadata: main data elements sourceAddrType=$sourceAddrType, " +
                 "sourceAddr=$sourceAddrString, sourceAdvertiserSid=$sourceAdvertiserSid, " +
                 "broadcastId=$broadcastId, broadcastName=$broadcastName, " +
-                "publicBroadcastMetadata=${publicBroadcastMetadata != null}, " +
+                "streamMetadata=${streamMetadata != null}, " +
                 "paSyncInterval=$paSyncInterval, " +
-                "broadcastCode=${broadcastCode?.toString(Charsets.UTF_8)}")
-        Log.d(TAG, "Not used in current code, but part of the specification: " +
-                "qrCodeVersion=$qrCodeVersion, androidVersion=$androidVersion, " +
-                "vendorDataListSize=${vendorDataList.size}")
+                "broadcastCode=${broadcastCode?.toString(Charsets.UTF_8)}, " +
+                "audioConfigQualityStandard=$audioConfigQualityStandard, " +
+                "audioConfigQualityHigh=$audioConfigQualityHigh")
+
         val adapter = BluetoothAdapter.getDefaultAdapter()
+        // Check parsed elements data
+        require(broadcastName != null) {
+            "broadcastName($broadcastName) must present in QR code string"
+        }
+        var addr = sourceAddrString
+        var addrType = sourceAddrType
+        if (sourceAddrString != null) {
+            require(sourceAddrType != BluetoothDevice.ADDRESS_TYPE_UNKNOWN) {
+                "sourceAddrType($sourceAddrType) must present if address present"
+            }
+        } else {
+            // Use placeholder device if not present
+            addr = "FF:FF:FF:FF:FF:FF"
+            addrType = BluetoothDevice.ADDRESS_TYPE_RANDOM
+        }
+        val device = adapter.getRemoteLeDevice(requireNotNull(addr), addrType)
+
         // add source device and set broadcast code
-        val device = adapter.getRemoteLeDevice(requireNotNull(sourceAddrString), sourceAddrType)
+        var audioConfigQuality = BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_NONE or
+                (if (audioConfigQualityStandard != -1) audioConfigQualityStandard else 0) or
+                (if (audioConfigQualityHigh != -1) audioConfigQualityHigh else 0)
+
+        // process subgroup data
+        // metadata should include at least 1 subgroup for metadata, add a placeholder group if not present
+        numSubgroups = if (numSubgroups > 0) numSubgroups else 1
+        for (i in 0 until numSubgroups) {
+            val bisSync = subgroupBisSyncList.getOrNull(i)
+            val bisNum = subgroupNumOfBisesList.getOrNull(i)
+            val metadata = subgroupMetadataList.getOrNull(i)
+
+            val channels = convertToChannels(bisSync, bisNum)
+            val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder()
+                    .setAudioLocation(0).build()
+            val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
+                setCodecId(SUBGROUP_LC3_CODEC_ID)
+                setCodecSpecificConfig(audioCodecConfigMetadata)
+                setContentMetadata(
+                        BluetoothLeAudioContentMetadata.fromRawBytes(metadata ?: ByteArray(0)))
+                channels.forEach(::addChannel)
+            }.build()
+
+            Log.d(TAG, "parseQrCodeToMetadata: subgroup $i elements bisSync=$bisSync, " +
+                    "bisNum=$bisNum, metadata=${metadata != null}")
+
+            builder.addSubgroup(subgroup)
+        }
+
         builder.apply {
             setSourceDevice(device, sourceAddrType)
             setSourceAdvertisingSid(sourceAdvertiserSid)
             setBroadcastId(broadcastId)
             setBroadcastName(broadcastName)
-            setPublicBroadcast(publicBroadcastMetadata != null)
-            setPublicBroadcastMetadata(publicBroadcastMetadata)
+            // QR code should set PBP(public broadcast profile) for auracast
+            setPublicBroadcast(true)
+            setPublicBroadcastMetadata(streamMetadata)
             setPaSyncInterval(paSyncInterval)
             setEncrypted(broadcastCode != null)
             setBroadcastCode(broadcastCode)
             // Presentation delay is unknown and not useful when adding source
             // Broadcast sink needs to sync to the Broadcast source to get presentation delay
             setPresentationDelayMicros(0)
+            setAudioConfigQuality(audioConfigQuality)
         }
         return builder.build()
     }
 
-    private fun parseSubgroupData(input: String): BluetoothLeBroadcastSubgroup {
-        Log.d(TAG, "parseSubgroupData: $input")
-        val fields = input.split(DELIMITER_BT_LEVEL_2)
-        var bisSync: UInt? = null
-        var bisMask: UInt? = null
-        var metadata: ByteArray? = null
-
-        fields.forEach { field ->
-            val(key, value) = field.split(DELIMITER_KEY_VALUE)
-            when (key) {
-                KEY_BTSG_BIS_SYNC -> {
-                    require(bisSync == null) { "Duplicate bisSync: $input" }
-                    bisSync = value.toUInt()
-                }
-                KEY_BTSG_BIS_MASK -> {
-                    require(bisMask == null) { "Duplicate bisMask: $input" }
-                    bisMask = value.toUInt()
-                }
-                KEY_BTSG_AUDIO_CONTENT -> {
-                    require(metadata == null) { "Duplicate metadata: $input" }
-                    metadata = Base64.decode(value, Base64.NO_WRAP)
-                }
-            }
-        }
-        val channels = convertToChannels(requireNotNull(bisSync), requireNotNull(bisMask))
-        val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder()
-                .setAudioLocation(0).build()
-        return BluetoothLeBroadcastSubgroup.Builder().apply {
-            setCodecId(SUBGROUP_LC3_CODEC_ID)
-            setCodecSpecificConfig(audioCodecConfigMetadata)
-            setContentMetadata(
-                    BluetoothLeAudioContentMetadata.fromRawBytes(metadata ?: ByteArray(0)))
-            channels.forEach(::addChannel)
-        }.build()
-    }
-
-    private fun parseVendorData(input: String): Pair<Int, ByteArray?> {
-        var companyId = -1
-        var data: ByteArray? = null
-        val fields = input.split(DELIMITER_BT_LEVEL_2)
-        fields.forEach { field ->
-            val(key, value) = field.split(DELIMITER_KEY_VALUE)
-            when (key) {
-                KEY_BTVSD_COMPANY_ID -> {
-                    require(companyId == -1) { "Duplicate companyId: $input" }
-                    companyId = value.toInt()
-                }
-                KEY_BTVSD_VENDOR_DATA -> {
-                    require(data == null) { "Duplicate data: $input" }
-                    data = Base64.decode(value, Base64.NO_WRAP)
-                }
-            }
-        }
-        return Pair(companyId, data)
-    }
-
-    private fun getBisSyncFromChannels(channels: List<BluetoothLeBroadcastChannel>): UInt {
+    private fun getBisSyncFromChannels(
+        channels: List<BluetoothLeBroadcastChannel>
+    ): Pair<UInt, UInt> {
         var bisSync = 0u
-        // channel index starts from 1
-        channels.forEach { channel ->
-            if (channel.isSelected && channel.channelIndex > 0) {
-                bisSync = bisSync or (1u shl (channel.channelIndex - 1))
-            }
-        }
-        // No channel is selected means no preference on Android platform
-        return if (bisSync == 0u) BIS_SYNC_NO_PREFERENCE else bisSync
-    }
-
-    private fun getBisMaskFromChannels(channels: List<BluetoothLeBroadcastChannel>): UInt {
-        var bisMask = 0u
+        var bisCount = 0u
         // channel index starts from 1
         channels.forEach { channel ->
             if (channel.channelIndex > 0) {
-                bisMask = bisMask or (1u shl (channel.channelIndex - 1))
+                bisCount++
+                if (channel.isSelected) {
+                    bisSync = bisSync or (1u shl (channel.channelIndex - 1))
+                }
             }
         }
-        return bisMask
+        // No channel is selected means no preference on Android platform
+        return if (bisSync == 0u) Pair(BIS_SYNC_NO_PREFERENCE, bisCount)
+                else Pair(bisSync, bisCount)
     }
 
-    private fun convertToChannels(bisSync: UInt, bisMask: UInt):
-            List<BluetoothLeBroadcastChannel> {
-        Log.d(TAG, "convertToChannels: bisSync=$bisSync, bisMask=$bisMask")
-        var selectionMask = bisSync
-        if (bisSync != BIS_SYNC_NO_PREFERENCE) {
-            require(bisMask == (bisMask or bisSync)) {
-                "bisSync($bisSync) must select a subset of bisMask($bisMask) if it has preferences"
-            }
-        } else {
-            // No channel preference means no channel is selected
-            selectionMask = 0u
-        }
+    private fun convertToChannels(
+        bisSync: UInt?,
+        bisNum: UInt?
+    ): List<BluetoothLeBroadcastChannel> {
+        Log.d(TAG, "convertToChannels: bisSync=$bisSync, bisNum=$bisNum")
+        // if no BIS_SYNC or BIS_NUM available or BIS_SYNC is no preference
+        // return empty channel map with one placeholder channel
+        var selectedChannels = if (bisSync != null && bisNum != null) bisSync else 0u
         val channels = mutableListOf<BluetoothLeBroadcastChannel>()
         val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder()
                 .setAudioLocation(0).build()
+
+        if (bisSync == BIS_SYNC_NO_PREFERENCE || selectedChannels == 0u) {
+            // No channel preference means no channel is selected
+            // Generate one placeholder channel for metadata
+            val channel = BluetoothLeBroadcastChannel.Builder().apply {
+                setSelected(false)
+                setChannelIndex(1)
+                setCodecMetadata(audioCodecConfigMetadata)
+            }
+            return listOf(channel.build())
+        }
+
         for (i in 0 until BIS_SYNC_MAX_CHANNEL) {
             val channelMask = 1u shl i
-            if ((bisMask and channelMask) != 0u) {
+            if ((selectedChannels and channelMask) != 0u) {
                 val channel = BluetoothLeBroadcastChannel.Builder().apply {
-                    setSelected((selectionMask and channelMask) != 0u)
+                    setSelected(true)
                     setChannelIndex(i + 1)
                     setCodecMetadata(audioCodecConfigMetadata)
                 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 581c7de..a376c1f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -92,13 +92,13 @@
         }
     }
 
-    protected String mPackageName;
+    @NonNull protected final String mPackageName;
     private MediaDevice mCurrentConnectedDevice;
     private final LocalBluetoothManager mBluetoothManager;
     private final Map<String, RouteListingPreference.Item> mPreferenceItemMap =
             new ConcurrentHashMap<>();
 
-    public InfoMediaManager(
+    /* package */ InfoMediaManager(
             Context context,
             @NonNull String packageName,
             Notification notification,
@@ -112,7 +112,7 @@
     /** Creates an instance of InfoMediaManager. */
     public static InfoMediaManager createInstance(
             Context context,
-            String packageName,
+            @Nullable String packageName,
             Notification notification,
             LocalBluetoothManager localBluetoothManager) {
 
@@ -148,8 +148,7 @@
     }
 
     private void updateRouteListingPreference() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
-                && !TextUtils.isEmpty(mPackageName)) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
             RouteListingPreference routeListingPreference =
                     getRouteListingPreference();
             Api34Impl.onRouteListingPreferenceUpdated(routeListingPreference,
@@ -218,11 +217,7 @@
     protected final void rebuildDeviceList() {
         mMediaDevices.clear();
         mCurrentConnectedDevice = null;
-        if (TextUtils.isEmpty(mPackageName)) {
-            buildAllRoutes();
-        } else {
-            buildAvailableRoutes();
-        }
+        buildAvailableRoutes();
     }
 
     protected final void notifyCurrentConnectedDeviceChanged() {
@@ -250,12 +245,8 @@
             return;
         }
 
-        if (TextUtils.isEmpty(mPackageName)) {
-            connectDeviceWithoutPackageName(device);
-        } else {
-            device.setConnectedRecord();
-            transferToRoute(device.mRouteInfo);
-        }
+        device.setConnectedRecord();
+        transferToRoute(device.mRouteInfo);
     }
 
     /**
@@ -265,11 +256,6 @@
      * @return If add device successful return {@code true}, otherwise return {@code false}
      */
     boolean addDeviceToPlayMedia(MediaDevice device) {
-        if (TextUtils.isEmpty(mPackageName)) {
-            Log.w(TAG, "addDeviceToPlayMedia() package name is null or empty!");
-            return false;
-        }
-
         final RoutingSessionInfo info = getRoutingSessionInfo();
         if (info == null || !info.getSelectableRoutes().contains(device.mRouteInfo.getId())) {
             Log.w(TAG, "addDeviceToPlayMedia() Ignoring selecting a non-selectable device : "
@@ -306,7 +292,6 @@
 
     boolean preferRouteListingOrdering() {
         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
-                && !TextUtils.isEmpty(mPackageName)
                 && Api34Impl.preferRouteListingOrdering(getRouteListingPreference());
     }
 
@@ -326,11 +311,6 @@
      * @return If device stop successful return {@code true}, otherwise return {@code false}
      */
     boolean removeDeviceFromPlayMedia(MediaDevice device) {
-        if (TextUtils.isEmpty(mPackageName)) {
-            Log.w(TAG, "removeDeviceFromMedia() package name is null or empty!");
-            return false;
-        }
-
         final RoutingSessionInfo info = getRoutingSessionInfo();
         if (info == null || !info.getSelectedRoutes().contains(device.mRouteInfo.getId())) {
             Log.w(TAG, "removeDeviceFromMedia() Ignoring deselecting a non-deselectable device : "
@@ -346,11 +326,6 @@
      * Release session to stop playing media on MediaDevice.
      */
     boolean releaseSession() {
-        if (TextUtils.isEmpty(mPackageName)) {
-            Log.w(TAG, "releaseSession() package name is null or empty!");
-            return false;
-        }
-
         final RoutingSessionInfo sessionInfo = getRoutingSessionInfo();
         if (sessionInfo == null) {
             Log.w(TAG, "releaseSession() Ignoring release session : " + mPackageName);
@@ -367,11 +342,6 @@
      */
     @NonNull
     List<MediaDevice> getSelectableMediaDevices() {
-        if (TextUtils.isEmpty(mPackageName)) {
-            Log.w(TAG, "getSelectableMediaDevices() package name is null or empty!");
-            return Collections.emptyList();
-        }
-
         final RoutingSessionInfo info = getRoutingSessionInfo();
         if (info == null) {
             Log.w(TAG, "getSelectableMediaDevices() cannot find selectable MediaDevice from : "
@@ -394,11 +364,6 @@
      */
     @NonNull
     List<MediaDevice> getDeselectableMediaDevices() {
-        if (TextUtils.isEmpty(mPackageName)) {
-            Log.d(TAG, "getDeselectableMediaDevices() package name is null or empty!");
-            return Collections.emptyList();
-        }
-
         final RoutingSessionInfo info = getRoutingSessionInfo();
         if (info == null) {
             Log.d(TAG, "getDeselectableMediaDevices() cannot find deselectable MediaDevice from : "
@@ -462,11 +427,6 @@
      * @param volume the value of volume
      */
     void adjustSessionVolume(int volume) {
-        if (TextUtils.isEmpty(mPackageName)) {
-            Log.w(TAG, "adjustSessionVolume() package name is null or empty!");
-            return;
-        }
-
         final RoutingSessionInfo info = getRoutingSessionInfo();
         if (info == null) {
             Log.w(TAG, "adjustSessionVolume() can't found corresponding RoutingSession with : "
@@ -484,11 +444,6 @@
      * @return  maximum volume of the session, and return -1 if not found.
      */
     public int getSessionVolumeMax() {
-        if (TextUtils.isEmpty(mPackageName)) {
-            Log.w(TAG, "getSessionVolumeMax() package name is null or empty!");
-            return -1;
-        }
-
         final RoutingSessionInfo info = getRoutingSessionInfo();
         if (info == null) {
             Log.w(TAG, "getSessionVolumeMax() can't find corresponding RoutingSession with : "
@@ -505,11 +460,6 @@
      * @return current volume of the session, and return -1 if not found.
      */
     public int getSessionVolume() {
-        if (TextUtils.isEmpty(mPackageName)) {
-            Log.w(TAG, "getSessionVolume() package name is null or empty!");
-            return -1;
-        }
-
         final RoutingSessionInfo info = getRoutingSessionInfo();
         if (info == null) {
             Log.w(TAG, "getSessionVolume() can't find corresponding RoutingSession with : "
@@ -521,11 +471,6 @@
     }
 
     CharSequence getSessionName() {
-        if (TextUtils.isEmpty(mPackageName)) {
-            Log.w(TAG, "Unable to get session name. The package name is null or empty!");
-            return null;
-        }
-
         final RoutingSessionInfo info = getRoutingSessionInfo();
         if (info == null) {
             Log.w(TAG, "Unable to get session name for package: " + mPackageName);
@@ -548,20 +493,6 @@
 
     // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
     @SuppressWarnings("NewApi")
-    private void buildAllRoutes() {
-        for (MediaRoute2Info route : getAllRoutes()) {
-            if (DEBUG) {
-                Log.d(TAG, "buildAllRoutes() route : " + route.getName() + ", volume : "
-                        + route.getVolume() + ", type : " + route.getType());
-            }
-            if (route.isSystemRoute()) {
-                addMediaDevice(route);
-            }
-        }
-    }
-
-    // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
-    @SuppressWarnings("NewApi")
     private synchronized void buildAvailableRoutes() {
         for (MediaRoute2Info route : getAvailableRoutes()) {
             if (DEBUG) {
@@ -594,8 +525,7 @@
                 infos.add(transferableRoute);
             }
         }
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
-                && !TextUtils.isEmpty(mPackageName)) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
             RouteListingPreference routeListingPreference = getRouteListingPreference();
             if (routeListingPreference != null) {
                 final List<RouteListingPreference.Item> preferenceRouteListing =
@@ -679,7 +609,7 @@
                 break;
         }
 
-        if (mediaDevice != null && !TextUtils.isEmpty(mPackageName)
+        if (mediaDevice != null
                 && getRoutingSessionInfo().getSelectedRoutes().contains(route.getId())) {
             mediaDevice.setState(STATE_SELECTED);
             if (mCurrentConnectedDevice == null) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
index 97bbf12..cf8906d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
@@ -51,9 +51,9 @@
 
     private final Executor mExecutor = Executors.newSingleThreadExecutor();
 
-    public ManagerInfoMediaManager(
+    /* package */ ManagerInfoMediaManager(
             Context context,
-            String packageName,
+            @NonNull String packageName,
             Notification notification,
             LocalBluetoothManager localBluetoothManager) {
         super(context, packageName, notification, localBluetoothManager);
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
index 9d578bc..5b1c8ef 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
@@ -41,7 +41,7 @@
 
     NoOpInfoMediaManager(
             Context context,
-            String packageName,
+            @NonNull String packageName,
             Notification notification,
             LocalBluetoothManager localBluetoothManager) {
         super(context, packageName, notification, localBluetoothManager);
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
index aef09ac..c8c8b672 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
@@ -65,9 +65,9 @@
             };
 
     // TODO (b/321969740): Plumb target UserHandle between UMO and RouterInfoMediaManager.
-    public RouterInfoMediaManager(
+    /* package */ RouterInfoMediaManager(
             Context context,
-            String packageName,
+            @NonNull String packageName,
             Notification notification,
             LocalBluetoothManager localBluetoothManager)
             throws PackageNotAvailableException {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
index f0330c4..290e63ca 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
@@ -212,28 +212,6 @@
     }
 
     @Test
-    public void onRouteAdded_buildAllRoutes_shouldAddMediaDevice() {
-        final MediaRoute2Info info = mock(MediaRoute2Info.class);
-        when(info.getId()).thenReturn(TEST_ID);
-        when(info.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME);
-        when(info.isSystemRoute()).thenReturn(true);
-
-        final List<MediaRoute2Info> routes = new ArrayList<>();
-        routes.add(info);
-        mShadowRouter2Manager.setAllRoutes(routes);
-
-        final MediaDevice mediaDevice = mInfoMediaManager.findMediaDevice(TEST_ID);
-        assertThat(mediaDevice).isNull();
-
-        mInfoMediaManager.mPackageName = "";
-        mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated();
-
-        final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0);
-        assertThat(infoDevice.getId()).isEqualTo(TEST_ID);
-        assertThat(mInfoMediaManager.mMediaDevices).hasSize(routes.size());
-    }
-
-    @Test
     public void onPreferredFeaturesChanged_samePackageName_shouldAddMediaDevice() {
         final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>();
         final RoutingSessionInfo sessionInfo = mock(RoutingSessionInfo.class);
@@ -436,29 +414,6 @@
     }
 
     @Test
-    public void onRoutesChanged_buildAllRoutes_shouldAddMediaDevice() {
-        final MediaRoute2Info info = mock(MediaRoute2Info.class);
-        when(info.getId()).thenReturn(TEST_ID);
-        when(info.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME);
-        when(info.isSystemRoute()).thenReturn(true);
-        when(info.getDeduplicationIds()).thenReturn(Set.of());
-
-        final List<MediaRoute2Info> routes = new ArrayList<>();
-        routes.add(info);
-        mShadowRouter2Manager.setAllRoutes(routes);
-
-        final MediaDevice mediaDevice = mInfoMediaManager.findMediaDevice(TEST_ID);
-        assertThat(mediaDevice).isNull();
-
-        mInfoMediaManager.mPackageName = "";
-        mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated();
-
-        final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0);
-        assertThat(infoDevice.getId()).isEqualTo(TEST_ID);
-        assertThat(mInfoMediaManager.mMediaDevices).hasSize(routes.size());
-    }
-
-    @Test
     public void hasPreferenceRouteListing_oldSdkVersion_returnsFalse() {
         assertThat(mInfoMediaManager.preferRouteListingOrdering()).isFalse();
     }
@@ -587,36 +542,6 @@
     }
 
     @Test
-    public void onRoutesRemoved_buildAllRoutes_shouldAddMediaDevice() {
-        final MediaRoute2Info info = mock(MediaRoute2Info.class);
-        when(info.getId()).thenReturn(TEST_ID);
-        when(info.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME);
-        when(info.isSystemRoute()).thenReturn(true);
-
-        final List<MediaRoute2Info> routes = new ArrayList<>();
-        routes.add(info);
-        when(mRouterManager.getAllRoutes()).thenReturn(routes);
-
-        final MediaDevice mediaDevice = mInfoMediaManager.findMediaDevice(TEST_ID);
-        assertThat(mediaDevice).isNull();
-
-        mInfoMediaManager.mPackageName = "";
-        mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated();
-
-        final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0);
-        assertThat(infoDevice.getId()).isEqualTo(TEST_ID);
-        assertThat(mInfoMediaManager.mMediaDevices).hasSize(routes.size());
-    }
-
-    @Test
-    public void addDeviceToPlayMedia_packageNameIsNull_returnFalse() {
-        mInfoMediaManager.mPackageName = null;
-        final MediaDevice device = mock(MediaDevice.class);
-
-        assertThat(mInfoMediaManager.addDeviceToPlayMedia(device)).isFalse();
-    }
-
-    @Test
     public void addDeviceToPlayMedia_containSelectableRoutes_returnTrue() {
         final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>();
         final RoutingSessionInfo info = mock(RoutingSessionInfo.class);
@@ -660,14 +585,6 @@
     }
 
     @Test
-    public void removeDeviceFromMedia_packageNameIsNull_returnFalse() {
-        mInfoMediaManager.mPackageName = null;
-        final MediaDevice device = mock(MediaDevice.class);
-
-        assertThat(mInfoMediaManager.removeDeviceFromPlayMedia(device)).isFalse();
-    }
-
-    @Test
     public void removeDeviceFromMedia_containSelectedRoutes_returnTrue() {
         final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>();
         final RoutingSessionInfo info = mock(RoutingSessionInfo.class);
@@ -711,13 +628,6 @@
     }
 
     @Test
-    public void getSelectableMediaDevice_packageNameIsNull_returnFalse() {
-        mInfoMediaManager.mPackageName = null;
-
-        assertThat(mInfoMediaManager.getSelectableMediaDevices()).isEmpty();
-    }
-
-    @Test
     public void getSelectableMediaDevice_notContainPackageName_returnEmpty() {
         final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>();
         final RoutingSessionInfo info = mock(RoutingSessionInfo.class);
@@ -730,13 +640,6 @@
     }
 
     @Test
-    public void getDeselectableMediaDevice_packageNameIsNull_returnFalse() {
-        mInfoMediaManager.mPackageName = null;
-
-        assertThat(mInfoMediaManager.getDeselectableMediaDevices()).isEmpty();
-    }
-
-    @Test
     public void getDeselectableMediaDevice_checkList() {
         final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>();
         final RoutingSessionInfo info = mock(RoutingSessionInfo.class);
@@ -761,20 +664,6 @@
     }
 
     @Test
-    public void adjustSessionVolume_packageNameIsNull_noCrash() {
-        mInfoMediaManager.mPackageName = null;
-
-        mInfoMediaManager.adjustSessionVolume(10);
-    }
-
-    @Test
-    public void getSessionVolumeMax_packageNameIsNull_returnNotFound() {
-        mInfoMediaManager.mPackageName = null;
-
-        assertThat(mInfoMediaManager.getSessionVolumeMax()).isEqualTo(-1);
-    }
-
-    @Test
     public void getSessionVolumeMax_containPackageName_returnMaxVolume() {
         final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>();
         final RoutingSessionInfo info = mock(RoutingSessionInfo.class);
@@ -800,13 +689,6 @@
     }
 
     @Test
-    public void getSessionVolume_packageNameIsNull_returnNotFound() {
-        mInfoMediaManager.mPackageName = null;
-
-        assertThat(mInfoMediaManager.getSessionVolume()).isEqualTo(-1);
-    }
-
-    @Test
     public void getSessionVolume_containPackageName_returnMaxVolume() {
         final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>();
         final RoutingSessionInfo info = mock(RoutingSessionInfo.class);
@@ -841,13 +723,6 @@
     }
 
     @Test
-    public void releaseSession_packageNameIsNull_returnFalse() {
-        mInfoMediaManager.mPackageName = null;
-
-        assertThat(mInfoMediaManager.releaseSession()).isFalse();
-    }
-
-    @Test
     public void releaseSession_removeSuccessfully_returnTrue() {
         final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>();
         final RoutingSessionInfo info = mock(RoutingSessionInfo.class);
@@ -860,13 +735,6 @@
     }
 
     @Test
-    public void getSessionName_packageNameIsNull_returnNull() {
-        mInfoMediaManager.mPackageName = null;
-
-        assertThat(mInfoMediaManager.getSessionName()).isNull();
-    }
-
-    @Test
     public void getSessionName_routeSessionInfoIsNull_returnNull() {
         final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>();
         final RoutingSessionInfo info = null;
@@ -942,32 +810,6 @@
     }
 
     @Test
-    public void onTransferred_buildAllRoutes_shouldAddMediaDevice() {
-        final MediaRoute2Info info = mock(MediaRoute2Info.class);
-        final RoutingSessionInfo sessionInfo = mock(RoutingSessionInfo.class);
-        mInfoMediaManager.registerCallback(mCallback);
-
-        when(info.getId()).thenReturn(TEST_ID);
-        when(info.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME);
-        when(info.isSystemRoute()).thenReturn(true);
-
-        final List<MediaRoute2Info> routes = new ArrayList<>();
-        routes.add(info);
-        mShadowRouter2Manager.setAllRoutes(routes);
-
-        final MediaDevice mediaDevice = mInfoMediaManager.findMediaDevice(TEST_ID);
-        assertThat(mediaDevice).isNull();
-
-        mInfoMediaManager.mPackageName = "";
-        mInfoMediaManager.mMediaRouterCallback.onTransferred(sessionInfo, sessionInfo);
-
-        final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0);
-        assertThat(infoDevice.getId()).isEqualTo(TEST_ID);
-        assertThat(mInfoMediaManager.mMediaDevices).hasSize(routes.size());
-        verify(mCallback).onConnectedDeviceChanged(null);
-    }
-
-    @Test
     public void onSessionUpdated_shouldDispatchDeviceListAdded() {
         final MediaRoute2Info info = mock(MediaRoute2Info.class);
         when(info.getId()).thenReturn(TEST_ID);
@@ -978,7 +820,6 @@
         routes.add(info);
         mShadowRouter2Manager.setAllRoutes(routes);
 
-        mInfoMediaManager.mPackageName = "";
         mInfoMediaManager.registerCallback(mCallback);
 
         mInfoMediaManager.mMediaRouterCallback.onSessionUpdated(mock(RoutingSessionInfo.class));
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java
index fde378f..3adb204 100644
--- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowRouter2Manager.java
@@ -27,12 +27,13 @@
 import org.robolectric.annotation.Implements;
 import org.robolectric.shadow.api.Shadow;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @Implements(MediaRouter2Manager.class)
 public class ShadowRouter2Manager {
 
-    private List<MediaRoute2Info> mAvailableRoutes;
+    private List<MediaRoute2Info> mAvailableRoutes = new ArrayList<>();
     private List<MediaRoute2Info> mAllRoutes;
     private List<MediaRoute2Info> mDeselectableRoutes;
     private List<RoutingSessionInfo> mRemoteSessions;
diff --git a/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt b/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt
index 27d7078..1ad20dc 100644
--- a/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt
+++ b/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt
@@ -32,7 +32,7 @@
 class BluetoothLeBroadcastMetadataExtTest {
 
     @Test
-    fun toQrCodeString() {
+    fun toQrCodeString_encrypted() {
         val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
             setCodecId(0x6)
             val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder().build()
@@ -70,6 +70,37 @@
     }
 
     @Test
+    fun toQrCodeString_non_encrypted() {
+        val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
+            setCodecId(0x6)
+            val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder().build()
+            setContentMetadata(BluetoothLeAudioContentMetadata.Builder()
+                .build())
+            setCodecSpecificConfig(audioCodecConfigMetadata)
+            addChannel(BluetoothLeBroadcastChannel.Builder().apply {
+                setSelected(true)
+                setChannelIndex(1)
+                setCodecMetadata(audioCodecConfigMetadata)
+            }.build())
+        }.build()
+
+        val metadata = BluetoothLeBroadcastMetadata.Builder().apply {
+            setSourceDevice(DevicePublic, BluetoothDevice.ADDRESS_TYPE_PUBLIC)
+            setSourceAdvertisingSid(1)
+            setBroadcastId(0xDE51E9)
+            setBroadcastName("Hockey")
+            setAudioConfigQuality(BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_STANDARD)
+            setPaSyncInterval(0xFFFF)
+            setEncrypted(false)
+            addSubgroup(subgroup)
+        }.build()
+
+        val qrCodeString = metadata.toQrCodeString()
+
+        assertThat(qrCodeString).isEqualTo(QR_CODE_STRING_NON_ENCRYPTED)
+    }
+
+    @Test
     fun toQrCodeString_NoChannelSelected() {
         val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
             setCodecId(0x6)
@@ -102,6 +133,7 @@
             addSubgroup(subgroup)
         }.build()
 
+        // if no channel is selected, no preference(0xFFFFFFFFu) will be set in BIS
         val qrCodeString = metadata.toQrCodeString()
 
         val parsedMetadata =
@@ -111,13 +143,11 @@
         assertThat(parsedMetadata.subgroups).isNotNull()
         assertThat(parsedMetadata.subgroups.size).isEqualTo(1)
         assertThat(parsedMetadata.subgroups[0].channels).isNotNull()
-        assertThat(parsedMetadata.subgroups[0].channels.size).isEqualTo(2)
+        assertThat(parsedMetadata.subgroups[0].channels.size).isEqualTo(1)
         assertThat(parsedMetadata.subgroups[0].hasChannelPreference()).isFalse()
-        // Input order does not matter due to parsing through bisMask
+        // placeholder channel with not selected
         assertThat(parsedMetadata.subgroups[0].channels[0].channelIndex).isEqualTo(1)
         assertThat(parsedMetadata.subgroups[0].channels[0].isSelected).isFalse()
-        assertThat(parsedMetadata.subgroups[0].channels[1].channelIndex).isEqualTo(2)
-        assertThat(parsedMetadata.subgroups[0].channels[1].isSelected).isFalse()
     }
 
     @Test
@@ -162,13 +192,11 @@
         assertThat(parsedMetadata.subgroups).isNotNull()
         assertThat(parsedMetadata.subgroups.size).isEqualTo(1)
         assertThat(parsedMetadata.subgroups[0].channels).isNotNull()
-        // Only selected channel can be recovered
-        assertThat(parsedMetadata.subgroups[0].channels.size).isEqualTo(2)
+        // Only selected channel can be recovered, non-selected ones will be ignored
+        assertThat(parsedMetadata.subgroups[0].channels.size).isEqualTo(1)
         assertThat(parsedMetadata.subgroups[0].hasChannelPreference()).isTrue()
-        assertThat(parsedMetadata.subgroups[0].channels[0].channelIndex).isEqualTo(1)
-        assertThat(parsedMetadata.subgroups[0].channels[0].isSelected).isFalse()
-        assertThat(parsedMetadata.subgroups[0].channels[1].channelIndex).isEqualTo(2)
-        assertThat(parsedMetadata.subgroups[0].channels[1].isSelected).isTrue()
+        assertThat(parsedMetadata.subgroups[0].channels[0].channelIndex).isEqualTo(2)
+        assertThat(parsedMetadata.subgroups[0].channels[0].isSelected).isTrue()
     }
 
     @Test
@@ -180,16 +208,34 @@
         assertThat(qrCodeString).isEqualTo(QR_CODE_STRING)
     }
 
+    @Test
+    fun decodeAndEncodeAgain_sameString_non_encrypted() {
+        val metadata =
+                BluetoothLeBroadcastMetadataExt
+                        .convertToBroadcastMetadata(QR_CODE_STRING_NON_ENCRYPTED)!!
+
+        val qrCodeString = metadata.toQrCodeString()
+
+        assertThat(qrCodeString).isEqualTo(QR_CODE_STRING_NON_ENCRYPTED)
+    }
+
     private companion object {
         const val TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"
+        const val TEST_DEVICE_ADDRESS_PUBLIC = "AA:BB:CC:00:11:22"
 
         val Device: BluetoothDevice =
             BluetoothAdapter.getDefaultAdapter().getRemoteLeDevice(TEST_DEVICE_ADDRESS,
                 BluetoothDevice.ADDRESS_TYPE_RANDOM)
 
+        val DevicePublic: BluetoothDevice =
+            BluetoothAdapter.getDefaultAdapter().getRemoteLeDevice(TEST_DEVICE_ADDRESS_PUBLIC,
+                BluetoothDevice.ADDRESS_TYPE_PUBLIC)
+
         const val QR_CODE_STRING =
-            "BT:R:65536;T:1;D:00-A1-A1-A1-A1-A1;AS:1;B:123456;BN:VGVzdA==;" +
-            "PM:BgNwVGVzdA==;SI:160;C:VGVzdENvZGU=;SG:BS:3,BM:3,AC:BQNUZXN0BARlbmc=;" +
-            "VN:U;;"
+            "BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;" +
+            "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;"
+        const val QR_CODE_STRING_NON_ENCRYPTED =
+            "BLUETOOTH:UUID:184F;BN:SG9ja2V5;AT:0;AD:AABBCC001122;BI:DE51E9;SQ:1;AS:1;PI:FFFF;" +
+            "NS:1;BS:1;NB:1;;"
     }
 }
\ No newline at end of file
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 aa56736..76931a2 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
@@ -22,12 +22,15 @@
 import android.view.WindowInsets
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.LifecycleOwner
+import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.compose.theme.PlatformTheme
 import com.android.compose.ui.platform.DensityAwareComposeView
 import com.android.internal.policy.ScreenDecorationsUtils
@@ -89,12 +92,18 @@
     ) {
         activity.setContent {
             PlatformTheme {
-                CommunalHub(
-                    viewModel = viewModel,
-                    onOpenWidgetPicker = onOpenWidgetPicker,
-                    widgetConfigurator = widgetConfigurator,
-                    onEditDone = onEditDone,
-                )
+                Box(
+                    modifier =
+                        Modifier.fillMaxSize()
+                            .background(LocalAndroidColorScheme.current.outlineVariant),
+                ) {
+                    CommunalHub(
+                        viewModel = viewModel,
+                        onOpenWidgetPicker = onOpenWidgetPicker,
+                        widgetConfigurator = widgetConfigurator,
+                        onEditDone = onEditDone,
+                    )
+                }
             }
         }
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index bc85513..be5aa8a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -1,6 +1,7 @@
 package com.android.systemui.communal.ui.compose
 
 import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
@@ -12,6 +13,7 @@
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.FixedSizeEdgeDetector
+import com.android.compose.animation.scene.LowestZIndexScenePicker
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneScope
@@ -21,6 +23,7 @@
 import com.android.compose.animation.scene.observableTransitionState
 import com.android.compose.animation.scene.transitions
 import com.android.compose.animation.scene.updateSceneTransitionLayoutState
+import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
 import com.android.systemui.communal.ui.compose.extensions.allowGestures
@@ -31,16 +34,25 @@
 
 object Communal {
     object Elements {
+        val Scrim = ElementKey("Scrim", scenePicker = LowestZIndexScenePicker)
         val Content = ElementKey("CommunalContent")
     }
 }
 
 val sceneTransitions = transitions {
-    from(TransitionSceneKey.Blank, to = TransitionSceneKey.Communal) {
-        spec = tween(durationMillis = 500)
-
+    to(TransitionSceneKey.Communal) {
+        spec = tween(durationMillis = 1000)
         translate(Communal.Elements.Content, Edge.Right)
-        fade(Communal.Elements.Content)
+        timestampRange(startMillis = 167, endMillis = 334) {
+            fade(Communal.Elements.Scrim)
+            fade(Communal.Elements.Content)
+        }
+    }
+    to(TransitionSceneKey.Blank) {
+        spec = tween(durationMillis = 1000)
+        translate(Communal.Elements.Content, Edge.Right)
+        timestampRange(endMillis = 167) { fade(Communal.Elements.Content) }
+        timestampRange(startMillis = 167, endMillis = 334) { fade(Communal.Elements.Scrim) }
     }
 }
 
@@ -111,6 +123,12 @@
     viewModel: BaseCommunalViewModel,
     modifier: Modifier = Modifier,
 ) {
+    Box(
+        modifier =
+            Modifier.element(Communal.Elements.Scrim)
+                .fillMaxSize()
+                .background(LocalAndroidColorScheme.current.outlineVariant),
+    )
     Box(modifier.element(Communal.Elements.Content)) { CommunalHub(viewModel = viewModel) }
 }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 090750e..cddd4fa 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -141,7 +141,6 @@
         modifier =
             modifier
                 .fillMaxSize()
-                .background(LocalAndroidColorScheme.current.outlineVariant)
                 .pointerInput(gridState, contentOffset, contentListState) {
                     // If not in edit mode, don't allow selecting items.
                     if (!viewModel.isEditMode) return@pointerInput
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index c9b5b75..33be1dc 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -44,7 +44,6 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertThrows
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -248,11 +247,9 @@
     }
 
     @Test
-    @Ignore
-    fun elementIsReusedInSameSceneAndBetweenScenes() {
+    fun elementIsReusedBetweenScenes() {
         var currentScene by mutableStateOf(TestScenes.SceneA)
         var sceneCState by mutableStateOf(0)
-        var sceneDState by mutableStateOf(0)
         val key = TestElements.Foo
         var nullableLayoutImpl: SceneTransitionLayoutImpl? = null
 
@@ -270,19 +267,6 @@
                 scene(TestScenes.SceneC) {
                     when (sceneCState) {
                         0 -> Row(Modifier.element(key)) {}
-                        1 -> Column(Modifier.element(key)) {}
-                        else -> {
-                            /* Nothing */
-                        }
-                    }
-                }
-                scene(TestScenes.SceneD) {
-                    // We should be able to extract the modifier before assigning it to different
-                    // nodes.
-                    val childModifier = Modifier.element(key)
-                    when (sceneDState) {
-                        0 -> Row(childModifier) {}
-                        1 -> Column(childModifier) {}
                         else -> {
                             /* Nothing */
                         }
@@ -315,35 +299,10 @@
         assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
         assertThat(element.sceneStates.keys).containsExactly(TestScenes.SceneC)
 
-        // Scene C, state 1: the same element is reused.
+        // Scene C, state 1: the element is removed from the map.
         sceneCState = 1
         rule.waitForIdle()
 
-        assertThat(layoutImpl.elements.keys).containsExactly(key)
-        assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
-        assertThat(element.sceneStates.keys).containsExactly(TestScenes.SceneC)
-
-        // Scene D, state 0: the same element is reused.
-        currentScene = TestScenes.SceneD
-        sceneDState = 0
-        rule.waitForIdle()
-
-        assertThat(layoutImpl.elements.keys).containsExactly(key)
-        assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
-        assertThat(element.sceneStates.keys).containsExactly(TestScenes.SceneD)
-
-        // Scene D, state 1: the same element is reused.
-        sceneDState = 1
-        rule.waitForIdle()
-
-        assertThat(layoutImpl.elements.keys).containsExactly(key)
-        assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
-        assertThat(element.sceneStates.keys).containsExactly(TestScenes.SceneD)
-
-        // Scene D, state 2: the element is removed from the map.
-        sceneDState = 2
-        rule.waitForIdle()
-
         assertThat(element.sceneStates).isEmpty()
         assertThat(layoutImpl.elements).isEmpty()
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
index 8a35ef1..a6715df 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
@@ -8,7 +8,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.complication.ComplicationHostViewController
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
+import com.android.systemui.dreams.ui.viewmodel.DreamOverlayViewModel
 import com.android.systemui.log.core.FakeLogBuffer
 import com.android.systemui.statusbar.BlurUtils
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -47,7 +47,7 @@
     @Mock private lateinit var statusBarViewController: DreamOverlayStatusBarViewController
     @Mock private lateinit var stateController: DreamOverlayStateController
     @Mock private lateinit var configController: ConfigurationController
-    @Mock private lateinit var transitionViewModel: DreamingToLockscreenTransitionViewModel
+    @Mock private lateinit var transitionViewModel: DreamOverlayViewModel
     private val logBuffer = FakeLogBuffer.Factory.create()
     private lateinit var controller: DreamOverlayAnimationsController
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index 9368097..3484025 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -197,7 +197,16 @@
             runCurrent()
         }
 
-        assertThat(startedSteps).isEqualTo(listOf(steps[0], steps[3], steps[6]))
+        assertThat(startedSteps)
+            .isEqualTo(
+                listOf(
+                    // The initial transition will also get sent when collect started
+                    TransitionStep(OFF, LOCKSCREEN, 0f, STARTED),
+                    steps[0],
+                    steps[3],
+                    steps[6]
+                )
+            )
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt
index 837a9db..d33c10e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt
@@ -20,6 +20,7 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -66,6 +67,25 @@
     }
 
     @Test
+    fun alpha_WhenNotGone_clockMigrationFlagIsOff_emitsKeyguardAlpha() =
+        testScope.runTest {
+            mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+            val alpha by collectLastValue(underTest.alpha)
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                testScope = testScope,
+            )
+
+            keyguardRepository.setKeyguardAlpha(0.5f)
+            assertThat(alpha).isEqualTo(0.5f)
+
+            keyguardRepository.setKeyguardAlpha(0.8f)
+            assertThat(alpha).isEqualTo(0.8f)
+        }
+
+    @Test
     fun alpha_WhenGoneToAod() =
         testScope.runTest {
             val alpha by collectLastValue(underTest.alpha)
@@ -112,6 +132,7 @@
     @Test
     fun alpha_whenGone_equalsZero() =
         testScope.runTest {
+            mSetFlagsRule.enableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
             val alpha by collectLastValue(underTest.alpha)
 
             keyguardTransitionRepository.sendTransitionStep(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModelTest.kt
new file mode 100644
index 0000000..4defe8a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModelTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.coroutines.collectValues
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DreamingToGlanceableHubTransitionViewModelTest : SysuiTestCase() {
+    val kosmos = testKosmos()
+    val testScope = kosmos.testScope
+
+    val underTest by lazy { kosmos.dreamingToGlanceableHubTransitionViewModel }
+
+    @Test
+    fun dreamOverlayAlpha() =
+        testScope.runTest {
+            val values by collectValues(underTest.dreamOverlayAlpha)
+            assertThat(values).isEmpty()
+
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                listOf(
+                    // Should start running here...
+                    step(0f, TransitionState.STARTED),
+                    step(0f),
+                    step(0.1f),
+                    step(0.5f),
+                    // Up to here...
+                    step(1f),
+                ),
+                testScope,
+            )
+
+            assertThat(values).hasSize(4)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+        }
+
+    @Test
+    fun dreamOverlayTranslationX() =
+        testScope.runTest {
+            val values by collectValues(underTest.dreamOverlayTranslationX(100))
+            assertThat(values).isEmpty()
+
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                listOf(
+                    step(0f, TransitionState.STARTED),
+                    step(0.3f),
+                    step(0.6f),
+                ),
+                testScope,
+            )
+
+            assertThat(values).hasSize(3)
+            values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
+        }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.DREAMING,
+            to = KeyguardState.GLANCEABLE_HUB,
+            value = value,
+            transitionState = state,
+            ownerName = "DreamingToGlanceableHubTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt
new file mode 100644
index 0000000..64125f1
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.google.common.collect.Range
+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
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() {
+    val kosmos = testKosmos()
+    val testScope = kosmos.testScope
+
+    val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    val configurationRepository = kosmos.fakeConfigurationRepository
+    val underTest by lazy { kosmos.glanceableHubToLockscreenTransitionViewModel }
+
+    @Test
+    fun lockscreenFadeIn() =
+        testScope.runTest {
+            val values by collectValues(underTest.keyguardAlpha)
+            assertThat(values).containsExactly(0f)
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                listOf(
+                    step(0f, TransitionState.STARTED),
+                    // Should start running here...
+                    step(0.1f),
+                    step(0.2f),
+                    step(0.3f),
+                    step(0.4f),
+                    // ...up to here
+                    step(0.5f),
+                    step(0.6f),
+                    step(0.7f),
+                    step(0.8f),
+                    step(1f),
+                ),
+                testScope,
+            )
+
+            assertThat(values).hasSize(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+        }
+
+    @Test
+    fun lockscreenTranslationX() =
+        testScope.runTest {
+            configurationRepository.setDimensionPixelSize(
+                R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x,
+                100
+            )
+            val values by collectValues(underTest.keyguardTranslationX)
+            assertThat(values).isEmpty()
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                listOf(
+                    step(0f, TransitionState.STARTED),
+                    step(0f),
+                    step(0.3f),
+                    step(0.5f),
+                    step(1f),
+                ),
+                testScope,
+            )
+
+            assertThat(values).hasSize(5)
+            values.forEach { assertThat(it.value).isIn(Range.closed(-100f, 0f)) }
+        }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.GLANCEABLE_HUB,
+            to = KeyguardState.LOCKSCREEN,
+            value = value,
+            transitionState = state,
+            ownerName = this::class.java.simpleName
+        )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelTest.kt
new file mode 100644
index 0000000..241d0b8
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelTest.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.google.common.collect.Range
+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
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LockscreenToGlanceableHubTransitionViewModelTest : SysuiTestCase() {
+    val kosmos = testKosmos()
+    val testScope = kosmos.testScope
+
+    val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    val configurationRepository = kosmos.fakeConfigurationRepository
+    val underTest by lazy { kosmos.lockscreenToGlanceableHubTransitionViewModel }
+
+    @Test
+    fun lockscreenFadeOut() =
+        testScope.runTest {
+            val values by collectValues(underTest.keyguardAlpha)
+            assertThat(values).containsExactly(1f)
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                listOf(
+                    // Should start running here
+                    step(0f, TransitionState.STARTED),
+                    step(0.1f),
+                    step(0.2f),
+                    // ...up to here
+                    step(0.3f),
+                    step(0.4f),
+                    step(0.5f),
+                    step(0.6f),
+                    step(0.7f),
+                    step(0.8f),
+                    // ...up to here
+                    step(1f),
+                ),
+                testScope,
+            )
+
+            assertThat(values).hasSize(4)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+        }
+
+    @Test
+    fun lockscreenTranslationX() =
+        testScope.runTest {
+            configurationRepository.setDimensionPixelSize(
+                R.dimen.lockscreen_to_hub_transition_lockscreen_translation_x,
+                -100
+            )
+            val values by collectValues(underTest.keyguardTranslationX)
+            assertThat(values).isEmpty()
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                listOf(
+                    step(0f, TransitionState.STARTED),
+                    step(0f),
+                    step(0.3f),
+                    step(0.5f),
+                    step(1f),
+                ),
+                testScope,
+            )
+
+            assertThat(values).hasSize(5)
+            values.forEach { assertThat(it.value).isIn(Range.closed(-100f, 0f)) }
+        }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.GLANCEABLE_HUB,
+            value = value,
+            transitionState = state,
+            ownerName = this::class.java.simpleName
+        )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
index 15cf83c..47e1ee9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
@@ -119,15 +119,19 @@
     fun lockscreenAlpha_runDimissFromKeyguard() =
         testScope.runTest {
             val values by collectValues(underTest.lockscreenAlpha)
+            sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(true)
             runCurrent()
 
-            sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(true)
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.PRIMARY_BOUNCER,
+                to = KeyguardState.GONE,
+                testScope,
+            )
 
-            keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
-            keyguardTransitionRepository.sendTransitionStep(step(1f))
-
-            assertThat(values.size).isEqualTo(2)
-            values.forEach { assertThat(it).isEqualTo(1f) }
+            assertThat(values[0]).isEqualTo(1f)
+            assertThat(values[1]).isEqualTo(1f)
+            // Ensure FINISHED sets alpha to 0
+            assertThat(values[2]).isEqualTo(0f)
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
index 2ea12ef..8ae9172 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
@@ -26,6 +26,7 @@
 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.domain.model.TileModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
 import com.android.systemui.util.mockito.any
@@ -65,6 +66,7 @@
         MockitoAnnotations.initMocks(this)
 
         whenever(currentTilesInteractor.userId).thenReturn(MutableStateFlow(USER))
+        whenever(currentTilesInteractor.currentTiles).thenReturn(MutableStateFlow(emptyList()))
     }
 
     @Test
@@ -201,6 +203,45 @@
             assertThat(autoAddedTiles).doesNotContain(SPEC)
         }
 
+    @Test
+    fun autoAddable_trackIfNotAdded_currentTile_markedAsAdded() =
+        testScope.runTest {
+            val fakeTile = FakeQSTile(USER).apply { tileSpec = SPEC.spec }
+            val fakeCurrentTileModel = TileModel(SPEC, fakeTile)
+            whenever(currentTilesInteractor.currentTiles)
+                .thenReturn(MutableStateFlow(listOf(fakeCurrentTileModel)))
+
+            val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
+            val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.IfNotAdded(SPEC))
+
+            underTest = createInteractor(setOf(fakeAutoAddable))
+            runCurrent()
+
+            assertThat(autoAddedTiles).contains(SPEC)
+        }
+
+    @Test
+    fun autoAddable_trackIfNotAdded_tileAddedToCurrentTiles_markedAsAdded() =
+        testScope.runTest {
+            val fakeTile = FakeQSTile(USER).apply { tileSpec = SPEC.spec }
+            val fakeCurrentTileModel = TileModel(SPEC, fakeTile)
+            val currentTilesFlow = MutableStateFlow(emptyList<TileModel>())
+
+            whenever(currentTilesInteractor.currentTiles).thenReturn(currentTilesFlow)
+
+            val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
+            val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.IfNotAdded(SPEC))
+
+            underTest = createInteractor(setOf(fakeAutoAddable))
+            runCurrent()
+
+            assertThat(autoAddedTiles).doesNotContain(SPEC)
+
+            currentTilesFlow.value = listOf(fakeCurrentTileModel)
+
+            assertThat(autoAddedTiles).contains(SPEC)
+        }
+
     private fun createInteractor(autoAddables: Set<AutoAddable>): AutoAddInteractor {
         return AutoAddInteractor(
                 autoAddables,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractorTest.kt
new file mode 100644
index 0000000..a5c5544
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractorTest.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.custom.domain.interactor
+
+import android.content.ComponentName
+import android.content.pm.UserInfo
+import android.graphics.drawable.Icon
+import android.service.quicksettings.Tile
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.external.componentName
+import com.android.systemui.qs.external.iQSTileService
+import com.android.systemui.qs.external.tileServiceManagerFacade
+import com.android.systemui.qs.external.tileServicesFacade
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.impl.custom.TileSubject.Companion.assertThat
+import com.android.systemui.qs.tiles.impl.custom.customTileDefaultsRepository
+import com.android.systemui.qs.tiles.impl.custom.customTileInteractor
+import com.android.systemui.qs.tiles.impl.custom.customTilePackagesUpdatesRepository
+import com.android.systemui.qs.tiles.impl.custom.customTileRepository
+import com.android.systemui.qs.tiles.impl.custom.customTileServiceInteractor
+import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
+import com.android.systemui.qs.tiles.impl.custom.tileSpec
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.data.repository.userRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class CustomTileDataInteractorTest : SysuiTestCase() {
+
+    private val kosmos =
+        testKosmos().apply {
+            componentName = TEST_COMPONENT
+            tileSpec = TileSpec.create(componentName)
+        }
+    private val underTest =
+        with(kosmos) {
+            CustomTileDataInteractor(
+                tileSpec = tileSpec,
+                defaultsRepository = customTileDefaultsRepository,
+                serviceInteractor = customTileServiceInteractor,
+                customTileInteractor = customTileInteractor,
+                packageUpdatesRepository = customTilePackagesUpdatesRepository,
+                userRepository = userRepository,
+                tileScope = testScope.backgroundScope,
+            )
+        }
+
+    private suspend fun setup() {
+        with(kosmos) {
+            fakeUserRepository.setUserInfos(listOf(TEST_USER_1))
+            fakeUserRepository.setSelectedUserInfo(TEST_USER_1)
+        }
+    }
+
+    @Test
+    fun activeTileIsNotBoundUntilDataCollected() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                customTileRepository.setTileActive(true)
+
+                runCurrent()
+
+                assertThat(iQSTileService.isTileListening).isFalse()
+                assertThat(tileServiceManagerFacade.isBound).isFalse()
+            }
+        }
+
+    @Test
+    fun notActiveTileIsNotBoundUntilDataCollected() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                customTileRepository.setTileActive(false)
+
+                runCurrent()
+
+                assertThat(iQSTileService.isTileListening).isFalse()
+                assertThat(tileServiceManagerFacade.isBound).isFalse()
+            }
+        }
+
+    @Test
+    fun tileIsUnboundWhenDataIsNotListened() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                customTileRepository.setTileActive(false)
+                customTileDefaultsRepository.putDefaults(
+                    TEST_USER_1.userHandle,
+                    componentName,
+                    CustomTileDefaults.Result(TEST_TILE.icon, TEST_TILE.label),
+                )
+                val dataJob =
+                    underTest
+                        .tileData(TEST_USER_1.userHandle, flowOf(DataUpdateTrigger.InitialRequest))
+                        .launchIn(backgroundScope)
+                runCurrent()
+                tileServiceManagerFacade.processPendingBind()
+                assertThat(iQSTileService.isTileListening).isTrue()
+                assertThat(tileServiceManagerFacade.isBound).isTrue()
+
+                dataJob.cancel()
+                runCurrent()
+
+                assertThat(iQSTileService.isTileListening).isFalse()
+                assertThat(tileServiceManagerFacade.isBound).isFalse()
+            }
+        }
+
+    @Test
+    fun tileDataCollection() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                customTileDefaultsRepository.putDefaults(
+                    TEST_USER_1.userHandle,
+                    componentName,
+                    CustomTileDefaults.Result(TEST_TILE.icon, TEST_TILE.label),
+                )
+                val tileData by
+                    collectLastValue(
+                        underTest.tileData(
+                            TEST_USER_1.userHandle,
+                            flowOf(DataUpdateTrigger.InitialRequest)
+                        )
+                    )
+                runCurrent()
+                tileServicesFacade.customTileInterface!!.updateTileState(TEST_TILE, 1)
+
+                runCurrent()
+
+                with(tileData!!) {
+                    assertThat(user.identifier).isEqualTo(TEST_USER_1.id)
+                    assertThat(componentName).isEqualTo(componentName)
+                    assertThat(tile).isEqualTo(TEST_TILE)
+                    assertThat(callingAppUid).isEqualTo(1)
+                    assertThat(hasPendingBind).isEqualTo(true)
+                    assertThat(isToggleable).isEqualTo(false)
+                    assertThat(defaultTileIcon).isEqualTo(TEST_TILE.icon)
+                    assertThat(defaultTileLabel).isEqualTo(TEST_TILE.label)
+                }
+            }
+        }
+
+    @Test
+    fun tileAvailableWhenDefaultsAreLoaded() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                customTileDefaultsRepository.putDefaults(
+                    TEST_USER_1.userHandle,
+                    tileSpec.componentName,
+                    CustomTileDefaults.Result(TEST_TILE.icon, TEST_TILE.label),
+                )
+
+                val isAvailable by collectValues(underTest.availability(TEST_USER_1.userHandle))
+                runCurrent()
+
+                assertThat(isAvailable).containsExactlyElementsIn(arrayOf(true)).inOrder()
+            }
+        }
+
+    @Test
+    fun tileUnavailableWhenDefaultsAreNotLoaded() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                customTileDefaultsRepository.putDefaults(
+                    TEST_USER_1.userHandle,
+                    tileSpec.componentName,
+                    CustomTileDefaults.Error,
+                )
+
+                val isAvailable by collectValues(underTest.availability(TEST_USER_1.userHandle))
+                runCurrent()
+
+                assertThat(isAvailable).containsExactlyElementsIn(arrayOf(false)).inOrder()
+            }
+        }
+
+    @Test
+    fun tileAvailabilityUndefinedWhenDefaultsAreLoadedForAnotherUser() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                customTileDefaultsRepository.putDefaults(
+                    TEST_USER_2.userHandle,
+                    tileSpec.componentName,
+                    CustomTileDefaults.Error,
+                )
+
+                val isAvailable by collectValues(underTest.availability(TEST_USER_1.userHandle))
+                runCurrent()
+
+                assertThat(isAvailable).containsExactlyElementsIn(arrayOf()).inOrder()
+            }
+        }
+
+    private companion object {
+
+        val TEST_COMPONENT = ComponentName("test.pkg", "test.cls")
+        val TEST_USER_1 = UserInfo(1, "first user", UserInfo.FLAG_MAIN)
+        val TEST_USER_2 = UserInfo(2, "second user", UserInfo.FLAG_MAIN)
+        val TEST_TILE =
+            Tile().apply {
+                label = "test_tile_1"
+                icon = Icon.createWithContentUri("file://test_1")
+            }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
index 995d6ac..9546a32 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
@@ -25,7 +25,6 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
-import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.external.TileServiceKey
 import com.android.systemui.qs.pipeline.shared.TileSpec
@@ -35,6 +34,7 @@
 import com.android.systemui.qs.tiles.impl.custom.customTileStatePersister
 import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
 import com.android.systemui.qs.tiles.impl.custom.tileSpec
+import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.first
@@ -50,16 +50,16 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 class CustomTileInteractorTest : SysuiTestCase() {
 
-    private val kosmos = Kosmos().apply { tileSpec = TileSpec.create(TEST_COMPONENT) }
+    private val kosmos = testKosmos().apply { tileSpec = TileSpec.create(TEST_COMPONENT) }
 
     private val underTest: CustomTileInteractor =
         with(kosmos) {
             CustomTileInteractor(
-                tileSpec,
-                customTileDefaultsRepository,
-                customTileRepository,
-                testScope.backgroundScope,
-                testScope.testScheduler,
+                tileSpec = tileSpec,
+                defaultsRepository = customTileDefaultsRepository,
+                customTileRepository = customTileRepository,
+                tileScope = testScope.backgroundScope,
+                backgroundContext = testScope.testScheduler,
             )
         }
 
@@ -69,14 +69,14 @@
             testScope.runTest {
                 customTileRepository.setTileActive(true)
                 customTileStatePersister.persistState(
-                    TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
-                    TEST_TILE,
+                    TileServiceKey(TEST_COMPONENT, TEST_USER_1.identifier),
+                    TEST_TILE_1,
                 )
 
-                underTest.initForUser(TEST_USER)
+                underTest.initForUser(TEST_USER_1)
 
-                assertThat(underTest.getTile(TEST_USER)).isEqualTo(TEST_TILE)
-                assertThat(underTest.getTiles(TEST_USER).first()).isEqualTo(TEST_TILE)
+                assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
+                assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
             }
         }
 
@@ -86,18 +86,18 @@
             testScope.runTest {
                 customTileRepository.setTileActive(false)
                 customTileStatePersister.persistState(
-                    TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
-                    TEST_TILE,
+                    TileServiceKey(TEST_COMPONENT, TEST_USER_1.identifier),
+                    TEST_TILE_1,
                 )
-                val tiles = collectValues(underTest.getTiles(TEST_USER))
-                val initJob = launch { underTest.initForUser(TEST_USER) }
+                val tiles = collectValues(underTest.getTiles(TEST_USER_1))
+                val initJob = launch { underTest.initForUser(TEST_USER_1) }
 
-                underTest.updateTile(TEST_TILE)
+                underTest.updateTile(TEST_TILE_1)
                 runCurrent()
                 initJob.join()
 
                 assertThat(tiles()).hasSize(1)
-                assertThat(tiles().last()).isEqualTo(TEST_TILE)
+                assertThat(tiles().last()).isEqualTo(TEST_TILE_1)
             }
         }
 
@@ -107,34 +107,34 @@
             testScope.runTest {
                 customTileRepository.setTileActive(false)
                 customTileStatePersister.persistState(
-                    TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
-                    TEST_TILE,
+                    TileServiceKey(TEST_COMPONENT, TEST_USER_1.identifier),
+                    TEST_TILE_1,
                 )
-                val tiles = collectValues(underTest.getTiles(TEST_USER))
-                val initJob = launch { underTest.initForUser(TEST_USER) }
+                val tiles = collectValues(underTest.getTiles(TEST_USER_1))
+                val initJob = launch { underTest.initForUser(TEST_USER_1) }
 
-                customTileDefaultsRepository.putDefaults(TEST_USER, TEST_COMPONENT, TEST_DEFAULTS)
-                customTileDefaultsRepository.requestNewDefaults(TEST_USER, TEST_COMPONENT)
+                customTileDefaultsRepository.putDefaults(TEST_USER_1, TEST_COMPONENT, TEST_DEFAULTS)
+                customTileDefaultsRepository.requestNewDefaults(TEST_USER_1, TEST_COMPONENT)
                 runCurrent()
                 initJob.join()
 
                 assertThat(tiles()).hasSize(1)
-                assertThat(tiles().last()).isEqualTo(TEST_TILE)
+                assertThat(tiles().last()).isEqualTo(TEST_TILE_1)
             }
         }
 
     @Test(expected = IllegalStateException::class)
     fun getTileBeforeInitThrows() =
-        with(kosmos) { testScope.runTest { underTest.getTile(TEST_USER) } }
+        with(kosmos) { testScope.runTest { underTest.getTile(TEST_USER_1) } }
 
     @Test
     fun initSuspendsForActiveTileNotRestoredAndNotUpdated() =
         with(kosmos) {
             testScope.runTest {
                 customTileRepository.setTileActive(true)
-                val tiles = collectValues(underTest.getTiles(TEST_USER))
+                val tiles = collectValues(underTest.getTiles(TEST_USER_1))
 
-                val initJob = backgroundScope.launch { underTest.initForUser(TEST_USER) }
+                val initJob = backgroundScope.launch { underTest.initForUser(TEST_USER_1) }
                 advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS)
 
                 // Is still suspended
@@ -149,12 +149,12 @@
             testScope.runTest {
                 customTileRepository.setTileActive(false)
                 customTileStatePersister.persistState(
-                    TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
-                    TEST_TILE,
+                    TileServiceKey(TEST_COMPONENT, TEST_USER_1.identifier),
+                    TEST_TILE_1,
                 )
-                val tiles = collectValues(underTest.getTiles(TEST_USER))
+                val tiles = collectValues(underTest.getTiles(TEST_USER_1))
 
-                val initJob = backgroundScope.launch { underTest.initForUser(TEST_USER) }
+                val initJob = backgroundScope.launch { underTest.initForUser(TEST_USER_1) }
                 advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS)
 
                 // Is still suspended
@@ -176,18 +176,89 @@
         }
     }
 
+    @Test
+    fun activeFollowsTheRepository() {
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileActive(false)
+                assertThat(underTest.isTileActive()).isFalse()
+
+                customTileRepository.setTileActive(true)
+                assertThat(underTest.isTileActive()).isTrue()
+            }
+        }
+    }
+
+    @Test
+    fun initForTheSameUserProcessedOnce() =
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileActive(false)
+                customTileStatePersister.persistState(
+                    TileServiceKey(TEST_COMPONENT, TEST_USER_1.identifier),
+                    TEST_TILE_1,
+                )
+                val tiles = collectValues(underTest.getTiles(TEST_USER_1))
+                val initJob = launch {
+                    underTest.initForUser(TEST_USER_1)
+                    underTest.initForUser(TEST_USER_1)
+                }
+
+                underTest.updateTile(TEST_TILE_1)
+                runCurrent()
+                initJob.join()
+
+                assertThat(tiles()).hasSize(1)
+                assertThat(tiles().last()).isEqualTo(TEST_TILE_1)
+            }
+        }
+
+    @Test
+    fun initForDifferentUsersProcessedOnce() =
+        with(kosmos) {
+            testScope.runTest {
+                customTileRepository.setTileActive(true)
+                customTileStatePersister.persistState(
+                    TileServiceKey(TEST_COMPONENT, TEST_USER_1.identifier),
+                    TEST_TILE_1,
+                )
+                customTileStatePersister.persistState(
+                    TileServiceKey(TEST_COMPONENT, TEST_USER_2.identifier),
+                    TEST_TILE_2,
+                )
+                val tiles1 by collectValues(underTest.getTiles(TEST_USER_1))
+                val tiles2 by collectValues(underTest.getTiles(TEST_USER_2))
+
+                val initJob = launch {
+                    underTest.initForUser(TEST_USER_1)
+                    underTest.initForUser(TEST_USER_2)
+                }
+                runCurrent()
+                initJob.join()
+
+                assertThat(tiles1).isEmpty()
+                assertThat(tiles2).hasSize(1)
+                assertThat(tiles2.last()).isEqualTo(TEST_TILE_2)
+            }
+        }
+
     private companion object {
 
         val TEST_COMPONENT = ComponentName("test.pkg", "test.cls")
-        val TEST_USER = UserHandle.of(1)!!
-        val TEST_TILE by lazy {
+        val TEST_USER_1 = UserHandle.of(1)!!
+        val TEST_USER_2 = UserHandle.of(2)!!
+        val TEST_TILE_1 by lazy {
             Tile().apply {
                 label = "test_tile_1"
                 icon = Icon.createWithContentUri("file://test_1")
             }
         }
-        val TEST_DEFAULTS by lazy {
-            CustomTileDefaults.Result(TEST_TILE.icon, TEST_TILE.label)
+        val TEST_TILE_2 by lazy {
+            Tile().apply {
+                label = "test_tile_2"
+                icon = Icon.createWithContentUri("file://test_2")
+            }
         }
+        val TEST_DEFAULTS by lazy { CustomTileDefaults.Result(TEST_TILE_1.icon, TEST_TILE_1.label) }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt
new file mode 100644
index 0000000..a2127a4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.custom.domain.interactor
+
+import android.app.IUriGrantsManager
+import android.content.ComponentName
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
+import android.graphics.drawable.TestStubDrawable
+import android.os.UserHandle
+import android.service.quicksettings.Tile
+import android.widget.Button
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject.Companion.assertThat
+import com.android.systemui.qs.tiles.impl.custom.customTileQsTileConfig
+import com.android.systemui.qs.tiles.impl.custom.domain.CustomTileMapper
+import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
+import com.android.systemui.qs.tiles.impl.custom.tileSpec
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CustomTileMapperTest : SysuiTestCase() {
+
+    private val uriGrantsManager: IUriGrantsManager = mock {}
+    private val kosmos = testKosmos().apply { tileSpec = TileSpec.Companion.create(TEST_COMPONENT) }
+    private val underTest by lazy {
+        CustomTileMapper(
+            context = mock { whenever(createContextAsUser(any(), any())).thenReturn(context) },
+            uriGrantsManager = uriGrantsManager,
+        )
+    }
+
+    @Test
+    fun stateHasPendingBinding() =
+        with(kosmos) {
+            testScope.runTest {
+                val actual =
+                    underTest.map(
+                        customTileQsTileConfig,
+                        createModel(hasPendingBind = true),
+                    )
+                val expected =
+                    createTileState(
+                        activationState = QSTileState.ActivationState.UNAVAILABLE,
+                        actions = setOf(QSTileState.UserAction.LONG_CLICK),
+                    )
+
+                assertThat(actual).isEqualTo(expected)
+            }
+        }
+
+    @Test
+    fun stateActive() =
+        with(kosmos) {
+            testScope.runTest {
+                val actual =
+                    underTest.map(
+                        customTileQsTileConfig,
+                        createModel(tileState = Tile.STATE_ACTIVE),
+                    )
+                val expected =
+                    createTileState(
+                        activationState = QSTileState.ActivationState.ACTIVE,
+                    )
+
+                assertThat(actual).isEqualTo(expected)
+            }
+        }
+
+    @Test
+    fun stateInactive() =
+        with(kosmos) {
+            testScope.runTest {
+                val actual =
+                    underTest.map(
+                        customTileQsTileConfig,
+                        createModel(tileState = Tile.STATE_INACTIVE),
+                    )
+                val expected =
+                    createTileState(
+                        activationState = QSTileState.ActivationState.INACTIVE,
+                    )
+
+                assertThat(actual).isEqualTo(expected)
+            }
+        }
+
+    @Test
+    fun stateUnavailable() =
+        with(kosmos) {
+            testScope.runTest {
+                val actual =
+                    underTest.map(
+                        customTileQsTileConfig,
+                        createModel(tileState = Tile.STATE_UNAVAILABLE),
+                    )
+                val expected =
+                    createTileState(
+                        activationState = QSTileState.ActivationState.UNAVAILABLE,
+                        actions = setOf(QSTileState.UserAction.LONG_CLICK),
+                    )
+
+                assertThat(actual).isEqualTo(expected)
+            }
+        }
+
+    @Test
+    fun tileWithChevron() =
+        with(kosmos) {
+            testScope.runTest {
+                val actual =
+                    underTest.map(
+                        customTileQsTileConfig,
+                        createModel(isToggleable = false),
+                    )
+                val expected =
+                    createTileState(
+                        sideIcon = QSTileState.SideViewIcon.Chevron,
+                        a11yClass = Button::class.qualifiedName,
+                    )
+
+                assertThat(actual).isEqualTo(expected)
+            }
+        }
+
+    @Test
+    fun defaultIconFallback() =
+        with(kosmos) {
+            testScope.runTest {
+                val actual =
+                    underTest.map(
+                        customTileQsTileConfig,
+                        createModel(tileIcon = createIcon(RuntimeException(), false)),
+                    )
+                val expected =
+                    createTileState(
+                        activationState = QSTileState.ActivationState.INACTIVE,
+                        icon = DEFAULT_DRAWABLE,
+                    )
+
+                assertThat(actual).isEqualTo(expected)
+            }
+        }
+
+    @Test
+    fun failedToLoadIconTileIsInactive() =
+        with(kosmos) {
+            testScope.runTest {
+                val actual =
+                    underTest.map(
+                        customTileQsTileConfig,
+                        createModel(
+                            tileIcon = createIcon(RuntimeException(), false),
+                            defaultTileIcon = createIcon(null, true)
+                        ),
+                    )
+                val expected =
+                    createTileState(
+                        icon = null,
+                        activationState = QSTileState.ActivationState.INACTIVE,
+                    )
+
+                assertThat(actual).isEqualTo(expected)
+            }
+        }
+
+    private fun Kosmos.createModel(
+        tileState: Int = Tile.STATE_ACTIVE,
+        tileIcon: Icon = createIcon(DRAWABLE, false),
+        hasPendingBind: Boolean = false,
+        isToggleable: Boolean = true,
+        defaultTileIcon: Icon = createIcon(DEFAULT_DRAWABLE, true),
+    ) =
+        CustomTileDataModel(
+            UserHandle.of(1),
+            tileSpec.componentName,
+            Tile().apply {
+                state = tileState
+                label = "test label"
+                subtitle = "test subtitle"
+                icon = tileIcon
+                contentDescription = "test content description"
+            },
+            callingAppUid = 0,
+            hasPendingBind = hasPendingBind,
+            isToggleable = isToggleable,
+            defaultTileLabel = "test default tile label",
+            defaultTileIcon = defaultTileIcon,
+        )
+
+    private fun createIcon(drawable: Drawable?, isDefault: Boolean): Icon = mock {
+        if (isDefault) {
+            whenever(loadDrawable(any())).thenReturn(drawable)
+        } else {
+            whenever(loadDrawableCheckingUriGrant(any(), any(), any(), any())).thenReturn(drawable)
+        }
+    }
+
+    private fun createIcon(exception: RuntimeException, isDefault: Boolean): Icon = mock {
+        if (isDefault) {
+            whenever(loadDrawable(any())).thenThrow(exception)
+        } else {
+            whenever(loadDrawableCheckingUriGrant(any(), eq(uriGrantsManager), any(), any()))
+                .thenThrow(exception)
+        }
+    }
+
+    private fun createTileState(
+        activationState: QSTileState.ActivationState = QSTileState.ActivationState.ACTIVE,
+        icon: Drawable? = DRAWABLE,
+        sideIcon: QSTileState.SideViewIcon = QSTileState.SideViewIcon.None,
+        actions: Set<QSTileState.UserAction> =
+            setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+        a11yClass: String? = Switch::class.qualifiedName,
+    ): QSTileState {
+        return QSTileState(
+            { icon?.let { com.android.systemui.common.shared.model.Icon.Loaded(icon, null) } },
+            "test label",
+            activationState,
+            "test subtitle",
+            actions,
+            "test content description",
+            null,
+            sideIcon,
+            QSTileState.EnabledState.ENABLED,
+            a11yClass,
+        )
+    }
+
+    private companion object {
+        val TEST_COMPONENT = ComponentName("test.pkg", "test.cls")
+
+        val DEFAULT_DRAWABLE = TestStubDrawable("default_icon_drawable")
+        val DRAWABLE = TestStubDrawable("icon_drawable")
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractorTest.kt
new file mode 100644
index 0000000..c709f16
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractorTest.kt
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.custom.domain.interactor
+
+import android.app.PendingIntent
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.content.pm.UserInfo
+import android.graphics.drawable.Icon
+import android.provider.Settings
+import android.service.quicksettings.Tile
+import android.service.quicksettings.TileService
+import android.view.IWindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.external.componentName
+import com.android.systemui.qs.external.iQSTileService
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.intentInputs
+import com.android.systemui.qs.tiles.base.actions.pendingIntentInputs
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx.click
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx.longClick
+import com.android.systemui.qs.tiles.impl.custom.customTileServiceInteractor
+import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
+import com.android.systemui.qs.tiles.impl.custom.qsTileLogger
+import com.android.systemui.qs.tiles.impl.custom.tileSpec
+import com.android.systemui.testKosmos
+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.nullable
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CustomTileUserActionInteractorTest : SysuiTestCase() {
+
+    private val inputHandler = FakeQSTileIntentUserInputHandler()
+    private val packageManagerFacade = FakePackageManagerFacade()
+    private val windowManagerFacade = FakeWindowManagerFacade()
+    private val kosmos =
+        testKosmos().apply {
+            componentName = TEST_COMPONENT
+            tileSpec = TileSpec.create(componentName)
+            testCase = this@CustomTileUserActionInteractorTest
+        }
+
+    private val underTest =
+        with(kosmos) {
+            CustomTileUserActionInteractor(
+                context =
+                    mock {
+                        whenever(packageManager).thenReturn(packageManagerFacade.packageManager)
+                    },
+                tileSpec = tileSpec,
+                qsTileLogger = qsTileLogger,
+                windowManager = windowManagerFacade.windowManager,
+                displayTracker = mock {},
+                qsTileIntentUserInputHandler = inputHandler,
+                backgroundContext = testDispatcher,
+                serviceInteractor = customTileServiceInteractor,
+            )
+        }
+
+    private suspend fun setup() {
+        with(kosmos) {
+            fakeUserRepository.setUserInfos(listOf(TEST_USER_1))
+            fakeUserRepository.setSelectedUserInfo(TEST_USER_1)
+        }
+    }
+
+    @Test
+    fun clickStartsActivityWhenPossible() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                underTest.handleInput(
+                    click(customTileModel(activityLaunchForClick = pendingIntent()))
+                )
+
+                assertThat(windowManagerFacade.isTokenGranted).isTrue()
+                assertThat(inputHandler.pendingIntentInputs).hasSize(1)
+                assertThat(iQSTileService.clicks).hasSize(0)
+            }
+        }
+
+    @Test
+    fun clickPassedToTheServiceWhenNoActivity() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                packageManagerFacade.resolutionResult = null
+                underTest.handleInput(click(customTileModel(activityLaunchForClick = null)))
+
+                assertThat(windowManagerFacade.isTokenGranted).isTrue()
+                assertThat(inputHandler.pendingIntentInputs).hasSize(0)
+                assertThat(iQSTileService.clicks).hasSize(1)
+            }
+        }
+
+    @Test
+    fun longClickOpensResolvedIntent() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                packageManagerFacade.resolutionResult =
+                    ActivityInfo().apply {
+                        packageName = "resolved.pkg"
+                        name = "Test"
+                    }
+                underTest.handleInput(longClick(customTileModel()))
+
+                assertThat(inputHandler.intentInputs).hasSize(1)
+                with(inputHandler.intentInputs.first()) {
+                    assertThat(intent.action).isEqualTo(TileService.ACTION_QS_TILE_PREFERENCES)
+                    assertThat(intent.component).isEqualTo(ComponentName("resolved.pkg", "Test"))
+                    assertThat(
+                            intent.getParcelableExtra(
+                                Intent.EXTRA_COMPONENT_NAME,
+                                ComponentName::class.java
+                            )
+                        )
+                        .isEqualTo(componentName)
+                    assertThat(intent.getIntExtra(TileService.EXTRA_STATE, Int.MAX_VALUE))
+                        .isEqualTo(111)
+                }
+            }
+        }
+
+    @Test
+    fun longClickOpensDefaultIntentWhenNoResolved() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                underTest.handleInput(longClick(customTileModel()))
+
+                assertThat(inputHandler.intentInputs).hasSize(1)
+                with(inputHandler.intentInputs.first()) {
+                    assertThat(intent.action)
+                        .isEqualTo(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+                    assertThat(intent.data.toString()).isEqualTo("package:test.pkg")
+                }
+            }
+        }
+
+    @Test
+    fun revokeTokenDoesntRevokeWhenShowingDialog() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                underTest.handleInput(click(customTileModel()))
+                underTest.setShowingDialog(true)
+
+                underTest.revokeToken(false)
+
+                assertThat(windowManagerFacade.isTokenGranted).isTrue()
+            }
+        }
+
+    @Test
+    fun forceRevokeTokenRevokesWhenShowingDialog() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                underTest.handleInput(click(customTileModel()))
+                underTest.setShowingDialog(true)
+
+                underTest.revokeToken(true)
+
+                assertThat(windowManagerFacade.isTokenGranted).isFalse()
+            }
+        }
+
+    @Test
+    fun revokeTokenRevokesWhenNotShowingDialog() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                underTest.handleInput(click(customTileModel()))
+                underTest.setShowingDialog(false)
+
+                underTest.revokeToken(false)
+
+                assertThat(windowManagerFacade.isTokenGranted).isFalse()
+            }
+        }
+
+    @Test
+    fun startActivityDoesntStartWithNoToken() =
+        with(kosmos) {
+            testScope.runTest {
+                setup()
+                underTest.startActivityAndCollapse(mock())
+
+                // Checking all types of inputs
+                assertThat(inputHandler.handledInputs).isEmpty()
+            }
+        }
+
+    private fun pendingIntent(): PendingIntent = mock { whenever(isActivity).thenReturn(true) }
+
+    private fun Kosmos.customTileModel(
+        componentName: ComponentName = tileSpec.componentName,
+        activityLaunchForClick: PendingIntent? = null,
+        tileState: Int = 111,
+    ) =
+        CustomTileDataModel(
+            TEST_USER_1.userHandle,
+            componentName,
+            Tile().also {
+                it.activityLaunchForClick = activityLaunchForClick
+                it.state = tileState
+            },
+            callingAppUid = 0,
+            hasPendingBind = false,
+            isToggleable = false,
+            defaultTileLabel = "default_label",
+            defaultTileIcon = Icon.createWithContentUri("default_icon"),
+        )
+
+    private class FakePackageManagerFacade(val packageManager: PackageManager = mock()) {
+
+        var resolutionResult: ActivityInfo? = null
+
+        init {
+            whenever(packageManager.resolveActivityAsUser(any(), any<Int>(), any())).then {
+                ResolveInfo().apply { activityInfo = resolutionResult }
+            }
+        }
+    }
+
+    private class FakeWindowManagerFacade(val windowManager: IWindowManager = mock()) {
+
+        var isTokenGranted: Boolean = false
+            private set
+
+        init {
+            with(windowManager) {
+                whenever(removeWindowToken(any(), any())).then {
+                    isTokenGranted = false
+                    Unit
+                }
+                whenever(addWindowToken(any(), any(), any(), nullable())).then {
+                    isTokenGranted = true
+                    Unit
+                }
+            }
+        }
+    }
+
+    private companion object {
+
+        val TEST_COMPONENT = ComponentName("test.pkg", "test.cls")
+        val TEST_USER_1 = UserInfo(1, "first user", UserInfo.FLAG_MAIN)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerMainThreadTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerMainThreadTest.java
deleted file mode 100644
index d0e05fa..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerMainThreadTest.java
+++ /dev/null
@@ -1,614 +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.statusbar;
-
-import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED;
-import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS;
-import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS;
-import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS;
-import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS;
-
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.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.when;
-
-import android.app.ActivityManager;
-import android.app.KeyguardManager;
-import android.app.Notification;
-import android.app.admin.DevicePolicyManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.UserInfo;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.provider.Settings;
-import android.util.SparseArray;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.widget.LockPatternUtils;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FakeFeatureFlagsClassic;
-import com.android.systemui.flags.Flags;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.recents.OverviewProxyService;
-import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.NotificationLockscreenUserManager.NotificationStateChangedListener;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
-import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
-import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.settings.FakeSettings;
-
-import com.google.android.collect.Lists;
-
-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;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.concurrent.Executor;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class NotificationLockscreenUserManagerMainThreadTest extends SysuiTestCase {
-    @Mock
-    private NotificationPresenter mPresenter;
-    @Mock
-    private UserManager mUserManager;
-    @Mock
-    private UserTracker mUserTracker;
-
-    // Dependency mocks:
-    @Mock
-    private NotificationVisibilityProvider mVisibilityProvider;
-    @Mock
-    private CommonNotifCollection mNotifCollection;
-    @Mock
-    private DevicePolicyManager mDevicePolicyManager;
-    @Mock
-    private NotificationClickNotifier mClickNotifier;
-    @Mock
-    private OverviewProxyService mOverviewProxyService;
-    @Mock
-    private KeyguardManager mKeyguardManager;
-    @Mock
-    private DeviceProvisionedController mDeviceProvisionedController;
-    @Mock
-    private StatusBarStateController mStatusBarStateController;
-    @Mock
-    private BroadcastDispatcher mBroadcastDispatcher;
-    @Mock
-    private KeyguardStateController mKeyguardStateController;
-
-    private UserInfo mCurrentUser;
-    private UserInfo mSecondaryUser;
-    private UserInfo mWorkUser;
-    private UserInfo mCommunalUser;
-    private FakeSettings mSettings;
-    private TestNotificationLockscreenUserManager mLockscreenUserManager;
-    private NotificationEntry mCurrentUserNotif;
-    private NotificationEntry mSecondaryUserNotif;
-    private NotificationEntry mWorkProfileNotif;
-    private final FakeFeatureFlagsClassic mFakeFeatureFlags = new FakeFeatureFlagsClassic();
-    private Executor mMainExecutor = Runnable::run; // Direct executor
-    private Executor mBackgroundExecutor = Runnable::run; // Direct executor
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-
-        mFakeFeatureFlags.set(Flags.NOTIF_LS_BACKGROUND_THREAD, false);
-
-        int currentUserId = ActivityManager.getCurrentUser();
-        when(mUserTracker.getUserId()).thenReturn(currentUserId);
-        mSettings = new FakeSettings();
-        mSettings.setUserId(ActivityManager.getCurrentUser());
-        mCurrentUser = new UserInfo(currentUserId, "", 0);
-        mSecondaryUser = new UserInfo(currentUserId + 1, "", 0);
-        mWorkUser = new UserInfo(currentUserId + 2, "" /* name */, null /* iconPath */, 0,
-                UserManager.USER_TYPE_PROFILE_MANAGED);
-        mCommunalUser = new UserInfo(currentUserId + 3, "" /* name */, null /* iconPath */, 0,
-                UserManager.USER_TYPE_PROFILE_COMMUNAL);
-
-        when(mKeyguardManager.getPrivateNotificationsAllowed()).thenReturn(true);
-        when(mUserManager.getProfiles(currentUserId)).thenReturn(Lists.newArrayList(
-                mCurrentUser, mWorkUser));
-        when(mUserManager.getProfilesIncludingCommunal(currentUserId)).thenReturn(
-                Lists.newArrayList(mCurrentUser, mWorkUser, mCommunalUser));
-        when(mUserManager.getProfiles(mSecondaryUser.id)).thenReturn(Lists.newArrayList(
-                mSecondaryUser));
-        when(mUserManager.getProfilesIncludingCommunal(mSecondaryUser.id)).thenReturn(
-                Lists.newArrayList(mSecondaryUser, mCommunalUser));
-
-        Notification notifWithPrivateVisibility = new Notification();
-        notifWithPrivateVisibility.visibility = Notification.VISIBILITY_PRIVATE;
-        mCurrentUserNotif = new NotificationEntryBuilder()
-                .setNotification(notifWithPrivateVisibility)
-                .setUser(new UserHandle(mCurrentUser.id))
-                .build();
-        mSecondaryUserNotif = new NotificationEntryBuilder()
-                .setNotification(notifWithPrivateVisibility)
-                .setUser(new UserHandle(mSecondaryUser.id))
-                .build();
-        mWorkProfileNotif = new NotificationEntryBuilder()
-                .setNotification(notifWithPrivateVisibility)
-                .setUser(new UserHandle(mWorkUser.id))
-                .build();
-
-        mLockscreenUserManager = new TestNotificationLockscreenUserManager(mContext);
-        mLockscreenUserManager.setUpWithPresenter(mPresenter);
-    }
-
-    private void changeSetting(String setting) {
-        final Collection<Uri> lockScreenUris = new ArrayList<>();
-        lockScreenUris.add(Settings.Secure.getUriFor(setting));
-        mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false,
-            lockScreenUris, 0);
-    }
-
-    @Test
-    public void testGetCurrentProfiles() {
-        final SparseArray<UserInfo> expectedCurProfiles = new SparseArray<>();
-        expectedCurProfiles.put(mCurrentUser.id, mCurrentUser);
-        expectedCurProfiles.put(mWorkUser.id, mWorkUser);
-        if (android.multiuser.Flags.supportCommunalProfile()) {
-            expectedCurProfiles.put(mCommunalUser.id, mCommunalUser);
-        }
-        assertTrue(mLockscreenUserManager.getCurrentProfiles().contentEquals(expectedCurProfiles));
-
-        mLockscreenUserManager.mUserChangedCallback.onUserChanging(mSecondaryUser.id, mContext);
-
-        final SparseArray<UserInfo> expectedSecProfiles = new SparseArray<>();
-        expectedSecProfiles.put(mSecondaryUser.id, mSecondaryUser);
-        if (android.multiuser.Flags.supportCommunalProfile()) {
-            expectedSecProfiles.put(mCommunalUser.id, mCommunalUser);
-        }
-        assertTrue(mLockscreenUserManager.getCurrentProfiles().contentEquals(expectedSecProfiles));
-    }
-
-    @Test
-    public void testLockScreenShowNotificationsFalse() {
-        mSettings.putInt(LOCK_SCREEN_SHOW_NOTIFICATIONS, 0);
-        changeSetting(LOCK_SCREEN_SHOW_NOTIFICATIONS);
-        assertFalse(mLockscreenUserManager.shouldShowLockscreenNotifications());
-    }
-
-    @Test
-    public void testLockScreenShowNotificationsTrue() {
-        mSettings.putInt(LOCK_SCREEN_SHOW_NOTIFICATIONS, 1);
-        changeSetting(LOCK_SCREEN_SHOW_NOTIFICATIONS);
-        assertTrue(mLockscreenUserManager.shouldShowLockscreenNotifications());
-    }
-
-    @Test
-    public void testLockScreenAllowPrivateNotificationsTrue() {
-        mSettings.putInt(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-        assertTrue(mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(mCurrentUser.id));
-    }
-
-    @Test
-    public void testLockScreenAllowPrivateNotificationsFalse() {
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0,
-                mCurrentUser.id);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-        assertFalse(mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(mCurrentUser.id));
-    }
-
-    @Test
-    public void testLockScreenAllowsWorkPrivateNotificationsFalse() {
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0,
-                mWorkUser.id);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-        assertFalse(mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(mWorkUser.id));
-    }
-
-    @Test
-    public void testLockScreenAllowsWorkPrivateNotificationsTrue() {
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
-                mWorkUser.id);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-        assertTrue(mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(mWorkUser.id));
-    }
-
-    @Test
-    public void testCurrentUserPrivateNotificationsNotRedacted() {
-        // GIVEN current user doesn't allow private notifications to show
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0,
-                mCurrentUser.id);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-
-        // THEN current user's notification is redacted
-        assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
-    }
-
-    @Test
-    public void testCurrentUserPrivateNotificationsRedacted() {
-        // GIVEN current user allows private notifications to show
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
-                mCurrentUser.id);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-
-        // THEN current user's notification isn't redacted
-        assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
-    }
-
-    @Test
-    public void testWorkPrivateNotificationsRedacted() {
-        // GIVEN work profile doesn't private notifications to show
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0,
-                mWorkUser.id);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-
-        // THEN work profile notification is redacted
-        assertTrue(mLockscreenUserManager.needsRedaction(mWorkProfileNotif));
-        assertFalse(mLockscreenUserManager.allowsManagedPrivateNotificationsInPublic());
-    }
-
-    @Test
-    public void testWorkPrivateNotificationsNotRedacted() {
-        // GIVEN work profile allows private notifications to show
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
-                mWorkUser.id);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-
-        // THEN work profile notification isn't redacted
-        assertFalse(mLockscreenUserManager.needsRedaction(mWorkProfileNotif));
-        assertTrue(mLockscreenUserManager.allowsManagedPrivateNotificationsInPublic());
-    }
-
-    @Test
-    public void testWorkPrivateNotificationsNotRedacted_otherUsersRedacted() {
-        // GIVEN work profile allows private notifications to show but the other users don't
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
-                mWorkUser.id);
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0,
-                mCurrentUser.id);
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0,
-                mSecondaryUser.id);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-
-        // THEN the work profile notification doesn't need to be redacted
-        assertFalse(mLockscreenUserManager.needsRedaction(mWorkProfileNotif));
-
-        // THEN the current user and secondary user notifications do need to be redacted
-        assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
-        assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif));
-    }
-
-    @Test
-    public void testWorkProfileRedacted_otherUsersNotRedacted() {
-        // GIVEN work profile doesn't allow private notifications to show but the other users do
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0,
-                mWorkUser.id);
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
-                mCurrentUser.id);
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
-                mSecondaryUser.id);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-
-        // THEN the work profile notification needs to be redacted
-        assertTrue(mLockscreenUserManager.needsRedaction(mWorkProfileNotif));
-
-        // THEN the current user and secondary user notifications don't need to be redacted
-        assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
-        assertFalse(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif));
-    }
-
-    @Test
-    public void testSecondaryUserNotRedacted_currentUserRedacted() {
-        // GIVEN secondary profile allows private notifications to show but the current user
-        // doesn't allow private notifications to show
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0,
-                mCurrentUser.id);
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
-                mSecondaryUser.id);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-
-        // THEN the secondary profile notification still needs to be redacted because the current
-        // user's setting takes precedence
-        assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif));
-    }
-
-    @Test
-    public void testUserSwitchedCallsOnUserSwitching() {
-        mLockscreenUserManager.getUserTrackerCallbackForTest().onUserChanging(mSecondaryUser.id,
-                mContext);
-        verify(mPresenter, times(1)).onUserSwitched(mSecondaryUser.id);
-    }
-
-    @Test
-    public void testIsLockscreenPublicMode() {
-        assertFalse(mLockscreenUserManager.isLockscreenPublicMode(mCurrentUser.id));
-        mLockscreenUserManager.setLockscreenPublicMode(true, mCurrentUser.id);
-        assertTrue(mLockscreenUserManager.isLockscreenPublicMode(mCurrentUser.id));
-    }
-
-    @Test
-    public void testUpdateIsPublicMode() {
-        when(mKeyguardStateController.isMethodSecure()).thenReturn(true);
-
-        NotificationStateChangedListener listener = mock(NotificationStateChangedListener.class);
-        mLockscreenUserManager.addNotificationStateChangedListener(listener);
-        mLockscreenUserManager.mCurrentProfiles.append(0, mock(UserInfo.class));
-
-        // first call explicitly sets user 0 to not public; notifies
-        mLockscreenUserManager.updatePublicMode();
-        assertFalse(mLockscreenUserManager.isLockscreenPublicMode(0));
-        verify(listener).onNotificationStateChanged();
-        clearInvocations(listener);
-
-        // calling again has no changes; does not notify
-        mLockscreenUserManager.updatePublicMode();
-        assertFalse(mLockscreenUserManager.isLockscreenPublicMode(0));
-        verify(listener, never()).onNotificationStateChanged();
-
-        // Calling again with keyguard now showing makes user 0 public; notifies
-        when(mKeyguardStateController.isShowing()).thenReturn(true);
-        mLockscreenUserManager.updatePublicMode();
-        assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0));
-        verify(listener).onNotificationStateChanged();
-        clearInvocations(listener);
-
-        // calling again has no changes; does not notify
-        mLockscreenUserManager.updatePublicMode();
-        assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0));
-        verify(listener, never()).onNotificationStateChanged();
-    }
-
-    @Test
-    public void testDevicePolicyDoesNotAllowNotifications() {
-        // User allows them
-        mSettings.putIntForUser(LOCK_SCREEN_SHOW_NOTIFICATIONS, 1, mCurrentUser.id);
-        changeSetting(LOCK_SCREEN_SHOW_NOTIFICATIONS);
-
-        // DevicePolicy hides notifs on lockscreen
-        when(mDevicePolicyManager.getKeyguardDisabledFeatures(null, mCurrentUser.id))
-                .thenReturn(KEYGUARD_DISABLE_SECURE_NOTIFICATIONS);
-
-        BroadcastReceiver.PendingResult pr = new BroadcastReceiver.PendingResult(
-                0, null, null, 0, true, false, null, mCurrentUser.id, 0);
-        mLockscreenUserManager.mAllUsersReceiver.setPendingResult(pr);
-        mLockscreenUserManager.mAllUsersReceiver.onReceive(mContext,
-                new Intent(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED));
-
-        assertFalse(mLockscreenUserManager.userAllowsNotificationsInPublic(mCurrentUser.id));
-    }
-
-    @Test
-    public void testDevicePolicyDoesNotAllowNotifications_secondary() {
-        Mockito.clearInvocations(mDevicePolicyManager);
-        // User allows notifications
-        mSettings.putIntForUser(LOCK_SCREEN_SHOW_NOTIFICATIONS, 1, mSecondaryUser.id);
-        changeSetting(LOCK_SCREEN_SHOW_NOTIFICATIONS);
-
-        // DevicePolicy hides notifications
-        when(mDevicePolicyManager.getKeyguardDisabledFeatures(null, mSecondaryUser.id))
-                .thenReturn(KEYGUARD_DISABLE_SECURE_NOTIFICATIONS);
-
-        BroadcastReceiver.PendingResult pr = new BroadcastReceiver.PendingResult(
-                0, null, null, 0, true, false, null, mSecondaryUser.id, 0);
-        mLockscreenUserManager.mAllUsersReceiver.setPendingResult(pr);
-        mLockscreenUserManager.mAllUsersReceiver.onReceive(mContext,
-                new Intent(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED));
-
-        assertFalse(mLockscreenUserManager.userAllowsNotificationsInPublic(mSecondaryUser.id));
-    }
-
-    @Test
-    public void testDevicePolicy_noPrivateNotifications() {
-        Mockito.clearInvocations(mDevicePolicyManager);
-        // User allows notifications
-        mSettings.putIntForUser(LOCK_SCREEN_SHOW_NOTIFICATIONS, 1, mCurrentUser.id);
-        changeSetting(LOCK_SCREEN_SHOW_NOTIFICATIONS);
-
-        // DevicePolicy hides sensitive content
-        when(mDevicePolicyManager.getKeyguardDisabledFeatures(null, mCurrentUser.id))
-                .thenReturn(KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS);
-
-        BroadcastReceiver.PendingResult pr = new BroadcastReceiver.PendingResult(
-                0, null, null, 0, true, false, null, mCurrentUser.id, 0);
-        mLockscreenUserManager.mAllUsersReceiver.setPendingResult(pr);
-        mLockscreenUserManager.mAllUsersReceiver.onReceive(mContext,
-                new Intent(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED));
-
-        assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
-    }
-
-    @Test
-    public void testDevicePolicy_noPrivateNotifications_userAll() {
-        // User allows notifications
-        mSettings.putIntForUser(LOCK_SCREEN_SHOW_NOTIFICATIONS, 1, mCurrentUser.id);
-        changeSetting(LOCK_SCREEN_SHOW_NOTIFICATIONS);
-
-        // DevicePolicy hides sensitive content
-        when(mDevicePolicyManager.getKeyguardDisabledFeatures(null, mCurrentUser.id))
-                .thenReturn(KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS);
-
-        BroadcastReceiver.PendingResult pr = new BroadcastReceiver.PendingResult(
-                0, null, null, 0, true, false, null, mCurrentUser.id, 0);
-        mLockscreenUserManager.mAllUsersReceiver.setPendingResult(pr);
-        mLockscreenUserManager.mAllUsersReceiver.onReceive(mContext,
-                new Intent(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED));
-
-        assertTrue(mLockscreenUserManager.needsRedaction(new NotificationEntryBuilder()
-                .setNotification(new Notification())
-                .setUser(UserHandle.ALL)
-                .build()));
-    }
-
-    @Test
-    public void testDevicePolicyPrivateNotifications_secondary() {
-        Mockito.clearInvocations(mDevicePolicyManager);
-        // User allows notifications
-        mSettings.putIntForUser(LOCK_SCREEN_SHOW_NOTIFICATIONS, 1, mSecondaryUser.id);
-        changeSetting(LOCK_SCREEN_SHOW_NOTIFICATIONS);
-
-        // DevicePolicy hides sensitive content
-        when(mDevicePolicyManager.getKeyguardDisabledFeatures(null, mSecondaryUser.id))
-                .thenReturn(KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS);
-
-        BroadcastReceiver.PendingResult pr = new BroadcastReceiver.PendingResult(
-                0, null, null, 0, true, false, null, mSecondaryUser.id, 0);
-        mLockscreenUserManager.mAllUsersReceiver.setPendingResult(pr);
-        mLockscreenUserManager.mAllUsersReceiver.onReceive(mContext,
-                new Intent(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED));
-
-        mLockscreenUserManager.mUserChangedCallback.onUserChanging(mSecondaryUser.id, mContext);
-        assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif));
-    }
-
-    @Test
-    public void testHideNotifications_primary() {
-        mSettings.putIntForUser(LOCK_SCREEN_SHOW_NOTIFICATIONS, 0, mCurrentUser.id);
-        changeSetting(LOCK_SCREEN_SHOW_NOTIFICATIONS);
-
-        assertFalse(mLockscreenUserManager.userAllowsNotificationsInPublic(mCurrentUser.id));
-    }
-
-    @Test
-    public void testHideNotifications_secondary() {
-        mSettings.putIntForUser(LOCK_SCREEN_SHOW_NOTIFICATIONS, 0, mSecondaryUser.id);
-        changeSetting(LOCK_SCREEN_SHOW_NOTIFICATIONS);
-
-        assertFalse(mLockscreenUserManager.userAllowsNotificationsInPublic(mSecondaryUser.id));
-    }
-
-    @Test
-    public void testHideNotifications_secondary_userSwitch() {
-        mSettings.putIntForUser(LOCK_SCREEN_SHOW_NOTIFICATIONS, 0, mSecondaryUser.id);
-        changeSetting(LOCK_SCREEN_SHOW_NOTIFICATIONS);
-
-        mLockscreenUserManager.mUserChangedCallback.onUserChanging(mSecondaryUser.id, mContext);
-
-        assertFalse(mLockscreenUserManager.userAllowsNotificationsInPublic(mSecondaryUser.id));
-    }
-
-    @Test
-    public void testShowNotifications_secondary_userSwitch() {
-        mSettings.putIntForUser(LOCK_SCREEN_SHOW_NOTIFICATIONS, 1, mSecondaryUser.id);
-        changeSetting(LOCK_SCREEN_SHOW_NOTIFICATIONS);
-
-        mLockscreenUserManager.mUserChangedCallback.onUserChanging(mSecondaryUser.id, mContext);
-
-        assertTrue(mLockscreenUserManager.userAllowsNotificationsInPublic(mSecondaryUser.id));
-    }
-
-    @Test
-    public void testUserAllowsNotificationsInPublic_keyguardManagerNoPrivateNotifications() {
-        // DevicePolicy allows notifications
-        when(mDevicePolicyManager.getKeyguardDisabledFeatures(null, mCurrentUser.id))
-                .thenReturn(0);
-        BroadcastReceiver.PendingResult pr = new BroadcastReceiver.PendingResult(
-                0, null, null, 0, true, false, null, mCurrentUser.id, 0);
-        mLockscreenUserManager.mAllUsersReceiver.setPendingResult(pr);
-        mLockscreenUserManager.mAllUsersReceiver.onReceive(mContext,
-                new Intent(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED));
-
-        // KeyguardManager does not allow notifications
-        when(mKeyguardManager.getPrivateNotificationsAllowed()).thenReturn(false);
-
-        // User allows notifications
-        mSettings.putIntForUser(LOCK_SCREEN_SHOW_NOTIFICATIONS, 1, mCurrentUser.id);
-        // We shouldn't need to call this method, but getPrivateNotificationsAllowed has no
-        // callback, so it's only updated when the setting is
-        changeSetting(LOCK_SCREEN_SHOW_NOTIFICATIONS);
-
-        assertFalse(mLockscreenUserManager.userAllowsNotificationsInPublic(mCurrentUser.id));
-    }
-
-    @Test
-    public void testUserAllowsNotificationsInPublic_settingsChange() {
-        // User allows notifications
-        mSettings.putIntForUser(LOCK_SCREEN_SHOW_NOTIFICATIONS, 1, mCurrentUser.id);
-        changeSetting(LOCK_SCREEN_SHOW_NOTIFICATIONS);
-
-        assertTrue(mLockscreenUserManager.userAllowsNotificationsInPublic(mCurrentUser.id));
-
-        // User disables
-        mSettings.putIntForUser(LOCK_SCREEN_SHOW_NOTIFICATIONS, 0, mCurrentUser.id);
-        changeSetting(LOCK_SCREEN_SHOW_NOTIFICATIONS);
-
-        assertFalse(mLockscreenUserManager.userAllowsNotificationsInPublic(mCurrentUser.id));
-    }
-
-    private class TestNotificationLockscreenUserManager
-            extends NotificationLockscreenUserManagerImpl {
-        public TestNotificationLockscreenUserManager(Context context) {
-            super(
-                    context,
-                    mBroadcastDispatcher,
-                    mDevicePolicyManager,
-                    mUserManager,
-                    mUserTracker,
-                    (() -> mVisibilityProvider),
-                    (() -> mNotifCollection),
-                    mClickNotifier,
-                    (() -> mOverviewProxyService),
-                    NotificationLockscreenUserManagerMainThreadTest.this.mKeyguardManager,
-                    mStatusBarStateController,
-                    mMainExecutor,
-                    mBackgroundExecutor,
-                    mDeviceProvisionedController,
-                    mKeyguardStateController,
-                    mSettings,
-                    mock(DumpManager.class),
-                    mock(LockPatternUtils.class),
-                    mFakeFeatureFlags);
-        }
-
-        public BroadcastReceiver getBaseBroadcastReceiverForTest() {
-            return mBaseBroadcastReceiver;
-        }
-
-        public UserTracker.Callback getUserTrackerCallbackForTest() {
-            return mUserChangedCallback;
-        }
-
-        public ContentObserver getLockscreenSettingsObserverForTest() {
-            return mLockscreenSettingsObserver;
-        }
-
-        public ContentObserver getSettingsObserverForTest() {
-            return mSettingsObserver;
-        }
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index bcc0710..d505b27 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -172,8 +172,6 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
-        mFakeFeatureFlags.set(Flags.NOTIF_LS_BACKGROUND_THREAD, true);
-
         int currentUserId = ActivityManager.getCurrentUser();
         when(mUserTracker.getUserId()).thenReturn(currentUserId);
         mSettings = new FakeSettings();
diff --git a/packages/SystemUI/proguard_common.flags b/packages/SystemUI/proguard_common.flags
index f9546c4..162d8ae 100644
--- a/packages/SystemUI/proguard_common.flags
+++ b/packages/SystemUI/proguard_common.flags
@@ -17,10 +17,6 @@
   <1> *;
 }
 
--keepclasseswithmembers class * {
-    public <init>(android.content.Context, android.util.AttributeSet);
-}
-
 -keep class androidx.core.app.CoreComponentFactory
 
 # Keep the wm shell lib
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index a681da3..65120a9 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1514,6 +1514,12 @@
     <!-- The amount of vertical offset for the keyguard during the full shade transition. -->
     <dimen name="lockscreen_shade_keyguard_transition_vertical_offset">0dp</dimen>
 
+    <!-- LOCKSCREEN -> GLANCEABLE_HUB transition: Amount to shift lockscreen content on entering -->
+    <dimen name="lockscreen_to_hub_transition_lockscreen_translation_x">-824dp</dimen>
+
+    <!-- GLANCEABLE_HUB -> LOCKSCREEN transition: Amount to shift lockscreen content on entering -->
+    <dimen name="hub_to_lockscreen_transition_lockscreen_translation_x">824dp</dimen>
+
     <!-- Distance that the full shade transition takes in order for media to fully transition to
          the shade -->
     <dimen name="lockscreen_shade_media_transition_distance">120dp</dimen>
@@ -1857,6 +1863,7 @@
     <dimen name="dream_overlay_y_offset">80dp</dimen>
     <dimen name="dream_overlay_entry_y_offset">40dp</dimen>
     <dimen name="dream_overlay_exit_y_offset">40dp</dimen>
+    <dimen name="dream_overlay_exit_x_offset">824dp</dimen>
 
     <dimen name="status_view_margin_horizontal">0dp</dimen>
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
index 23afb7c..4997370 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
@@ -62,7 +62,7 @@
         userManager.getCredentialOwnerProfile(userId)
 
     override fun getParentProfileIdOrSelfId(userId: Int): Int =
-        userManager.getProfileParent(userId).id
+        userManager.getProfileParent(userId)?.id ?: userManager.getCredentialOwnerProfile(userId)
 
     override fun verifyCredential(
         request: BiometricPromptRequest.Credential,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index 557ad13..9000da3 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -33,20 +33,16 @@
 import com.android.systemui.complication.ComplicationLayoutParams.POSITION_TOP
 import com.android.systemui.complication.ComplicationLayoutParams.Position
 import com.android.systemui.dreams.dagger.DreamOverlayModule
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
+import com.android.systemui.dreams.ui.viewmodel.DreamOverlayViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.dagger.DreamLog
-import com.android.systemui.res.R
 import com.android.systemui.statusbar.BlurUtils
 import com.android.systemui.statusbar.CrossFadeHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
 import javax.inject.Inject
 import javax.inject.Named
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.launch
 
 /** Controller for dream overlay animations. */
@@ -58,7 +54,7 @@
     private val mStatusBarViewController: DreamOverlayStatusBarViewController,
     private val mOverlayStateController: DreamOverlayStateController,
     @Named(DreamOverlayModule.DREAM_BLUR_RADIUS) private val mDreamBlurRadius: Int,
-    private val transitionViewModel: DreamingToLockscreenTransitionViewModel,
+    private val dreamOverlayViewModel: DreamOverlayViewModel,
     private val configController: ConfigurationController,
     @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DURATION)
     private val mDreamInBlurAnimDurationMs: Long,
@@ -91,59 +87,45 @@
         this.view = view
 
         view.repeatWhenAttached {
-            val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
-            val configCallback =
-                object : ConfigurationListener {
-                    override fun onDensityOrFontScaleChanged() {
-                        configurationBasedDimensions.value = loadFromResources(view)
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                launch {
+                    dreamOverlayViewModel.dreamOverlayTranslationY.collect { px ->
+                        ComplicationLayoutParams.iteratePositions(
+                            { position: Int -> setElementsTranslationYAtPosition(px, position) },
+                            POSITION_TOP or POSITION_BOTTOM
+                        )
                     }
                 }
 
-            configController.addCallback(configCallback)
+                launch {
+                    dreamOverlayViewModel.dreamOverlayTranslationX.collect { px ->
+                        ComplicationLayoutParams.iteratePositions(
+                            { position: Int -> setElementsTranslationXAtPosition(px, position) },
+                            POSITION_TOP or POSITION_BOTTOM
+                        )
+                    }
+                }
 
-            try {
-                repeatOnLifecycle(Lifecycle.State.CREATED) {
-                    /* Translation animations, when moving from DREAMING->LOCKSCREEN state */
-                    launch {
-                        configurationBasedDimensions
-                            .flatMapLatest {
-                                transitionViewModel.dreamOverlayTranslationY(it.translationYPx)
-                            }
-                            .collect { px ->
-                                ComplicationLayoutParams.iteratePositions(
-                                    { position: Int ->
-                                        setElementsTranslationYAtPosition(px, position)
-                                    },
-                                    POSITION_TOP or POSITION_BOTTOM
+                launch {
+                    dreamOverlayViewModel.dreamOverlayAlpha.collect { alpha ->
+                        ComplicationLayoutParams.iteratePositions(
+                            { position: Int ->
+                                setElementsAlphaAtPosition(
+                                    alpha = alpha,
+                                    position = position,
+                                    fadingOut = true,
                                 )
-                            }
-                    }
-
-                    /* Alpha animations, when moving from DREAMING->LOCKSCREEN state */
-                    launch {
-                        transitionViewModel.dreamOverlayAlpha.collect { alpha ->
-                            ComplicationLayoutParams.iteratePositions(
-                                { position: Int ->
-                                    setElementsAlphaAtPosition(
-                                        alpha = alpha,
-                                        position = position,
-                                        fadingOut = true,
-                                    )
-                                },
-                                POSITION_TOP or POSITION_BOTTOM
-                            )
-                        }
-                    }
-
-                    launch {
-                        transitionViewModel.transitionEnded.collect { _ ->
-                            mOverlayStateController.setExitAnimationsRunning(false)
-                        }
+                            },
+                            POSITION_TOP or POSITION_BOTTOM
+                        )
                     }
                 }
-            } finally {
-                // Ensure the callback is removed when cancellation happens
-                configController.removeCallback(configCallback)
+
+                launch {
+                    dreamOverlayViewModel.transitionEnded.collect { _ ->
+                        mOverlayStateController.setExitAnimationsRunning(false)
+                    }
+                }
             }
         }
     }
@@ -373,14 +355,10 @@
         }
     }
 
-    private fun loadFromResources(view: View): ConfigurationBasedDimensions {
-        return ConfigurationBasedDimensions(
-            translationYPx =
-                view.resources.getDimensionPixelSize(R.dimen.dream_overlay_exit_y_offset),
-        )
+    /** Sets x translation of complications at the specified position. */
+    private fun setElementsTranslationXAtPosition(translationX: Float, position: Int) {
+        mComplicationHostViewController.getViewsAtPosition(position).forEach { v ->
+            v.translationX = translationX
+        }
     }
-
-    private data class ConfigurationBasedDimensions(
-        val translationYPx: Int,
-    )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamOverlayViewModel.kt
new file mode 100644
index 0000000..dd67a4c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamOverlayViewModel.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams.ui.viewmodel
+
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToGlanceableHubTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.merge
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class DreamOverlayViewModel
+@Inject
+constructor(
+    configurationInteractor: ConfigurationInteractor,
+    private val toGlanceableHubTransitionViewModel: DreamingToGlanceableHubTransitionViewModel,
+    private val toLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
+) {
+
+    val dreamOverlayTranslationX: Flow<Float> =
+        configurationInteractor
+            .dimensionPixelSize(R.dimen.dream_overlay_exit_x_offset)
+            .flatMapLatest { px: Int ->
+                toGlanceableHubTransitionViewModel.dreamOverlayTranslationX(px)
+            }
+
+    val dreamOverlayTranslationY: Flow<Float> =
+        configurationInteractor
+            .dimensionPixelSize(R.dimen.dream_overlay_exit_y_offset)
+            .flatMapLatest { px: Int ->
+                toLockscreenTransitionViewModel.dreamOverlayTranslationY(px)
+            }
+
+    val dreamOverlayAlpha: Flow<Float> =
+        merge(
+            toLockscreenTransitionViewModel.dreamOverlayAlpha,
+            toGlanceableHubTransitionViewModel.dreamOverlayAlpha,
+        )
+
+    val transitionEnded = toLockscreenTransitionViewModel.transitionEnded
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 6eff792..56162ae 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -102,11 +102,6 @@
             default = true
         )
 
-    // TODO(b/301955929)
-    @JvmField
-    val NOTIF_LS_BACKGROUND_THREAD =
-            releasedFlag("notification_lockscreen_mgr_bg_thread")
-
     // 200 - keyguard/lockscreen
     // ** Flag retired **
     // public static final BooleanFlag KEYGUARD_LAYOUT =
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 13ffd63..c6594ef 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
@@ -18,6 +18,7 @@
 
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
+import com.android.systemui.Flags.communalHub
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
@@ -44,6 +45,7 @@
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
+    private val glanceableHubTransitions: GlanceableHubTransitions,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.DREAMING,
@@ -57,6 +59,17 @@
         listenForDreamingToGone()
         listenForDreamingToAodOrDozing()
         listenForTransitionToCamera(scope, keyguardInteractor)
+        listenForDreamingToGlanceableHub()
+    }
+
+    private fun listenForDreamingToGlanceableHub() {
+        if (!communalHub()) return
+        glanceableHubTransitions.listenForGlanceableHubTransition(
+            transitionName = "listenForDreamingToGlanceableHub",
+            transitionOwnerName = TAG,
+            fromState = KeyguardState.DREAMING,
+            toState = KeyguardState.GLANCEABLE_HUB,
+        )
     }
 
     fun startToLockscreenTransition() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 71d941a..fbf195e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -20,7 +20,6 @@
 import com.android.app.animation.Interpolators
 import com.android.app.tracing.coroutines.launch
 import com.android.systemui.Flags
-import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
@@ -31,7 +30,7 @@
 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleMultiple
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
-import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
@@ -71,7 +70,11 @@
     override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
         return ValueAnimator().apply {
             interpolator = Interpolators.LINEAR
-            duration = DEFAULT_DURATION.inWholeMilliseconds
+            duration =
+                when (toState) {
+                    KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
+                    else -> DEFAULT_DURATION
+                }.inWholeMilliseconds
         }
     }
 
@@ -80,10 +83,11 @@
      * transition.
      */
     private fun listenForHubToLockscreen() {
-        glanceableHubTransitions.listenForLockscreenAndHubTransition(
+        glanceableHubTransitions.listenForGlanceableHubTransition(
             transitionName = "listenForHubToLockscreen",
             transitionOwnerName = TAG,
-            toScene = CommunalSceneKey.Blank,
+            fromState = KeyguardState.GLANCEABLE_HUB,
+            toState = KeyguardState.LOCKSCREEN,
         )
     }
 
@@ -175,7 +179,7 @@
 
     companion object {
         const val TAG = "FromGlanceableHubTransitionInteractor"
-        val DEFAULT_DURATION = 400.milliseconds
+        val DEFAULT_DURATION = 1.seconds
         val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION
     }
 }
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 57e9ac7..40b2c63 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
@@ -19,7 +19,6 @@
 import android.animation.ValueAnimator
 import android.util.MathUtils
 import com.android.app.animation.Interpolators
-import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
@@ -39,6 +38,7 @@
 import java.util.UUID
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
@@ -361,10 +361,11 @@
             return
         }
 
-        glanceableHubTransitions.listenForLockscreenAndHubTransition(
+        glanceableHubTransitions.listenForGlanceableHubTransition(
             transitionName = "listenForLockscreenToGlanceableHub",
             transitionOwnerName = TAG,
-            toScene = CommunalSceneKey.Communal
+            fromState = KeyguardState.LOCKSCREEN,
+            toState = KeyguardState.GLANCEABLE_HUB,
         )
     }
 
@@ -380,6 +381,7 @@
                     KeyguardState.AOD -> TO_AOD_DURATION
                     KeyguardState.DOZING -> TO_DOZING_DURATION
                     KeyguardState.DREAMING_LOCKSCREEN_HOSTED -> TO_DREAMING_HOSTED_DURATION
+                    KeyguardState.GLANCEABLE_HUB -> TO_GLANCEABLE_HUB_DURATION
                     else -> DEFAULT_DURATION
                 }.inWholeMilliseconds
         }
@@ -395,6 +397,6 @@
         val TO_AOD_DURATION = 500.milliseconds
         val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION
         val TO_GONE_DURATION = DEFAULT_DURATION
-        val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION
+        val TO_GLANCEABLE_HUB_DURATION = 1.seconds
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
index ca66153..809c0aee 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
@@ -52,20 +52,18 @@
      * externally. The progress is used for both transitions caused by user touch input or by
      * programmatic changes.
      */
-    fun listenForLockscreenAndHubTransition(
+    fun listenForGlanceableHubTransition(
         transitionName: String,
         transitionOwnerName: String,
-        toScene: CommunalSceneKey
+        fromState: KeyguardState,
+        toState: KeyguardState,
     ) {
-        val fromState: KeyguardState
-        val toState: KeyguardState
-        if (toScene == CommunalSceneKey.Blank) {
-            fromState = KeyguardState.GLANCEABLE_HUB
-            toState = KeyguardState.LOCKSCREEN
-        } else {
-            fromState = KeyguardState.LOCKSCREEN
-            toState = KeyguardState.GLANCEABLE_HUB
-        }
+        val toScene =
+            if (toState == KeyguardState.GLANCEABLE_HUB) {
+                CommunalSceneKey.Communal
+            } else {
+                CommunalSceneKey.Blank
+            }
         var transitionId: UUID? = null
         scope.launch("$transitionOwnerName#$transitionName") {
             communalInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index 8b278cd..b8ba098 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -185,13 +185,16 @@
             return getOrCreateFlow(edge)
                 .map { step ->
                     StateToValue(
-                            step.transitionState,
-                            when (step.transitionState) {
-                                STARTED -> stepToValue(step)
-                                RUNNING -> stepToValue(step)
-                                CANCELED -> onCancel?.invoke()
-                                FINISHED -> onFinish?.invoke()
-                            }
+                            from = step.from,
+                            to = step.to,
+                            transitionState = step.transitionState,
+                            value =
+                                when (step.transitionState) {
+                                    STARTED -> stepToValue(step)
+                                    RUNNING -> stepToValue(step)
+                                    CANCELED -> onCancel?.invoke()
+                                    FINISHED -> onFinish?.invoke()
+                                }
                         )
                         .also { logger.logTransitionStep(name, step, it.value) }
                 }
@@ -208,6 +211,10 @@
 }
 
 data class StateToValue(
+    val from: KeyguardState? = null,
+    val to: KeyguardState? = null,
     val transitionState: TransitionState = TransitionState.FINISHED,
     val value: Float? = 0f,
-)
+) {
+    fun isToOrFrom(state: KeyguardState) = from == state || to == state
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index c58a03c..ed48848 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.common.shared.model.TintedIcon
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
@@ -101,6 +102,10 @@
         val burnInLayerId = R.id.burn_in_layer
         val aodNotificationIconContainerId = R.id.aod_notification_icon_container
         val largeClockId = R.id.lockscreen_clock_view_large
+        val indicationArea = R.id.keyguard_indication_area
+        val startButton = R.id.start_button
+        val endButton = R.id.end_button
+        val lockIcon = R.id.lock_icon_view
 
         if (keyguardBottomAreaRefactor()) {
             view.setOnTouchListener { _, event ->
@@ -200,10 +205,29 @@
                         launch {
                             burnInParams
                                 .flatMapLatest { params -> viewModel.translationX(params) }
-                                .collect { x ->
-                                    childViews[burnInLayerId]?.translationX = x
-                                    childViews[largeClockId]?.translationX = x
-                                    childViews[aodNotificationIconContainerId]?.translationX = x
+                                .collect { state ->
+                                    val px = state.value ?: return@collect
+                                    when {
+                                        state.isToOrFrom(KeyguardState.AOD) -> {
+                                            childViews[largeClockId]?.translationX = px
+                                            childViews[burnInLayerId]?.translationX = px
+                                            childViews[aodNotificationIconContainerId]
+                                                ?.translationX = px
+                                        }
+                                        state.isToOrFrom(KeyguardState.GLANCEABLE_HUB) -> {
+                                            for ((key, childView) in childViews.entries) {
+                                                when (key) {
+                                                    indicationArea,
+                                                    startButton,
+                                                    endButton,
+                                                    lockIcon -> {
+                                                        // Do not move these views
+                                                    }
+                                                    else -> childView.translationX = px
+                                                }
+                                            }
+                                        }
+                                    }
                                 }
                         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt
index f208e85..8a3b57b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt
@@ -18,7 +18,9 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
@@ -38,6 +40,7 @@
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
     goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel,
+    keyguardInteractor: KeyguardInteractor,
 ) {
 
     /** The alpha level for the entire lockscreen while in AOD. */
@@ -46,7 +49,8 @@
                 keyguardTransitionInteractor.transitions,
                 goneToAodTransitionViewModel.enterFromTopAnimationAlpha.onStart { emit(0f) },
                 goneToDozingTransitionViewModel.lockscreenAlpha.onStart { emit(0f) },
-            ) { step, goneToAodAlpha, goneToDozingAlpha ->
+                keyguardInteractor.keyguardAlpha.onStart { emit(1f) },
+            ) { step, goneToAodAlpha, goneToDozingAlpha, keyguardAlpha ->
                 if (step.to == GONE) {
                     // When transitioning to GONE, only emit a value when complete as other
                     // transitions may be controlling the alpha fade
@@ -57,6 +61,8 @@
                     emit(goneToAodAlpha)
                 } else if (step.from == GONE && step.to == DOZING) {
                     emit(goneToDozingAlpha)
+                } else if (!migrateClocksToBlueprint()) {
+                    emit(keyguardAlpha)
                 }
             }
             .distinctUntilChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt
new file mode 100644
index 0000000..374a932
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.app.animation.Interpolators.EMPHASIZED
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.flow.Flow
+
+@SysUISingleton
+class DreamingToGlanceableHubTransitionViewModel
+@Inject
+constructor(animationFlow: KeyguardTransitionAnimationFlow) {
+
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = TO_GLANCEABLE_HUB_DURATION,
+            from = KeyguardState.DREAMING,
+            to = KeyguardState.GLANCEABLE_HUB,
+        )
+
+    fun dreamOverlayTranslationX(translatePx: Int): Flow<Float> {
+        return transitionAnimation.sharedFlow(
+            duration = TO_GLANCEABLE_HUB_DURATION,
+            onStep = { it * -translatePx },
+            interpolator = EMPHASIZED,
+            name = "DREAMING->GLANCEABLE_HUB: overlayTranslationX",
+        )
+    }
+
+    val dreamOverlayAlpha: Flow<Float> =
+        transitionAnimation.sharedFlow(
+            duration = 167.milliseconds,
+            onStep = { 1f - it },
+            name = "DREAMING->GLANCEABLE_HUB: dreamOverlayAlpha",
+        )
+
+    private companion object {
+        val TO_GLANCEABLE_HUB_DURATION = 1.seconds
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
index 6aa2eca..e5b5964 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
@@ -16,13 +16,22 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.app.animation.Interpolators.EMPHASIZED
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.StateToValue
+import com.android.systemui.res.R
 import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
 
 /**
  * Breaks down GLANCEABLE_HUB->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -33,32 +42,43 @@
 class GlanceableHubToLockscreenTransitionViewModel
 @Inject
 constructor(
+    configurationInteractor: ConfigurationInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
     private val transitionAnimation =
         animationFlow.setup(
-            duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION,
+            duration = TO_LOCKSCREEN_DURATION,
             from = KeyguardState.GLANCEABLE_HUB,
             to = KeyguardState.LOCKSCREEN,
         )
 
-    // TODO(b/315205222): implement full animation spec instead of just a simple fade.
     val keyguardAlpha: Flow<Float> =
-        transitionAnimation.sharedFlow(
-            duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION,
-            onStep = { it },
-            onFinish = { 1f },
-            onCancel = { 0f },
-            name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha",
-        )
+        transitionAnimation
+            .sharedFlow(
+                duration = 167.milliseconds,
+                startTime = 167.milliseconds,
+                onStep = { it },
+                onFinish = { 1f },
+                onCancel = { 0f },
+                name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha",
+            )
+            .onStart { emit(0f) }
 
-    // TODO(b/315205216): implement full animation spec instead of just a simple fade.
-    val notificationAlpha: Flow<Float> =
-        transitionAnimation.sharedFlow(
-            duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION,
-            onStep = { it },
-            onFinish = { 1f },
-            onCancel = { 0f },
-            name = "GLANCEABLE_HUB->LOCKSCREEN: notificationAlpha",
-        )
+    val keyguardTranslationX: Flow<StateToValue> =
+        configurationInteractor
+            .dimensionPixelSize(R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x)
+            .flatMapLatest { translatePx: Int ->
+                transitionAnimation.sharedFlowWithState(
+                    duration = TO_LOCKSCREEN_DURATION,
+                    onStep = { value -> -translatePx + value * translatePx },
+                    interpolator = EMPHASIZED,
+                    onCancel = { -translatePx.toFloat() },
+                    name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardTranslationX"
+                )
+            }
+
+    val notificationAlpha: Flow<Float> = keyguardAlpha
+
+    val notificationTranslationX: Flow<Float> =
+        keyguardTranslationX.map { it.value }.filterNotNull()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
index 188be24..4db942cc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
@@ -20,7 +20,9 @@
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -57,6 +59,7 @@
     lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel,
     lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel,
     lockscreenToPrimaryBouncerTransitionViewModel: LockscreenToPrimaryBouncerTransitionViewModel,
+    transitionInteractor: KeyguardTransitionInteractor,
 ) {
 
     data class PreviewMode(
@@ -71,6 +74,24 @@
      */
     private val previewMode = MutableStateFlow(PreviewMode())
 
+    private val showingLockscreen: Flow<Boolean> =
+        transitionInteractor.finishedKeyguardState.map { keyguardState ->
+            keyguardState == KeyguardState.LOCKSCREEN
+        }
+
+    /** The only time the expansion is important is while lockscreen is actively displayed */
+    private val shadeExpansionAlpha =
+        combine(
+            showingLockscreen,
+            shadeInteractor.anyExpansion,
+        ) { showingLockscreen, expansion ->
+            if (showingLockscreen) {
+                1 - expansion
+            } else {
+                0f
+            }
+        }
+
     /**
      * ID of the slot that's currently selected in the preview that renders exclusively in the
      * wallpaper picker application. This is ignored for the actual, real lock screen experience.
@@ -101,7 +122,7 @@
             lockscreenToGoneTransitionViewModel.shortcutsAlpha,
             lockscreenToOccludedTransitionViewModel.shortcutsAlpha,
             lockscreenToPrimaryBouncerTransitionViewModel.shortcutsAlpha,
-            shadeInteractor.qsExpansion.map { 1 - it },
+            shadeExpansionAlpha,
         )
 
     /** The source of truth of alpha for all of the quick affordances on lockscreen */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index f790d35..3511933 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.ui.StateToValue
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
@@ -165,8 +166,12 @@
         return aodBurnInViewModel.translationY(params)
     }
 
-    fun translationX(params: BurnInParameters): Flow<Float> {
-        return aodBurnInViewModel.translationX(params)
+    fun translationX(params: BurnInParameters): Flow<StateToValue> {
+        return merge(
+            aodBurnInViewModel.translationX(params).map { StateToValue(to = AOD, value = it) },
+            lockscreenToGlanceableHubTransitionViewModel.keyguardTranslationX,
+            glanceableHubToLockscreenTransitionViewModel.keyguardTranslationX,
+        )
     }
 
     fun scale(params: BurnInParameters): Flow<BurnInScaleViewModel> {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
index 3afa49e..978e71e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
@@ -16,13 +16,22 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.app.animation.Interpolators.EMPHASIZED
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.StateToValue
+import com.android.systemui.res.R
 import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
 
 /**
  * Breaks down LOCKSCREEN->GLANCEABLE_HUB transition into discrete steps for corresponding views to
@@ -33,6 +42,7 @@
 class LockscreenToGlanceableHubTransitionViewModel
 @Inject
 constructor(
+    configurationInteractor: ConfigurationInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) {
     private val transitionAnimation =
@@ -42,23 +52,35 @@
             to = KeyguardState.GLANCEABLE_HUB,
         )
 
-    // TODO(b/315205222): implement full animation spec instead of just a simple fade.
     val keyguardAlpha: Flow<Float> =
-        transitionAnimation.sharedFlow(
-            duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
-            onStep = { 1f - it },
-            onFinish = { 0f },
-            onCancel = { 1f },
-            name = "LOCKSCREEN->GLANCEABLE_HUB: keyguardAlpha",
-        )
+        transitionAnimation
+            .sharedFlow(
+                duration = 167.milliseconds,
+                onStep = { 1f - it },
+                onFinish = { 0f },
+                onCancel = { 1f },
+                name = "LOCKSCREEN->GLANCEABLE_HUB: keyguardAlpha",
+            )
+            .onStart { emit(1f) }
 
-    // TODO(b/315205216): implement full animation spec instead of just a simple fade.
-    val notificationAlpha: Flow<Float> =
-        transitionAnimation.sharedFlow(
-            duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
-            onStep = { 1f - it },
-            onFinish = { 0f },
-            onCancel = { 1f },
-            name = "LOCKSCREEN->GLANCEABLE_HUB: notificationAlpha",
-        )
+    val keyguardTranslationX: Flow<StateToValue> =
+        configurationInteractor
+            .dimensionPixelSize(R.dimen.lockscreen_to_hub_transition_lockscreen_translation_x)
+            .flatMapLatest { translatePx: Int ->
+                transitionAnimation.sharedFlowWithState(
+                    duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+                    onStep = { value -> value * translatePx },
+                    // Move notifications back to their original position since they can be
+                    // accessed from the shade.
+                    onFinish = { 0f },
+                    onCancel = { 0f },
+                    interpolator = EMPHASIZED,
+                    name = "LOCKSCREEN->GLANCEABLE_HUB: keyguardTranslationX"
+                )
+            }
+
+    val notificationAlpha: Flow<Float> = keyguardAlpha
+
+    val notificationTranslationX: Flow<Float> =
+        keyguardTranslationX.map { it.value }.filterNotNull()
 }
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 4f28b46..378ce52 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
@@ -108,6 +108,7 @@
                     0f
                 }
             },
+            onFinish = { 0f },
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
index e15e038..c629337 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
@@ -230,18 +230,12 @@
         val currentAllowMediaPlayerOnLockScreen = allowMediaPlayerOnLockScreen
         val useSplitShade = useSplitShade
         val shouldBeVisibleForSplitShade = shouldBeVisibleForSplitShade()
-
         visible =
             isMediaHostVisible &&
                 isBypassNotEnabled &&
                 keyguardOrUserSwitcher &&
                 currentAllowMediaPlayerOnLockScreen &&
                 shouldBeVisibleForSplitShade
-        if (visible) {
-            showMediaPlayer()
-        } else {
-            hideMediaPlayer()
-        }
         logger.logRefreshMediaPosition(
             reason = reason,
             visible = visible,
@@ -251,8 +245,17 @@
             mediaHostVisible = isMediaHostVisible,
             bypassNotEnabled = isBypassNotEnabled,
             currentAllowMediaPlayerOnLockScreen = currentAllowMediaPlayerOnLockScreen,
-            shouldBeVisibleForSplitShade = shouldBeVisibleForSplitShade
+            shouldBeVisibleForSplitShade = shouldBeVisibleForSplitShade,
         )
+        val currActiveContainer = activeContainer
+
+        logger.logActiveMediaContainer("before refreshMediaPosition", currActiveContainer)
+        if (visible) {
+            showMediaPlayer()
+        } else {
+            hideMediaPlayer()
+        }
+        logger.logActiveMediaContainer("after refreshMediaPosition", currActiveContainer)
 
         lastUsedStatusBarState = currentState
     }
@@ -293,9 +296,11 @@
     }
 
     private fun setVisibility(view: ViewGroup?, newVisibility: Int) {
-        val previousVisibility = view?.visibility
-        view?.visibility = newVisibility
-        if (previousVisibility != newVisibility) {
+        val currentMediaContainer = view ?: return
+
+        val previousVisibility = currentMediaContainer.visibility
+        currentMediaContainer.visibility = newVisibility
+        if (previousVisibility != newVisibility && currentMediaContainer is MediaContainerView) {
             visibilityChangedListener?.invoke(newVisibility == View.VISIBLE)
         }
     }
@@ -325,4 +330,7 @@
             }
         }
     }
+
+    private val activeContainer: ViewGroup? =
+        if (useSplitShade) splitShadeContainer else singlePaneContainer
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerLogger.kt
index 41fef88..0dd4b58 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerLogger.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media.controls.ui
 
+import android.view.ViewGroup
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel.DEBUG
 import com.android.systemui.log.dagger.KeyguardMediaControllerLog
@@ -36,8 +37,8 @@
         mediaHostVisible: Boolean,
         bypassNotEnabled: Boolean,
         currentAllowMediaPlayerOnLockScreen: Boolean,
-        shouldBeVisibleForSplitShade: Boolean
-    ) =
+        shouldBeVisibleForSplitShade: Boolean,
+    ) {
         logBuffer.log(
             TAG,
             DEBUG,
@@ -63,6 +64,19 @@
                     "shouldBeVisibleForSplitShade=$str3)"
             }
         )
+    }
+
+    fun logActiveMediaContainer(reason: String, activeContainer: ViewGroup?) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = reason
+                str2 = activeContainer.toString()
+            },
+            { "activeMediaContainerVisibility(reason=$str1, activeContainer=$str2)" }
+        )
+    }
 
     private companion object {
         private const val TAG = "KeyguardMediaControllerLog"
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
index cde3835..187b444 100644
--- 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
@@ -35,6 +35,7 @@
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.flow.take
@@ -55,6 +56,7 @@
 ) : Dumpable {
 
     private val initialized = AtomicBoolean(false)
+    private lateinit var currentTilesInteractor: CurrentTilesInteractor
 
     /** Start collection of signals following the user from [currentTilesInteractor]. */
     fun init(currentTilesInteractor: CurrentTilesInteractor) {
@@ -62,58 +64,74 @@
             return
         }
 
+        this.currentTilesInteractor = currentTilesInteractor
         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)
-                                }
-                                is AutoAddSignal.RemoveTracking -> {
-                                    qsPipelineLogger.logTileUnmarked(userId, signal.spec)
-                                    repository.unmarkTileAdded(userId, signal.spec)
-                                }
-                            }
-                        }
+                    launch { collectAutoAddSignalsForUser(userId) }
+                    launch { markTrackIfNotAddedTilesThatAreCurrent(userId) }
                 }
             }
         }
     }
 
+    private suspend fun markTrackIfNotAddedTilesThatAreCurrent(userId: Int) {
+        val trackIfNotAddedSpecs =
+            autoAddables
+                .map { it.autoAddTracking }
+                .filterIsInstance<AutoAddTracking.IfNotAdded>()
+                .map { it.spec }
+        currentTilesInteractor.currentTiles
+            .map { tiles -> tiles.map { it.spec } }
+            .collect {
+                it.filter { it in trackIfNotAddedSpecs }
+                    .forEach { spec -> repository.markTileAdded(userId, spec) }
+            }
+    }
+
+    private suspend fun CoroutineScope.collectAutoAddSignalsForUser(userId: Int) {
+        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)
+                    }
+                    is AutoAddSignal.RemoveTracking -> {
+                        qsPipelineLogger.logTileUnmarked(userId, signal.spec)
+                        repository.unmarkTileAdded(userId, signal.spec)
+                    }
+                }
+            }
+    }
+
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         with(pw.asIndenting()) {
             println("AutoAddables:")
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractor.kt
index cff95d8..1b3e585 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractor.kt
@@ -68,8 +68,7 @@
                     serviceInteractor.setUser(user)
 
                     // Wait for the CustomTileInteractor to become initialized first, because
-                    // binding
-                    // the service might access it
+                    // binding the service might access it
                     customTileInteractor.initForUser(user)
                     // Bind the TileService for not active tile
                     serviceInteractor.bindOnStart()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt
index fd96fc5..3e507cd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt
@@ -77,6 +77,13 @@
     suspend fun isTileToggleable(): Boolean = customTileRepository.isTileToggleable()
 
     /**
+     * True if the tile is active and false the otherwise. This effectively is a value of the
+     * [android.service.quicksettings.TileService.META_DATA_ACTIVE_TILE]. This is not the same as
+     * [Tile.STATE_ACTIVE].
+     */
+    suspend fun isTileActive(): Boolean = customTileRepository.isTileActive()
+
+    /**
      * Initializes the repository for the current user. Suspends until it's safe to call [getTile]
      * which needs at least one of the following:
      * - defaults are loaded;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileServiceInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileServiceInteractor.kt
index acff40f..79e903c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileServiceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileServiceInteractor.kt
@@ -58,7 +58,7 @@
     private val activityStarter: ActivityStarter,
     private val userActionInteractor: Lazy<CustomTileUserActionInteractor>,
     private val customTileInteractor: CustomTileInteractor,
-    private val userRepository: UserRepository,
+    userRepository: UserRepository,
     private val qsTileLogger: QSTileLogger,
     private val tileServices: TileServices,
     @QSTileScope private val tileScope: CoroutineScope,
@@ -78,10 +78,10 @@
         get() = tileReceivingInterface.mutableRefreshEvents
 
     /** Clears all pending binding for an active tile and binds not active one. */
-    fun bindOnStart() {
+    suspend fun bindOnStart() {
         try {
             with(getTileServiceManager()) {
-                if (isActiveTile) {
+                if (customTileInteractor.isTileActive()) {
                     clearPendingBind()
                 } else {
                     setBindRequested(true)
@@ -94,10 +94,10 @@
     }
 
     /** Binds active tile WITHOUT CLEARING pending binds. */
-    fun bindOnClick() {
+    suspend fun bindOnClick() {
         try {
             with(getTileServiceManager()) {
-                if (isActiveTile) {
+                if (customTileInteractor.isTileActive()) {
                     setBindRequested(true)
                     tileServiceInterface.onStartListening()
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt
index c3e1fea..a16ac36 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt
@@ -77,7 +77,7 @@
             qsTileLogger.logCustomTileUserActionDelivered(tileSpec)
         }
 
-    private fun click(
+    private suspend fun click(
         view: View?,
         activityLaunchForClick: PendingIntent?,
     ) {
@@ -114,9 +114,6 @@
     }
 
     fun startActivityAndCollapse(pendingIntent: PendingIntent) {
-        if (!pendingIntent.isActivity) {
-            return
-        }
         if (!isTokenGranted) {
             return
         }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 99e91c1..e577178 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -511,6 +511,12 @@
                             return true;
                         }
                     }
+                } else if (migrateClocksToBlueprint()) {
+                    // This final check handles swipes on HUNs and when Pulsing
+                    if (!bouncerShowing && didNotificationPanelInterceptEvent(ev)) {
+                        mShadeLogger.d("NSWVC: intercepted for HUN/PULSING");
+                        return true;
+                    }
                 }
                 return false;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index ca19f71..3908ede 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -615,14 +615,7 @@
             args.argi2 = state1;
             args.argi3 = state2;
             args.argi4 = animate ? 1 : 0;
-            Message msg = mHandler.obtainMessage(MSG_DISABLE, args);
-            if (Looper.myLooper() == mHandler.getLooper()) {
-                // If its the right looper execute immediately so hides can be handled quickly.
-                mHandler.handleMessage(msg);
-                msg.recycle();
-            } else {
-                msg.sendToTarget();
-            }
+            mHandler.obtainMessage(MSG_DISABLE, args).sendToTarget();
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index e598242..a4741a5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -25,8 +25,11 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
 import android.graphics.Matrix;
@@ -71,6 +74,7 @@
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.settingslib.Utils;
 import com.android.systemui.res.R;
+import com.android.systemui.statusbar.phone.CentralSurfaces;
 
 import com.google.android.material.bottomsheet.BottomSheetBehavior;
 import com.google.android.material.bottomsheet.BottomSheetDialog;
@@ -114,6 +118,7 @@
     private Button mButtonInput;
     private Button mButtonOpenApps;
     private Button mButtonSpecificApp;
+    private CharSequence mCurrentAppPackageName;
     private TextView mNoSearchResults;
 
     private final SparseArray<String> mSpecialCharacterNames = new SparseArray<>();
@@ -412,8 +417,10 @@
         mWindowManager.requestAppKeyboardShortcuts(result -> {
             // Add specific app shortcuts
             if (result.isEmpty()) {
+                mCurrentAppPackageName = null;
                 mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, false);
             } else {
+                mCurrentAppPackageName = result.get(0).getPackageName();
                 mSpecificAppGroup.addAll(reMapToKeyboardShortcutMultiMappingGroup(result));
                 mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, true);
             }
@@ -823,6 +830,7 @@
         mNoSearchResults = keyboardShortcutsView.findViewById(R.id.shortcut_search_no_result);
         mKeyboardShortcutsBottomSheetDialog.setContentView(keyboardShortcutsView);
         setButtonsDefaultStatus(keyboardShortcutsView);
+        populateCurrentAppButton();
         populateKeyboardShortcutSearchList(shortcutsContainer);
 
         // Workaround for solve issue about dialog not full expanded when landscape.
@@ -1272,6 +1280,41 @@
         mFullButtonList.add(mButtonSpecificApp);
     }
 
+    private void resetCurrentAppButton() {
+        if (mButtonSpecificApp == null) {
+            return;
+        }
+        mButtonSpecificApp.setText(
+                mContext.getString(R.string.keyboard_shortcut_search_category_current_app));
+        // TODO(b/325252986): Reset icon once the icon is implemented
+    }
+
+    private void populateCurrentAppButton() {
+        if (mButtonSpecificApp == null) {
+            return;
+        }
+        if (mCurrentAppPackageName != null) {
+            final int userId = mContext.getUserId();
+            try {
+                PackageManager pmUser = CentralSurfaces.getPackageManagerForUser(
+                        mContext,
+                        userId);
+                ApplicationInfo appInfo = pmUser.getApplicationInfoAsUser(
+                        mCurrentAppPackageName.toString(),
+                        0,
+                        userId);
+                // According to the API, we will always get a label
+                mButtonSpecificApp.setText(pmUser.getApplicationLabel(appInfo));
+                // TODO(b/325252986): Show icon once it has been defined
+            } catch (NameNotFoundException e) {
+                Log.e(TAG, "Package name not found", e);
+                resetCurrentAppButton();
+            }
+        } else {
+            resetCurrentAppButton();
+        }
+    }
+
     private void setButtonFocusColor(int i, boolean isFocused) {
         if (isFocused) {
             mFullButtonList.get(i).setTextColor(getColorOfTextColorOnAccent());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 9916ef6..1a06eec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -156,14 +156,12 @@
             final String action = intent.getAction();
 
             if (ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED.equals(action)) {
-                if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
-                    mKeyguardAllowingNotifications =
-                            intent.getBooleanExtra(EXTRA_KM_PRIVATE_NOTIFS_ALLOWED, false);
-                    if (mCurrentUserId == getSendingUserId()) {
-                        boolean changed = updateLockscreenNotificationSetting();
-                        if (changed) {
-                            notifyNotificationStateChanged();
-                        }
+                mKeyguardAllowingNotifications =
+                        intent.getBooleanExtra(EXTRA_KM_PRIVATE_NOTIFS_ALLOWED, false);
+                if (mCurrentUserId == getSendingUserId()) {
+                    boolean changed = updateLockscreenNotificationSetting();
+                    if (changed) {
+                        notifyNotificationStateChanged();
                     }
                 }
             }
@@ -176,36 +174,26 @@
             final String action = intent.getAction();
 
             if (ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals(action)) {
-                if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
-                    boolean changed = false;
-                    int sendingUserId = getSendingUserId();
-                    if (sendingUserId == USER_ALL) {
-                        // When a Device Owner triggers changes it's sent as USER_ALL. Normalize
-                        // the user before calling into DPM
-                        sendingUserId = mCurrentUserId;
-                        @SuppressLint("MissingPermission")
-                        List<UserInfo> users = mUserManager.getUsers();
-                        for (int i = users.size() - 1; i >= 0; i--) {
-                            changed |= updateDpcSettings(users.get(i).id);
-                        }
-                    } else {
-                        changed |= updateDpcSettings(sendingUserId);
-                    }
-
-                    if (mCurrentUserId == sendingUserId) {
-                        changed |= updateLockscreenNotificationSetting();
-                    }
-                    if (changed) {
-                        notifyNotificationStateChanged();
+                boolean changed = false;
+                int sendingUserId = getSendingUserId();
+                if (sendingUserId == USER_ALL) {
+                    // When a Device Owner triggers changes it's sent as USER_ALL. Normalize
+                    // the user before calling into DPM
+                    sendingUserId = mCurrentUserId;
+                    @SuppressLint("MissingPermission")
+                    List<UserInfo> users = mUserManager.getUsers();
+                    for (int i = users.size() - 1; i >= 0; i--) {
+                        changed |= updateDpcSettings(users.get(i).id);
                     }
                 } else {
-                    if (isCurrentProfile(getSendingUserId())) {
-                        mUsersAllowingPrivateNotifications.clear();
-                        updateLockscreenNotificationSetting();
-                        // TODO(b/231976036): Consolidate pipeline invalidations related to this
-                        //  event
-                        // notifyNotificationStateChanged();
-                    }
+                    changed |= updateDpcSettings(sendingUserId);
+                }
+
+                if (mCurrentUserId == sendingUserId) {
+                    changed |= updateLockscreenNotificationSetting();
+                }
+                if (changed) {
+                    notifyNotificationStateChanged();
                 }
             }
         }
@@ -225,12 +213,10 @@
                 updateCurrentProfilesCache();
             } else if (Objects.equals(action, Intent.ACTION_USER_ADDED)){
                 updateCurrentProfilesCache();
-                if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
-                    final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
-                    mBackgroundExecutor.execute(() -> {
-                        initValuesForUser(userId);
-                    });
-                }
+                final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
+                mBackgroundExecutor.execute(() -> {
+                    initValuesForUser(userId);
+                });
             } else if (profileAvailabilityActions(action)) {
                 updateCurrentProfilesCache();
             } else if (Objects.equals(action, Intent.ACTION_USER_UNLOCKED)) {
@@ -360,28 +346,16 @@
     }
 
     private void init() {
-        mLockscreenSettingsObserver = new ExecutorContentObserver(
-                mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)
-                        ? mBackgroundExecutor
-                        : mMainExecutor) {
+        mLockscreenSettingsObserver = new ExecutorContentObserver(mBackgroundExecutor) {
 
             @Override
             public void onChange(boolean selfChange, Collection<Uri> uris, int flags) {
-                if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
-                    @SuppressLint("MissingPermission")
-                    List<UserInfo> users = mUserManager.getUsers();
-                    for (int i = users.size() - 1; i >= 0; i--) {
-                        onChange(selfChange, uris, flags,users.get(i).getUserHandle());
-                    }
-                } else {
-                    // We don't know which user changed LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS or
-                    // LOCK_SCREEN_SHOW_NOTIFICATIONS, so we just dump our cache ...
-                    mUsersAllowingPrivateNotifications.clear();
-                    mUsersAllowingNotifications.clear();
-                    // ... and refresh all the notifications
-                    updateLockscreenNotificationSetting();
-                    notifyNotificationStateChanged();
+                @SuppressLint("MissingPermission")
+                List<UserInfo> users = mUserManager.getUsers();
+                for (int i = users.size() - 1; i >= 0; i--) {
+                    onChange(selfChange, uris, flags,users.get(i).getUserHandle());
                 }
+
             }
 
             // Note: even though this is an override, this method is not called by the OS
@@ -390,22 +364,20 @@
             @Override
             public void onChange(boolean selfChange, Collection<Uri> uris,
                     int flags, UserHandle user) {
-                if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
-                    boolean changed = false;
-                    for (Uri uri: uris) {
-                        if (SHOW_LOCKSCREEN.equals(uri)) {
-                            changed |= updateUserShowSettings(user.getIdentifier());
-                        } else if (SHOW_PRIVATE_LOCKSCREEN.equals(uri)) {
-                            changed |= updateUserShowPrivateSettings(user.getIdentifier());
-                        }
+                boolean changed = false;
+                for (Uri uri: uris) {
+                    if (SHOW_LOCKSCREEN.equals(uri)) {
+                        changed |= updateUserShowSettings(user.getIdentifier());
+                    } else if (SHOW_PRIVATE_LOCKSCREEN.equals(uri)) {
+                        changed |= updateUserShowPrivateSettings(user.getIdentifier());
                     }
+                }
 
-                    if (mCurrentUserId == user.getIdentifier()) {
-                        changed |= updateLockscreenNotificationSetting();
-                    }
-                    if (changed) {
-                        notifyNotificationStateChanged();
-                    }
+                if (mCurrentUserId == user.getIdentifier()) {
+                    changed |= updateLockscreenNotificationSetting();
+                }
+                if (changed) {
+                    notifyNotificationStateChanged();
                 }
             }
         };
@@ -432,16 +404,10 @@
                 mLockscreenSettingsObserver,
                 USER_ALL);
 
-        if (!mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
-            mContext.getContentResolver().registerContentObserver(
-                    Settings.Global.getUriFor(Settings.Global.ZEN_MODE), false,
-                    mSettingsObserver);
-        }
 
         mBroadcastDispatcher.registerReceiver(mAllUsersReceiver,
                 new IntentFilter(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED),
-                mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)
-                        ? mBackgroundExecutor : null, UserHandle.ALL);
+                mBackgroundExecutor, UserHandle.ALL);
         if (keyguardPrivateNotifications()) {
             mBroadcastDispatcher.registerReceiver(mKeyguardReceiver,
                     new IntentFilter(ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED),
@@ -471,17 +437,13 @@
         mCurrentUserId = mUserTracker.getUserId(); // in case we reg'd receiver too late
         updateCurrentProfilesCache();
 
-        if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
-            // Set  up
-            mBackgroundExecutor.execute(() -> {
-                @SuppressLint("MissingPermission") List<UserInfo> users = mUserManager.getUsers();
-                for (int i = users.size() - 1; i >= 0; i--) {
-                    initValuesForUser(users.get(i).id);
-                }
-            });
-        } else {
-            mSettingsObserver.onChange(false);  // set up
-        }
+        // Set  up
+        mBackgroundExecutor.execute(() -> {
+            @SuppressLint("MissingPermission") List<UserInfo> users = mUserManager.getUsers();
+            for (int i = users.size() - 1; i >= 0; i--) {
+                initValuesForUser(users.get(i).id);
+            }
+        });
     }
 
     private void initValuesForUser(@UserIdInt int userId) {
@@ -519,26 +481,15 @@
         boolean show;
         boolean allowedByDpm;
 
-        if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
-            if (keyguardPrivateNotifications()) {
-                show = mUsersUsersAllowingNotifications.get(mCurrentUserId);
-            } else {
-                show = mUsersUsersAllowingNotifications.get(mCurrentUserId)
-                        && mKeyguardAllowingNotifications;
-            }
-            // If DPC never notified us about a user, that means they have no policy for the user,
-            // and they allow the behavior
-            allowedByDpm = mUsersDpcAllowingNotifications.get(mCurrentUserId, true);
+        if (keyguardPrivateNotifications()) {
+            show = mUsersUsersAllowingNotifications.get(mCurrentUserId);
         } else {
-            show = mSecureSettings.getIntForUser(
-                    LOCK_SCREEN_SHOW_NOTIFICATIONS,
-                    1,
-                    mCurrentUserId) != 0;
-            final int dpmFlags = mDevicePolicyManager.getKeyguardDisabledFeatures(
-                    null /* admin */, mCurrentUserId);
-            allowedByDpm = (dpmFlags
-                    & KEYGUARD_DISABLE_SECURE_NOTIFICATIONS) == 0;
+            show = mUsersUsersAllowingNotifications.get(mCurrentUserId)
+                    && mKeyguardAllowingNotifications;
         }
+        // If DPC never notified us about a user, that means they have no policy for the user,
+        // and they allow the behavior
+        allowedByDpm = mUsersDpcAllowingNotifications.get(mCurrentUserId, true);
 
         final boolean oldValue = mShowLockscreenNotifications;
         setShowLockscreenNotifications(show && allowedByDpm);
@@ -600,42 +551,24 @@
      * when the lockscreen is in "public" (secure & locked) mode?
      */
     public boolean userAllowsPrivateNotificationsInPublic(int userHandle) {
-        if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
-            if (userHandle == USER_ALL) {
-                userHandle = mCurrentUserId;
-            }
-            if (mUsersUsersAllowingPrivateNotifications.indexOfKey(userHandle) < 0) {
-                Log.i(TAG, "Asking for redact notifs setting too early", new Throwable());
-                return false;
-            }
-            if (mUsersDpcAllowingPrivateNotifications.indexOfKey(userHandle) < 0) {
-                Log.i(TAG, "Asking for redact notifs dpm override too early", new Throwable());
-                return false;
-            }
-            if (keyguardPrivateNotifications()) {
-                return mUsersUsersAllowingPrivateNotifications.get(userHandle)
-                        && mUsersDpcAllowingPrivateNotifications.get(userHandle)
-                        && mKeyguardAllowingNotifications;
-            } else {
-                return mUsersUsersAllowingPrivateNotifications.get(userHandle)
-                        && mUsersDpcAllowingPrivateNotifications.get(userHandle);
-            }
+        if (userHandle == USER_ALL) {
+            userHandle = mCurrentUserId;
+        }
+        if (mUsersUsersAllowingPrivateNotifications.indexOfKey(userHandle) < 0) {
+            Log.i(TAG, "Asking for redact notifs setting too early", new Throwable());
+            return false;
+        }
+        if (mUsersDpcAllowingPrivateNotifications.indexOfKey(userHandle) < 0) {
+            Log.i(TAG, "Asking for redact notifs dpm override too early", new Throwable());
+            return false;
+        }
+        if (keyguardPrivateNotifications()) {
+            return mUsersUsersAllowingPrivateNotifications.get(userHandle)
+                    && mUsersDpcAllowingPrivateNotifications.get(userHandle)
+                    && mKeyguardAllowingNotifications;
         } else {
-            if (userHandle == USER_ALL) {
-                return true;
-            }
-
-            if (mUsersAllowingPrivateNotifications.indexOfKey(userHandle) < 0) {
-                final boolean allowedByUser = 0 != mSecureSettings.getIntForUser(
-                        LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0, userHandle);
-                final boolean allowedByDpm = adminAllowsKeyguardFeature(userHandle,
-                        KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS);
-                final boolean allowed = allowedByUser && allowedByDpm;
-                mUsersAllowingPrivateNotifications.append(userHandle, allowed);
-                return allowed;
-            }
-
-            return mUsersAllowingPrivateNotifications.get(userHandle);
+            return mUsersUsersAllowingPrivateNotifications.get(userHandle)
+                    && mUsersDpcAllowingPrivateNotifications.get(userHandle);
         }
     }
 
@@ -688,48 +621,30 @@
      * "public" (secure & locked) mode?
      */
     public boolean userAllowsNotificationsInPublic(int userHandle) {
-        if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
-            // Unlike 'show private', settings does not show a copy of this setting for each
-            // profile, so it inherits from the parent user.
-            if (userHandle == USER_ALL || mCurrentManagedProfiles.contains(userHandle)) {
-                userHandle = mCurrentUserId;
-            }
-            if (mUsersUsersAllowingNotifications.indexOfKey(userHandle) < 0) {
-                // TODO(b/301955929): STOP_SHIP (stop flag flip): remove this read and use a safe
-                // default value before moving to 'released'
-                Log.wtf(TAG, "Asking for show notifs setting too early", new Throwable());
-                updateUserShowSettings(userHandle);
-            }
-            if (mUsersDpcAllowingNotifications.indexOfKey(userHandle) < 0) {
-                // TODO(b/301955929): STOP_SHIP (stop flag flip): remove this read and use a safe
-                // default value before moving to 'released'
-                Log.wtf(TAG, "Asking for show notifs dpm override too early", new Throwable());
-                updateDpcSettings(userHandle);
-            }
-            if (keyguardPrivateNotifications()) {
-                return mUsersUsersAllowingNotifications.get(userHandle)
-                        && mUsersDpcAllowingNotifications.get(userHandle);
-            } else {
-                return mUsersUsersAllowingNotifications.get(userHandle)
-                        && mUsersDpcAllowingNotifications.get(userHandle)
-                        && mKeyguardAllowingNotifications;
-            }
+        // Unlike 'show private', settings does not show a copy of this setting for each
+        // profile, so it inherits from the parent user.
+        if (userHandle == USER_ALL || mCurrentManagedProfiles.contains(userHandle)) {
+            userHandle = mCurrentUserId;
+        }
+        if (mUsersUsersAllowingNotifications.indexOfKey(userHandle) < 0) {
+            // TODO(b/301955929): STOP_SHIP (stop flag flip): remove this read and use a safe
+            // default value before moving to 'released'
+            Log.wtf(TAG, "Asking for show notifs setting too early", new Throwable());
+            updateUserShowSettings(userHandle);
+        }
+        if (mUsersDpcAllowingNotifications.indexOfKey(userHandle) < 0) {
+            // TODO(b/301955929): STOP_SHIP (stop flag flip): remove this read and use a safe
+            // default value before moving to 'released'
+            Log.wtf(TAG, "Asking for show notifs dpm override too early", new Throwable());
+            updateDpcSettings(userHandle);
+        }
+        if (keyguardPrivateNotifications()) {
+            return mUsersUsersAllowingNotifications.get(userHandle)
+                    && mUsersDpcAllowingNotifications.get(userHandle);
         } else {
-            if (isCurrentProfile(userHandle) && userHandle != mCurrentUserId) {
-                return true;
-            }
-
-            if (mUsersAllowingNotifications.indexOfKey(userHandle) < 0) {
-                final boolean allowedByUser = 0 != mSecureSettings.getIntForUser(
-                        LOCK_SCREEN_SHOW_NOTIFICATIONS, 0, userHandle);
-                final boolean allowedByDpm = adminAllowsKeyguardFeature(userHandle,
-                        KEYGUARD_DISABLE_SECURE_NOTIFICATIONS);
-                final boolean allowedBySystem = mKeyguardManager.getPrivateNotificationsAllowed();
-                final boolean allowed = allowedByUser && allowedByDpm && allowedBySystem;
-                mUsersAllowingNotifications.append(userHandle, allowed);
-                return allowed;
-            }
-            return mUsersAllowingNotifications.get(userHandle);
+            return mUsersUsersAllowingNotifications.get(userHandle)
+                    && mUsersDpcAllowingNotifications.get(userHandle)
+                    && mKeyguardAllowingNotifications;
         }
     }
 
@@ -766,13 +681,7 @@
             return true;
         }
         NotificationEntry entry = mCommonNotifCollectionLazy.get().getEntry(key);
-        if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
-            return entry != null && entry.isChannelVisibilityPrivate();
-        } else {
-            return entry != null
-                    && entry.getRanking().getLockscreenVisibilityOverride()
-                    == Notification.VISIBILITY_PRIVATE;
-        }
+        return entry != null && entry.isChannelVisibilityPrivate();
     }
 
     @SuppressLint("MissingPermission")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index 9fb453a..65ab4fd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -136,7 +136,7 @@
         }
 
         launch {
-            viewModel.clearAllButton.accessibilityDescriptionId.collect { textId ->
+            viewModel.manageOrHistoryButton.accessibilityDescriptionId.collect { textId ->
                 footer.setManageOrHistoryButtonDescription(textId)
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
index d7fe36f..332ece4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
@@ -219,15 +219,11 @@
     }
 
     private fun isRankingVisibilitySecret(entry: NotificationEntry): Boolean {
-        return if (featureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
-            // ranking.lockscreenVisibilityOverride contains possibly out of date DPC and Setting
-            // info, and NotificationLockscreenUserManagerImpl is already listening for updates
-            // to those
-            entry.ranking.channel != null && entry.ranking.channel.lockscreenVisibility ==
+        // ranking.lockscreenVisibilityOverride contains possibly out of date DPC and Setting
+        // info, and NotificationLockscreenUserManagerImpl is already listening for updates
+        // to those
+        return entry.ranking.channel != null && entry.ranking.channel.lockscreenVisibility ==
                     VISIBILITY_SECRET
-        } else {
-            entry.ranking.lockscreenVisibilityOverride == VISIBILITY_SECRET
-        }
     }
 
     override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run {
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 d2ff266..715505d 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
@@ -1039,6 +1039,11 @@
         mView.setTranslationY(translationY);
     }
 
+    /** Set view x-translation */
+    public void setTranslationX(float translationX) {
+        mView.setTranslationX(translationX);
+    }
+
     public int indexOfChild(View view) {
         return mView.indexOfChild(view);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
index 4897b42..534e5c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.view
 
+import android.os.Trace
 import android.service.notification.NotificationListenerService
 import androidx.annotation.VisibleForTesting
 import com.android.internal.statusbar.IStatusBarService
@@ -182,6 +183,8 @@
 
             maybeLogVisibilityChanges(newlyVisible, noLongerVisible, activeNotifCount)
             updateExpansionStates(newlyVisible, noLongerVisible)
+            Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Active]", activeNotifCount)
+            Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Visible]", newVisibilities.size)
 
             lastLoggedVisibilities.clear()
             lastLoggedVisibilities.putAll(newVisibilities)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index daea8af..77e146b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -144,6 +144,8 @@
                             .collect { y -> controller.setTranslationY(y) }
                     }
 
+                    launch { viewModel.translationX.collect { x -> controller.translationX = x } }
+
                     if (!sceneContainerFlags.isEnabled()) {
                         launch {
                             viewModel.expansionAlpha(viewState).collect {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index f325157..6b949a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -363,14 +363,9 @@
                 lockscreenToGlanceableHubRunning,
                 glanceableHubToLockscreenRunning,
                 merge(
-                        lockscreenToGlanceableHubTransitionViewModel.notificationAlpha,
-                        glanceableHubToLockscreenTransitionViewModel.notificationAlpha,
-                    )
-                    .onStart {
-                        // Transition flows don't emit a value on start, kick things off so the
-                        // combine starts.
-                        emit(1f)
-                    }
+                    lockscreenToGlanceableHubTransitionViewModel.notificationAlpha,
+                    glanceableHubToLockscreenTransitionViewModel.notificationAlpha,
+                )
             ) { lockscreenToGlanceableHubRunning, glanceableHubToLockscreenRunning, alpha ->
                 if (isOnGlanceableHubWithoutShade) {
                     // Notifications should not be visible on the glanceable hub.
@@ -409,6 +404,16 @@
     }
 
     /**
+     * The container may need to be translated in the x direction as the keyguard fades out, such as
+     * when swiping open the glanceable hub from the lockscreen.
+     */
+    val translationX: Flow<Float> =
+        merge(
+            lockscreenToGlanceableHubTransitionViewModel.notificationTranslationX,
+            glanceableHubToLockscreenTransitionViewModel.notificationTranslationX,
+        )
+
+    /**
      * When on keyguard, there is limited space to display notifications so calculate how many could
      * be shown. Otherwise, there is no limit since the vertical space will be scrollable.
      *
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
index 569e064..8690d4e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
@@ -99,6 +99,16 @@
         }
     }
 
+    @Test
+    fun useCredentialOwnerWhenParentProfileIsNull() {
+        val value = 1
+
+        whenever(userManager.getProfileParent(eq(USER_ID))).thenReturn(null)
+        whenever(userManager.getCredentialOwnerProfile(eq(USER_ID))).thenReturn(value)
+
+        assertThat(interactor.getParentProfileIdOrSelfId(USER_ID)).isEqualTo(value)
+    }
+
     @Test fun pinCredentialWhenGood() = pinCredential(goodCredential())
 
     @Test fun pinCredentialWhenBad() = pinCredential(badCredential())
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 5b93df5..a5d577dc 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
@@ -57,7 +57,6 @@
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceTimeBy
-import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -148,6 +147,7 @@
                     keyguardInteractor = keyguardInteractor,
                     transitionRepository = transitionRepository,
                     transitionInteractor = transitionInteractor,
+                    glanceableHubTransitions = glanceableHubTransitions,
                 )
                 .apply { start() }
 
@@ -1372,6 +1372,44 @@
         }
 
     @Test
+    fun dreamingToGlanceableHub() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to DREAMING
+            keyguardRepository.setDreaming(true)
+            runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.DREAMING)
+            runCurrent()
+
+            // WHEN a transition to the glanceable hub starts
+            val currentScene = CommunalSceneKey.Blank
+            val targetScene = CommunalSceneKey.Communal
+
+            val progress = MutableStateFlow(0f)
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Transition(
+                        fromScene = currentScene,
+                        toScene = targetScene,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            communalInteractor.setTransitionState(transitionState)
+            progress.value = .1f
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    ownerName = FromDreamingTransitionInteractor::class.simpleName,
+                    from = KeyguardState.DREAMING,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                    animatorAssertion = { it.isNull() }, // transition should be manually animated
+                )
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
     fun lockscreenToOccluded() =
         testScope.runTest {
             // GIVEN a prior transition has run to LOCKSCREEN
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
index 0e9197e..f0607f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
@@ -22,7 +22,8 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
+import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.kosmos.testScope
@@ -51,8 +52,8 @@
         underTest =
             animationFlow.setup(
                 duration = 1000.milliseconds,
-                from = KeyguardState.GONE,
-                to = KeyguardState.DREAMING,
+                from = GONE,
+                to = DREAMING,
             )
     }
 
@@ -192,17 +193,65 @@
             runCurrent()
 
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
-            assertThat(animationValues).isEqualTo(StateToValue(TransitionState.STARTED, 0f))
+            assertThat(animationValues)
+                .isEqualTo(
+                    StateToValue(
+                        from = GONE,
+                        to = DREAMING,
+                        transitionState = TransitionState.STARTED,
+                        value = 0f
+                    )
+                )
             repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
-            assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 0.6f))
+            assertThat(animationValues)
+                .isEqualTo(
+                    StateToValue(
+                        from = GONE,
+                        to = DREAMING,
+                        transitionState = TransitionState.RUNNING,
+                        value = 0.6f
+                    )
+                )
             repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
-            assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 1.2f))
+            assertThat(animationValues)
+                .isEqualTo(
+                    StateToValue(
+                        from = GONE,
+                        to = DREAMING,
+                        transitionState = TransitionState.RUNNING,
+                        value = 1.2f
+                    )
+                )
             repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
-            assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 1.6f))
+            assertThat(animationValues)
+                .isEqualTo(
+                    StateToValue(
+                        from = GONE,
+                        to = DREAMING,
+                        transitionState = TransitionState.RUNNING,
+                        value = 1.6f
+                    )
+                )
             repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
-            assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 2f))
+            assertThat(animationValues)
+                .isEqualTo(
+                    StateToValue(
+                        from = GONE,
+                        to = DREAMING,
+                        transitionState = TransitionState.RUNNING,
+                        value = 2f
+                    )
+                )
             repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
-            assertThat(animationValues).isEqualTo(StateToValue(TransitionState.FINISHED, null))
+            assertThat(animationValues)
+                .isEqualTo(
+                    StateToValue(
+                        from = GONE,
+                        to = DREAMING,
+                        transitionState = TransitionState.FINISHED,
+                        value = null
+                    )
+                )
         }
 
     @Test
@@ -251,8 +300,8 @@
         state: TransitionState = TransitionState.RUNNING
     ): TransitionStep {
         return TransitionStep(
-            from = KeyguardState.GONE,
-            to = KeyguardState.DREAMING,
+            from = GONE,
+            to = DREAMING,
             value = value,
             transitionState = state,
             ownerName = "GoneToDreamingTransitionViewModelTest"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
index bfa8433..716c40d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
@@ -59,7 +59,14 @@
             // The animation should only start > .4f way through
             repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             assertThat(enterFromTopTranslationY)
-                .isEqualTo(StateToValue(TransitionState.STARTED, pixels))
+                .isEqualTo(
+                    StateToValue(
+                        from = KeyguardState.GONE,
+                        to = KeyguardState.AOD,
+                        transitionState = TransitionState.STARTED,
+                        value = pixels
+                    )
+                )
 
             repository.sendTransitionStep(step(.55f))
             assertThat(enterFromTopTranslationY!!.value ?: -1f).isIn(Range.closed(pixels, 0f))
@@ -70,7 +77,14 @@
             // At the end, the translation should be complete and set to zero
             repository.sendTransitionStep(step(1f))
             assertThat(enterFromTopTranslationY)
-                .isEqualTo(StateToValue(TransitionState.RUNNING, 0f))
+                .isEqualTo(
+                    StateToValue(
+                        from = KeyguardState.GONE,
+                        to = KeyguardState.AOD,
+                        transitionState = TransitionState.RUNNING,
+                        value = 0f
+                    )
+                )
         }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
index 18a34ba..1f14afa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
@@ -44,6 +44,8 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger
@@ -75,6 +77,7 @@
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.MockitoAnnotations
+import kotlin.test.assertEquals
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -130,6 +133,8 @@
     @Mock
     private lateinit var lockscreenToPrimaryBouncerTransitionViewModel:
         LockscreenToPrimaryBouncerTransitionViewModel
+    @Mock
+    private lateinit var transitionInteractor: KeyguardTransitionInteractor
 
     private lateinit var underTest: KeyguardQuickAffordancesCombinedViewModel
 
@@ -146,6 +151,8 @@
     // the viewModel does a `map { 1 - it }` on this value, which is why it's different
     private val intendedShadeAlphaMutableStateFlow: MutableStateFlow<Float> = MutableStateFlow(0f)
 
+    private val intendedFinishedKeyguardStateFlow = MutableStateFlow(KeyguardState.LOCKSCREEN)
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -242,6 +249,7 @@
 
         intendedAlphaMutableStateFlow.value = 1f
         intendedShadeAlphaMutableStateFlow.value = 0f
+        intendedFinishedKeyguardStateFlow.value = KeyguardState.LOCKSCREEN
         whenever(aodToLockscreenTransitionViewModel.shortcutsAlpha)
             .thenReturn(intendedAlphaMutableStateFlow)
         whenever(dozingToLockscreenTransitionViewModel.shortcutsAlpha).thenReturn(emptyFlow())
@@ -263,7 +271,9 @@
         whenever(lockscreenToOccludedTransitionViewModel.shortcutsAlpha).thenReturn(emptyFlow())
         whenever(lockscreenToPrimaryBouncerTransitionViewModel.shortcutsAlpha)
             .thenReturn(emptyFlow())
-        whenever(shadeInteractor.qsExpansion).thenReturn(intendedShadeAlphaMutableStateFlow)
+        whenever(shadeInteractor.anyExpansion).thenReturn(intendedShadeAlphaMutableStateFlow)
+        whenever(transitionInteractor.finishedKeyguardState)
+            .thenReturn(intendedFinishedKeyguardStateFlow)
 
         underTest =
             KeyguardQuickAffordancesCombinedViewModel(
@@ -304,7 +314,8 @@
                 lockscreenToGoneTransitionViewModel = lockscreenToGoneTransitionViewModel,
                 lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel,
                 lockscreenToPrimaryBouncerTransitionViewModel =
-                    lockscreenToPrimaryBouncerTransitionViewModel
+                    lockscreenToPrimaryBouncerTransitionViewModel,
+                transitionInteractor = transitionInteractor,
             )
     }
 
@@ -682,6 +693,30 @@
             )
         }
 
+    @Test
+    fun shadeExpansionAlpha_changes_whenOnLockscreen() =
+        testScope.runTest {
+            intendedFinishedKeyguardStateFlow.value = KeyguardState.LOCKSCREEN
+            intendedShadeAlphaMutableStateFlow.value = 0.25f
+            val underTest = collectLastValue(underTest.transitionAlpha)
+            assertEquals(0.75f, underTest())
+
+            intendedShadeAlphaMutableStateFlow.value = 0.3f
+            assertEquals(0.7f, underTest())
+        }
+
+    @Test
+    fun shadeExpansionAlpha_alwaysZero_whenNotOnLockscreen() =
+        testScope.runTest {
+            intendedFinishedKeyguardStateFlow.value = KeyguardState.GONE
+            intendedShadeAlphaMutableStateFlow.value = 0.5f
+            val underTest = collectLastValue(underTest.transitionAlpha)
+            assertEquals(0f, underTest())
+
+            intendedShadeAlphaMutableStateFlow.value = 0.25f
+            assertEquals(0f, underTest())
+        }
+
     private suspend fun setUpQuickAffordanceModel(
         position: KeyguardQuickAffordancePosition,
         testConfig: TestConfig,
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 c226790..b426d1d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -450,7 +450,7 @@
 
         mSetFlagsRule.enableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
 
-        // THEN touch should NOT be intercepted by NotificationShade
+        // THEN touch should be intercepted by NotificationShade
         assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isTrue()
     }
 
@@ -469,7 +469,35 @@
 
         mSetFlagsRule.enableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
 
-        // THEN touch should NOT be intercepted by NotificationShade
+        // THEN touch should be intercepted by NotificationShade
+        assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isTrue()
+    }
+
+    @Test
+    fun shouldInterceptTouchEvent_dozingAndPulsing_touchIntercepted() {
+        // GIVEN dozing
+        whenever(sysuiStatusBarStateController.isDozing).thenReturn(true)
+        // AND pulsing
+        whenever(dozeServiceHost.isPulsing()).thenReturn(true)
+        // AND status bar doesn't want it
+        whenever(statusBarKeyguardViewManager.shouldInterceptTouchEvent(DOWN_EVENT))
+            .thenReturn(false)
+        // AND shade is not fully expanded
+        whenever(notificationPanelViewController.isFullyExpanded()).thenReturn(false)
+        // AND the lock icon does NOT want the touch
+        whenever(lockIconViewController.willHandleTouchWhileDozing(DOWN_EVENT)).thenReturn(false)
+        // AND quick settings controller DOES want it
+        whenever(quickSettingsController.shouldQuickSettingsIntercept(any(), any(), any()))
+            .thenReturn(true)
+        // AND bouncer is not showing
+        whenever(centralSurfaces.isBouncerShowing()).thenReturn(false)
+        // AND panel view controller wants it
+        whenever(notificationPanelViewController.handleExternalInterceptTouch(DOWN_EVENT))
+            .thenReturn(true)
+
+        mSetFlagsRule.enableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+
+        // THEN touch should be intercepted by NotificationShade
         assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isTrue()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java
index 3811f04..06410cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java
@@ -431,28 +431,6 @@
 
     @Test
     public void publicMode_settingsDisallow() {
-        mFeatureFlags.set(Flags.NOTIF_LS_BACKGROUND_THREAD, true);
-        // GIVEN an 'unfiltered-keyguard-showing' state
-        setupUnfilteredState(mEntry);
-
-        // WHEN the notification's user is in public mode and settings are configured to disallow
-        // notifications in public mode
-        when(mLockscreenUserManager.isLockscreenPublicMode(NOTIF_USER_ID)).thenReturn(true);
-        when(mLockscreenUserManager.userAllowsNotificationsInPublic(NOTIF_USER_ID))
-                .thenReturn(false);
-
-        mEntry.setRanking(new RankingBuilder()
-                .setChannel(new NotificationChannel("1", "1", 4))
-                .setVisibilityOverride(VISIBILITY_NO_OVERRIDE)
-                .setKey(mEntry.getKey()).build());
-
-        // THEN filter out the entry
-        assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry));
-    }
-
-    @Test
-    public void publicMode_settingsDisallow_mainThread() {
-        mFeatureFlags.set(Flags.NOTIF_LS_BACKGROUND_THREAD, false);
         // GIVEN an 'unfiltered-keyguard-showing' state
         setupUnfilteredState(mEntry);
 
@@ -473,7 +451,6 @@
 
     @Test
     public void publicMode_nullChannel_allowed() {
-        mFeatureFlags.set(Flags.NOTIF_LS_BACKGROUND_THREAD, true);
         // GIVEN an 'unfiltered-keyguard-showing' state
         setupUnfilteredState(mEntry);
 
@@ -490,7 +467,6 @@
 
     @Test
     public void publicMode_notifDisallowed() {
-        mFeatureFlags.set(Flags.NOTIF_LS_BACKGROUND_THREAD, true);
         NotificationChannel channel = new NotificationChannel("1", "1", IMPORTANCE_HIGH);
         channel.setLockscreenVisibility(VISIBILITY_SECRET);
         // GIVEN an 'unfiltered-keyguard-showing' state
@@ -509,23 +485,6 @@
     }
 
     @Test
-    public void publicMode_notifDisallowed_mainThread() {
-        mFeatureFlags.set(Flags.NOTIF_LS_BACKGROUND_THREAD, false);
-        // GIVEN an 'unfiltered-keyguard-showing' state
-        setupUnfilteredState(mEntry);
-
-        // WHEN the notification's user is in public mode and settings are configured to disallow
-        // notifications in public mode
-        when(mLockscreenUserManager.isLockscreenPublicMode(CURR_USER_ID)).thenReturn(true);
-        mEntry.setRanking(new RankingBuilder()
-                .setKey(mEntry.getKey())
-                .setVisibilityOverride(VISIBILITY_SECRET).build());
-
-        // THEN filter out the entry
-        assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry));
-    }
-
-    @Test
     public void doesNotExceedThresholdToShow() {
         // GIVEN an 'unfiltered-keyguard-showing' state
         setupUnfilteredState(mEntry);
@@ -579,7 +538,6 @@
 
     @Test
     public void notificationChannelVisibilityNoOverride() {
-        mFeatureFlags.set(Flags.NOTIF_LS_BACKGROUND_THREAD, true);
         // GIVEN a VISIBILITY_PRIVATE notification
         NotificationEntryBuilder entryBuilder = new NotificationEntryBuilder()
                 .setUser(new UserHandle(NOTIF_USER_ID));
@@ -602,7 +560,6 @@
 
     @Test
     public void notificationChannelVisibilitySecret() {
-        mFeatureFlags.set(Flags.NOTIF_LS_BACKGROUND_THREAD, true);
         // GIVEN a VISIBILITY_PRIVATE notification
         NotificationEntryBuilder entryBuilder = new NotificationEntryBuilder()
                 .setUser(new UserHandle(NOTIF_USER_ID));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index b9b8722..3e0082c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -52,6 +52,7 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
+import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -274,8 +275,7 @@
                 )
             )
             runCurrent()
-            // Expected alpha is inverse of progress as notifications are fading away
-            assertThat(alpha).isEqualTo(1 - progress)
+            assertThat(alpha).isIn(Range.closed(0f, 1f))
 
             // Finish transition to glanceable hub
             keyguardTransitionRepository.sendTransitionStep(
diff --git a/packages/SystemUI/tests/utils/src/android/graphics/drawable/TestStubDrawable.kt b/packages/SystemUI/tests/utils/src/android/graphics/drawable/TestStubDrawable.kt
index b88f302..1a9f4b4 100644
--- a/packages/SystemUI/tests/utils/src/android/graphics/drawable/TestStubDrawable.kt
+++ b/packages/SystemUI/tests/utils/src/android/graphics/drawable/TestStubDrawable.kt
@@ -24,12 +24,27 @@
  * Stub drawable that does nothing. It's to be used in tests as a mock drawable and checked for the
  * same instance
  */
-class TestStubDrawable : Drawable() {
+class TestStubDrawable(private val name: String? = null) : Drawable() {
 
     override fun draw(canvas: Canvas) = Unit
     override fun setAlpha(alpha: Int) = Unit
     override fun setColorFilter(colorFilter: ColorFilter?) = Unit
     override fun getOpacity(): Int = PixelFormat.UNKNOWN
 
-    override fun equals(other: Any?): Boolean = this === other
+    override fun toString(): String {
+        return name ?: super.toString()
+    }
+
+    override fun getConstantState(): ConstantState =
+        TestStubConstantState(this, changingConfigurations)
+
+    private class TestStubConstantState(
+        private val drawable: Drawable,
+        private val changingConfigurations: Int,
+    ) : ConstantState() {
+
+        override fun newDrawable(): Drawable = drawable
+
+        override fun getChangingConfigurations(): Int = changingConfigurations
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index e20a0ab..a9a2d91 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -43,7 +43,7 @@
 class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitionRepository {
 
     private val _transitions =
-        MutableSharedFlow<TransitionStep>(replay = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+        MutableSharedFlow<TransitionStep>(replay = 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
     override val transitions: SharedFlow<TransitionStep> = _transitions
 
     init {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelKosmos.kt
index 9fb3284..f1784a8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelKosmos.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -28,5 +29,6 @@
         keyguardTransitionInteractor = keyguardTransitionInteractor,
         goneToAodTransitionViewModel = goneToAodTransitionViewModel,
         goneToDozingTransitionViewModel = goneToDozingTransitionViewModel,
+        keyguardInteractor = keyguardInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt
similarity index 60%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerFactoryKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt
index f8ce707..b370859 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.systemui.qs.external
+package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.util.mockito.mock
 
-/** Returns mocks */
-var Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by
-    Kosmos.Fixture { TileLifecycleManager.Factory { _, _ -> mock<TileLifecycleManager>() } }
+val Kosmos.dreamingToGlanceableHubTransitionViewModel by
+    Kosmos.Fixture {
+        DreamingToGlanceableHubTransitionViewModel(
+            animationFlow = keyguardTransitionAnimationFlow,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt
index 28fce77..b1c21b8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -26,6 +27,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 val Kosmos.glanceableHubToLockscreenTransitionViewModel by Fixture {
     GlanceableHubToLockscreenTransitionViewModel(
+        configurationInteractor = configurationInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelKosmos.kt
index 9fe4ea3..471381f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelKosmos.kt
@@ -18,13 +18,16 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
+@ExperimentalCoroutinesApi
 val Kosmos.lockscreenToGlanceableHubTransitionViewModel by Fixture {
     LockscreenToGlanceableHubTransitionViewModel(
+        configurationInteractor = configurationInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeIQSTileService.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeIQSTileService.kt
new file mode 100644
index 0000000..cff5980
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeIQSTileService.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.external
+
+import android.os.Binder
+import android.os.IBinder
+import android.service.quicksettings.IQSTileService
+
+class FakeIQSTileService : IQSTileService {
+
+    var isTileAdded: Boolean = false
+        private set
+    var isTileListening: Boolean = false
+        private set
+    var isUnlockComplete: Boolean = false
+    val clicks: List<IBinder?>
+        get() = mutableClicks
+
+    private val mutableClicks: MutableList<IBinder?> = mutableListOf()
+    private val binder = Binder()
+
+    override fun asBinder(): IBinder = binder
+
+    override fun onTileAdded() {
+        isTileAdded = true
+    }
+
+    override fun onTileRemoved() {
+        isTileAdded = false
+    }
+
+    override fun onStartListening() {
+        isTileListening = true
+    }
+
+    override fun onStopListening() {
+        isTileListening = false
+    }
+
+    override fun onClick(wtoken: IBinder?) {
+        mutableClicks.add(wtoken)
+    }
+
+    override fun onUnlockComplete() {
+        isUnlockComplete = true
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeTileServiceManagerFacade.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeTileServiceManagerFacade.kt
new file mode 100644
index 0000000..101335f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeTileServiceManagerFacade.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.external
+
+import android.service.quicksettings.IQSTileService
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+
+// TODO(b/299909989) Make a fake instead
+class FakeTileServiceManagerFacade(
+    private val iQSTileService: IQSTileService,
+    val tileServiceManager: TileServiceManager = mock {},
+) {
+
+    private var hasPendingBind: Boolean = false
+
+    var isBound: Boolean = false
+        private set
+
+    init {
+        with(tileServiceManager) {
+            whenever(tileService).thenReturn(iQSTileService)
+            whenever(setBindRequested(any())).then {
+                val isRequested: Boolean = it.getArgument(0)
+                hasPendingBind = isRequested
+                if (!isRequested) {
+                    isBound = false
+                }
+                Unit
+            }
+            whenever(clearPendingBind()).then {
+                hasPendingBind = false
+                Unit
+            }
+            whenever(hasPendingBind()).then { hasPendingBind }
+        }
+    }
+
+    fun processPendingBind() {
+        if (hasPendingBind) {
+            isBound = true
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeTileServicesFacade.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeTileServicesFacade.kt
new file mode 100644
index 0000000..0975e55
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeTileServicesFacade.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.qs.external
+
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+
+class FakeTileServicesFacade(
+    private val TileServiceManager: TileServiceManager,
+    val tileServices: TileServices = mock {}
+) {
+
+    var customTileInterface: CustomTileInterface? = null
+        private set
+
+    init {
+        with(tileServices) {
+            whenever(getTileWrapper(any())).then {
+                customTileInterface = it.getArgument(0)
+                TileServiceManager
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TilesExternalKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TilesExternalKosmos.kt
new file mode 100644
index 0000000..36c2c2b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TilesExternalKosmos.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.external
+
+import android.content.ComponentName
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.componentName: ComponentName by Kosmos.Fixture()
+
+/** Returns mocks */
+var Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by Kosmos.Fixture { mock {} }
+
+val Kosmos.iQSTileService: FakeIQSTileService by Kosmos.Fixture { FakeIQSTileService() }
+val Kosmos.tileServiceManagerFacade: FakeTileServiceManagerFacade by
+    Kosmos.Fixture { FakeTileServiceManagerFacade(iQSTileService) }
+
+val Kosmos.tileServiceManager: TileServiceManager by
+    Kosmos.Fixture { tileServiceManagerFacade.tileServiceManager }
+
+val Kosmos.tileServicesFacade: FakeTileServicesFacade by
+    Kosmos.Fixture { (FakeTileServicesFacade(tileServiceManager)) }
+val Kosmos.tileServices: TileServices by Kosmos.Fixture { tileServicesFacade.tileServices }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt
index 14f28fe..561e254 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt
@@ -17,19 +17,47 @@
 package com.android.systemui.qs.tiles.impl.custom
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.activityStarter
 import com.android.systemui.qs.external.FakeCustomTileStatePersister
+import com.android.systemui.qs.external.tileServices
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
 import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileDefaultsRepository
 import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTilePackageUpdatesRepository
 import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileRepository
 import com.android.systemui.qs.tiles.impl.custom.data.repository.FakePackageManagerAdapterFacade
+import com.android.systemui.qs.tiles.impl.custom.domain.interactor.CustomTileInteractor
+import com.android.systemui.qs.tiles.impl.custom.domain.interactor.CustomTileServiceInteractor
+import com.android.systemui.qs.tiles.impl.custom.domain.interactor.CustomTileUserActionInteractor
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder
+import com.android.systemui.user.data.repository.userRepository
+import com.android.systemui.util.mockito.mock
 
 var Kosmos.tileSpec: TileSpec.CustomTileSpec by Kosmos.Fixture()
 
+var Kosmos.customTileQsTileConfig: QSTileConfig by
+    Kosmos.Fixture { QSTileConfigTestBuilder.build { tileSpec = this@Fixture.tileSpec } }
+val Kosmos.qsTileLogger: QSTileLogger by Kosmos.Fixture { mock {} }
+
 val Kosmos.customTileStatePersister: FakeCustomTileStatePersister by
     Kosmos.Fixture { FakeCustomTileStatePersister() }
 
+val Kosmos.customTileInteractor: CustomTileInteractor by
+    Kosmos.Fixture {
+        CustomTileInteractor(
+            tileSpec,
+            customTileDefaultsRepository,
+            customTileRepository,
+            testScope.backgroundScope,
+            testScope.testScheduler,
+        )
+    }
+
 val Kosmos.customTileRepository: FakeCustomTileRepository by
     Kosmos.Fixture {
         FakeCustomTileRepository(
@@ -48,3 +76,31 @@
 
 val Kosmos.packageManagerAdapterFacade: FakePackageManagerAdapterFacade by
     Kosmos.Fixture { FakePackageManagerAdapterFacade(tileSpec.componentName) }
+
+val Kosmos.customTileServiceInteractor: CustomTileServiceInteractor by
+    Kosmos.Fixture {
+        CustomTileServiceInteractor(
+            tileSpec,
+            activityStarter,
+            { customTileUserActionInteractor },
+            customTileInteractor,
+            userRepository,
+            qsTileLogger,
+            tileServices,
+            testScope.backgroundScope,
+        )
+    }
+
+val Kosmos.customTileUserActionInteractor: CustomTileUserActionInteractor by
+    Kosmos.Fixture {
+        CustomTileUserActionInteractor(
+            testCase.context,
+            tileSpec,
+            qsTileLogger,
+            mock {},
+            mock {},
+            FakeQSTileIntentUserInputHandler(),
+            testDispatcher,
+            customTileServiceInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt
index 9d0faca..4f5c9b4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt
@@ -70,7 +70,7 @@
         }
 
         /** Shortcut for `Truth.assertAbout(states()).that(state)`. */
-        fun assertThat(state: QSTileState?): QSTileStateSubject =
-            Truth.assertAbout(states()).that(state)
+        fun assertThat(actual: QSTileState?): QSTileStateSubject =
+            Truth.assertAbout(states()).that(actual)
     }
 }
diff --git a/ravenwood/run-ravenwood-tests.sh b/ravenwood/run-ravenwood-tests.sh
index 259aa70..a303626 100755
--- a/ravenwood/run-ravenwood-tests.sh
+++ b/ravenwood/run-ravenwood-tests.sh
@@ -20,5 +20,9 @@
 # "echo" is to remove the newlines
 all_tests="$all_tests $(echo $(${0%/*}/list-ravenwood-tests.sh) )"
 
-echo "Running tests: $all_tests"
-atest $all_tests
+run() {
+    echo "Running: $*"
+    "${@}"
+}
+
+run ${ATEST:-atest} $all_tests
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 351760b..a5bbc7e 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
@@ -599,7 +599,7 @@
                     callback.onFullScreenMagnificationActivationState(
                             mDisplayId, mMagnificationActivated);
                 });
-                mControllerCtx.getWindowManager().setForceShowMagnifiableBounds(
+                mControllerCtx.getWindowManager().setFullscreenMagnificationActivated(
                         mDisplayId, activated);
             }
 
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index eb661ce..6f45f60 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -2417,7 +2417,7 @@
         if (android.os.Flags.allowPrivateProfile()
                 && android.multiuser.Flags.disablePrivateSpaceItemsOnHome()) {
             // Do not add widget providers for profiles with items restricted on home screen.
-            if (mUserManager
+            if (info != null && mUserManager
                     .getUserProperties(info.getProfile()).areItemsRestrictedOnHomeScreen()) {
                 return false;
             }
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index 2e14abb..9189ea7 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -332,7 +332,6 @@
     private ArrayMap<String, Set<String>> mPackageToUserTypeBlacklist = new ArrayMap<>();
 
     private final ArraySet<String> mRollbackWhitelistedPackages = new ArraySet<>();
-    private final ArraySet<String> mAutomaticRollbackDenylistedPackages = new ArraySet<>();
     private final ArraySet<String> mWhitelistedStagedInstallers = new ArraySet<>();
     // A map from package name of vendor APEXes that can be updated to an installer package name
     // allowed to install updates for it.
@@ -499,10 +498,6 @@
         return mRollbackWhitelistedPackages;
     }
 
-    public Set<String> getAutomaticRollbackDenylistedPackages() {
-        return mAutomaticRollbackDenylistedPackages;
-    }
-
     public Set<String> getWhitelistedStagedInstallers() {
         return mWhitelistedStagedInstallers;
     }
@@ -1481,16 +1476,6 @@
                         }
                         XmlUtils.skipCurrentTag(parser);
                     } break;
-                    case "automatic-rollback-denylisted-app": {
-                        String pkgname = parser.getAttributeValue(null, "package");
-                        if (pkgname == null) {
-                            Slog.w(TAG, "<" + name + "> without package in " + permFile
-                                    + " at " + parser.getPositionDescription());
-                        } else {
-                            mAutomaticRollbackDenylistedPackages.add(pkgname);
-                        }
-                        XmlUtils.skipCurrentTag(parser);
-                    } break;
                     case "whitelisted-staged-installer": {
                         if (allowAppConfigs) {
                             String pkgname = parser.getAttributeValue(null, "package");
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index ef93400..cb6d26f 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -5673,7 +5673,7 @@
             final boolean isMuted = isStreamMutedByRingerOrZenMode(streamType);
             final boolean muteAllowedBySco =
                     !(shouldRingSco && streamType == AudioSystem.STREAM_RING);
-            final boolean shouldZenMute = shouldZenMuteStream(streamType);
+            final boolean shouldZenMute = isStreamAffectedByCurrentZen(streamType);
             final boolean shouldMute = shouldZenMute || (ringerModeMute
                     && isStreamAffectedByRingerMode(streamType) && muteAllowedBySco);
             if (isMuted == shouldMute) continue;
@@ -6937,24 +6937,8 @@
         return (mRingerModeAffectedStreams & (1 << streamType)) != 0;
     }
 
-    private boolean shouldZenMuteStream(int streamType) {
-        if (mNm.getZenMode() != Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) {
-            return false;
-        }
-
-        NotificationManager.Policy zenPolicy = mNm.getConsolidatedNotificationPolicy();
-        final boolean muteAlarms = (zenPolicy.priorityCategories
-                & NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS) == 0;
-        final boolean muteMedia = (zenPolicy.priorityCategories
-                & NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA) == 0;
-        final boolean muteSystem = (zenPolicy.priorityCategories
-                & NotificationManager.Policy.PRIORITY_CATEGORY_SYSTEM) == 0;
-        final boolean muteNotificationAndRing = ZenModeConfig
-                .areAllPriorityOnlyRingerSoundsMuted(zenPolicy);
-        return muteAlarms && isAlarm(streamType)
-                || muteMedia && isMedia(streamType)
-                || muteSystem && isSystem(streamType)
-                || muteNotificationAndRing && isNotificationOrRinger(streamType);
+    public boolean isStreamAffectedByCurrentZen(int streamType) {
+        return (mZenModeAffectedStreams & (1 << streamType)) != 0;
     }
 
     private boolean isStreamMutedByRingerOrZenMode(int streamType) {
@@ -6962,11 +6946,9 @@
     }
 
     /**
-     * Notifications, ringer and system sounds are controlled by the ringer:
-     * {@link ZenModeHelper.RingerModeDelegate#getRingerModeAffectedStreams(int)} but can
-     * also be muted by DND based on the DND mode:
-     * DND total silence: media and alarms streams can be muted by DND
-     * DND alarms only: no streams additionally controlled by DND
+     * Volume streams can be muted based on the current DND state:
+     * DND total silence: ringer, notification, system, media and alarms streams muted by DND
+     * DND alarms only:  ringer, notification, system streams muted by DND
      * DND priority only: alarms, media, system, ringer and notification streams can be muted by
      * DND.  The current applied zenPolicy determines which streams will be muted by DND.
      * @return true if changed, else false
@@ -6976,12 +6958,20 @@
             return false;
         }
 
+        // If DND is off, no streams are muted by DND
         int zenModeAffectedStreams = 0;
         final int zenMode = mNm.getZenMode();
 
         if (zenMode == Settings.Global.ZEN_MODE_NO_INTERRUPTIONS) {
+            zenModeAffectedStreams |= 1 << AudioManager.STREAM_SYSTEM;
+            zenModeAffectedStreams |= 1 << AudioManager.STREAM_NOTIFICATION;
+            zenModeAffectedStreams |= 1 << AudioManager.STREAM_RING;
             zenModeAffectedStreams |= 1 << AudioManager.STREAM_ALARM;
             zenModeAffectedStreams |= 1 << AudioManager.STREAM_MUSIC;
+        } else if (zenMode == Settings.Global.ZEN_MODE_ALARMS) {
+            zenModeAffectedStreams |= 1 << AudioManager.STREAM_SYSTEM;
+            zenModeAffectedStreams |= 1 << AudioManager.STREAM_NOTIFICATION;
+            zenModeAffectedStreams |= 1 << AudioManager.STREAM_RING;
         } else if (zenMode == Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) {
             NotificationManager.Policy zenPolicy = mNm.getConsolidatedNotificationPolicy();
             if ((zenPolicy.priorityCategories
@@ -7023,7 +7013,6 @@
                 ((1 << AudioSystem.STREAM_RING)|(1 << AudioSystem.STREAM_NOTIFICATION)|
                  (1 << AudioSystem.STREAM_SYSTEM)|(1 << AudioSystem.STREAM_SYSTEM_ENFORCED)),
                  UserHandle.USER_CURRENT);
-
         if (mIsSingleVolume) {
             ringerModeAffectedStreams = 0;
         } else if (mRingerModeDelegate != null) {
diff --git a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java
index ebc784d..4b9f2cf 100644
--- a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java
+++ b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java
@@ -227,7 +227,12 @@
                 "LAUNCH_DEFAULT_FITNESS"),
         LAUNCH_APPLICATION_BY_PACKAGE_NAME(
                 FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_APPLICATION_BY_PACKAGE_NAME,
-                "LAUNCH_APPLICATION_BY_PACKAGE_NAME");
+                "LAUNCH_APPLICATION_BY_PACKAGE_NAME"),
+        DESKTOP_MODE(
+                FrameworkStatsLog
+                        .KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__DESKTOP_MODE,
+                "DESKTOP_MODE");
+
 
         private final int mValue;
         private final String mName;
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 3e8af27..e7e8096 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -312,14 +312,15 @@
                 }
                 user.mPriorityStack.onSessionActiveStateChanged(record);
             }
-            boolean allowRunningInForeground = record.isActive()
-                    && (playbackState == null || playbackState.isActive());
+            boolean isUserEngaged =
+                    record.isActive() && (playbackState == null || playbackState.isActive());
 
             Log.d(TAG, "onSessionActiveStateChanged: "
                     + "record=" + record
                     + "playbackState=" + playbackState
-                    + "allowRunningInForeground=" + allowRunningInForeground);
-            setForegroundServiceAllowance(record, allowRunningInForeground);
+                    + "allowRunningInForeground=" + isUserEngaged);
+            setForegroundServiceAllowance(record, /* allowRunningInForeground= */ isUserEngaged);
+            reportMediaInteractionEvent(record, isUserEngaged);
             mHandler.postSessionsChanged(record);
         }
     }
@@ -417,12 +418,14 @@
             }
             user.mPriorityStack.onPlaybackStateChanged(record, shouldUpdatePriority);
             if (playbackState != null) {
-                boolean allowRunningInForeground = playbackState.isActive() && record.isActive();
+                boolean isUserEngaged = playbackState.isActive() && record.isActive();
                 Log.d(TAG, "onSessionPlaybackStateChanged: "
                         + "record=" + record
                         + "playbackState=" + playbackState
-                        + "allowRunningInForeground=" + allowRunningInForeground);
-                setForegroundServiceAllowance(record, allowRunningInForeground);
+                        + "allowRunningInForeground=" + isUserEngaged);
+                setForegroundServiceAllowance(
+                        record, /* allowRunningInForeground= */ isUserEngaged);
+                reportMediaInteractionEvent(record, isUserEngaged);
             }
         }
     }
@@ -590,6 +593,7 @@
 
         Log.d(TAG, "destroySessionLocked: record=" + session);
         setForegroundServiceAllowance(session, /* allowRunningInForeground= */ false);
+        reportMediaInteractionEvent(session, /* userEngaged= */ false);
         mHandler.postSessionsChanged(session);
     }
 
@@ -608,11 +612,9 @@
         if (allowRunningInForeground) {
             mActivityManagerInternal.startForegroundServiceDelegate(
                     foregroundServiceDelegationOptions, /* connection= */ null);
-            reportMediaInteractionEvent(record, /* userEngaged= */ true);
         } else {
             mActivityManagerInternal.stopForegroundServiceDelegate(
                     foregroundServiceDelegationOptions);
-            reportMediaInteractionEvent(record, /* userEngaged= */ false);
         }
     }
 
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 53ae60b..7455fe0 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -5720,6 +5720,14 @@
         }
 
         @Override
+        @Condition.State
+        public int getAutomaticZenRuleState(@NonNull String id) {
+            Objects.requireNonNull(id, "id is null");
+            enforcePolicyAccess(Binder.getCallingUid(), "getAutomaticZenRuleState");
+            return mZenModeHelper.getAutomaticZenRuleState(id);
+        }
+
+        @Override
         public void setAutomaticZenRuleState(String id, Condition condition) {
             Objects.requireNonNull(id, "id is null");
             Objects.requireNonNull(condition, "Condition is null");
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 54de197..efb8c84 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -856,6 +856,20 @@
         }
     }
 
+    @Condition.State
+    int getAutomaticZenRuleState(String id) {
+        synchronized (mConfigLock) {
+            if (mConfig == null) {
+                return Condition.STATE_UNKNOWN;
+            }
+            ZenRule rule = mConfig.automaticRules.get(id);
+            if (rule == null || !canManageAutomaticZenRule(rule)) {
+                return Condition.STATE_UNKNOWN;
+            }
+            return rule.condition != null ? rule.condition.state : Condition.STATE_FALSE;
+        }
+    }
+
     void setAutomaticZenRuleState(String id, Condition condition, @ConfigChangeOrigin int origin,
             int callingUid) {
         requirePublicOrigin("setAutomaticZenRuleState", origin);
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index 524bad5..b6daed1 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -30,6 +30,7 @@
 import android.content.pm.IBackgroundInstallControlService;
 import android.content.pm.InstallSourceInfo;
 import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ParceledListSlice;
@@ -46,6 +47,7 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.AtomicFile;
+import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArrayMap;
 import android.util.SparseSetArray;
@@ -63,8 +65,10 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 import java.util.ListIterator;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TreeSet;
 
@@ -103,6 +107,24 @@
     private final SparseArrayMap<String, TreeSet<ForegroundTimeFrame>>
             mInstallerForegroundTimeFrames = new SparseArrayMap<>();
 
+    @VisibleForTesting
+    protected final PackageManagerInternal.PackageListObserver mPackageObserver =
+            new PackageManagerInternal.PackageListObserver() {
+                @Override
+                public void onPackageAdded(String packageName, int uid) {
+                    final int userId = UserHandle.getUserId(uid);
+                    mHandler.obtainMessage(MSG_PACKAGE_ADDED, userId, 0, packageName)
+                            .sendToTarget();
+                }
+
+                @Override
+                public void onPackageRemoved(String packageName, int uid) {
+                    final int userId = UserHandle.getUserId(uid);
+                    mHandler.obtainMessage(MSG_PACKAGE_REMOVED, userId, 0, packageName)
+                            .sendToTarget();
+                }
+            };
+
     public BackgroundInstallControlService(@NonNull Context context) {
         this(new InjectorImpl(context));
     }
@@ -258,6 +280,7 @@
 
         String installerPackageName;
         String initiatingPackageName;
+
         try {
             final InstallSourceInfo installInfo = mPackageManager.getInstallSourceInfo(packageName);
             installerPackageName = installInfo.getInstallingPackageName();
@@ -280,7 +303,8 @@
 
         // convert up-time to current time.
         final long installTimestamp =
-                System.currentTimeMillis() - (SystemClock.uptimeMillis() - appInfo.createTimestamp);
+                System.currentTimeMillis() - (SystemClock.uptimeMillis()
+                        - retrieveInstallStartTimestamp(packageName, userId, appInfo));
 
         if (installedByAdb(initiatingPackageName)
                 || wasForegroundInstallation(installerPackageName, userId, installTimestamp)) {
@@ -293,6 +317,35 @@
         writeBackgroundInstalledPackagesToDisk();
     }
 
+    private long retrieveInstallStartTimestamp(String packageName,
+                                               int userId, ApplicationInfo appInfo) {
+        long installStartTimestamp = appInfo.createTimestamp;
+
+        try {
+            Optional<PackageInstaller.SessionInfo> latestInstallSession =
+                    getLatestInstallSession(packageName, userId);
+            if (latestInstallSession.isEmpty()) {
+                Slog.w(TAG, "Package's historical install session not found, falling back "
+                        + "to appInfo.createTimestamp: " + packageName);
+            } else {
+                installStartTimestamp = latestInstallSession.get().getCreatedMillis();
+            }
+        } catch (Exception e) {
+            Slog.w(TAG, "Retrieval of install time from historical session failed, falling "
+                    + "back to appInfo.createTimestamp");
+            Slog.w(TAG, Log.getStackTraceString(e));
+        }
+        return installStartTimestamp;
+    }
+
+    private Optional<PackageInstaller.SessionInfo> getLatestInstallSession(
+            String packageName, int userId) {
+        List<PackageInstaller.SessionInfo> historicalSessions =
+                mPackageManagerInternal.getHistoricalSessions(userId).getList();
+        return historicalSessions.stream().filter(s -> packageName.equals(s.getAppPackageName()))
+                .max(Comparator.comparingLong(PackageInstaller.SessionInfo::getCreatedMillis));
+    }
+
     // ADB sets installerPackageName to null, this creates a loophole to bypass BIC which will be
     // addressed with b/265203007
     private boolean installedByAdb(String initiatingPackageName) {
@@ -496,22 +549,7 @@
             publishBinderService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE, mBinderService);
         }
 
-        mPackageManagerInternal.getPackageList(
-                new PackageManagerInternal.PackageListObserver() {
-                    @Override
-                    public void onPackageAdded(String packageName, int uid) {
-                        final int userId = UserHandle.getUserId(uid);
-                        mHandler.obtainMessage(MSG_PACKAGE_ADDED, userId, 0, packageName)
-                                .sendToTarget();
-                    }
-
-                    @Override
-                    public void onPackageRemoved(String packageName, int uid) {
-                        final int userId = UserHandle.getUserId(uid);
-                        mHandler.obtainMessage(MSG_PACKAGE_REMOVED, userId, 0, packageName)
-                                .sendToTarget();
-                    }
-                });
+        mPackageManagerInternal.getPackageList(mPackageObserver);
     }
 
     // The foreground time frame (ForegroundTimeFrame) represents the period
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index 76d87ff..d8d8dd2 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -350,18 +350,50 @@
         public void registerPackageInstallerCallback(String callingPackage,
                 IPackageInstallerCallback callback) {
             verifyCallingPackage(callingPackage);
-            UserHandle callingIdUserHandle = new UserHandle(getCallingUserId());
-            getPackageInstallerService().registerCallback(callback, eventUserId ->
-                            isEnabledProfileOf(callingIdUserHandle,
-                                    new UserHandle(eventUserId), "shouldReceiveEvent"));
+            BroadcastCookie callerInfo =
+                    new BroadcastCookie(
+                            new UserHandle(getCallingUserId()),
+                            callingPackage,
+                            getCallingPid(),
+                            getCallingUid());
+            getPackageInstallerService()
+                    .registerCallback(
+                            callback,
+                            eventUserId ->
+                                    isEnabledProfileOf(
+                                            callerInfo,
+                                            new UserHandle(eventUserId),
+                                            "shouldReceiveEvent"));
+        }
+
+        @Override
+        public List<UserHandle> getUserProfiles() {
+            int[] userIds;
+            if (!canAccessHiddenProfile(getCallingUid(), getCallingPid())) {
+                userIds = mUm.getProfileIdsExcludingHidden(getCallingUserId(), /* enabled= */ true);
+            } else {
+                userIds = mUm.getEnabledProfileIds(getCallingUserId());
+            }
+            final List<UserHandle> result = new ArrayList<>(userIds.length);
+            for (int userId : userIds) {
+                result.add(UserHandle.of(userId));
+            }
+            return result;
         }
 
         @Override
         public ParceledListSlice<SessionInfo> getAllSessions(String callingPackage) {
             verifyCallingPackage(callingPackage);
             List<SessionInfo> sessionInfos = new ArrayList<>();
-            int[] userIds = mUm.getEnabledProfileIds(getCallingUserId());
             final int callingUid = Binder.getCallingUid();
+
+            int[] userIds;
+            if (!canAccessHiddenProfile(callingUid, Binder.getCallingPid())) {
+                userIds = mUm.getProfileIdsExcludingHidden(getCallingUserId(), /* enabled= */ true);
+            } else {
+                userIds = mUm.getEnabledProfileIds(getCallingUserId());
+            }
+
             final long token = Binder.clearCallingIdentity();
             try {
                 for (int userId : userIds) {
@@ -389,7 +421,7 @@
                     mPackageInstallerService = ((PackageInstallerService) ((IPackageManager)
                             ServiceManager.getService("package")).getPackageInstaller());
                 } catch (RemoteException e) {
-                    Slog.wtf(TAG, "Error gettig IPackageInstaller", e);
+                    Slog.wtf(TAG, "Error getting IPackageInstaller", e);
                 }
             }
             return mPackageInstallerService;
@@ -470,57 +502,86 @@
                             + targetUserId + " from " + callingUserId + " not allowed");
                     return false;
                 }
-
-                if (areHiddenApisChecksEnabled()
-                        && mUm.getUserProperties(UserHandle.of(targetUserId))
-                                        .getProfileApiVisibility()
-                                == UserProperties.PROFILE_API_VISIBILITY_HIDDEN
-                        && !canAccessHiddenProfileInjected(callingUid, callingPid)) {
-                    return false;
-                }
             } finally {
                 injectRestoreCallingIdentity(ident);
             }
 
+            if (isHiddenProfile(UserHandle.of(targetUserId))
+                    && !canAccessHiddenProfile(callingUid, callingPid)) {
+                return false;
+            }
+
             return mUserManagerInternal.isProfileAccessible(callingUserId, targetUserId,
                     message, true);
         }
 
-        boolean areHiddenApisChecksEnabled() {
-            return android.os.Flags.allowPrivateProfile()
-                    && Flags.enableLauncherAppsHiddenProfileChecks()
-                    && Flags.enablePermissionToAccessHiddenProfiles();
+        private boolean isHiddenProfile(UserHandle targetUser) {
+            if (!Flags.enableLauncherAppsHiddenProfileChecks()) {
+                return false;
+            }
+
+            long identity = injectClearCallingIdentity();
+            try {
+                UserProperties properties = mUm.getUserProperties(targetUser);
+                if (properties == null) {
+                    return false;
+                }
+
+                return properties.getProfileApiVisibility()
+                        == UserProperties.PROFILE_API_VISIBILITY_HIDDEN;
+            } catch (IllegalArgumentException e) {
+                return false;
+            } finally {
+                injectRestoreCallingIdentity(identity);
+            }
         }
 
         private void verifyCallingPackage(String callingPackage) {
             verifyCallingPackage(callingPackage, injectBinderCallingUid());
         }
 
-        boolean canAccessHiddenProfileInjected(int callingUid, int callingPid) {
-            AndroidPackage callingPackage = mPackageManagerInternal.getPackage(callingUid);
-            if (callingPackage == null) {
-                return false;
-            }
-
-            if (!mRoleManager
-                    .getRoleHoldersAsUser(
-                            RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid))
-                    .contains(callingPackage.getPackageName())) {
-                return false;
-            }
-
-            if (mContext.checkPermission(
-                            Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL, callingPid, callingUid)
-                    == PackageManager.PERMISSION_GRANTED) {
+        private boolean canAccessHiddenProfile(int callingUid, int callingPid) {
+            if (!areHiddenApisChecksEnabled()) {
                 return true;
             }
 
-            // TODO(b/321988638): add option to disable with a flag
-            return mContext.checkPermission(
-                            android.Manifest.permission.ACCESS_HIDDEN_PROFILES,
-                            callingPid,
-                            callingUid)
-                    == PackageManager.PERMISSION_GRANTED;
+            long ident = injectClearCallingIdentity();
+            try {
+                AndroidPackage callingPackage = mPackageManagerInternal.getPackage(callingUid);
+                if (callingPackage == null) {
+                    return false;
+                }
+
+                if (!mRoleManager
+                        .getRoleHoldersAsUser(
+                                RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid))
+                        .contains(callingPackage.getPackageName())) {
+                    return false;
+                }
+                if (mContext.checkPermission(
+                                Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL,
+                                callingPid,
+                                callingUid)
+                        == PackageManager.PERMISSION_GRANTED) {
+                    return true;
+                }
+
+                // TODO(b/321988638): add option to disable with a flag
+                return mContext.checkPermission(
+                                android.Manifest.permission.ACCESS_HIDDEN_PROFILES,
+                                callingPid,
+                                callingUid)
+                        == PackageManager.PERMISSION_GRANTED;
+            } finally {
+                injectRestoreCallingIdentity(ident);
+            }
+        }
+
+        private boolean areHiddenApisChecksEnabled() {
+            return android.os.Flags.allowPrivateProfile()
+                    && Flags.enableHidingProfiles()
+                    && Flags.enableLauncherAppsHiddenProfileChecks()
+                    && Flags.enablePermissionToAccessHiddenProfiles();
         }
 
         @VisibleForTesting // We override it in unit tests
@@ -2068,12 +2129,18 @@
                     });
         }
 
-        /** Checks if user is a profile of or same as listeningUser.
-         * and the user is enabled. */
-        private boolean isEnabledProfileOf(UserHandle listeningUser, UserHandle user,
-                String debugMsg) {
-            return mUserManagerInternal.isProfileAccessible(listeningUser.getIdentifier(),
-                    user.getIdentifier(), debugMsg, false);
+        /**
+         * Checks if user is a profile of or same as listeningUser and the target user is enabled
+         * and accessible for caller.
+         */
+        private boolean isEnabledProfileOf(
+                BroadcastCookie cookie, UserHandle user, String debugMsg) {
+            if (isHiddenProfile(user)
+                    && !canAccessHiddenProfile(cookie.callingUid, cookie.callingPid)) {
+                return false;
+            }
+            return mUserManagerInternal.isProfileAccessible(
+                    cookie.user.getIdentifier(), user.getIdentifier(), debugMsg, false);
         }
 
         /**
@@ -2305,7 +2372,7 @@
                                         mListeners.getBroadcastItem(i);
                                 final BroadcastCookie cookie =
                                         (BroadcastCookie) mListeners.getBroadcastCookie(i);
-                                if (!isEnabledProfileOf(cookie.user, user, "onPackageRemoved")) {
+                                if (!isEnabledProfileOf(cookie, user, "onPackageRemoved")) {
                                     continue;
                                 }
                                 if (!isCallingAppIdAllowed(appIdAllowList, UserHandle.getAppId(
@@ -2344,7 +2411,7 @@
                     for (int i = 0; i < n; i++) {
                         IOnAppsChangedListener listener = mListeners.getBroadcastItem(i);
                         BroadcastCookie cookie = (BroadcastCookie) mListeners.getBroadcastCookie(i);
-                        if (!isEnabledProfileOf(cookie.user, user, "onPackageAdded")) {
+                        if (!isEnabledProfileOf(cookie, user, "onPackageAdded")) {
                             continue;
                         }
                         if (!isPackageVisibleToListener(packageName, cookie, user)) {
@@ -2378,7 +2445,7 @@
                     for (int i = 0; i < n; i++) {
                         IOnAppsChangedListener listener = mListeners.getBroadcastItem(i);
                         BroadcastCookie cookie = (BroadcastCookie) mListeners.getBroadcastCookie(i);
-                        if (!isEnabledProfileOf(cookie.user, user, "onPackageModified")) {
+                        if (!isEnabledProfileOf(cookie, user, "onPackageModified")) {
                             continue;
                         }
                         if (!isPackageVisibleToListener(packageName, cookie, user)) {
@@ -2403,7 +2470,7 @@
                     for (int i = 0; i < n; i++) {
                         IOnAppsChangedListener listener = mListeners.getBroadcastItem(i);
                         BroadcastCookie cookie = (BroadcastCookie) mListeners.getBroadcastCookie(i);
-                        if (!isEnabledProfileOf(cookie.user, user, "onPackagesAvailable")) {
+                        if (!isEnabledProfileOf(cookie, user, "onPackagesAvailable")) {
                             continue;
                         }
                         final String[] filteredPackages =
@@ -2433,7 +2500,7 @@
                     for (int i = 0; i < n; i++) {
                         IOnAppsChangedListener listener = mListeners.getBroadcastItem(i);
                         BroadcastCookie cookie = (BroadcastCookie) mListeners.getBroadcastCookie(i);
-                        if (!isEnabledProfileOf(cookie.user, user, "onPackagesUnavailable")) {
+                        if (!isEnabledProfileOf(cookie, user, "onPackagesUnavailable")) {
                             continue;
                         }
                         final String[] filteredPackages =
@@ -2477,7 +2544,7 @@
                     for (int i = 0; i < n; i++) {
                         IOnAppsChangedListener listener = mListeners.getBroadcastItem(i);
                         BroadcastCookie cookie = (BroadcastCookie) mListeners.getBroadcastCookie(i);
-                        if (!isEnabledProfileOf(cookie.user, user, "onPackagesSuspended")) {
+                        if (!isEnabledProfileOf(cookie, user, "onPackagesSuspended")) {
                             continue;
                         }
                         final String[] filteredPackagesWithoutExtras =
@@ -2514,7 +2581,7 @@
                     for (int i = 0; i < n; i++) {
                         IOnAppsChangedListener listener = mListeners.getBroadcastItem(i);
                         BroadcastCookie cookie = (BroadcastCookie) mListeners.getBroadcastCookie(i);
-                        if (!isEnabledProfileOf(cookie.user, user, "onPackagesUnsuspended")) {
+                        if (!isEnabledProfileOf(cookie, user, "onPackagesUnsuspended")) {
                             continue;
                         }
                         final String[] filteredPackages =
@@ -2551,7 +2618,7 @@
                     for (int i = 0; i < n; i++) {
                         IOnAppsChangedListener listener = mListeners.getBroadcastItem(i);
                         BroadcastCookie cookie = (BroadcastCookie) mListeners.getBroadcastCookie(i);
-                        if (!isEnabledProfileOf(cookie.user, user, "onShortcutChanged")) {
+                        if (!isEnabledProfileOf(cookie, user, "onShortcutChanged")) {
                             continue;
                         }
                         if (!isPackageVisibleToListener(packageName, cookie, user)) {
@@ -2625,7 +2692,7 @@
                     for (int i = 0; i < n; i++) {
                         IOnAppsChangedListener listener = mListeners.getBroadcastItem(i);
                         BroadcastCookie cookie = (BroadcastCookie) mListeners.getBroadcastCookie(i);
-                        if (!isEnabledProfileOf(cookie.user, mUser, "onLoadingProgressChanged")) {
+                        if (!isEnabledProfileOf(cookie, mUser, "onLoadingProgressChanged")) {
                             continue;
                         }
                         if (!isPackageVisibleToListener(mPackageName, cookie, mUser)) {
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index f222fe9..b505a7c 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -973,7 +973,7 @@
         mUsers = users != null ? users : new SparseArray<>();
         mHandler = new MainHandler();
         mInternalExecutor = new ThreadPoolExecutor(/* corePoolSize */ 0, /* maximumPoolSize */ 1,
-                /* keepAliveTime */ 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
+                /* keepAliveTime */ 24, TimeUnit.HOURS, new LinkedBlockingQueue<>());
         mUserVisibilityMediator = new UserVisibilityMediator(mHandler);
         mUserDataPreparer = userDataPreparer;
         mUserTypes = UserTypeFactory.getUserTypes();
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index bc26018..b5cd943 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -3509,6 +3509,16 @@
                     }
                 }
                 break;
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+                if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) {
+                    StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
+                    if (statusbar != null) {
+                        statusbar.enterDesktop(event.getDisplayId());
+                        logKeyboardSystemsEvent(event, KeyboardLogEvent.DESKTOP_MODE);
+                        return true;
+                    }
+                }
+                break;
             case KeyEvent.KEYCODE_DPAD_LEFT:
                 if (firstDown && event.isMetaPressed()) {
                     if (event.isCtrlPressed()) {
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index 359678b..2a93255 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -1212,13 +1212,20 @@
         rollback.makeAvailable();
         mPackageHealthObserver.notifyRollbackAvailable(rollback.info);
 
-        // TODO(zezeozue): Provide API to explicitly start observing instead
-        // of doing this for all rollbacks. If we do this for all rollbacks,
-        // should document in PackageInstaller.SessionParams#setEnableRollback
-        // After enabling and committing any rollback, observe packages and
-        // prepare to rollback if packages crashes too frequently.
-        mPackageHealthObserver.startObservingHealth(rollback.getPackageNames(),
-                mRollbackLifetimeDurationInMillis);
+        if (Flags.recoverabilityDetection()) {
+            if (rollback.info.getRollbackImpactLevel() == PackageManager.ROLLBACK_USER_IMPACT_LOW) {
+                // TODO(zezeozue): Provide API to explicitly start observing instead
+                // of doing this for all rollbacks. If we do this for all rollbacks,
+                // should document in PackageInstaller.SessionParams#setEnableRollback
+                // After enabling and committing any rollback, observe packages and
+                // prepare to rollback if packages crashes too frequently.
+                mPackageHealthObserver.startObservingHealth(rollback.getPackageNames(),
+                        mRollbackLifetimeDurationInMillis);
+            }
+        } else {
+            mPackageHealthObserver.startObservingHealth(rollback.getPackageNames(),
+                    mRollbackLifetimeDurationInMillis);
+        }
         runExpiration();
     }
 
diff --git a/services/core/java/com/android/server/search/Searchables.java b/services/core/java/com/android/server/search/Searchables.java
index 6e1e979..7b39775 100644
--- a/services/core/java/com/android/server/search/Searchables.java
+++ b/services/core/java/com/android/server/search/Searchables.java
@@ -147,6 +147,9 @@
             Log.e(LOG_TAG, "Error getting activity info " + re);
             return null;
         }
+        if (ai == null) {
+            return null;
+        }
         String refActivityName = null;
 
         // First look for activity-specific reference
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 118985a..18afafd 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -52,6 +52,7 @@
 import android.app.ILocalWallpaperColorConsumer;
 import android.app.IWallpaperManager;
 import android.app.IWallpaperManagerCallback;
+import android.app.KeyguardManager;
 import android.app.PendingIntent;
 import android.app.UidObserver;
 import android.app.UserSwitchObserver;
@@ -1792,10 +1793,27 @@
                     systemWallpaper.wallpaperObserver = new WallpaperObserver(systemWallpaper);
                     systemWallpaper.wallpaperObserver.startWatching();
                 }
-                if (lockWallpaper != systemWallpaper)  {
-                    switchWallpaper(lockWallpaper, null);
+                if (Flags.reorderWallpaperDuringUserSwitch()) {
+                    if (mLastLockWallpaper != null) {
+                        detachWallpaperLocked(mLastLockWallpaper);
+                    }
+                    if (mLastWallpaper != null) {
+                        detachWallpaperLocked(mLastWallpaper);
+                    }
+                    if (lockWallpaper == systemWallpaper)  {
+                        switchWallpaper(systemWallpaper, reply);
+                    } else {
+                        KeyguardManager km = mContext.getSystemService(KeyguardManager.class);
+                        boolean isDeviceSecure = km != null && km.isDeviceSecure(userId);
+                        switchWallpaper(isDeviceSecure ? lockWallpaper : systemWallpaper, reply);
+                        switchWallpaper(isDeviceSecure ? systemWallpaper : lockWallpaper, null);
+                    }
+                } else {
+                    if (lockWallpaper != systemWallpaper)  {
+                        switchWallpaper(lockWallpaper, null);
+                    }
+                    switchWallpaper(systemWallpaper, reply);
                 }
-                switchWallpaper(systemWallpaper, reply);
                 mInitialUserSwitch = false;
             }
 
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
index 00c3026..d05482d 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
@@ -55,6 +55,7 @@
 import com.android.server.utils.quota.MultiRateLimiter;
 
 import java.io.FileDescriptor;
+import java.time.Duration;
 import java.util.HashSet;
 import java.util.Objects;
 import java.util.Set;
@@ -106,7 +107,7 @@
     private final Context mContext;
     private final AtomicInteger mNextDataRequestObserverId = new AtomicInteger(1);
     private final Set<DataRequestObserverContext> mDataRequestObserverContexts = new HashSet<>();
-    private final MultiRateLimiter mDataRequestRateLimiter;
+    @NonNull private volatile MultiRateLimiter mDataRequestRateLimiter;
     volatile boolean mIsServiceEnabled;
 
     public WearableSensingManagerService(Context context) {
@@ -238,6 +239,57 @@
         }
     }
 
+    /**
+     * Sets the window size used in data request rate limiting.
+     *
+     * <p>The new value will not be reflected in {@link
+     * WearableSensingDataRequest#getRateLimitWindowSize()}.
+     *
+     * <p>{@code windowSize} will be automatically capped between
+     * com.android.server.utils.quota.QuotaTracker#MIN_WINDOW_SIZE_MS and
+     * com.android.server.utils.quota.QuotaTracker#MAX_WINDOW_SIZE_MS
+     *
+     * <p>The current rate limit will also be reset.
+     *
+     * <p>This method is only used for testing and must not be called in production code because
+     * it effectively bypasses the rate limiting introduced to enhance privacy protection.
+     */
+    @VisibleForTesting
+    void setDataRequestRateLimitWindowSize(@NonNull Duration windowSize) {
+        Slog.w(
+                TAG,
+                TextUtils.formatSimple(
+                        "Setting the data request rate limit window size to %s. This also resets"
+                            + " the current limit and should only be callable from a test.",
+                        windowSize));
+        mDataRequestRateLimiter =
+                new MultiRateLimiter.Builder(mContext)
+                        .addRateLimit(WearableSensingDataRequest.getRateLimit(), windowSize)
+                        .build();
+    }
+
+    /**
+     * Resets the window size used in data request rate limiting back to the default value.
+     *
+     * <p>The current rate limit will also be reset.
+     *
+     * <p>This method is only used for testing and must not be called in production code because
+     * it effectively bypasses the rate limiting introduced to enhance privacy protection.
+     */
+    @VisibleForTesting
+    void resetDataRequestRateLimitWindowSize() {
+        Slog.w(
+                TAG,
+                "Resetting the data request rate limit window size back to the default value. This"
+                    + " also resets the current limit and should only be callable from a test.");
+        mDataRequestRateLimiter =
+                new MultiRateLimiter.Builder(mContext)
+                        .addRateLimit(
+                                WearableSensingDataRequest.getRateLimit(),
+                                WearableSensingDataRequest.getRateLimitWindowSize())
+                        .build();
+    }
+
     private DataRequestObserverContext getDataRequestObserverContext(
             int dataType, int userId, PendingIntent dataRequestPendingIntent) {
         synchronized (mDataRequestObserverContexts) {
diff --git a/services/core/java/com/android/server/wearable/WearableSensingShellCommand.java b/services/core/java/com/android/server/wearable/WearableSensingShellCommand.java
index 842bccb..0a9cf34 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingShellCommand.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingShellCommand.java
@@ -29,6 +29,7 @@
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PrintWriter;
+import java.time.Duration;
 
 final class WearableSensingShellCommand extends ShellCommand {
     private static final String TAG = WearableSensingShellCommand.class.getSimpleName();
@@ -90,6 +91,8 @@
                 return getBoundPackageName();
             case "set-temporary-service":
                 return setTemporaryService();
+            case "set-data-request-rate-limit-window-size":
+                return setDataRequestRateLimitWindowSize();
             default:
                 return handleDefaultCommands(cmd);
         }
@@ -114,6 +117,11 @@
         pw.println("  set-temporary-service USER_ID [PACKAGE_NAME] [COMPONENT_NAME DURATION]");
         pw.println("    Temporarily (for DURATION ms) changes the service implementation.");
         pw.println("    To reset, call with just the USER_ID argument.");
+        pw.println("  set-data-request-rate-limit-window-size WINDOW_SIZE");
+        pw.println("    Set the window size used in data request rate limiting to WINDOW_SIZE"
+                + " seconds.");
+        pw.println("    positive WINDOW_SIZE smaller than 20 will be automatically set to 20.");
+        pw.println("    To reset, call with 0 or a negative WINDOW_SIZE.");
     }
 
     private int createDataStream() {
@@ -209,4 +217,20 @@
         resultPrinter.println(componentName == null ? "" : componentName.getPackageName());
         return 0;
     }
+
+    private int setDataRequestRateLimitWindowSize() {
+        Slog.d(TAG, "setDataRequestRateLimitWindowSize");
+        int windowSizeSeconds = Integer.parseInt(getNextArgRequired());
+        if (windowSizeSeconds <= 0) {
+            mService.resetDataRequestRateLimitWindowSize();
+        } else {
+            // 20 is the minimum window size supported by the rate limiter.
+            // It is defined by com.android.server.utils.quota.QuotaTracker#MIN_WINDOW_SIZE_MS
+            if (windowSizeSeconds < 20) {
+                windowSizeSeconds = 20;
+            }
+            mService.setDataRequestRateLimitWindowSize(Duration.ofSeconds(windowSizeSeconds));
+        }
+        return 0;
+    }
 }
diff --git a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
index e5c743c..fd4b061 100644
--- a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
+++ b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
@@ -147,6 +147,7 @@
     @Nullable
     protected abstract ActivityRecord findAppTokenForSnapshot(TYPE source);
     protected abstract boolean use16BitFormat();
+    protected abstract Rect getLetterboxInsets(ActivityRecord topActivity);
 
     /**
      * This is different than {@link #recordSnapshotInner(TYPE)} because it doesn't store
@@ -309,7 +310,7 @@
         final WindowState mainWindow = result.second;
         final Rect contentInsets = getSystemBarInsets(mainWindow.getFrame(),
                 mainWindow.getInsetsStateWithVisibilityOverride());
-        final Rect letterboxInsets = activity.getLetterboxInsets();
+        final Rect letterboxInsets = getLetterboxInsets(activity);
         InsetUtils.addInsets(contentInsets, letterboxInsets);
         builder.setIsRealSnapshot(true);
         builder.setId(System.currentTimeMillis());
@@ -335,22 +336,27 @@
         final Configuration taskConfig = activity.getTask().getConfiguration();
         final int displayRotation = taskConfig.windowConfiguration.getDisplayRotation();
         final Rect outCrop = new Rect();
+        final Point taskSize = new Point();
         final Transition.ChangeInfo changeInfo = mCurrentChangeInfo;
         if (changeInfo != null && changeInfo.mRotation != displayRotation) {
             // For example, the source is closing and display rotation changes at the same time.
             // The snapshot should record the state in previous rotation.
             outCrop.set(changeInfo.mAbsoluteBounds);
+            taskSize.set(changeInfo.mAbsoluteBounds.right, changeInfo.mAbsoluteBounds.bottom);
             builder.setRotation(changeInfo.mRotation);
             builder.setOrientation(changeInfo.mAbsoluteBounds.height()
                     >= changeInfo.mAbsoluteBounds.width()
                     ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE);
         } else {
-            outCrop.set(taskConfig.windowConfiguration.getBounds());
+            final Configuration srcConfig = source.getConfiguration();
+            outCrop.set(srcConfig.windowConfiguration.getBounds());
+            final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
+            taskSize.set(taskBounds.width(), taskBounds.height());
             builder.setRotation(displayRotation);
-            builder.setOrientation(taskConfig.orientation);
+            builder.setOrientation(srcConfig.orientation);
         }
         outCrop.offsetTo(0, 0);
-        builder.setTaskSize(new Point(outCrop.right, outCrop.bottom));
+        builder.setTaskSize(taskSize);
         return outCrop;
     }
 
@@ -438,7 +444,7 @@
             return null;
         }
         final Rect contentInsets = new Rect(systemBarInsets);
-        final Rect letterboxInsets = topActivity.getLetterboxInsets();
+        final Rect letterboxInsets = getLetterboxInsets(topActivity);
         InsetUtils.addInsets(contentInsets, letterboxInsets);
         // Note, the app theme snapshot is never translucent because we enforce a non-translucent
         // color above
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index ee865d3..7ccc250 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -523,15 +523,15 @@
                 || mWindowsForAccessibilityObserver.size() > 0);
     }
 
-    void setForceShowMagnifiableBounds(int displayId, boolean show) {
+    void setFullscreenMagnificationActivated(int displayId, boolean activated) {
         if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
-            mAccessibilityTracing.logTrace(TAG + ".setForceShowMagnifiableBounds",
-                    FLAGS_MAGNIFICATION_CALLBACK, "displayId=" + displayId + "; show=" + show);
+            mAccessibilityTracing.logTrace(TAG + ".setFullscreenMagnificationActivated",
+                    FLAGS_MAGNIFICATION_CALLBACK,
+                    "displayId=" + displayId + "; activated=" + activated);
         }
         final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId);
         if (displayMagnifier != null) {
-            displayMagnifier.setForceShowMagnifiableBounds(show);
-            displayMagnifier.showMagnificationBoundsIfNeeded();
+            displayMagnifier.setFullscreenMagnificationActivated(activated);
         }
     }
 
@@ -624,10 +624,21 @@
 
         private final long mLongAnimationDuration;
 
-        private boolean mForceShowMagnifiableBounds = false;
+        private boolean mIsFullscreenMagnificationActivated = false;
+        private final Region mMagnificationRegion = new Region();
+        private final Region mOldMagnificationRegion = new Region();
 
         private final MagnificationSpec mMagnificationSpec = new MagnificationSpec();
 
+        // Following fields are used for computing magnification region
+        private final Path mCircularPath;
+        private int mTempLayer = 0;
+        private final Point mScreenSize = new Point();
+        private final SparseArray<WindowState> mTempWindowStates =
+                new SparseArray<WindowState>();
+        private final RectF mTempRectF = new RectF();
+        private final Matrix mTempMatrix = new Matrix();
+
         DisplayMagnifier(WindowManagerService windowManagerService,
                 DisplayContent displayContent,
                 Display display,
@@ -643,6 +654,15 @@
                     AccessibilityController.getAccessibilityControllerInternal(mService);
             mLongAnimationDuration = mDisplayContext.getResources().getInteger(
                     com.android.internal.R.integer.config_longAnimTime);
+            if (mDisplayContext.getResources().getConfiguration().isScreenRound()) {
+                mCircularPath = new Path();
+
+                getDisplaySizeLocked(mScreenSize);
+                final int centerXY = mScreenSize.x / 2;
+                mCircularPath.addCircle(centerXY, centerXY, centerXY, Path.Direction.CW);
+            } else {
+                mCircularPath = null;
+            }
             if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
                 mAccessibilityTracing.logTrace(LOG_TAG + ".DisplayMagnifier.constructor",
                         FLAGS_MAGNIFICATION_CALLBACK,
@@ -650,6 +670,7 @@
                                 + displayContent + "}; display={" + display + "}; callbacks={"
                                 + callbacks + "}");
             }
+            recomputeBounds();
         }
 
         void setMagnificationSpec(MagnificationSpec spec) {
@@ -658,7 +679,7 @@
                         FLAGS_MAGNIFICATION_CALLBACK, "spec={" + spec + "}");
             }
             updateMagnificationSpec(spec);
-            mMagnifedViewport.recomputeBounds();
+            recomputeBounds();
 
             mService.applyMagnificationSpecLocked(mDisplay.getDisplayId(), spec);
             mService.scheduleAnimationLocked();
@@ -670,30 +691,26 @@
             } else {
                 mMagnificationSpec.clear();
             }
-            // If this message is pending we are in a rotation animation and do not want
-            // to show the border. We will do so when the pending message is handled.
-            if (!mHandler.hasMessages(
-                    MyHandler.MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED)) {
-                mMagnifedViewport.setMagnifiedRegionBorderShown(
-                        isForceShowingMagnifiableBounds(), true);
-            }
+
+            mMagnifedViewport.setShowMagnifiedBorderIfNeeded();
         }
 
-        void setForceShowMagnifiableBounds(boolean show) {
+        void setFullscreenMagnificationActivated(boolean activated) {
             if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
-                mAccessibilityTracing.logTrace(LOG_TAG + ".setForceShowMagnifiableBounds",
-                        FLAGS_MAGNIFICATION_CALLBACK, "show=" + show);
+                mAccessibilityTracing.logTrace(LOG_TAG + ".setFullscreenMagnificationActivated",
+                        FLAGS_MAGNIFICATION_CALLBACK, "activated=" + activated);
             }
-            mForceShowMagnifiableBounds = show;
-            mMagnifedViewport.setMagnifiedRegionBorderShown(show, true);
+            mIsFullscreenMagnificationActivated = activated;
+            mMagnifedViewport.setMagnifiedRegionBorderShown(activated, true);
+            mMagnifedViewport.showMagnificationBoundsIfNeeded();
         }
 
-        boolean isForceShowingMagnifiableBounds() {
+        boolean isFullscreenMagnificationActivated() {
             if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
-                mAccessibilityTracing.logTrace(LOG_TAG + ".isForceShowingMagnifiableBounds",
+                mAccessibilityTracing.logTrace(LOG_TAG + ".isFullscreenMagnificationActivated",
                         FLAGS_MAGNIFICATION_CALLBACK);
             }
-            return mForceShowMagnifiableBounds;
+            return mIsFullscreenMagnificationActivated;
         }
 
         void onWindowLayersChanged() {
@@ -704,7 +721,7 @@
             if (DEBUG_LAYERS) {
                 Slog.i(LOG_TAG, "Layers changed.");
             }
-            mMagnifedViewport.recomputeBounds();
+            recomputeBounds();
             mService.scheduleAnimationLocked();
         }
 
@@ -718,6 +735,8 @@
                 Slog.i(LOG_TAG, "Rotation: " + Surface.rotationToString(rotation)
                         + " displayId: " + displayContent.getDisplayId());
             }
+
+            recomputeBounds();
             mMagnifedViewport.onDisplaySizeChanged();
             mHandler.sendEmptyMessage(MyHandler.MESSAGE_NOTIFY_DISPLAY_SIZE_CHANGED);
         }
@@ -733,7 +752,7 @@
                         + AppTransition.appTransitionOldToString(transition)
                         + " displayId: " + displayId);
             }
-            final boolean isMagnifierActivated = isForceShowingMagnifiableBounds();
+            final boolean isMagnifierActivated = isFullscreenMagnificationActivated();
             if (isMagnifierActivated) {
                 switch (transition) {
                     case WindowManager.TRANSIT_OLD_ACTIVITY_OPEN:
@@ -758,7 +777,7 @@
                 Slog.i(LOG_TAG, "Window transition: " + WindowManager.transitTypeToString(type)
                         + " displayId: " + displayId);
             }
-            final boolean isMagnifierActivated = isForceShowingMagnifiableBounds();
+            final boolean isMagnifierActivated = isFullscreenMagnificationActivated();
             if (isMagnifierActivated) {
                 // All opening/closing situations.
                 switch (type) {
@@ -782,7 +801,7 @@
                         + AppTransition.appTransitionOldToString(transition)
                         + " displayId: " + windowState.getDisplayId());
             }
-            final boolean isMagnifierActivated = isForceShowingMagnifiableBounds();
+            final boolean isMagnifierActivated = isFullscreenMagnificationActivated();
             final int type = windowState.mAttrs.type;
             switch (transition) {
                 case WindowManagerPolicy.TRANSIT_ENTER:
@@ -835,9 +854,7 @@
         }
 
         void getMagnifiedFrameInContentCoords(Rect rect) {
-            Region magnificationRegion = new Region();
-            mMagnifedViewport.getMagnificationRegion(magnificationRegion);
-            magnificationRegion.getBounds(rect);
+            mMagnificationRegion.getBounds(rect);
             rect.offset((int) -mMagnificationSpec.offsetX, (int) -mMagnificationSpec.offsetY);
             rect.scale(1.0f / mMagnificationSpec.scale);
         }
@@ -872,8 +889,8 @@
                         "outMagnificationRegion={" + outMagnificationRegion + "}");
             }
             // Make sure we're working with the most current bounds
-            mMagnifedViewport.recomputeBounds();
-            mMagnifedViewport.getMagnificationRegion(outMagnificationRegion);
+            recomputeBounds();
+            outMagnificationRegion.set(mMagnificationRegion);
         }
 
         boolean isMagnifying() {
@@ -887,16 +904,6 @@
             mMagnifedViewport.destroyWindow();
         }
 
-        // Can be called outside of a surface transaction
-        void showMagnificationBoundsIfNeeded() {
-            if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
-                mAccessibilityTracing.logTrace(LOG_TAG + ".showMagnificationBoundsIfNeeded",
-                        FLAGS_MAGNIFICATION_CALLBACK);
-            }
-            mHandler.obtainMessage(MyHandler.MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED)
-                    .sendToTarget();
-        }
-
         void drawMagnifiedRegionBorderIfNeeded() {
             if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
                 mAccessibilityTracing.logTrace(LOG_TAG + ".drawMagnifiedRegionBorderIfNeeded",
@@ -905,26 +912,169 @@
             mMagnifedViewport.drawWindowIfNeeded();
         }
 
+        void recomputeBounds() {
+            getDisplaySizeLocked(mScreenSize);
+            final int screenWidth = mScreenSize.x;
+            final int screenHeight = mScreenSize.y;
+
+            mMagnificationRegion.set(0, 0, 0, 0);
+            final Region availableBounds = mTempRegion1;
+            availableBounds.set(0, 0, screenWidth, screenHeight);
+
+            if (mCircularPath != null) {
+                availableBounds.setPath(mCircularPath, availableBounds);
+            }
+
+            Region nonMagnifiedBounds = mTempRegion4;
+            nonMagnifiedBounds.set(0, 0, 0, 0);
+
+            SparseArray<WindowState> visibleWindows = mTempWindowStates;
+            visibleWindows.clear();
+            populateWindowsOnScreen(visibleWindows);
+
+            final int visibleWindowCount = visibleWindows.size();
+            for (int i = visibleWindowCount - 1; i >= 0; i--) {
+                WindowState windowState = visibleWindows.valueAt(i);
+                final int windowType = windowState.mAttrs.type;
+                if (isExcludedWindowType(windowType)
+                        || ((windowState.mAttrs.privateFlags
+                        & PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION) != 0)
+                        || ((windowState.mAttrs.privateFlags
+                        & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0)) {
+                    continue;
+                }
+
+                // Consider the touchable portion of the window
+                Matrix matrix = mTempMatrix;
+                populateTransformationMatrix(windowState, matrix);
+                Region touchableRegion = mTempRegion3;
+                windowState.getTouchableRegion(touchableRegion);
+                Rect touchableFrame = mTempRect1;
+                touchableRegion.getBounds(touchableFrame);
+                RectF windowFrame = mTempRectF;
+                windowFrame.set(touchableFrame);
+                windowFrame.offset(-windowState.getFrame().left,
+                        -windowState.getFrame().top);
+                matrix.mapRect(windowFrame);
+                Region windowBounds = mTempRegion2;
+                windowBounds.set((int) windowFrame.left, (int) windowFrame.top,
+                        (int) windowFrame.right, (int) windowFrame.bottom);
+                // Only update new regions
+                Region portionOfWindowAlreadyAccountedFor = mTempRegion3;
+                portionOfWindowAlreadyAccountedFor.set(mMagnificationRegion);
+                portionOfWindowAlreadyAccountedFor.op(nonMagnifiedBounds, Region.Op.UNION);
+                windowBounds.op(portionOfWindowAlreadyAccountedFor, Region.Op.DIFFERENCE);
+
+                if (windowState.shouldMagnify()) {
+                    mMagnificationRegion.op(windowBounds, Region.Op.UNION);
+                    mMagnificationRegion.op(availableBounds, Region.Op.INTERSECT);
+                } else {
+                    nonMagnifiedBounds.op(windowBounds, Region.Op.UNION);
+                    availableBounds.op(windowBounds, Region.Op.DIFFERENCE);
+                }
+
+                // If the navigation bar window doesn't have touchable region, count
+                // navigation bar insets into nonMagnifiedBounds. It happens when
+                // navigation mode is gestural.
+                if (isUntouchableNavigationBar(windowState, mTempRegion3)) {
+                    final Rect navBarInsets = getSystemBarInsetsFrame(windowState);
+                    nonMagnifiedBounds.op(navBarInsets, Region.Op.UNION);
+                    availableBounds.op(navBarInsets, Region.Op.DIFFERENCE);
+                }
+
+                // Count letterbox into nonMagnifiedBounds
+                if (windowState.areAppWindowBoundsLetterboxed()) {
+                    Region letterboxBounds = getLetterboxBounds(windowState);
+                    nonMagnifiedBounds.op(letterboxBounds, Region.Op.UNION);
+                    availableBounds.op(letterboxBounds, Region.Op.DIFFERENCE);
+                }
+
+                // Update accounted bounds
+                Region accountedBounds = mTempRegion2;
+                accountedBounds.set(mMagnificationRegion);
+                accountedBounds.op(nonMagnifiedBounds, Region.Op.UNION);
+                accountedBounds.op(0, 0, screenWidth, screenHeight, Region.Op.INTERSECT);
+
+                if (accountedBounds.isRect()) {
+                    Rect accountedFrame = mTempRect1;
+                    accountedBounds.getBounds(accountedFrame);
+                    if (accountedFrame.width() == screenWidth
+                            && accountedFrame.height() == screenHeight) {
+                        break;
+                    }
+                }
+            }
+            visibleWindows.clear();
+
+            mMagnifedViewport.intersectWithDrawBorderInset(screenWidth, screenHeight);
+
+
+            final boolean magnifiedChanged =
+                    !mOldMagnificationRegion.equals(mMagnificationRegion);
+            if (magnifiedChanged) {
+                mMagnifedViewport.updateBorderDrawingStatus(screenWidth, screenHeight);
+
+                mOldMagnificationRegion.set(mMagnificationRegion);
+                final SomeArgs args = SomeArgs.obtain();
+                args.arg1 = Region.obtain(mMagnificationRegion);
+                mHandler.obtainMessage(
+                                MyHandler.MESSAGE_NOTIFY_MAGNIFICATION_REGION_CHANGED, args)
+                        .sendToTarget();
+            }
+        }
+
+        private Region getLetterboxBounds(WindowState windowState) {
+            final ActivityRecord appToken = windowState.mActivityRecord;
+            if (appToken == null) {
+                return new Region();
+            }
+
+            final Rect boundsWithoutLetterbox = windowState.getBounds();
+            final Rect letterboxInsets = appToken.getLetterboxInsets();
+
+            final Rect boundsIncludingLetterbox = Rect.copyOrNull(boundsWithoutLetterbox);
+            // Letterbox insets from mActivityRecord are positive, so we negate them to grow the
+            // bounds to include the letterbox.
+            boundsIncludingLetterbox.inset(
+                    Insets.subtract(Insets.NONE, Insets.of(letterboxInsets)));
+
+            final Region letterboxBounds = new Region();
+            letterboxBounds.set(boundsIncludingLetterbox);
+            letterboxBounds.op(boundsWithoutLetterbox, Region.Op.DIFFERENCE);
+            return letterboxBounds;
+        }
+
+        private boolean isExcludedWindowType(int windowType) {
+            return windowType == TYPE_MAGNIFICATION_OVERLAY
+                    // Omit the touch region of window magnification to avoid the cut out of the
+                    // magnification and the magnified center of window magnification could be
+                    // in the bounds
+                    || windowType == TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY;
+        }
+
+        private void populateWindowsOnScreen(SparseArray<WindowState> outWindows) {
+            mTempLayer = 0;
+            mDisplayContent.forAllWindows((w) -> {
+                if (w.isOnScreen() && w.isVisible()
+                        && (w.mAttrs.alpha != 0)) {
+                    mTempLayer++;
+                    outWindows.put(mTempLayer, w);
+                }
+            }, /* traverseTopToBottom= */ false);
+        }
+
+        private void getDisplaySizeLocked(Point outSize) {
+            final Rect bounds =
+                    mDisplayContent.getConfiguration().windowConfiguration.getBounds();
+            outSize.set(bounds.width(), bounds.height());
+        }
+
         void dump(PrintWriter pw, String prefix) {
             mMagnifedViewport.dump(pw, prefix);
         }
 
         private final class MagnifiedViewport {
 
-            private final SparseArray<WindowState> mTempWindowStates =
-                    new SparseArray<WindowState>();
-
-            private final RectF mTempRectF = new RectF();
-
-            private final Point mScreenSize = new Point();
-
-            private final Matrix mTempMatrix = new Matrix();
-
-            private final Region mMagnificationRegion = new Region();
-            private final Region mOldMagnificationRegion = new Region();
-
-            private final Path mCircularPath;
-
             private final float mBorderWidth;
             private final int mHalfBorderWidth;
             private final int mDrawBorderInset;
@@ -932,7 +1082,6 @@
             private final ViewportWindow mWindow;
 
             private boolean mFullRedrawNeeded;
-            private int mTempLayer = 0;
 
             MagnifiedViewport() {
                 mBorderWidth = mDisplayContext.getResources().getDimension(
@@ -940,186 +1089,59 @@
                 mHalfBorderWidth = (int) Math.ceil(mBorderWidth / 2);
                 mDrawBorderInset = (int) mBorderWidth / 2;
                 mWindow = new ViewportWindow(mDisplayContext);
+            }
 
-                if (mDisplayContext.getResources().getConfiguration().isScreenRound()) {
-                    mCircularPath = new Path();
-
-                    getDisplaySizeLocked(mScreenSize);
-                    final int centerXY = mScreenSize.x / 2;
-                    mCircularPath.addCircle(centerXY, centerXY, centerXY, Path.Direction.CW);
+            void updateBorderDrawingStatus(int screenWidth, int screenHeight) {
+                mWindow.setBounds(mMagnificationRegion);
+                final Rect dirtyRect = mTempRect1;
+                if (mFullRedrawNeeded) {
+                    mFullRedrawNeeded = false;
+                    dirtyRect.set(mDrawBorderInset, mDrawBorderInset,
+                            screenWidth - mDrawBorderInset,
+                            screenHeight - mDrawBorderInset);
+                    mWindow.invalidate(dirtyRect);
                 } else {
-                    mCircularPath = null;
+                    final Region dirtyRegion = mTempRegion3;
+                    dirtyRegion.set(mMagnificationRegion);
+                    dirtyRegion.op(mOldMagnificationRegion, Region.Op.XOR);
+                    dirtyRegion.getBounds(dirtyRect);
+                    mWindow.invalidate(dirtyRect);
                 }
-
-                recomputeBounds();
             }
 
-            void getMagnificationRegion(@NonNull Region outMagnificationRegion) {
-                outMagnificationRegion.set(mMagnificationRegion);
+            void setShowMagnifiedBorderIfNeeded() {
+                // If this message is pending, we are in a rotation animation and do not want
+                // to show the border. We will do so when the pending message is handled.
+                if (!mHandler.hasMessages(
+                        MyHandler.MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED)) {
+                    setMagnifiedRegionBorderShown(
+                            isFullscreenMagnificationActivated(), true);
+                }
             }
 
-            void recomputeBounds() {
-                getDisplaySizeLocked(mScreenSize);
-                final int screenWidth = mScreenSize.x;
-                final int screenHeight = mScreenSize.y;
-
-                mMagnificationRegion.set(0, 0, 0, 0);
-                final Region availableBounds = mTempRegion1;
-                availableBounds.set(0, 0, screenWidth, screenHeight);
-
-                if (mCircularPath != null) {
-                    availableBounds.setPath(mCircularPath, availableBounds);
+            // Can be called outside of a surface transaction
+            void showMagnificationBoundsIfNeeded() {
+                if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
+                    mAccessibilityTracing.logTrace(LOG_TAG + ".showMagnificationBoundsIfNeeded",
+                            FLAGS_MAGNIFICATION_CALLBACK);
                 }
+                mHandler.obtainMessage(MyHandler.MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED)
+                        .sendToTarget();
+            }
 
-                Region nonMagnifiedBounds = mTempRegion4;
-                nonMagnifiedBounds.set(0, 0, 0, 0);
-
-                SparseArray<WindowState> visibleWindows = mTempWindowStates;
-                visibleWindows.clear();
-                populateWindowsOnScreen(visibleWindows);
-
-                final int visibleWindowCount = visibleWindows.size();
-                for (int i = visibleWindowCount - 1; i >= 0; i--) {
-                    WindowState windowState = visibleWindows.valueAt(i);
-                    final int windowType = windowState.mAttrs.type;
-                    if (isExcludedWindowType(windowType)
-                            || ((windowState.mAttrs.privateFlags
-                            & PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION) != 0)
-                            || ((windowState.mAttrs.privateFlags
-                            & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0)) {
-                        continue;
-                    }
-
-                    // Consider the touchable portion of the window
-                    Matrix matrix = mTempMatrix;
-                    populateTransformationMatrix(windowState, matrix);
-                    Region touchableRegion = mTempRegion3;
-                    windowState.getTouchableRegion(touchableRegion);
-                    Rect touchableFrame = mTempRect1;
-                    touchableRegion.getBounds(touchableFrame);
-                    RectF windowFrame = mTempRectF;
-                    windowFrame.set(touchableFrame);
-                    windowFrame.offset(-windowState.getFrame().left,
-                            -windowState.getFrame().top);
-                    matrix.mapRect(windowFrame);
-                    Region windowBounds = mTempRegion2;
-                    windowBounds.set((int) windowFrame.left, (int) windowFrame.top,
-                            (int) windowFrame.right, (int) windowFrame.bottom);
-                    // Only update new regions
-                    Region portionOfWindowAlreadyAccountedFor = mTempRegion3;
-                    portionOfWindowAlreadyAccountedFor.set(mMagnificationRegion);
-                    portionOfWindowAlreadyAccountedFor.op(nonMagnifiedBounds, Region.Op.UNION);
-                    windowBounds.op(portionOfWindowAlreadyAccountedFor, Region.Op.DIFFERENCE);
-
-                    if (windowState.shouldMagnify()) {
-                        mMagnificationRegion.op(windowBounds, Region.Op.UNION);
-                        mMagnificationRegion.op(availableBounds, Region.Op.INTERSECT);
-                    } else {
-                        nonMagnifiedBounds.op(windowBounds, Region.Op.UNION);
-                        availableBounds.op(windowBounds, Region.Op.DIFFERENCE);
-                    }
-
-                    // If the navigation bar window doesn't have touchable region, count
-                    // navigation bar insets into nonMagnifiedBounds. It happens when
-                    // navigation mode is gestural.
-                    if (isUntouchableNavigationBar(windowState, mTempRegion3)) {
-                        final Rect navBarInsets = getSystemBarInsetsFrame(windowState);
-                        nonMagnifiedBounds.op(navBarInsets, Region.Op.UNION);
-                        availableBounds.op(navBarInsets, Region.Op.DIFFERENCE);
-                    }
-
-                    // Count letterbox into nonMagnifiedBounds
-                    if (windowState.areAppWindowBoundsLetterboxed()) {
-                        Region letterboxBounds = getLetterboxBounds(windowState);
-                        nonMagnifiedBounds.op(letterboxBounds, Region.Op.UNION);
-                        availableBounds.op(letterboxBounds, Region.Op.DIFFERENCE);
-                    }
-
-                    // Update accounted bounds
-                    Region accountedBounds = mTempRegion2;
-                    accountedBounds.set(mMagnificationRegion);
-                    accountedBounds.op(nonMagnifiedBounds, Region.Op.UNION);
-                    accountedBounds.op(0, 0, screenWidth, screenHeight, Region.Op.INTERSECT);
-
-                    if (accountedBounds.isRect()) {
-                        Rect accountedFrame = mTempRect1;
-                        accountedBounds.getBounds(accountedFrame);
-                        if (accountedFrame.width() == screenWidth
-                                && accountedFrame.height() == screenHeight) {
-                            break;
-                        }
-                    }
-                }
-                visibleWindows.clear();
-
+            void intersectWithDrawBorderInset(int screenWidth, int screenHeight) {
                 mMagnificationRegion.op(mDrawBorderInset, mDrawBorderInset,
                         screenWidth - mDrawBorderInset, screenHeight - mDrawBorderInset,
                         Region.Op.INTERSECT);
-
-                final boolean magnifiedChanged =
-                        !mOldMagnificationRegion.equals(mMagnificationRegion);
-                if (magnifiedChanged) {
-                    mWindow.setBounds(mMagnificationRegion);
-                    final Rect dirtyRect = mTempRect1;
-                    if (mFullRedrawNeeded) {
-                        mFullRedrawNeeded = false;
-                        dirtyRect.set(mDrawBorderInset, mDrawBorderInset,
-                                screenWidth - mDrawBorderInset,
-                                screenHeight - mDrawBorderInset);
-                        mWindow.invalidate(dirtyRect);
-                    } else {
-                        final Region dirtyRegion = mTempRegion3;
-                        dirtyRegion.set(mMagnificationRegion);
-                        dirtyRegion.op(mOldMagnificationRegion, Region.Op.XOR);
-                        dirtyRegion.getBounds(dirtyRect);
-                        mWindow.invalidate(dirtyRect);
-                    }
-
-                    mOldMagnificationRegion.set(mMagnificationRegion);
-                    final SomeArgs args = SomeArgs.obtain();
-                    args.arg1 = Region.obtain(mMagnificationRegion);
-                    mHandler.obtainMessage(
-                            MyHandler.MESSAGE_NOTIFY_MAGNIFICATION_REGION_CHANGED, args)
-                            .sendToTarget();
-                }
-            }
-
-            private Region getLetterboxBounds(WindowState windowState) {
-                final ActivityRecord appToken = windowState.mActivityRecord;
-                if (appToken == null) {
-                    return new Region();
-                }
-
-                final Rect boundsWithoutLetterbox = windowState.getBounds();
-                final Rect letterboxInsets = appToken.getLetterboxInsets();
-
-                final Rect boundsIncludingLetterbox = Rect.copyOrNull(boundsWithoutLetterbox);
-                // Letterbox insets from mActivityRecord are positive, so we negate them to grow the
-                // bounds to include the letterbox.
-                boundsIncludingLetterbox.inset(
-                        Insets.subtract(Insets.NONE, Insets.of(letterboxInsets)));
-
-                final Region letterboxBounds = new Region();
-                letterboxBounds.set(boundsIncludingLetterbox);
-                letterboxBounds.op(boundsWithoutLetterbox, Region.Op.DIFFERENCE);
-                return letterboxBounds;
-            }
-
-            private boolean isExcludedWindowType(int windowType) {
-                return windowType == TYPE_MAGNIFICATION_OVERLAY
-                        // Omit the touch region of window magnification to avoid the cut out of the
-                        // magnification and the magnified center of window magnification could be
-                        // in the bounds
-                        || windowType == TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY;
             }
 
             void onDisplaySizeChanged() {
-                // If we are showing the magnification border, hide it immediately so
+                // If fullscreen magnification is activated, hide the border immediately so
                 // the user does not see strange artifacts during display size changed caused by
                 // rotation or folding/unfolding the device. In the rotation case, the screenshot
                 // used for rotation already has the border. After the rotation is complete
                 // we will show the border.
-                if (isForceShowingMagnifiableBounds()) {
+                if (isFullscreenMagnificationActivated()) {
                     setMagnifiedRegionBorderShown(false, false);
                     final long delay = (long) (mLongAnimationDuration
                             * mService.getWindowAnimationScaleLocked());
@@ -1127,7 +1149,6 @@
                             MyHandler.MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED);
                     mHandler.sendMessageDelayed(message, delay);
                 }
-                recomputeBounds();
                 mWindow.updateSize();
             }
 
@@ -1148,23 +1169,6 @@
                 mWindow.releaseSurface();
             }
 
-            private void populateWindowsOnScreen(SparseArray<WindowState> outWindows) {
-                mTempLayer = 0;
-                mDisplayContent.forAllWindows((w) -> {
-                    if (w.isOnScreen() && w.isVisible()
-                            && (w.mAttrs.alpha != 0)) {
-                        mTempLayer++;
-                        outWindows.put(mTempLayer, w);
-                    }
-                }, false /* traverseTopToBottom */ );
-            }
-
-            private void getDisplaySizeLocked(Point outSize) {
-                final Rect bounds =
-                        mDisplayContent.getConfiguration().windowConfiguration.getBounds();
-                outSize.set(bounds.width(), bounds.height());
-            }
-
             void dump(PrintWriter pw, String prefix) {
                 mWindow.dump(pw, prefix);
             }
@@ -1490,7 +1494,7 @@
 
                     case MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED : {
                         synchronized (mService.mGlobalLock) {
-                            if (isForceShowingMagnifiableBounds()) {
+                            if (isFullscreenMagnificationActivated()) {
                                 mMagnifedViewport.setMagnifiedRegionBorderShown(true, true);
                                 mService.scheduleAnimationLocked();
                             }
diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
index f83003d..62fb4bf 100644
--- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java
+++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
@@ -21,6 +21,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
+import android.graphics.Rect;
 import android.os.Environment;
 import android.os.SystemProperties;
 import android.os.Trace;
@@ -617,6 +618,12 @@
         return mPersistInfoProvider.use16BitFormat();
     }
 
+    @Override
+    protected Rect getLetterboxInsets(ActivityRecord topActivity) {
+        // Do not capture letterbox for ActivityRecord
+        return Letterbox.EMPTY_RECT;
+    }
+
     @NonNull
     private SparseArray<UserSavedFile> getUserFiles(int userId) {
         if (mUserSavedFiles.get(userId) == null) {
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 38ee456..b51f899 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -35,6 +35,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.content.res.Configuration;
 import android.content.res.ResourceId;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -1065,8 +1066,9 @@
 
             if (mOpenActivities != null) {
                 for (int i = mOpenActivities.length - 1; i >= 0; --i) {
-                    if (mOpenActivities[i].mLaunchTaskBehind) {
-                        restoreLaunchBehind(mOpenActivities[i]);
+                    final ActivityRecord resetActivity = mOpenActivities[i];
+                    if (resetActivity.mLaunchTaskBehind) {
+                        restoreLaunchBehind(resetActivity);
                     }
                 }
             }
@@ -1233,8 +1235,7 @@
                         represent.allowEnterPip);
             }
 
-            void createStartingSurface(@NonNull WindowContainer closeWindow,
-                    ActivityRecord[] visibleOpenActivities) {
+            void createStartingSurface(ActivityRecord[] visibleOpenActivities) {
                 if (mAdaptors[0].mSwitchType == DIALOG_CLOSE) {
                     return;
                 }
@@ -1253,12 +1254,15 @@
                     return;
                 }
                 final TaskSnapshot snapshot = getSnapshot(mainOpen, visibleOpenActivities);
+                // If there is only one adaptor, attach the windowless window to top activity,
+                // because fixed rotation only applies on activity.
+                // Note that embedded activity won't use fixed rotation.
+                final Configuration openConfig = mAdaptors.length == 1
+                        ? mainActivity.getConfiguration() : openTask.getConfiguration();
                 mRequestedStartingSurfaceId = openTask.mAtmService.mTaskOrganizerController
                         .addWindowlessStartingSurface(openTask, mainActivity,
-                        // Choose configuration from closeWindow, because the configuration
-                        // of opening target may not update before resume, so the starting
-                        // surface should occlude it entirely.
-                        mRemoteAnimationTarget.leash, snapshot, closeWindow.getConfiguration(),
+                                mAdaptors.length == 1 ? mainActivity.getSurfaceControl()
+                                        : mRemoteAnimationTarget.leash, snapshot, openConfig,
                             new IWindowlessStartingSurfaceCallback.Stub() {
                             // Once the starting surface has been created in shell, it will call
                             // onSurfaceAdded to pass the created surface to core, so if a
@@ -1290,10 +1294,7 @@
                 if (mStartingSurface != null && mStartingSurface.isValid()) {
                     SurfaceControl.Transaction transaction = reparentTransaction != null
                             ? reparentTransaction : mAdaptors[0].mTarget.getPendingTransaction();
-                    if (mAdaptors.length == 1) {
-                        transaction.reparent(mStartingSurface,
-                                        mAdaptors[0].mTarget.getSurfaceControl());
-                    } else {
+                    if (mAdaptors.length != 1) {
                         // More than one opening window, reparent starting surface to leaf task.
                         final WindowContainer wc = mAdaptors[0].mTarget;
                         final Task task = wc.asActivityRecord() != null
@@ -1499,16 +1500,15 @@
 
             /**
              * Apply preview strategy on the opening target
-             * @param closeWindow The close window, where it's configuration should cover all
-             *                    open target(s).
+             *
              * @param openAnimationAdaptor The animator who can create starting surface.
              * @param visibleOpenActivities  The visible activities in opening targets.
              */
-            private void applyPreviewStrategy(@NonNull WindowContainer closeWindow,
+            private void applyPreviewStrategy(
                     @NonNull BackWindowAnimationAdaptorWrapper openAnimationAdaptor,
                     @NonNull ActivityRecord[] visibleOpenActivities) {
                 if (isSupportWindowlessSurface() && mShowWindowlessSurface && !mIsLaunchBehind) {
-                    openAnimationAdaptor.createStartingSurface(closeWindow, visibleOpenActivities);
+                    openAnimationAdaptor.createStartingSurface(visibleOpenActivities);
                 } else {
                     for (int i = visibleOpenActivities.length - 1; i >= 0; --i) {
                         setLaunchBehind(visibleOpenActivities[i]);
@@ -1539,7 +1539,7 @@
                 }
                 mCloseTarget.mTransitionController.mSnapshotController
                         .mActivitySnapshotController.clearOnBackPressedActivities();
-                applyPreviewStrategy(mCloseTarget, mOpenAnimAdaptor, openingActivities);
+                applyPreviewStrategy(mOpenAnimAdaptor, openingActivities);
 
                 final IBackAnimationFinishedCallback callback = makeAnimationFinishedCallback();
                 final RemoteAnimationTarget[] targets = getAnimationTargets();
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 86ca1ea..fdae53f 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -94,6 +94,11 @@
     private static final long ASM_GRACEPERIOD_TIMEOUT_MS = TIMEOUT_MS;
     private static final int ASM_GRACEPERIOD_MAX_REPEATS = 5;
     private static final int NO_PROCESS_UID = -1;
+
+    static final String AUTO_OPT_IN_NOT_PENDING_INTENT = "notPendingIntent";
+    static final String AUTO_OPT_IN_CALL_FOR_RESULT = "callForResult";
+    static final String AUTO_OPT_IN_SAME_UID = "sameUid";
+
     /** If enabled the creator will not allow BAL on its behalf by default. */
     @ChangeId
     @EnabledAfter(targetSdkVersion = UPSIDE_DOWN_CAKE)
@@ -232,9 +237,9 @@
         private final boolean mCallingUidHasAnyVisibleWindow;
         private final @ActivityManager.ProcessState int mCallingUidProcState;
         private final boolean mIsCallingUidPersistentSystemProcess;
-        private final BackgroundStartPrivileges mBalAllowedByPiSender;
-        private final BackgroundStartPrivileges mBalAllowedByPiCreatorWithHardening;
-        private final BackgroundStartPrivileges mBalAllowedByPiCreator;
+        final BackgroundStartPrivileges mBalAllowedByPiSender;
+        final BackgroundStartPrivileges mBalAllowedByPiCreatorWithHardening;
+        final BackgroundStartPrivileges mBalAllowedByPiCreator;
         private final String mRealCallingPackage;
         private final int mRealCallingUid;
         private final int mRealCallingPid;
@@ -248,11 +253,12 @@
         private final WindowProcessController mRealCallerApp;
         private final boolean mIsCallForResult;
         private final ActivityOptions mCheckedOptions;
-        private final String mAutoOptInReason;
+        final String mAutoOptInReason;
+        private final boolean mAutoOptInCaller;
         private BalVerdict mResultForCaller;
         private BalVerdict mResultForRealCaller;
 
-        private BalState(int callingUid, int callingPid, final String callingPackage,
+        @VisibleForTesting BalState(int callingUid, int callingPid, final String callingPackage,
                  int realCallingUid, int realCallingPid,
                  WindowProcessController callerApp,
                  PendingIntentRecord originatingPendingIntent,
@@ -280,26 +286,27 @@
             if (!balImproveRealCallerVisibilityCheck()) {
                 // without this fix the auto-opt ins below would violate CTS tests
                 mAutoOptInReason = null;
-            } else if (mIsCallForResult) {
-                mAutoOptInReason = "callForResult";
+                mAutoOptInCaller = false;
             } else if (originatingPendingIntent == null) {
-                mAutoOptInReason = "notPendingIntent";
+                mAutoOptInReason = AUTO_OPT_IN_NOT_PENDING_INTENT;
+                mAutoOptInCaller = true;
+            } else if (mIsCallForResult) {
+                mAutoOptInReason = AUTO_OPT_IN_CALL_FOR_RESULT;
+                mAutoOptInCaller = false;
             } else if (callingUid == realCallingUid && !balRequireOptInSameUid()) {
-                mAutoOptInReason = "sameUid";
+                mAutoOptInReason = AUTO_OPT_IN_SAME_UID;
+                mAutoOptInCaller = false;
             } else {
                 mAutoOptInReason = null;
+                mAutoOptInCaller = false;
             }
 
-            if (mAutoOptInReason != null) {
+            if (mAutoOptInCaller) {
                 // grant BAL privileges unless explicitly opted out
                 mBalAllowedByPiCreatorWithHardening = mBalAllowedByPiCreator =
                         callerBackgroundActivityStartMode == MODE_BACKGROUND_ACTIVITY_START_DENIED
                                 ? BackgroundStartPrivileges.NONE
                                 : BackgroundStartPrivileges.ALLOW_BAL;
-                mBalAllowedByPiSender = realCallerBackgroundActivityStartMode
-                        == MODE_BACKGROUND_ACTIVITY_START_DENIED
-                        ? BackgroundStartPrivileges.NONE
-                        : BackgroundStartPrivileges.ALLOW_BAL;
             } else {
                 // for PendingIntents we restrict BAL based on target_sdk
                 mBalAllowedByPiCreatorWithHardening = getBackgroundStartPrivilegesAllowedByCreator(
@@ -312,10 +319,21 @@
                 mBalAllowedByPiCreator = balRequireOptInByPendingIntentCreator()
                         ? mBalAllowedByPiCreatorWithHardening
                         : mBalAllowedByPiCreatorWithoutHardening;
+            }
+
+            if (mAutoOptInReason != null) {
+                // grant BAL privileges unless explicitly opted out
+                mBalAllowedByPiSender = realCallerBackgroundActivityStartMode
+                        == MODE_BACKGROUND_ACTIVITY_START_DENIED
+                        ? BackgroundStartPrivileges.NONE
+                        : BackgroundStartPrivileges.ALLOW_BAL;
+            } else {
+                // for PendingIntents we restrict BAL based on target_sdk
                 mBalAllowedByPiSender =
                         PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(
                                 checkedOptions, realCallingUid, mRealCallingPackage);
             }
+
             mAppSwitchState = mService.getBalAppSwitchesState();
             mCallingUidProcState = mService.mActiveUids.getUidState(callingUid);
             mIsCallingUidPersistentSystemProcess =
@@ -407,7 +425,7 @@
             return mRealCallingUid != NO_PROCESS_UID;
         }
 
-        private boolean isPendingIntent() {
+        boolean isPendingIntent() {
             return mOriginatingPendingIntent != null && hasRealCaller();
         }
 
@@ -485,23 +503,19 @@
         }
 
         public boolean callerExplicitOptInOrAutoOptIn() {
-            if (mAutoOptInReason == null) {
-                return mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
-                        == MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
-            } else {
-                return mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
-                        != MODE_BACKGROUND_ACTIVITY_START_DENIED;
+            if (mAutoOptInCaller) {
+                return !callerExplicitOptOut();
             }
+            return mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
+                    == MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
         }
 
         public boolean realCallerExplicitOptInOrAutoOptIn() {
-            if (mAutoOptInReason == null) {
-                return mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
-                        == MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
-            } else {
-                return mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
-                        != MODE_BACKGROUND_ACTIVITY_START_DENIED;
+            if (mAutoOptInReason != null) {
+                return !realCallerExplicitOptOut();
             }
+            return mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
+                    == MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
         }
 
         public boolean callerExplicitOptOut() {
@@ -523,6 +537,11 @@
             return mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
                     != MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
         }
+
+        @Override
+        public String toString() {
+            return dump();
+        }
     }
 
     static class BalVerdict {
@@ -972,7 +991,7 @@
      * String, int, boolean, boolean, boolean, long, long, long)} for details on the
      * exceptions.
      */
-    private BalVerdict checkProcessAllowsBal(WindowProcessController app,
+    @VisibleForTesting BalVerdict checkProcessAllowsBal(WindowProcessController app,
             BalState state) {
         if (app == null) {
             return BalVerdict.BLOCK;
diff --git a/services/core/java/com/android/server/wm/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java
index e66321a..362d4ef 100644
--- a/services/core/java/com/android/server/wm/Letterbox.java
+++ b/services/core/java/com/android/server/wm/Letterbox.java
@@ -49,7 +49,7 @@
  */
 public class Letterbox {
 
-    private static final Rect EMPTY_RECT = new Rect();
+    static final Rect EMPTY_RECT = new Rect();
     private static final Point ZERO_POINT = new Point(0, 0);
 
     private final Supplier<SurfaceControl.Builder> mSurfaceControlFactory;
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 8b622d2..4218f8f 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -270,6 +270,11 @@
         return source.getTaskDescription();
     }
 
+    @Override
+    protected Rect getLetterboxInsets(ActivityRecord topActivity) {
+        return topActivity.getLetterboxInsets();
+    }
+
     void getClosingTasksInner(Task task, ArraySet<Task> outClosingTasks) {
         // Since RecentsAnimation will handle task snapshot while switching apps with the
         // best capture timing (e.g. IME window capture),
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 70775530..503f925 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -267,7 +267,8 @@
         mSyncEngine.addOnIdleListener(this::tryStartCollectFromQueue);
     }
 
-    private void detachPlayer() {
+    @VisibleForTesting
+    void detachPlayer() {
         if (mTransitionPlayer == null) return;
         // Immediately set to null so that nothing inadvertently starts/queues.
         mTransitionPlayer = null;
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 669c61c..ddda3f4 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -409,13 +409,12 @@
     public abstract void setMagnificationSpec(int displayId, MagnificationSpec spec);
 
     /**
-     * Set by the accessibility framework to indicate whether the magnifiable regions of the display
-     * should be shown.
+     * Set by the accessibility framework to indicate whether fullscreen magnification is activated.
      *
      * @param displayId The logical display id.
-     * @param show {@code true} to show magnifiable region bounds, {@code false} to hide
+     * @param activated The activation of fullscreen magnification
      */
-    public abstract void setForceShowMagnifiableBounds(int displayId, boolean show);
+    public abstract void setFullscreenMagnificationActivated(int displayId, boolean activated);
 
     /**
      * Obtains the magnification regions.
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 61480d2..8ebe826 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -7858,10 +7858,11 @@
         }
 
         @Override
-        public void setForceShowMagnifiableBounds(int displayId, boolean show) {
+        public void setFullscreenMagnificationActivated(int displayId, boolean activated) {
             synchronized (mGlobalLock) {
                 if (mAccessibilityController.hasCallbacks()) {
-                    mAccessibilityController.setForceShowMagnifiableBounds(displayId, show);
+                    mAccessibilityController
+                            .setFullscreenMagnificationActivated(displayId, activated);
                 } else {
                     throw new IllegalStateException("Magnification callbacks not set!");
                 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/rollback/RollbackPackageHealthObserverTest.java b/services/tests/mockingservicestests/src/com/android/server/rollback/RollbackPackageHealthObserverTest.java
index a140730..d6e246f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/rollback/RollbackPackageHealthObserverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/rollback/RollbackPackageHealthObserverTest.java
@@ -23,7 +23,13 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
 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;
@@ -33,14 +39,17 @@
 import android.content.rollback.PackageRollbackInfo;
 import android.content.rollback.RollbackInfo;
 import android.content.rollback.RollbackManager;
-import android.util.Log;
-import android.util.Xml;
+import android.crashrecovery.flags.Flags;
+import android.os.Handler;
+import android.os.MessageQueue;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.server.PackageWatchdog;
 import com.android.server.SystemConfig;
+import com.android.server.pm.ApexManager;
 
 import org.junit.After;
 import org.junit.Before;
@@ -49,18 +58,16 @@
 import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoSession;
 import org.mockito.quality.Strictness;
 import org.mockito.stubbing.Answer;
-import org.xmlpull.v1.XmlPullParser;
 
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
+import java.time.Duration;
 import java.util.List;
-import java.util.Scanner;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 
 @RunWith(AndroidJUnit4.class)
@@ -78,10 +85,18 @@
     @Mock
     PackageManager mMockPackageManager;
 
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private ApexManager mApexManager;
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private MockitoSession mSession;
     private static final String APP_A = "com.package.a";
     private static final String APP_B = "com.package.b";
+    private static final String APP_C = "com.package.c";
     private static final long VERSION_CODE = 1L;
+    private static final long VERSION_CODE_2 = 2L;
     private static final String LOG_TAG = "RollbackPackageHealthObserverTest";
 
     private SystemConfig mSysConfig;
@@ -101,7 +116,6 @@
         // Mock PackageWatchdog
         doAnswer((Answer<PackageWatchdog>) invocationOnMock -> mMockPackageWatchdog)
                 .when(() -> PackageWatchdog.getInstance(mMockContext));
-
     }
 
     @After
@@ -121,7 +135,7 @@
     @Test
     public void testHealthCheckLevels() {
         RollbackPackageHealthObserver observer =
-                spy(new RollbackPackageHealthObserver(mMockContext));
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
         VersionedPackage testFailedPackage = new VersionedPackage(APP_A, VERSION_CODE);
         VersionedPackage secondFailedPackage = new VersionedPackage(APP_B, VERSION_CODE);
 
@@ -165,14 +179,14 @@
     @Test
     public void testIsPersistent() {
         RollbackPackageHealthObserver observer =
-                spy(new RollbackPackageHealthObserver(mMockContext));
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
         assertTrue(observer.isPersistent());
     }
 
     @Test
     public void testMayObservePackage_withoutAnyRollback() {
         RollbackPackageHealthObserver observer =
-                spy(new RollbackPackageHealthObserver(mMockContext));
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
         when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
         when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of());
         assertFalse(observer.mayObservePackage(APP_A));
@@ -182,7 +196,7 @@
     public void testMayObservePackage_forPersistentApp()
             throws PackageManager.NameNotFoundException {
         RollbackPackageHealthObserver observer =
-                spy(new RollbackPackageHealthObserver(mMockContext));
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
         ApplicationInfo info = new ApplicationInfo();
         info.flags = ApplicationInfo.FLAG_PERSISTENT | ApplicationInfo.FLAG_SYSTEM;
         when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
@@ -197,7 +211,7 @@
     public void testMayObservePackage_forNonPersistentApp()
             throws PackageManager.NameNotFoundException {
         RollbackPackageHealthObserver observer =
-                spy(new RollbackPackageHealthObserver(mMockContext));
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
         when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
         when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(mRollbackInfo));
         when(mRollbackInfo.getPackages()).thenReturn(List.of(mPackageRollbackInfo));
@@ -208,96 +222,720 @@
     }
 
     /**
-     * Test that isAutomaticRollbackDenied works correctly when packages that are not
-     * denied are sent.
+     * Test that when impactLevel is low returns user impact level 70
      */
     @Test
-    public void isRollbackAllowedTest_false() throws IOException {
-        final String contents =
-                "<config>\n"
-                + "    <automatic-rollback-denylisted-app package=\"com.android.vending\" />\n"
-                + "</config>";
-        final File folder = createTempSubfolder("folder");
-        createTempFile(folder, "automatic-rollback-denylisted-app.xml", contents);
+    public void healthCheckFailed_impactLevelLow_onePackage()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo(appAFrom, appATo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(1, List.of(packageRollbackInfo),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+        VersionedPackage secondFailedPackage = new VersionedPackage(APP_B, VERSION_CODE);
 
-        readPermissions(folder, /* Grant all permission flags */ ~0);
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(rollbackInfo1));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
 
-        assertThat(RollbackPackageHealthObserver.isAutomaticRollbackDenied(mSysConfig,
-                new VersionedPackage("com.test.package", 1))).isEqualTo(false);
+        assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_70,
+                observer.onHealthCheckFailed(secondFailedPackage,
+                        PackageWatchdog.FAILURE_REASON_APP_CRASH, 1));
     }
 
     /**
-     * Test that isAutomaticRollbackDenied works correctly when packages that are
-     * denied are sent.
+     * HealthCheckFailed should only return low impact rollbacks. High impact rollbacks are only
+     * for bootloop.
      */
     @Test
-    public void isRollbackAllowedTest_true() throws IOException {
-        final String contents =
-                "<config>\n"
-                + "    <automatic-rollback-denylisted-app package=\"com.android.vending\" />\n"
-                + "</config>";
-        final File folder = createTempSubfolder("folder");
-        createTempFile(folder, "automatic-rollback-denylisted-app.xml", contents);
+    public void healthCheckFailed_impactLevelHigh_onePackage()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo(appAFrom, appATo,
+                null, null, false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(1, List.of(packageRollbackInfo),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_HIGH);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+        VersionedPackage secondFailedPackage = new VersionedPackage(APP_B, VERSION_CODE);
 
-        readPermissions(folder, /* Grant all permission flags */ ~0);
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(rollbackInfo1));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
 
-        assertThat(RollbackPackageHealthObserver.isAutomaticRollbackDenied(mSysConfig,
-                new VersionedPackage("com.android.vending", 1))).isEqualTo(true);
+        assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_0,
+                observer.onHealthCheckFailed(secondFailedPackage,
+                        PackageWatchdog.FAILURE_REASON_APP_CRASH, 1));
     }
 
     /**
-     * Test that isAutomaticRollbackDenied works correctly when no config is present
+     * When the rollback impact level is manual only return user impact level 0. (User impact level
+     * 0 is ignored by package watchdog)
      */
     @Test
-    public void isRollbackAllowedTest_noConfig() throws IOException {
-        final File folder = createTempSubfolder("folder");
+    public void healthCheckFailed_impactLevelManualOnly_onePackage()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo(appAFrom, appATo,
+                null, null, false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(1, List.of(packageRollbackInfo),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_ONLY_MANUAL);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+        VersionedPackage secondFailedPackage = new VersionedPackage(APP_B, VERSION_CODE);
 
-        readPermissions(folder, /* Grant all permission flags */ ~0);
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(rollbackInfo1));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
 
-        assertThat(RollbackPackageHealthObserver.isAutomaticRollbackDenied(mSysConfig,
-                new VersionedPackage("com.android.vending", 1))).isEqualTo(false);
+        assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_0,
+                observer.onHealthCheckFailed(secondFailedPackage,
+                        PackageWatchdog.FAILURE_REASON_APP_CRASH, 1));
     }
 
     /**
-     * Creates folderName/fileName in the mTemporaryFolder and fills it with the contents.
-     *
-     * @param folder   pre-existing subdirectory of mTemporaryFolder to put the file
-     * @param fileName name of the file (e.g. filename.xml) to create
-     * @param contents contents to write to the file
-     * @return the newly created file
+     * When both low impact and high impact are present, return 70.
      */
-    private File createTempFile(File folder, String fileName, String contents)
-            throws IOException {
-        File file = new File(folder, fileName);
-        BufferedWriter bw = new BufferedWriter(new FileWriter(file));
-        bw.write(contents);
-        bw.close();
+    @Test
+    public void healthCheckFailed_impactLevelLowAndHigh_onePackage()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo(appAFrom, appATo,
+                null, null, false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(1, List.of(packageRollbackInfo),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        VersionedPackage appBFrom = new VersionedPackage(APP_B, VERSION_CODE_2);
+        VersionedPackage appBTo = new VersionedPackage(APP_B, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoB = new PackageRollbackInfo(appBFrom, appBTo,
+                null, null, false, false,
+                null);
+        RollbackInfo rollbackInfo2 = new RollbackInfo(2, List.of(packageRollbackInfoB),
+                false, null, 222,
+                PackageManager.ROLLBACK_USER_IMPACT_HIGH);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+        VersionedPackage failedPackage = new VersionedPackage(APP_C, VERSION_CODE);
 
-        // Print to logcat for test debugging.
-        Log.d(LOG_TAG, "Contents of file " + file.getAbsolutePath());
-        Scanner input = new Scanner(file);
-        while (input.hasNextLine()) {
-            Log.d(LOG_TAG, input.nextLine());
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
+                List.of(rollbackInfo1, rollbackInfo2));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_70,
+                observer.onHealthCheckFailed(failedPackage,
+                        PackageWatchdog.FAILURE_REASON_APP_CRASH, 1));
+    }
+
+    /**
+     * When low impact rollback is available roll it back.
+     */
+    @Test
+    public void execute_impactLevelLow_nativeCrash_rollback()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        int rollbackId = 1;
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo(appAFrom, appATo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(rollbackId, List.of(packageRollbackInfo),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        VersionedPackage secondFailedPackage = new VersionedPackage(APP_B, VERSION_CODE);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(rollbackInfo1));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        observer.execute(secondFailedPackage,
+                PackageWatchdog.FAILURE_REASON_NATIVE_CRASH, 1);
+        waitForIdleHandler(observer.getHandler(), Duration.ofSeconds(10));
+
+        verify(mRollbackManager).getAvailableRollbacks();
+        verify(mRollbackManager).commitRollback(eq(rollbackId), any(), any());
+    }
+
+    /**
+     * Rollback the failing package if rollback is available for it
+     */
+    @Test
+    public void execute_impactLevelLow_rollbackFailedPackage()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        int rollbackId1 = 1;
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoA = new PackageRollbackInfo(appAFrom, appATo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(rollbackId1, List.of(packageRollbackInfoA),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        int rollbackId2 = 2;
+        VersionedPackage appBFrom = new VersionedPackage(APP_B, VERSION_CODE_2);
+        VersionedPackage appBTo = new VersionedPackage(APP_B, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoB = new PackageRollbackInfo(appBFrom, appBTo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo2 = new RollbackInfo(rollbackId2, List.of(packageRollbackInfoB),
+                false, null, 222,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+        ArgumentCaptor<Integer> argument = ArgumentCaptor.forClass(Integer.class);
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
+                List.of(rollbackInfo1, rollbackInfo2));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        observer.execute(appBFrom, PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+        waitForIdleHandler(observer.getHandler(), Duration.ofSeconds(10));
+
+        verify(mRollbackManager).commitRollback(argument.capture(), any(), any());
+        // Rollback package App B as the failing package is B
+        assertThat(argument.getValue()).isEqualTo(rollbackId2);
+    }
+
+    /**
+     * Rollback all available rollbacks if the rollback is not available for failing package.
+     */
+    @Test
+    public void execute_impactLevelLow_rollbackAll()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        int rollbackId1 = 1;
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoA = new PackageRollbackInfo(appAFrom, appATo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(rollbackId1, List.of(packageRollbackInfoA),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        int rollbackId2 = 2;
+        VersionedPackage appBFrom = new VersionedPackage(APP_B, VERSION_CODE_2);
+        VersionedPackage appBTo = new VersionedPackage(APP_B, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoB = new PackageRollbackInfo(appBFrom, appBTo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo2 = new RollbackInfo(rollbackId2, List.of(packageRollbackInfoB),
+                false, null, 222,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        VersionedPackage failedPackage = new VersionedPackage(APP_C, VERSION_CODE);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+        ArgumentCaptor<Integer> argument = ArgumentCaptor.forClass(Integer.class);
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
+                List.of(rollbackInfo1, rollbackInfo2));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        observer.execute(failedPackage, PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+        waitForIdleHandler(observer.getHandler(), Duration.ofSeconds(10));
+
+        verify(mRollbackManager, times(2)).commitRollback(
+                argument.capture(), any(), any());
+        // Rollback A and B when the failing package doesn't have a rollback
+        assertThat(argument.getAllValues()).isEqualTo(List.of(rollbackId1, rollbackId2));
+    }
+
+    /**
+     * rollback low impact package if both low and high impact packages are available
+     */
+    @Test
+    public void execute_impactLevelLowAndHigh_rollbackLow()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        int rollbackId1 = 1;
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoA = new PackageRollbackInfo(appAFrom, appATo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(rollbackId1, List.of(packageRollbackInfoA),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        int rollbackId2 = 2;
+        VersionedPackage appBFrom = new VersionedPackage(APP_B, VERSION_CODE_2);
+        VersionedPackage appBTo = new VersionedPackage(APP_B, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoB = new PackageRollbackInfo(appBFrom, appBTo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo2 = new RollbackInfo(rollbackId2, List.of(packageRollbackInfoB),
+                false, null, 222,
+                PackageManager.ROLLBACK_USER_IMPACT_HIGH);
+        VersionedPackage failedPackage = new VersionedPackage(APP_C, VERSION_CODE);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+        ArgumentCaptor<Integer> argument = ArgumentCaptor.forClass(Integer.class);
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
+                List.of(rollbackInfo1, rollbackInfo2));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        observer.execute(failedPackage, PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+        waitForIdleHandler(observer.getHandler(), Duration.ofSeconds(10));
+
+        verify(mRollbackManager, times(1)).commitRollback(
+                argument.capture(), any(), any());
+        // Rollback A and B when the failing package doesn't have a rollback
+        assertThat(argument.getAllValues()).isEqualTo(List.of(rollbackId1));
+    }
+
+    /**
+     * Don't roll back high impact package if only high impact package is available. high impact
+     * rollback to be rolled back only on bootloop.
+     */
+    @Test
+    public void execute_impactLevelHigh_rollbackHigh()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        int rollbackId2 = 2;
+        VersionedPackage appBFrom = new VersionedPackage(APP_B, VERSION_CODE_2);
+        VersionedPackage appBTo = new VersionedPackage(APP_B, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoB = new PackageRollbackInfo(appBFrom, appBTo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo2 = new RollbackInfo(rollbackId2, List.of(packageRollbackInfoB),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_HIGH);
+        VersionedPackage failedPackage = new VersionedPackage(APP_C, VERSION_CODE);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+        ArgumentCaptor<Integer> argument = ArgumentCaptor.forClass(Integer.class);
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(rollbackInfo2));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        observer.execute(failedPackage, PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+        waitForIdleHandler(observer.getHandler(), Duration.ofSeconds(10));
+
+        verify(mRollbackManager, never()).commitRollback(argument.capture(), any(), any());
+
+    }
+
+    /**
+     * Test that when impactLevel is low returns user impact level 70
+     */
+    @Test
+    public void onBootLoop_impactLevelLow_onePackage() throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo(appAFrom, appATo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(1, List.of(packageRollbackInfo),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(rollbackInfo1));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_70,
+                observer.onBootLoop(1));
+    }
+
+    @Test
+    public void onBootLoop_impactLevelHigh_onePackage()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo(appAFrom, appATo,
+                null, null, false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(1, List.of(packageRollbackInfo),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_HIGH);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(rollbackInfo1));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_90,
+                observer.onBootLoop(1));
+    }
+
+    /**
+     * When the rollback impact level is manual only return user impact level 0. (User impact level
+     * 0 is ignored by package watchdog)
+     */
+    @Test
+    public void onBootLoop_impactLevelManualOnly_onePackage()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo(appAFrom, appATo,
+                null, null, false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(1, List.of(packageRollbackInfo),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_ONLY_MANUAL);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(rollbackInfo1));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_0,
+                observer.onBootLoop(1));
+    }
+
+    /**
+     * When both low impact and high impact are present, return 70.
+     */
+    @Test
+    public void onBootLoop_impactLevelLowAndHigh_onePackage()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo(appAFrom, appATo,
+                null, null, false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(1, List.of(packageRollbackInfo),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        VersionedPackage appBFrom = new VersionedPackage(APP_B, VERSION_CODE_2);
+        VersionedPackage appBTo = new VersionedPackage(APP_B, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoB = new PackageRollbackInfo(appBFrom, appBTo,
+                null, null, false, false,
+                null);
+        RollbackInfo rollbackInfo2 = new RollbackInfo(2, List.of(packageRollbackInfoB),
+                false, null, 222,
+                PackageManager.ROLLBACK_USER_IMPACT_HIGH);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
+                List.of(rollbackInfo1, rollbackInfo2));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_70,
+                observer.onBootLoop(1));
+    }
+
+    /**
+     * Rollback all available rollbacks if the rollback is not available for failing package.
+     */
+    @Test
+    public void executeBootLoopMitigation_impactLevelLow_rollbackAll()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        int rollbackId1 = 1;
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoA = new PackageRollbackInfo(appAFrom, appATo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(rollbackId1, List.of(packageRollbackInfoA),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        int rollbackId2 = 2;
+        VersionedPackage appBFrom = new VersionedPackage(APP_B, VERSION_CODE_2);
+        VersionedPackage appBTo = new VersionedPackage(APP_B, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoB = new PackageRollbackInfo(appBFrom, appBTo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo2 = new RollbackInfo(rollbackId2, List.of(packageRollbackInfoB),
+                false, null, 222,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+        ArgumentCaptor<Integer> argument = ArgumentCaptor.forClass(Integer.class);
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
+                List.of(rollbackInfo1, rollbackInfo2));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        observer.executeBootLoopMitigation(1);
+        waitForIdleHandler(observer.getHandler(), Duration.ofSeconds(10));
+
+        verify(mRollbackManager, times(2)).commitRollback(
+                argument.capture(), any(), any());
+        // Rollback A and B when the failing package doesn't have a rollback
+        assertThat(argument.getAllValues()).isEqualTo(List.of(rollbackId1, rollbackId2));
+    }
+
+    /**
+     * rollback low impact package if both low and high impact packages are available
+     */
+    @Test
+    public void executeBootLoopMitigation_impactLevelLowAndHigh_rollbackLow()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        int rollbackId1 = 1;
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoA = new PackageRollbackInfo(appAFrom, appATo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(rollbackId1, List.of(packageRollbackInfoA),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        int rollbackId2 = 2;
+        VersionedPackage appBFrom = new VersionedPackage(APP_B, VERSION_CODE_2);
+        VersionedPackage appBTo = new VersionedPackage(APP_B, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoB = new PackageRollbackInfo(appBFrom, appBTo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo2 = new RollbackInfo(rollbackId2, List.of(packageRollbackInfoB),
+                false, null, 222,
+                PackageManager.ROLLBACK_USER_IMPACT_HIGH);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+        ArgumentCaptor<Integer> argument = ArgumentCaptor.forClass(Integer.class);
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
+                List.of(rollbackInfo1, rollbackInfo2));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        observer.executeBootLoopMitigation(1);
+        waitForIdleHandler(observer.getHandler(), Duration.ofSeconds(10));
+
+        verify(mRollbackManager, times(1)).commitRollback(
+                argument.capture(), any(), any());
+        // Rollback A and B when the failing package doesn't have a rollback
+        assertThat(argument.getAllValues()).isEqualTo(List.of(rollbackId1));
+    }
+
+    /**
+     * Rollback high impact package if only high impact package is available
+     */
+    @Test
+    public void executeBootLoopMitigation_impactLevelHigh_rollbackHigh()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        int rollbackId2 = 2;
+        VersionedPackage appBFrom = new VersionedPackage(APP_B, VERSION_CODE_2);
+        VersionedPackage appBTo = new VersionedPackage(APP_B, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoB = new PackageRollbackInfo(appBFrom, appBTo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo2 = new RollbackInfo(rollbackId2, List.of(packageRollbackInfoB),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_HIGH);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+        ArgumentCaptor<Integer> argument = ArgumentCaptor.forClass(Integer.class);
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(rollbackInfo2));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        observer.executeBootLoopMitigation(1);
+        waitForIdleHandler(observer.getHandler(), Duration.ofSeconds(10));
+
+        verify(mRollbackManager, times(1)).commitRollback(
+                argument.capture(), any(), any());
+        // Rollback high impact packages when no other rollback available
+        assertThat(argument.getAllValues()).isEqualTo(List.of(rollbackId2));
+    }
+
+    /**
+     * Rollback only low impact available rollbacks if both low and manual only are available.
+     */
+    @Test
+    public void execute_impactLevelLowAndManual_rollbackLowImpactOnly()
+            throws PackageManager.NameNotFoundException, InterruptedException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        int rollbackId1 = 1;
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoA = new PackageRollbackInfo(appAFrom, appATo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(rollbackId1, List.of(packageRollbackInfoA),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        int rollbackId2 = 2;
+        VersionedPackage appBFrom = new VersionedPackage(APP_B, VERSION_CODE_2);
+        VersionedPackage appBTo = new VersionedPackage(APP_B, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoB = new PackageRollbackInfo(appBFrom, appBTo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo2 = new RollbackInfo(rollbackId2, List.of(packageRollbackInfoB),
+                false, null, 222,
+                PackageManager.ROLLBACK_USER_IMPACT_ONLY_MANUAL);
+        VersionedPackage failedPackage = new VersionedPackage(APP_C, VERSION_CODE);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+        ArgumentCaptor<Integer> argument = ArgumentCaptor.forClass(Integer.class);
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
+                List.of(rollbackInfo1, rollbackInfo2));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        observer.execute(failedPackage, PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+        waitForIdleHandler(observer.getHandler(), Duration.ofSeconds(10));
+
+        verify(mRollbackManager, times(1)).commitRollback(
+                argument.capture(), any(), any());
+        assertThat(argument.getAllValues()).isEqualTo(List.of(rollbackId1));
+    }
+
+    /**
+     * Do not roll back if only manual rollback is available.
+     */
+    @Test
+    public void execute_impactLevelManual_rollbackLowImpactOnly()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        int rollbackId1 = 1;
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoA = new PackageRollbackInfo(appAFrom, appATo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(rollbackId1, List.of(packageRollbackInfoA),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_ONLY_MANUAL);
+        VersionedPackage failedPackage = new VersionedPackage(APP_C, VERSION_CODE);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+        ArgumentCaptor<Integer> argument = ArgumentCaptor.forClass(Integer.class);
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(rollbackInfo1));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        observer.execute(failedPackage, PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+        waitForIdleHandler(observer.getHandler(), Duration.ofSeconds(10));
+
+        verify(mRollbackManager, never()).commitRollback(argument.capture(), any(), any());
+    }
+
+    /**
+     * Rollback alphabetically first package if multiple high impact rollbacks are available.
+     */
+    @Test
+    public void executeBootLoopMitigation_impactLevelHighMultiplePackage_rollbackHigh()
+            throws PackageManager.NameNotFoundException {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        int rollbackId1 = 1;
+        VersionedPackage appBFrom = new VersionedPackage(APP_B, VERSION_CODE_2);
+        VersionedPackage appBTo = new VersionedPackage(APP_B, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoB = new PackageRollbackInfo(appBFrom, appBTo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo1 = new RollbackInfo(rollbackId1, List.of(packageRollbackInfoB),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_HIGH);
+        int rollbackId2 = 2;
+        VersionedPackage appAFrom = new VersionedPackage(APP_A, VERSION_CODE_2);
+        VersionedPackage appATo = new VersionedPackage(APP_A, VERSION_CODE);
+        PackageRollbackInfo packageRollbackInfoA = new PackageRollbackInfo(appAFrom, appATo,
+                null, null , false, false,
+                null);
+        RollbackInfo rollbackInfo2 = new RollbackInfo(rollbackId2, List.of(packageRollbackInfoA),
+                false, null, 111,
+                PackageManager.ROLLBACK_USER_IMPACT_HIGH);
+        VersionedPackage failedPackage = new VersionedPackage(APP_C, VERSION_CODE);
+        RollbackPackageHealthObserver observer =
+                spy(new RollbackPackageHealthObserver(mMockContext, mApexManager));
+        ArgumentCaptor<Integer> argument = ArgumentCaptor.forClass(Integer.class);
+
+        when(mMockContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        // Make the rollbacks available
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
+                List.of(rollbackInfo1, rollbackInfo2));
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getModuleInfo(any(), eq(0))).thenReturn(null);
+
+        observer.executeBootLoopMitigation(1);
+        waitForIdleHandler(observer.getHandler(), Duration.ofSeconds(10));
+
+        verify(mRollbackManager, times(1)).commitRollback(
+                argument.capture(), any(), any());
+        // Rollback APP_A because it is first alphabetically
+        assertThat(argument.getAllValues()).isEqualTo(List.of(rollbackId2));
+    }
+
+    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);
         }
-
-        return file;
-    }
-
-    private void readPermissions(File libraryDir, int permissionFlag) {
-        final XmlPullParser parser = Xml.newPullParser();
-        mSysConfig.readPermissions(parser, libraryDir, permissionFlag);
-    }
-
-    /**
-     * Creates folderName/fileName in the mTemporaryFolder and fills it with the contents.
-     *
-     * @param folderName subdirectory of mTemporaryFolder to put the file, creating if needed
-     * @return the folder
-     */
-    private File createTempSubfolder(String folderName)
-            throws IOException {
-        File folder = new File(mTemporaryFolder.getRoot(), folderName);
-        folder.mkdirs();
-        return folder;
     }
 }
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 b224773..f3cd0d6 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
@@ -1360,7 +1360,7 @@
                 DISPLAY_0, scale, Float.NaN, Float.NaN, true, SERVICE_ID_1);
 
         checkActivatedAndMagnifying(/* activated= */ true, /* magnifying= */ false, DISPLAY_0);
-        verify(mMockWindowManager).setForceShowMagnifiableBounds(DISPLAY_0, true);
+        verify(mMockWindowManager).setFullscreenMagnificationActivated(DISPLAY_0, true);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
index 1ae6e63..0d826df 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
@@ -43,8 +43,10 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.InstallSourceInfo;
 import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
+import android.content.pm.ParceledListSlice;
 import android.os.FileUtils;
 import android.os.Looper;
 import android.os.RemoteException;
@@ -115,9 +117,6 @@
     private BackgroundInstallControlCallbackHelper mCallbackHelper;
 
     @Captor
-    private ArgumentCaptor<PackageManagerInternal.PackageListObserver> mPackageListObserverCaptor;
-
-    @Captor
     private ArgumentCaptor<UsageEventListener> mUsageEventListenerCaptor;
 
     @Before
@@ -137,8 +136,8 @@
         mUsageEventListener = mUsageEventListenerCaptor.getValue();
 
         mBackgroundInstallControlService.onStart(true);
-        verify(mPackageManagerInternal).getPackageList(mPackageListObserverCaptor.capture());
-        mPackageListObserver = mPackageListObserverCaptor.getValue();
+
+        mPackageListObserver = mBackgroundInstallControlService.mPackageObserver;
     }
 
     @After
@@ -554,6 +553,7 @@
         assertEquals(0, foregroundTimeFrames.size());
     }
 
+    //package installed, but no UI interaction found
     @Test
     public void testHandleUsageEvent_packageAddedNoUsageEvent()
             throws NoSuchFieldException, PackageManager.NameNotFoundException {
@@ -571,12 +571,10 @@
         when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
                 .thenReturn(appInfo);
 
-        long createTimestamp =
-                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
         FieldSetter.setField(
                 appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
-                createTimestamp);
+                convertToTestAdjustTimestamp(PACKAGE_ADD_TIMESTAMP_1));
 
         int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
         assertEquals(USER_ID_1, UserHandle.getUserId(uid));
@@ -590,6 +588,10 @@
         assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
     }
 
+    private long convertToTestAdjustTimestamp(long timestamp) {
+        return timestamp - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+    }
+
     @Test
     public void testHandleUsageEvent_packageAddedInsideTimeFrame()
             throws NoSuchFieldException, PackageManager.NameNotFoundException {
@@ -607,12 +609,10 @@
         when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
                 .thenReturn(appInfo);
 
-        long createTimestamp =
-                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
         FieldSetter.setField(
                 appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
-                createTimestamp);
+                convertToTestAdjustTimestamp(PACKAGE_ADD_TIMESTAMP_1));
 
         int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
         assertEquals(USER_ID_1, UserHandle.getUserId(uid));
@@ -639,6 +639,122 @@
     }
 
     @Test
+    public void testHandleUsageEvent_fallsBackToAppInfoTimeWhenHistoricalSessionsNotFound()
+            throws NoSuchFieldException, PackageManager.NameNotFoundException {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        InstallSourceInfo installSourceInfo =
+                new InstallSourceInfo(
+                        /* initiatingPackageName= */ INSTALLER_NAME_1,
+                        /* initiatingPackageSigningInfo= */ null,
+                        /* originatingPackageName= */ null,
+                        /* installingPackageName= */ INSTALLER_NAME_1);
+        assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
+        when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
+        ApplicationInfo appInfo = mock(ApplicationInfo.class);
+
+        when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
+                .thenReturn(appInfo);
+
+        FieldSetter.setField(
+                appInfo,
+                ApplicationInfo.class.getDeclaredField("createTimestamp"),
+                // create timestamp is after generated foreground events (hence not considered
+                // foreground install)
+                convertToTestAdjustTimestamp(USAGE_EVENT_TIMESTAMP_2 + 1));
+
+        int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
+        assertEquals(USER_ID_1, UserHandle.getUserId(uid));
+
+        createPackageManagerHistoricalSessions(List.of(), USER_ID_1);
+
+        // The 2 relevants usage events are before the timeframe, the app is not considered
+        // foreground installed.
+        doReturn(PERMISSION_GRANTED)
+                .when(mPermissionManager)
+                .checkPermission(anyString(), anyString(), anyString(), anyInt());
+        generateUsageEvent(
+                UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1,
+                INSTALLER_NAME_1,
+                USAGE_EVENT_TIMESTAMP_1);
+        generateUsageEvent(
+                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+
+        mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
+        mTestLooper.dispatchAll();
+
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+        assertEquals(1, packages.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+    }
+
+    @Test
+    public void testHandleUsageEvent_usesHistoricalSessionCreateTimeWhenHistoricalSessionsFound()
+            throws NoSuchFieldException, PackageManager.NameNotFoundException {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        InstallSourceInfo installSourceInfo =
+                new InstallSourceInfo(
+                        /* initiatingPackageName= */ INSTALLER_NAME_1,
+                        /* initiatingPackageSigningInfo= */ null,
+                        /* originatingPackageName= */ null,
+                        /* installingPackageName= */ INSTALLER_NAME_1);
+        assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
+        when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
+        ApplicationInfo appInfo = mock(ApplicationInfo.class);
+
+        when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
+                .thenReturn(appInfo);
+
+        FieldSetter.setField(
+                appInfo,
+                ApplicationInfo.class.getDeclaredField("createTimestamp"),
+                //create timestamp is out of window of (after) the interact events
+                convertToTestAdjustTimestamp(USAGE_EVENT_TIMESTAMP_2 + 1));
+
+        int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
+        assertEquals(USER_ID_1, UserHandle.getUserId(uid));
+
+        PackageInstaller.SessionInfo installSession1 = mock(PackageInstaller.SessionInfo.class);
+        PackageInstaller.SessionInfo installSession2 = mock(PackageInstaller.SessionInfo.class);
+        doReturn(convertToTestAdjustTimestamp(0L)).when(installSession1).getCreatedMillis();
+        doReturn(convertToTestAdjustTimestamp(PACKAGE_ADD_TIMESTAMP_1)).when(installSession2)
+                .getCreatedMillis();
+        doReturn(PACKAGE_NAME_1).when(installSession1).getAppPackageName();
+        doReturn(PACKAGE_NAME_1).when(installSession2).getAppPackageName();
+        createPackageManagerHistoricalSessions(List.of(installSession1, installSession2),
+                USER_ID_1);
+
+        // The following 2 generated usage events occur after historical session create times hence,
+        // considered foreground install. The appInfo createTimestamp occurs after events, so the
+        // app would be considered background install if it falls back to it as reference create
+        // timestamp.
+        doReturn(PERMISSION_GRANTED)
+                .when(mPermissionManager)
+                .checkPermission(anyString(), anyString(), anyString(), anyInt());
+        generateUsageEvent(
+                UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1,
+                INSTALLER_NAME_1,
+                USAGE_EVENT_TIMESTAMP_1);
+        generateUsageEvent(
+                Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+
+        mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
+        mTestLooper.dispatchAll();
+
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+    }
+
+    private void createPackageManagerHistoricalSessions(
+            List<PackageInstaller.SessionInfo> sessions, int userId) {
+        ParceledListSlice<PackageInstaller.SessionInfo> mockParcelList =
+                mock(ParceledListSlice.class);
+        when(mockParcelList.getList()).thenReturn(sessions);
+        when(mPackageManagerInternal.getHistoricalSessions(userId)).thenReturn(mockParcelList);
+    }
+
+    @Test
     public void testHandleUsageEvent_packageAddedOutsideTimeFrame1()
             throws NoSuchFieldException, PackageManager.NameNotFoundException {
         assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
@@ -655,12 +771,10 @@
         when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
                 .thenReturn(appInfo);
 
-        long createTimestamp =
-                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
         FieldSetter.setField(
                 appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
-                createTimestamp);
+                convertToTestAdjustTimestamp(PACKAGE_ADD_TIMESTAMP_1));
 
         int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
         assertEquals(USER_ID_1, UserHandle.getUserId(uid));
@@ -708,12 +822,10 @@
         when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
                 .thenReturn(appInfo);
 
-        long createTimestamp =
-                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
         FieldSetter.setField(
                 appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
-                createTimestamp);
+                convertToTestAdjustTimestamp(PACKAGE_ADD_TIMESTAMP_1));
 
         int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
         assertEquals(USER_ID_1, UserHandle.getUserId(uid));
@@ -765,12 +877,10 @@
         when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
                 .thenReturn(appInfo);
 
-        long createTimestamp =
-                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
         FieldSetter.setField(
                 appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
-                createTimestamp);
+                convertToTestAdjustTimestamp(PACKAGE_ADD_TIMESTAMP_1));
 
         int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
         assertEquals(USER_ID_1, UserHandle.getUserId(uid));
@@ -818,12 +928,10 @@
         when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt()))
                 .thenReturn(appInfo);
 
-        long createTimestamp =
-                PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis());
         FieldSetter.setField(
                 appInfo,
                 ApplicationInfo.class.getDeclaredField("createTimestamp"),
-                createTimestamp);
+                convertToTestAdjustTimestamp(PACKAGE_ADD_TIMESTAMP_1));
 
         int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
         assertEquals(USER_ID_1, UserHandle.getUserId(uid));
diff --git a/services/tests/servicestests/src/com/android/server/search/SearchablesTest.java b/services/tests/servicestests/src/com/android/server/search/SearchablesTest.java
index 8bccce1..f5c6795 100644
--- a/services/tests/servicestests/src/com/android/server/search/SearchablesTest.java
+++ b/services/tests/servicestests/src/com/android/server/search/SearchablesTest.java
@@ -16,41 +16,60 @@
 
 package com.android.server.search;
 
-import android.app.SearchManager;
+import static org.junit.Assert.assertEquals;
+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.anyInt;
+import static org.mockito.Mockito.doReturn;
+
 import android.app.SearchableInfo;
 import android.app.SearchableInfo.ActionKeyInfo;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ProviderInfo;
-import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-import android.content.res.XmlResourceParser;
-import android.os.RemoteException;
-import com.android.server.search.Searchables;
-import android.test.AndroidTestCase;
+import android.content.pm.PackageManagerInternal;
 import android.test.MoreAsserts;
-import android.test.mock.MockContext;
-import android.test.mock.MockPackageManager;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.view.KeyEvent;
 
-import java.util.ArrayList;
-import java.util.List;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 
-/**
- * To launch this test from the command line:
- * 
- * adb shell am instrument -w \
- *   -e class com.android.unit_tests.SearchablesTest \
- *   com.android.unit_tests/android.test.InstrumentationTestRunner
- */
+import com.android.server.LocalServices;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
 @SmallTest
-public class SearchablesTest extends AndroidTestCase {
-    
+public class SearchablesTest {
+    @Mock protected PackageManagerInternal mPackageManagerInternal;
+
+    private Context mContext;
+
+    @Before
+    public final void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternal);
+
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+    }
+
+    @After
+    public final void tearDown() {
+        Mockito.framework().clearInlineMocks();
+    }
+
     /*
      * SearchableInfo tests
      *  Mock the context so I can provide very specific input data
@@ -69,15 +88,15 @@
      * Test that non-searchable activities return no searchable info (this would typically
      * trigger the use of the default searchable e.g. contacts)
      */
+    @Test
     public void testNonSearchable() {
         // test basic array & hashmap
         Searchables searchables = new Searchables(mContext, 0);
         searchables.updateSearchableList();
 
         // confirm that we return null for non-searchy activities
-        ComponentName nonActivity = new ComponentName(
-                            "com.android.frameworks.coretests",
-                            "com.android.frameworks.coretests.activity.NO_SEARCH_ACTIVITY");
+        ComponentName nonActivity = new ComponentName("com.android.frameworks.servicestests",
+                "com.android.frameworks.servicestests.activity.NO_SEARCH_ACTIVITY");
         SearchableInfo si = searchables.getSearchableInfo(nonActivity);
         assertNull(si);
     }
@@ -97,13 +116,11 @@
      *  getIcon works
 
      */
+    @Test
     public void testSearchablesListReal() {
-        MyMockPackageManager mockPM = new MyMockPackageManager(mContext.getPackageManager());
-        MyMockContext mockContext = new MyMockContext(mContext, mockPM);
+        doReturn(true).when(mPackageManagerInternal).canAccessComponent(anyInt(), any(), anyInt());
 
-        // build item list with real-world source data
-        mockPM.setSearchablesMode(MyMockPackageManager.SEARCHABLES_PASSTHROUGH);
-        Searchables searchables = new Searchables(mockContext, 0);
+        Searchables searchables = new Searchables(mContext, 0);
         searchables.updateSearchableList();
         // tests with "real" searchables (deprecate, this should be a unit test)
         ArrayList<SearchableInfo> searchablesList = searchables.getSearchablesList();
@@ -117,12 +134,11 @@
     /**
      * This round of tests confirms good operations with "zero" searchables found
      */
+    @Test
     public void testSearchablesListEmpty() {
-        MyMockPackageManager mockPM = new MyMockPackageManager(mContext.getPackageManager());
-        MyMockContext mockContext = new MyMockContext(mContext, mockPM);
+        doReturn(false).when(mPackageManagerInternal).canAccessComponent(anyInt(), any(), anyInt());
 
-        mockPM.setSearchablesMode(MyMockPackageManager.SEARCHABLES_MOCK_ZERO);
-        Searchables searchables = new Searchables(mockContext, 0);
+        Searchables searchables = new Searchables(mContext, 0);
         searchables.updateSearchableList();
         ArrayList<SearchableInfo> searchablesList = searchables.getSearchablesList();
         assertNotNull(searchablesList);
@@ -219,231 +235,6 @@
         if (s != null) {
             MoreAsserts.assertNotEqual(s, "");
         }
-    }    
-    
-    /**
-     * This is a mock for context.  Used to perform a true unit test on SearchableInfo.
-     * 
-     */
-    private class MyMockContext extends MockContext {
-        
-        protected Context mRealContext;
-        protected PackageManager mPackageManager;
-        
-        /**
-         * Constructor.
-         * 
-         * @param realContext Please pass in a real context for some pass-throughs to function.
-         */
-        MyMockContext(Context realContext, PackageManager packageManager) {
-            mRealContext = realContext;
-            mPackageManager = packageManager;
-        }
-        
-        /**
-         * Resources.  Pass through for now.
-         */
-        @Override
-        public Resources getResources() {
-            return mRealContext.getResources();
-        }
-
-        /**
-         * Package manager.  Pass through for now.
-         */
-        @Override
-        public PackageManager getPackageManager() {
-            return mPackageManager;
-        }
-
-        /**
-         * Package manager.  Pass through for now.
-         */
-        @Override
-        public Context createPackageContext(String packageName, int flags)
-                throws PackageManager.NameNotFoundException {
-            return mRealContext.createPackageContext(packageName, flags);
-        }
-
-        /**
-         * Message broadcast.  Pass through for now.
-         */
-        @Override
-        public void sendBroadcast(Intent intent) {
-            mRealContext.sendBroadcast(intent);
-        }
-    }
-
-/**
- * This is a mock for package manager.  Used to perform a true unit test on SearchableInfo.
- * 
- */
-    private class MyMockPackageManager extends MockPackageManager {
-        
-        public final static int SEARCHABLES_PASSTHROUGH = 0;
-        public final static int SEARCHABLES_MOCK_ZERO = 1;
-        public final static int SEARCHABLES_MOCK_ONEGOOD = 2;
-        public final static int SEARCHABLES_MOCK_ONEGOOD_ONEBAD = 3;
-        
-        protected PackageManager mRealPackageManager;
-        protected int mSearchablesMode;
-
-        public MyMockPackageManager(PackageManager realPM) {
-            mRealPackageManager = realPM;
-            mSearchablesMode = SEARCHABLES_PASSTHROUGH;
-        }
-
-        /**
-         * Set the mode for various tests.
-         */
-        public void setSearchablesMode(int newMode) {
-            switch (newMode) {
-            case SEARCHABLES_PASSTHROUGH:
-            case SEARCHABLES_MOCK_ZERO:
-                mSearchablesMode = newMode;
-                break;
-                
-            default:
-                throw new UnsupportedOperationException();       
-            }
-        }
-        
-        /**
-         * Find activities that support a given intent.
-         * 
-         * Retrieve all activities that can be performed for the given intent.
-         * 
-         * @param intent The desired intent as per resolveActivity().
-         * @param flags Additional option flags.  The most important is
-         *                    MATCH_DEFAULT_ONLY, to limit the resolution to only
-         *                    those activities that support the CATEGORY_DEFAULT.
-         * 
-         * @return A List<ResolveInfo> containing one entry for each matching
-         *         Activity. These are ordered from best to worst match -- that
-         *         is, the first item in the list is what is returned by
-         *         resolveActivity().  If there are no matching activities, an empty
-         *         list is returned.
-         */
-        @Override 
-        public List<ResolveInfo> queryIntentActivities(Intent intent, int flags) {
-            assertNotNull(intent);
-            assertTrue(intent.getAction().equals(Intent.ACTION_SEARCH)
-                    || intent.getAction().equals(Intent.ACTION_WEB_SEARCH)
-                    || intent.getAction().equals(SearchManager.INTENT_ACTION_GLOBAL_SEARCH));
-            switch (mSearchablesMode) {
-            case SEARCHABLES_PASSTHROUGH:
-                return mRealPackageManager.queryIntentActivities(intent, flags);
-            case SEARCHABLES_MOCK_ZERO:
-                return null;
-            default:
-                throw new UnsupportedOperationException();
-            }
-        }
-        
-        @Override
-        public ResolveInfo resolveActivity(Intent intent, int flags) {
-            assertNotNull(intent);
-            assertTrue(intent.getAction().equals(Intent.ACTION_WEB_SEARCH)
-                    || intent.getAction().equals(SearchManager.INTENT_ACTION_GLOBAL_SEARCH));
-            switch (mSearchablesMode) {
-            case SEARCHABLES_PASSTHROUGH:
-                return mRealPackageManager.resolveActivity(intent, flags);
-            case SEARCHABLES_MOCK_ZERO:
-                return null;
-            default:
-                throw new UnsupportedOperationException();
-            }
-        }
-
-        /**
-         * Retrieve an XML file from a package.  This is a low-level API used to
-         * retrieve XML meta data.
-         * 
-         * @param packageName The name of the package that this xml is coming from.
-         * Can not be null.
-         * @param resid The resource identifier of the desired xml.  Can not be 0.
-         * @param appInfo Overall information about <var>packageName</var>.  This
-         * may be null, in which case the application information will be retrieved
-         * for you if needed; if you already have this information around, it can
-         * be much more efficient to supply it here.
-         * 
-         * @return Returns an TypedXmlPullParser allowing you to parse out the XML
-         * data.  Returns null if the xml resource could not be found for any
-         * reason.
-         */
-        @Override 
-        public XmlResourceParser getXml(String packageName, int resid, ApplicationInfo appInfo) {
-            assertNotNull(packageName);
-            MoreAsserts.assertNotEqual(packageName, "");
-            MoreAsserts.assertNotEqual(resid, 0);
-            switch (mSearchablesMode) {
-            case SEARCHABLES_PASSTHROUGH:
-                return mRealPackageManager.getXml(packageName, resid, appInfo);
-            case SEARCHABLES_MOCK_ZERO:
-            default:
-                throw new UnsupportedOperationException();
-            }
-        }
-        
-        /**
-         * Find a single content provider by its base path name.
-         * 
-         * @param name The name of the provider to find.
-         * @param flags Additional option flags.  Currently should always be 0.
-         * 
-         * @return ContentProviderInfo Information about the provider, if found,
-         *         else null.
-         */
-        @Override 
-        public ProviderInfo resolveContentProvider(String name, int flags) {
-            assertNotNull(name);
-            MoreAsserts.assertNotEqual(name, "");
-            assertEquals(flags, 0);
-            switch (mSearchablesMode) {
-            case SEARCHABLES_PASSTHROUGH:
-                return mRealPackageManager.resolveContentProvider(name, flags);
-            case SEARCHABLES_MOCK_ZERO:
-            default:
-                throw new UnsupportedOperationException();
-            }
-        }
-
-        /**
-         * Get the activity information for a particular activity.
-         *
-         * @param name The name of the activity to find.
-         * @param flags Additional option flags.
-         *
-         * @return ActivityInfo Information about the activity, if found, else null.
-         */
-        @Override
-        public ActivityInfo getActivityInfo(ComponentName name, int flags)
-                throws NameNotFoundException {
-            assertNotNull(name);
-            MoreAsserts.assertNotEqual(name, "");
-            switch (mSearchablesMode) {
-            case SEARCHABLES_PASSTHROUGH:
-                return mRealPackageManager.getActivityInfo(name, flags);
-            case SEARCHABLES_MOCK_ZERO:
-                throw new NameNotFoundException();
-            default:
-                throw new UnsupportedOperationException();
-            }
-        }
-
-        @Override
-        public int checkPermission(String permName, String pkgName) {
-            assertNotNull(permName);
-            assertNotNull(pkgName);
-            switch (mSearchablesMode) {
-                case SEARCHABLES_PASSTHROUGH:
-                    return mRealPackageManager.checkPermission(permName, pkgName);
-                case SEARCHABLES_MOCK_ZERO:
-                    return PackageManager.PERMISSION_DENIED;
-                default:
-                    throw new UnsupportedOperationException();
-                }
-        }
     }
 }
 
diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
index eddff9ab..dfda1fc 100644
--- a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
@@ -602,56 +602,6 @@
     }
 
     /**
-     * Test that getRollbackDenylistedPackages works correctly for the tag:
-     * {@code automatic-rollback-denylisted-app}.
-     */
-    @Test
-    public void automaticRollbackDeny_vending() throws IOException {
-        final String contents =
-                "<config>\n"
-                + "    <automatic-rollback-denylisted-app package=\"com.android.vending\" />\n"
-                + "</config>";
-        final File folder = createTempSubfolder("folder");
-        createTempFile(folder, "automatic-rollback-denylisted-app.xml", contents);
-
-        readPermissions(folder, /* Grant all permission flags */ ~0);
-
-        assertThat(mSysConfig.getAutomaticRollbackDenylistedPackages())
-            .containsExactly("com.android.vending");
-    }
-
-    /**
-     * Test that getRollbackDenylistedPackages works correctly for the tag:
-     * {@code automatic-rollback-denylisted-app} without any packages.
-     */
-    @Test
-    public void automaticRollbackDeny_empty() throws IOException {
-        final String contents =
-                "<config>\n"
-                + "    <automatic-rollback-denylisted-app />\n"
-                + "</config>";
-        final File folder = createTempSubfolder("folder");
-        createTempFile(folder, "automatic-rollback-denylisted-app.xml", contents);
-
-        readPermissions(folder, /* Grant all permission flags */ ~0);
-
-        assertThat(mSysConfig.getAutomaticRollbackDenylistedPackages()).isEmpty();
-    }
-
-    /**
-     * Test that getRollbackDenylistedPackages works correctly for the tag:
-     * {@code automatic-rollback-denylisted-app} without the corresponding config.
-     */
-    @Test
-    public void automaticRollbackDeny_noConfig() throws IOException {
-        final File folder = createTempSubfolder("folder");
-
-        readPermissions(folder, /* Grant all permission flags */ ~0);
-
-        assertThat(mSysConfig.getAutomaticRollbackDenylistedPackages()).isEmpty();
-    }
-
-    /**
      * Tests that readPermissions works correctly for the tag: {@code update-ownership}.
      */
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 6e5c180..3f2ccaf 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -5384,6 +5384,7 @@
         ZenRule rule = new ZenRule();
         rule.pkg = pkg;
         rule.creationTime = createdAt.toEpochMilli();
+        rule.enabled = true;
         rule.deletionInstant = deletedAt;
         // Plus stuff so that isValidAutomaticRule() passes
         rule.name = "A rule from " + pkg + " created on " + createdAt;
@@ -5392,6 +5393,47 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void getAutomaticZenRuleState_ownedRule_returnsRuleState() {
+        String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+                        .setConfigurationActivity(
+                                new ComponentName(mContext.getPackageName(), "Blah"))
+                        .build(),
+                UPDATE_ORIGIN_APP, "reasons", CUSTOM_PKG_UID);
+
+        // Null condition -> STATE_FALSE
+        assertThat(mZenModeHelper.getAutomaticZenRuleState(id)).isEqualTo(Condition.STATE_FALSE);
+
+        mZenModeHelper.setAutomaticZenRuleState(id, CONDITION_TRUE, UPDATE_ORIGIN_APP,
+                CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.getAutomaticZenRuleState(id)).isEqualTo(Condition.STATE_TRUE);
+
+        mZenModeHelper.setAutomaticZenRuleState(id, CONDITION_FALSE, UPDATE_ORIGIN_APP,
+                CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.getAutomaticZenRuleState(id)).isEqualTo(Condition.STATE_FALSE);
+
+        mZenModeHelper.removeAutomaticZenRule(id, UPDATE_ORIGIN_APP, "", CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.getAutomaticZenRuleState(id)).isEqualTo(Condition.STATE_UNKNOWN);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void getAutomaticZenRuleState_notOwnedRule_returnsStateUnknown() {
+        // Assume existence of a system-owned rule that is currently ACTIVE.
+        ZenRule systemRule = newZenRule("android", Instant.now(), null);
+        systemRule.zenMode = ZEN_MODE_ALARMS;
+        systemRule.condition = new Condition(systemRule.conditionId, "on", Condition.STATE_TRUE);
+        ZenModeConfig config = mZenModeHelper.mConfig.copy();
+        config.automaticRules.put("systemRule", systemRule);
+        mZenModeHelper.setConfig(config, null, UPDATE_ORIGIN_INIT, "", Process.SYSTEM_UID);
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS);
+
+        assertThat(mZenModeHelper.getAutomaticZenRuleState("systemRule")).isEqualTo(
+                Condition.STATE_UNKNOWN);
+    }
+
+    @Test
     @EnableFlags(android.app.Flags.FLAG_MODES_API)
     public void testCallbacks_policy() throws Exception {
         setupZenConfig();
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
index 6853c4c..e904eae 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
@@ -223,7 +223,11 @@
                         KeyboardLogEvent.LAUNCH_DEFAULT_MAPS, KeyEvent.KEYCODE_M, META_ON},
                 {"Meta + S -> Launch Default Messaging App",
                         new int[]{META_KEY, KeyEvent.KEYCODE_S},
-                        KeyboardLogEvent.LAUNCH_DEFAULT_MESSAGING, KeyEvent.KEYCODE_S, META_ON}};
+                        KeyboardLogEvent.LAUNCH_DEFAULT_MESSAGING, KeyEvent.KEYCODE_S, META_ON},
+                {"Meta + Ctrl + DPAD_DOWN -> Enter desktop mode",
+                        new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_DOWN},
+                        KeyboardLogEvent.DESKTOP_MODE, KeyEvent.KEYCODE_DPAD_DOWN,
+                        META_ON | CTRL_ON}};
     }
 
     @Keep
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
index c99fda9..ef131ac 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
@@ -16,6 +16,10 @@
 
 package com.android.server.wm;
 
+import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PENDING_INTENT;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PERMISSION;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -25,6 +29,7 @@
 import android.app.ActivityOptions;
 import android.app.AppOpsManager;
 import android.app.BackgroundStartPrivileges;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManagerInternal;
@@ -51,7 +56,10 @@
 
 import java.lang.reflect.Field;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 
 /**
  * Tests for the {@link ActivityStarter} class.
@@ -73,9 +81,12 @@
     private static final String REGULAR_PACKAGE_1 = "package.app1";
     private static final String REGULAR_PACKAGE_2 = "package.app2";
 
-    public @Rule MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+    private static final Intent TEST_INTENT = new Intent()
+            .setComponent(new ComponentName("package.app3", "someClass"));
 
-    BackgroundActivityStartController mController;
+    public @Rule MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.LENIENT);
+
+    TestableBackgroundActivityStartController mController;
     @Mock
     ActivityMetricsLogger mActivityMetricsLogger;
     @Mock
@@ -112,6 +123,56 @@
     List<String> mShownToasts = new ArrayList<>();
     List<BalAllowedLog> mBalAllowedLogs = new ArrayList<>();
 
+    class TestableBackgroundActivityStartController extends BackgroundActivityStartController {
+        Optional<BalVerdict> mCallerVerdict = Optional.empty();
+        Optional<BalVerdict> mRealCallerVerdict = Optional.empty();
+        Map<WindowProcessController, BalVerdict> mProcessVerdicts = new HashMap<>();
+
+        TestableBackgroundActivityStartController(ActivityTaskManagerService service,
+                ActivityTaskSupervisor supervisor) {
+            super(service, supervisor);
+        }
+
+        @Override
+        protected void showToast(String toastText) {
+            mShownToasts.add(toastText);
+        }
+
+        @Override
+        protected void writeBalAllowedLog(String activityName, int code,
+                BackgroundActivityStartController.BalState state) {
+            mBalAllowedLogs.add(new BalAllowedLog(activityName, code));
+        }
+
+        @Override
+        BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) {
+            return mCallerVerdict.orElseGet(
+                    () -> super.checkBackgroundActivityStartAllowedByCaller(state));
+        }
+
+        public void setCallerVerdict(BalVerdict verdict) {
+            this.mCallerVerdict = Optional.of(verdict);
+        }
+
+        @Override
+        BalVerdict checkBackgroundActivityStartAllowedBySender(BalState state) {
+            return mRealCallerVerdict.orElseGet(
+                    () -> super.checkBackgroundActivityStartAllowedBySender(state));
+        }
+
+        public void setRealCallerVerdict(BalVerdict verdict) {
+            this.mRealCallerVerdict = Optional.of(verdict);
+        }
+
+        @Override
+        BalVerdict checkProcessAllowsBal(WindowProcessController app, BalState state) {
+            if (mProcessVerdicts.containsKey(app)) {
+                return mProcessVerdicts.get(app);
+            }
+            return super.checkProcessAllowsBal(app, state);
+        }
+    }
+
     @Before
     public void setUp() throws Exception {
         // wire objects
@@ -127,18 +188,10 @@
         //Mockito.when(mSupervisor.getBackgroundActivityLaunchController()).thenReturn(mController);
         setViaReflection(mSupervisor, "mRecentTasks", mRecentTasks);
 
-        mController = new BackgroundActivityStartController(mService, mSupervisor) {
-            @Override
-            protected void showToast(String toastText) {
-                mShownToasts.add(toastText);
-            }
+        mController = new TestableBackgroundActivityStartController(mService, mSupervisor);
 
-            @Override
-            protected void writeBalAllowedLog(String activityName, int code,
-                    BackgroundActivityStartController.BalState state) {
-                mBalAllowedLogs.add(new BalAllowedLog(activityName, code));
-            }
-        };
+        // nicer toString
+        Mockito.when(mPendingIntentRecord.toString()).thenReturn("PendingIntentRecord");
 
         // safe defaults
         Mockito.when(mAppOpsManager.checkOpNoThrow(
@@ -146,7 +199,6 @@
                 anyInt(), anyString())).thenReturn(AppOpsManager.MODE_DEFAULT);
         Mockito.when(mCallerApp.areBackgroundActivityStartsAllowed(anyInt())).thenReturn(
                 BalVerdict.BLOCK);
-
     }
 
     private void setViaReflection(Object o, String property, Object value) {
@@ -175,7 +227,7 @@
         int realCallingPid = NO_PID;
         PendingIntentRecord originatingPendingIntent = null;
         BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE;
-        Intent intent = new Intent();
+        Intent intent = TEST_INTENT;
         ActivityOptions checkedOptions = ActivityOptions.makeBasic();
 
         // call
@@ -186,17 +238,16 @@
 
         // assertions
         assertThat(verdict.getCode()).isEqualTo(BackgroundActivityStartController.BAL_BLOCK);
-
-        assertThat(mBalAllowedLogs).isEmpty();
+        assertThat(mBalAllowedLogs).isEmpty(); // not allowed
     }
 
+    // Tests for BackgroundActivityStartController.checkBackgroundActivityStart
+
     @Test
-    public void testRegularActivityStart_allowedBLPC_isAllowed() {
+    public void testRegularActivityStart_notAllowed_isBlocked() {
         // setup state
-        BalVerdict blpcVerdict = new BalVerdict(
-                BackgroundActivityStartController.BAL_ALLOW_PERMISSION, true, "Allowed by BLPC");
-        Mockito.when(mCallerApp.areBackgroundActivityStartsAllowed(anyInt())).thenReturn(
-                blpcVerdict);
+        mController.setCallerVerdict(BalVerdict.BLOCK);
+        mController.setRealCallerVerdict(BalVerdict.BLOCK);
 
         // prepare call
         int callingUid = REGULAR_UID_1;
@@ -206,7 +257,7 @@
         int realCallingPid = NO_PID;
         PendingIntentRecord originatingPendingIntent = null;
         BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE;
-        Intent intent = new Intent();
+        Intent intent = TEST_INTENT;
         ActivityOptions checkedOptions = ActivityOptions.makeBasic();
 
         // call
@@ -216,28 +267,27 @@
                 checkedOptions);
 
         // assertions
-        assertThat(verdict).isEqualTo(blpcVerdict);
-        assertThat(mBalAllowedLogs).containsExactly(
-                new BalAllowedLog("", BackgroundActivityStartController.BAL_ALLOW_PERMISSION));
+        assertThat(verdict).isEqualTo(BalVerdict.BLOCK);
+        assertThat(mBalAllowedLogs).isEmpty(); // not allowed
     }
 
     @Test
-    public void testRegularActivityStart_allowedByCallerBLPC_isAllowed() {
+    public void testRegularActivityStart_allowedByCaller_isAllowed() {
         // setup state
-        BalVerdict blpcVerdict = new BalVerdict(
-                BackgroundActivityStartController.BAL_ALLOW_PERMISSION, true, "Allowed by BLPC");
-        Mockito.when(mCallerApp.areBackgroundActivityStartsAllowed(anyInt())).thenReturn(
-                blpcVerdict);
+        BalVerdict callerVerdict = new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, false,
+                "CallerIsVisible");
+        mController.setCallerVerdict(callerVerdict);
+        mController.setRealCallerVerdict(BalVerdict.BLOCK);
 
         // prepare call
         int callingUid = REGULAR_UID_1;
         int callingPid = REGULAR_PID_1;
         final String callingPackage = REGULAR_PACKAGE_1;
-        int realCallingUid = REGULAR_UID_2;
-        int realCallingPid = REGULAR_PID_2;
-        PendingIntentRecord originatingPendingIntent = mPendingIntentRecord;
+        int realCallingUid = NO_UID;
+        int realCallingPid = NO_PID;
+        PendingIntentRecord originatingPendingIntent = null;
         BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE;
-        Intent intent = new Intent();
+        Intent intent = TEST_INTENT;
         ActivityOptions checkedOptions = ActivityOptions.makeBasic();
 
         // call
@@ -247,8 +297,312 @@
                 checkedOptions);
 
         // assertions
-        assertThat(verdict).isEqualTo(blpcVerdict);
+        assertThat(verdict).isEqualTo(callerVerdict);
+        assertThat(mBalAllowedLogs).isEmpty(); // non-critical exception
+    }
+
+    @Test
+    public void testRegularActivityStart_allowedByRealCaller_isAllowed() {
+        // setup state
+        BalVerdict realCallerVerdict = new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, false,
+                "RealCallerIsVisible");
+        mController.setCallerVerdict(BalVerdict.BLOCK);
+        mController.setRealCallerVerdict(realCallerVerdict);
+
+        // prepare call
+        int callingUid = REGULAR_UID_1;
+        int callingPid = REGULAR_PID_1;
+        final String callingPackage = REGULAR_PACKAGE_1;
+        int realCallingUid = NO_UID;
+        int realCallingPid = NO_PID;
+        PendingIntentRecord originatingPendingIntent = null;
+        BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE;
+        Intent intent = TEST_INTENT;
+        ActivityOptions checkedOptions = ActivityOptions.makeBasic();
+
+        // call
+        BalVerdict verdict = mController.checkBackgroundActivityStart(callingUid, callingPid,
+                callingPackage, realCallingUid, realCallingPid, mCallerApp,
+                originatingPendingIntent, forcedBalByPiSender, mResultRecord, intent,
+                checkedOptions);
+
+        // assertions
+        assertThat(verdict).isEqualTo(realCallerVerdict);
         assertThat(mBalAllowedLogs).containsExactly(
-                new BalAllowedLog("", BackgroundActivityStartController.BAL_ALLOW_PERMISSION));
+                new BalAllowedLog("package.app3/someClass", realCallerVerdict.getCode()));
+        // TODO questionable log (should we only log PIs?)
+    }
+
+    @Test
+    public void testRegularActivityStart_allowedByCallerAndRealCaller_returnsCallerVerdict() {
+        // setup state
+        BalVerdict callerVerdict =
+                new BalVerdict(BAL_ALLOW_PERMISSION, false, "CallerHasPermission");
+        BalVerdict realCallerVerdict =
+                new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, false, "RealCallerIsVisible");
+        mController.setCallerVerdict(callerVerdict);
+        mController.setRealCallerVerdict(realCallerVerdict);
+
+        // prepare call
+        int callingUid = REGULAR_UID_1;
+        int callingPid = REGULAR_PID_1;
+        final String callingPackage = REGULAR_PACKAGE_1;
+        int realCallingUid = NO_UID;
+        int realCallingPid = NO_PID;
+        PendingIntentRecord originatingPendingIntent = null;
+        BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE;
+        Intent intent = TEST_INTENT;
+        ActivityOptions checkedOptions = ActivityOptions.makeBasic();
+
+        // call
+        BalVerdict verdict = mController.checkBackgroundActivityStart(callingUid, callingPid,
+                callingPackage, realCallingUid, realCallingPid, mCallerApp,
+                originatingPendingIntent, forcedBalByPiSender, mResultRecord, intent,
+                checkedOptions);
+
+        // assertions
+        assertThat(verdict).isEqualTo(callerVerdict);
+        assertThat(mBalAllowedLogs).containsExactly(new BalAllowedLog("", callerVerdict.getCode()));
+    }
+
+    @Test
+    public void testPendingIntent_allowedByCallerAndRealCallerButOptOut_isBlocked() {
+        // setup state
+        BalVerdict callerVerdict =
+                new BalVerdict(BAL_ALLOW_PERMISSION, false, "CallerhasPermission");
+        BalVerdict realCallerVerdict =
+                new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, false, "RealCallerIsVisible");
+        mController.setCallerVerdict(callerVerdict);
+        mController.setRealCallerVerdict(realCallerVerdict);
+
+        // prepare call
+        int callingUid = REGULAR_UID_1;
+        int callingPid = REGULAR_PID_1;
+        final String callingPackage = REGULAR_PACKAGE_1;
+        int realCallingUid = NO_UID;
+        int realCallingPid = NO_PID;
+        PendingIntentRecord originatingPendingIntent = mPendingIntentRecord;
+        BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE;
+        Intent intent = TEST_INTENT;
+        ActivityOptions checkedOptions = ActivityOptions.makeBasic()
+                .setPendingIntentBackgroundActivityStartMode(
+                        ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED)
+                .setPendingIntentCreatorBackgroundActivityStartMode(
+                        ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED);
+
+        // call
+        BalVerdict verdict = mController.checkBackgroundActivityStart(callingUid, callingPid,
+                callingPackage, realCallingUid, realCallingPid, mCallerApp,
+                originatingPendingIntent, forcedBalByPiSender, mResultRecord, intent,
+                checkedOptions);
+
+        // assertions
+        assertThat(verdict).isEqualTo(BalVerdict.BLOCK);
+        assertThat(mBalAllowedLogs).isEmpty();
+    }
+
+    @Test
+    public void testPendingIntent_allowedByCallerAndOptIn_isAllowed() {
+        // setup state
+        BalVerdict callerVerdict =
+                new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, false, "CallerIsVisible");
+        mController.setCallerVerdict(callerVerdict);
+        mController.setRealCallerVerdict(BalVerdict.BLOCK);
+
+        // prepare call
+        int callingUid = REGULAR_UID_1;
+        int callingPid = REGULAR_PID_1;
+        final String callingPackage = REGULAR_PACKAGE_1;
+        int realCallingUid = NO_UID;
+        int realCallingPid = NO_PID;
+        PendingIntentRecord originatingPendingIntent = mPendingIntentRecord;
+        BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE;
+        Intent intent = TEST_INTENT;
+        ActivityOptions checkedOptions = ActivityOptions.makeBasic()
+                .setPendingIntentCreatorBackgroundActivityStartMode(
+                        ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+
+        // call
+        BalVerdict verdict = mController.checkBackgroundActivityStart(callingUid, callingPid,
+                callingPackage, realCallingUid, realCallingPid, mCallerApp,
+                originatingPendingIntent, forcedBalByPiSender, mResultRecord, intent,
+                checkedOptions);
+
+        // assertions
+        assertThat(verdict).isEqualTo(callerVerdict);
+        assertThat(mBalAllowedLogs).isEmpty();
+    }
+
+    @Test
+    public void testPendingIntent_allowedByRealCallerAndOptIn_isAllowed() {
+        // setup state
+        BalVerdict realCallerVerdict =
+                new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, false, "RealCallerIsVisible");
+        mController.setCallerVerdict(BalVerdict.BLOCK);
+        mController.setRealCallerVerdict(realCallerVerdict);
+
+        // prepare call
+        int callingUid = REGULAR_UID_1;
+        int callingPid = REGULAR_PID_1;
+        final String callingPackage = REGULAR_PACKAGE_1;
+        int realCallingUid = NO_UID;
+        int realCallingPid = NO_PID;
+        PendingIntentRecord originatingPendingIntent = mPendingIntentRecord;
+        BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE;
+        Intent intent = TEST_INTENT;
+        ActivityOptions checkedOptions = ActivityOptions.makeBasic()
+                .setPendingIntentBackgroundActivityStartMode(
+                        ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+
+        // call
+        BalVerdict verdict = mController.checkBackgroundActivityStart(callingUid, callingPid,
+                callingPackage, realCallingUid, realCallingPid, mCallerApp,
+                originatingPendingIntent, forcedBalByPiSender, mResultRecord, intent,
+                checkedOptions);
+
+        // assertions
+        assertThat(verdict).isEqualTo(realCallerVerdict);
+        assertThat(mBalAllowedLogs).containsExactly(
+                new BalAllowedLog("package.app3/someClass", BAL_ALLOW_PENDING_INTENT));
+
+    }
+
+    // Tests for BackgroundActivityStartController.checkBackgroundActivityStartAllowedByCaller
+
+    // Tests for BackgroundActivityStartController.checkBackgroundActivityStartAllowedBySender
+
+    // Tests for BalState
+
+    @Test
+    public void testBalState_regularStart_isAutoOptIn() {
+        // setup state
+
+        // prepare call
+        int callingUid = REGULAR_UID_1;
+        int callingPid = REGULAR_PID_1;
+        final String callingPackage = REGULAR_PACKAGE_1;
+        int realCallingUid = NO_UID;
+        int realCallingPid = NO_PID;
+        PendingIntentRecord originatingPendingIntent = null;
+        BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE;
+        Intent intent = TEST_INTENT;
+        ActivityOptions checkedOptions = ActivityOptions.makeBasic();
+        WindowProcessController callerApp = mCallerApp;
+        ActivityRecord resultRecord = null;
+
+        // call
+        BackgroundActivityStartController.BalState balState = mController
+                .new BalState(callingUid, callingPid, callingPackage, realCallingUid,
+                realCallingPid, callerApp, originatingPendingIntent, forcedBalByPiSender,
+                resultRecord, intent, checkedOptions);
+
+        // assertions
+        assertThat(balState.mAutoOptInReason).isEqualTo("notPendingIntent");
+        assertThat(balState.mBalAllowedByPiCreator).isEqualTo(BackgroundStartPrivileges.ALLOW_BAL);
+        assertThat(balState.mBalAllowedByPiSender).isEqualTo(BackgroundStartPrivileges.ALLOW_BAL);
+        assertThat(balState.callerExplicitOptInOrAutoOptIn()).isTrue();
+        assertThat(balState.callerExplicitOptInOrOut()).isFalse();
+        assertThat(balState.realCallerExplicitOptInOrAutoOptIn()).isTrue();
+        assertThat(balState.realCallerExplicitOptInOrOut()).isFalse();
+        assertThat(balState.toString()).isEqualTo(
+                "[callingPackage: package.app1; callingPackageTargetSdk: -1; callingUid: 10001; "
+                        + "callingPid: 11001; appSwitchState: 0; callingUidHasAnyVisibleWindow: "
+                        + "false; callingUidProcState: NONEXISTENT; "
+                        + "isCallingUidPersistentSystemProcess: false; forcedBalByPiSender: BSP"
+                        + ".NONE; intent: Intent { cmp=package.app3/someClass }; callerApp: "
+                        + "mCallerApp; inVisibleTask: false; balAllowedByPiCreator: BSP"
+                        + ".ALLOW_BAL; balAllowedByPiCreatorWithHardening: BSP.ALLOW_BAL; "
+                        + "resultIfPiCreatorAllowsBal: null; hasRealCaller: true; "
+                        + "isCallForResult: false; isPendingIntent: false; autoOptInReason: "
+                        + "notPendingIntent; realCallingPackage: uid=1[debugOnly]; "
+                        + "realCallingPackageTargetSdk: -1; realCallingUid: 1; realCallingPid: 1;"
+                        + " realCallingUidHasAnyVisibleWindow: false; realCallingUidProcState: "
+                        + "NONEXISTENT; isRealCallingUidPersistentSystemProcess: false; "
+                        + "originatingPendingIntent: null; realCallerApp: null; "
+                        + "balAllowedByPiSender: BSP.ALLOW_BAL; resultIfPiSenderAllowsBal: null]");
+    }
+
+    @Test
+    public void testBalState_pendingIntentForResult_isOptedInForSenderOnly() {
+        // setup state
+        Mockito.when(mPendingIntentRecord.toString()).thenReturn("PendingIntentRecord");
+
+        // prepare call
+        int callingUid = REGULAR_UID_1;
+        int callingPid = REGULAR_PID_1;
+        final String callingPackage = REGULAR_PACKAGE_1;
+        int realCallingUid = NO_UID;
+        int realCallingPid = NO_PID;
+        PendingIntentRecord originatingPendingIntent = mPendingIntentRecord;
+        BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE;
+        Intent intent = TEST_INTENT;
+        ActivityOptions checkedOptions = ActivityOptions.makeBasic();
+        WindowProcessController callerApp = mCallerApp;
+        ActivityRecord resultRecord = mResultRecord;
+
+        // call
+        BackgroundActivityStartController.BalState balState = mController
+                .new BalState(callingUid, callingPid, callingPackage, realCallingUid,
+                realCallingPid, callerApp, originatingPendingIntent, forcedBalByPiSender,
+                resultRecord, intent, checkedOptions);
+
+        // assertions
+        assertThat(balState.mAutoOptInReason).isEqualTo("callForResult");
+        assertThat(balState.mBalAllowedByPiCreator).isEqualTo(BackgroundStartPrivileges.NONE);
+        assertThat(balState.mBalAllowedByPiSender).isEqualTo(BackgroundStartPrivileges.ALLOW_BAL);
+        assertThat(balState.callerExplicitOptInOrAutoOptIn()).isFalse();
+        assertThat(balState.callerExplicitOptInOrOut()).isFalse();
+        assertThat(balState.realCallerExplicitOptInOrAutoOptIn()).isTrue();
+        assertThat(balState.realCallerExplicitOptInOrOut()).isFalse();
+    }
+
+    @Test
+    public void testBalState_pendingIntentWithDefaults_isOptedOut() {
+        // setup state
+
+        // prepare call
+        int callingUid = REGULAR_UID_1;
+        int callingPid = REGULAR_PID_1;
+        final String callingPackage = REGULAR_PACKAGE_1;
+        int realCallingUid = NO_UID;
+        int realCallingPid = NO_PID;
+        PendingIntentRecord originatingPendingIntent = mPendingIntentRecord;
+        BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE;
+        Intent intent = TEST_INTENT;
+        ActivityOptions checkedOptions = ActivityOptions.makeBasic();
+        WindowProcessController callerApp = mCallerApp;
+        ActivityRecord resultRecord = null;
+
+        // call
+        BackgroundActivityStartController.BalState balState = mController
+                .new BalState(callingUid, callingPid, callingPackage, realCallingUid,
+                realCallingPid, callerApp, originatingPendingIntent, forcedBalByPiSender,
+                resultRecord, intent, checkedOptions);
+
+        // assertions
+        assertThat(balState.mAutoOptInReason).isNull();
+        assertThat(balState.mBalAllowedByPiCreator).isEqualTo(BackgroundStartPrivileges.NONE);
+        assertThat(balState.mBalAllowedByPiSender).isEqualTo(BackgroundStartPrivileges.ALLOW_FGS);
+        assertThat(balState.isPendingIntent()).isTrue();
+        assertThat(balState.callerExplicitOptInOrAutoOptIn()).isFalse();
+        assertThat(balState.callerExplicitOptInOrOut()).isFalse();
+        assertThat(balState.realCallerExplicitOptInOrAutoOptIn()).isFalse();
+        assertThat(balState.realCallerExplicitOptInOrOut()).isFalse();
+        assertThat(balState.toString()).isEqualTo(
+                "[callingPackage: package.app1; callingPackageTargetSdk: -1; callingUid: 10001; "
+                        + "callingPid: 11001; appSwitchState: 0; callingUidHasAnyVisibleWindow: "
+                        + "false; callingUidProcState: NONEXISTENT; "
+                        + "isCallingUidPersistentSystemProcess: false; forcedBalByPiSender: BSP"
+                        + ".NONE; intent: Intent { cmp=package.app3/someClass }; callerApp: "
+                        + "mCallerApp; inVisibleTask: false; balAllowedByPiCreator: BSP"
+                        + ".NONE; balAllowedByPiCreatorWithHardening: BSP.NONE; "
+                        + "resultIfPiCreatorAllowsBal: null; hasRealCaller: true; "
+                        + "isCallForResult: false; isPendingIntent: true; autoOptInReason: "
+                        + "null; realCallingPackage: uid=1[debugOnly]; "
+                        + "realCallingPackageTargetSdk: -1; realCallingUid: 1; realCallingPid: 1;"
+                        + " realCallingUidHasAnyVisibleWindow: false; realCallingUidProcState: "
+                        + "NONEXISTENT; isRealCallingUidPersistentSystemProcess: false; "
+                        + "originatingPendingIntent: PendingIntentRecord; realCallerApp: null; "
+                        + "balAllowedByPiSender: BSP.ALLOW_FGS; resultIfPiSenderAllowsBal: null]");
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java b/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java
index 08e6396..402b704 100644
--- a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java
@@ -16,33 +16,23 @@
 
 package com.android.server.wm;
 
-import static android.view.WindowManager.TRANSIT_CHANGE;
-
+import static com.android.internal.R.bool.config_unfoldTransitionEnabled;
+import static com.android.server.wm.DeviceStateController.DeviceState.REAR;
 import static com.android.server.wm.DeviceStateController.DeviceState.FOLDED;
 import static com.android.server.wm.DeviceStateController.DeviceState.HALF_FOLDED;
 import static com.android.server.wm.DeviceStateController.DeviceState.OPEN;
 
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.when;
 
 import android.animation.ValueAnimator;
-import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
-import android.window.TransitionRequestInfo.DisplayChange;
-
-import static com.android.internal.R.bool.config_unfoldTransitionEnabled;
-import static com.android.server.wm.DeviceStateController.DeviceState.REAR;
 
 import androidx.test.filters.SmallTest;
 
@@ -50,7 +40,6 @@
 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;
 
@@ -62,20 +51,19 @@
  */
 @SmallTest
 @Presubmit
-public class PhysicalDisplaySwitchTransitionLauncherTest {
+@RunWith(WindowTestRunner.class)
+public class PhysicalDisplaySwitchTransitionLauncherTest extends WindowTestsBase {
 
     @Mock
-    DisplayContent mDisplayContent;
-    @Mock
     Context mContext;
     @Mock
     Resources mResources;
     @Mock
-    ActivityTaskManagerService mActivityTaskManagerService;
-    @Mock
     BLASTSyncEngine mSyncEngine;
-    @Mock
+
+    WindowTestsBase.TestTransitionPlayer mPlayer;
     TransitionController mTransitionController;
+    DisplayContent mDisplayContent;
 
     private PhysicalDisplaySwitchTransitionLauncher mTarget;
     private float mOriginalAnimationScale;
@@ -83,9 +71,14 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        mTransitionController = new WindowTestsBase.TestTransitionController(mAtm);
+        mTransitionController.setSyncEngine(mSyncEngine);
+        mPlayer = new WindowTestsBase.TestTransitionPlayer(
+                mTransitionController, mAtm.mWindowOrganizerController);
         when(mContext.getResources()).thenReturn(mResources);
-        mTarget = new PhysicalDisplaySwitchTransitionLauncher(mDisplayContent,
-                mActivityTaskManagerService, mContext, mTransitionController);
+        mDisplayContent = new TestDisplayContent.Builder(mAtm, 100, 150).build();
+        mTarget = new PhysicalDisplaySwitchTransitionLauncher(mDisplayContent, mAtm, mContext,
+                mTransitionController);
         mOriginalAnimationScale = ValueAnimator.getDurationScale();
     }
 
@@ -100,24 +93,23 @@
         mTarget.foldStateChanged(FOLDED);
 
         mTarget.foldStateChanged(OPEN);
+        final Rect origBounds = new Rect();
+        mDisplayContent.getBounds(origBounds);
+        origBounds.offsetTo(0, 0);
         mTarget.requestDisplaySwitchTransitionIfNeeded(
-                /* displayId= */ 123,
-                /* oldDisplayWidth= */ 100,
-                /* oldDisplayHeight= */ 150,
+                mDisplayContent.getDisplayId(),
+                origBounds.width(),
+                origBounds.height(),
                 /* newDisplayWidth= */ 200,
                 /* newDisplayHeight= */ 250
         );
 
-        ArgumentCaptor<DisplayChange> displayChangeArgumentCaptor =
-                ArgumentCaptor.forClass(DisplayChange.class);
-        verify(mTransitionController).requestTransitionIfNeeded(eq(TRANSIT_CHANGE), /* flags= */
-                eq(0), eq(mDisplayContent), eq(mDisplayContent), /* remoteTransition= */ isNull(),
-                displayChangeArgumentCaptor.capture());
-        assertThat(displayChangeArgumentCaptor.getValue().getDisplayId()).isEqualTo(123);
-        assertThat(displayChangeArgumentCaptor.getValue().getStartAbsBounds()).isEqualTo(
-                new Rect(0, 0, 100, 150));
-        assertThat(displayChangeArgumentCaptor.getValue().getEndAbsBounds()).isEqualTo(
-                new Rect(0, 0, 200, 250));
+        assertNotNull(mPlayer.mLastRequest);
+        assertEquals(mDisplayContent.getDisplayId(),
+                mPlayer.mLastRequest.getDisplayChange().getDisplayId());
+        assertEquals(origBounds, mPlayer.mLastRequest.getDisplayChange().getStartAbsBounds());
+        assertEquals(new Rect(0, 0, 200, 250),
+                mPlayer.mLastRequest.getDisplayChange().getEndAbsBounds());
     }
 
     @Test
@@ -148,7 +140,7 @@
         mTarget.foldStateChanged(FOLDED);
         mTarget.foldStateChanged(OPEN);
         requestDisplaySwitch();
-        clearInvocations(mTransitionController);
+        mPlayer.mLastRequest = null;
 
         requestDisplaySwitch();
 
@@ -220,7 +212,6 @@
 
     @Test
     public void testDisplaySwitchAfterUnfolding_otherCollectingTransition_collectsDisplaySwitch() {
-        givenCollectingTransition(createTransition(TRANSIT_CHANGE));
         givenAllAnimationsEnabled();
         mTarget.foldStateChanged(FOLDED);
 
@@ -228,7 +219,8 @@
         requestDisplaySwitch();
 
         // Collects to the current transition
-        verify(mTransitionController).collect(mDisplayContent);
+        assertTrue(mTransitionController.getCollectingTransition().mParticipants.contains(
+                mDisplayContent));
     }
 
 
@@ -245,20 +237,18 @@
     }
 
     private void assertTransitionRequested() {
-        verify(mTransitionController).requestTransitionIfNeeded(anyInt(), anyInt(), any(), any(),
-                any(), any());
+        assertNotNull(mPlayer.mLastRequest);
     }
 
     private void assertTransitionNotRequested() {
-        verify(mTransitionController, never()).requestTransitionIfNeeded(anyInt(), anyInt(), any(),
-                any(), any(), any());
+        assertNull(mPlayer.mLastRequest);
     }
 
     private void requestDisplaySwitch() {
         mTarget.requestDisplaySwitchTransitionIfNeeded(
-                /* displayId= */ 123,
-                /* oldDisplayWidth= */ 100,
-                /* oldDisplayHeight= */ 150,
+                mDisplayContent.getDisplayId(),
+                mDisplayContent.getBounds().width(),
+                mDisplayContent.getBounds().height(),
                 /* newDisplayWidth= */ 200,
                 /* newDisplayHeight= */ 250
         );
@@ -280,16 +270,11 @@
     }
 
     private void givenShellTransitionsEnabled(boolean enabled) {
-        when(mTransitionController.isShellTransitionsEnabled()).thenReturn(enabled);
-    }
-
-    private void givenCollectingTransition(@Nullable Transition transition) {
-        when(mTransitionController.isCollecting()).thenReturn(transition != null);
-        when(mTransitionController.getCollectingTransition()).thenReturn(transition);
-    }
-
-    private Transition createTransition(int type) {
-        return new Transition(type, /* flags= */ 0, mTransitionController, mSyncEngine);
+        if (enabled) {
+            mTransitionController.registerTransitionPlayer(mPlayer, null /* proc */);
+        } else {
+            mTransitionController.detachPlayer();
+        }
     }
 
     private void givenDisplayContentHasContent(boolean hasContent) {