Merge "Animate bubble bar icon sizes and padding" into main
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index ff97b22..9051ca8 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -5,7 +5,7 @@
 ktfmt = --kotlinlang-style
 
 [Tool Paths]
-ktfmt = ${REPO_ROOT}/prebuilts/build-tools/common/framework/ktfmt.jar
+ktfmt = ${REPO_ROOT}/external/ktfmt/ktfmt.sh
 
 [Hook Scripts]
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --config_xml tools/checkstyle.xml --sha ${PREUPLOAD_COMMIT}
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
index 0000c0d..9178062 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
@@ -35,7 +35,7 @@
 
 /** Manage recents related operations with desktop tasks */
 class DesktopRecentsTransitionController(
-    private val stateManager: StateManager<*>,
+    private val stateManager: StateManager<*, *>,
     private val systemUiProxy: SystemUiProxy,
     private val appThread: IApplicationThread,
     private val depthController: DepthController?
@@ -64,7 +64,7 @@
 
     private class RemoteDesktopLaunchTransitionRunner(
         private val desktopTaskView: DesktopTaskView,
-        private val stateManager: StateManager<*>,
+        private val stateManager: StateManager<*, *>,
         private val depthController: DepthController?,
         private val successCallback: Consumer<Boolean>?
     ) : RemoteTransitionStub() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 05c2f08..6b62c86 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -62,7 +62,6 @@
 import android.util.Log;
 import android.view.Display;
 import android.view.Gravity;
-import android.view.RoundedCorner;
 import android.view.Surface;
 import android.view.View;
 import android.view.WindowInsets;
@@ -166,7 +165,6 @@
     private final TaskbarControllers mControllers;
 
     private final WindowManager mWindowManager;
-    private final @Nullable RoundedCorner mLeftCorner, mRightCorner;
     private DeviceProfile mDeviceProfile;
     private WindowManager.LayoutParams mWindowLayoutParams;
     private boolean mIsFullscreen;
@@ -227,16 +225,8 @@
         Context c = getApplicationContext();
         mWindowManager = c.getSystemService(WindowManager.class);
 
-        boolean phoneMode = isPhoneMode();
-        mLeftCorner = phoneMode
-                ? null
-                : display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT);
-        mRightCorner = phoneMode
-                ? null
-                : display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT);
-
         // Inflate views.
-        int taskbarLayout = DisplayController.isTransientTaskbar(this) && !phoneMode
+        int taskbarLayout = DisplayController.isTransientTaskbar(this) && !isPhoneMode()
                 ? R.layout.transient_taskbar
                 : R.layout.taskbar;
         mDragLayer = (TaskbarDragLayer) mLayoutInflater.inflate(taskbarLayout, null, false);
@@ -615,12 +605,9 @@
         return mImeDrawsImeNavBar;
     }
 
-    public int getLeftCornerRadius() {
-        return mLeftCorner == null ? 0 : mLeftCorner.getRadius();
-    }
-
-    public int getRightCornerRadius() {
-        return mRightCorner == null ? 0 : mRightCorner.getRadius();
+    public int getCornerRadius() {
+        return isPhoneMode() ? 0 : getResources().getDimensionPixelSize(
+                R.dimen.persistent_taskbar_corner_radius);
     }
 
     public WindowManager.LayoutParams getWindowLayoutParams() {
@@ -1013,7 +1000,7 @@
 
 
         return mDeviceProfile.taskbarHeight
-                + Math.max(getLeftCornerRadius(), getRightCornerRadius())
+                + getCornerRadius()
                 + extraHeightForTaskbarTooltips;
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index bafd059..2737cbd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -37,8 +37,6 @@
 class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) {
 
     private val isInSetup: Boolean = !context.isUserSetupComplete
-    private val DARK_THEME_SHADOW_ALPHA = 51f
-    private val LIGHT_THEME_SHADOW_ALPHA = 25f
 
     private val maxTransientTaskbarHeight =
         context.transientTaskbarDeviceProfile.taskbarHeight.toFloat()
@@ -54,6 +52,7 @@
     var isAnimatingPinning = false
 
     val paint = Paint()
+    private val strokePaint = Paint()
     val lastDrawnTransientRect = RectF()
     var backgroundHeight = context.deviceProfile.taskbarHeight.toFloat()
     var translationYForSwipe = 0f
@@ -62,19 +61,18 @@
     private val transientBackgroundBounds = context.transientTaskbarBounds
 
     private val shadowAlpha: Float
+    private val strokeAlpha: Int
     private var shadowBlur = 0f
     private var keyShadowDistance = 0f
     private var bottomMargin = 0
 
-    private val fullLeftCornerRadius = context.leftCornerRadius.toFloat()
-    private val fullRightCornerRadius = context.rightCornerRadius.toFloat()
-    private var leftCornerRadius = fullLeftCornerRadius
-    private var rightCornerRadius = fullRightCornerRadius
+    private val fullCornerRadius = context.cornerRadius.toFloat()
+    private var cornerRadius = fullCornerRadius
     private var widthInsetPercentage = 0f
-    private val square: Path = Path()
-    private val circle: Path = Path()
-    private val invertedLeftCornerPath: Path = Path()
-    private val invertedRightCornerPath: Path = Path()
+    private val square = Path()
+    private val circle = Path()
+    private val invertedLeftCornerPath = Path()
+    private val invertedRightCornerPath = Path()
 
     private var stashedHandleWidth =
         context.resources.getDimensionPixelSize(R.dimen.taskbar_stashed_handle_width)
@@ -86,10 +84,18 @@
         paint.color = context.getColor(R.color.taskbar_background)
         paint.flags = Paint.ANTI_ALIAS_FLAG
         paint.style = Paint.Style.FILL
-
-        shadowAlpha =
-            if (Utilities.isDarkTheme(context)) DARK_THEME_SHADOW_ALPHA
-            else LIGHT_THEME_SHADOW_ALPHA
+        strokePaint.color = context.getColor(R.color.taskbar_stroke)
+        strokePaint.flags = Paint.ANTI_ALIAS_FLAG
+        strokePaint.style = Paint.Style.STROKE
+        strokePaint.strokeWidth =
+            context.resources.getDimension(R.dimen.transient_taskbar_stroke_width)
+        if (Utilities.isDarkTheme(context)) {
+            strokeAlpha = DARK_THEME_STROKE_ALPHA
+            shadowAlpha = DARK_THEME_SHADOW_ALPHA
+        } else {
+            strokeAlpha = LIGHT_THEME_STROKE_ALPHA
+            shadowAlpha = LIGHT_THEME_SHADOW_ALPHA
+        }
 
         setCornerRoundness(DEFAULT_ROUNDNESS)
     }
@@ -106,7 +112,7 @@
     }
 
     /**
-     * Sets the roundness of the round corner above Taskbar. No effect on transient Taskkbar.
+     * Sets the roundness of the round corner above Taskbar. No effect on transient Taskbar.
      *
      * @param cornerRoundness 0 has no round corner, 1 has complete round corner.
      */
@@ -115,21 +121,18 @@
             return
         }
 
-        leftCornerRadius = fullLeftCornerRadius * cornerRoundness
-        rightCornerRadius = fullRightCornerRadius * cornerRoundness
+        cornerRadius = fullCornerRadius * cornerRoundness
 
         // Create the paths for the inverted rounded corners above the taskbar. Start with a filled
         // square, and then subtract out a circle from the appropriate corner.
         square.reset()
-        square.addRect(0f, 0f, leftCornerRadius, leftCornerRadius, Path.Direction.CW)
+        square.addRect(0f, 0f, cornerRadius, cornerRadius, Path.Direction.CW)
         circle.reset()
-        circle.addCircle(leftCornerRadius, 0f, leftCornerRadius, Path.Direction.CW)
+        circle.addCircle(cornerRadius, 0f, cornerRadius, Path.Direction.CW)
         invertedLeftCornerPath.op(square, circle, Path.Op.DIFFERENCE)
 
-        square.reset()
-        square.addRect(0f, 0f, rightCornerRadius, rightCornerRadius, Path.Direction.CW)
         circle.reset()
-        circle.addCircle(0f, 0f, rightCornerRadius, Path.Direction.CW)
+        circle.addCircle(0f, 0f, cornerRadius, Path.Direction.CW)
         invertedRightCornerPath.op(square, circle, Path.Op.DIFFERENCE)
     }
 
@@ -163,10 +166,10 @@
         }
 
         // Draw the inverted rounded corners above the taskbar.
-        canvas.translate(0f, -leftCornerRadius)
+        canvas.translate(0f, -cornerRadius)
         canvas.drawPath(invertedLeftCornerPath, paint)
-        canvas.translate(0f, leftCornerRadius)
-        canvas.translate(canvas.width - rightCornerRadius, -rightCornerRadius)
+        canvas.translate(0f, cornerRadius)
+        canvas.translate(canvas.width - cornerRadius, -cornerRadius)
         canvas.drawPath(invertedRightCornerPath, paint)
     }
 
@@ -236,6 +239,7 @@
             keyShadowDistance,
             setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha))
         )
+        strokePaint.alpha = (paint.alpha * strokeAlpha) / 255
 
         lastDrawnTransientRect.set(
             transientBackgroundBounds.left + halfWidthDelta,
@@ -247,6 +251,7 @@
         lastDrawnTransientRect.inset(horizontalInset, 0f)
 
         canvas.drawRoundRect(lastDrawnTransientRect, radius, radius, paint)
+        canvas.drawRoundRect(lastDrawnTransientRect, radius, radius, strokePaint)
     }
 
     /**
@@ -259,5 +264,9 @@
 
     companion object {
         const val DEFAULT_ROUNDNESS = 1f
+        private const val DARK_THEME_STROKE_ALPHA = 51
+        private const val LIGHT_THEME_STROKE_ALPHA = 41
+        private const val DARK_THEME_SHADOW_ALPHA = 51f
+        private const val LIGHT_THEME_SHADOW_ALPHA = 25f
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 046f5b6..028df34 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -135,7 +135,6 @@
 
     private static final Executor BUBBLE_STATE_EXECUTOR = Executors.newSingleThreadExecutor(
             new SimpleThreadFactory("BubbleStateUpdates-", THREAD_PRIORITY_BACKGROUND));
-    private final Executor mMainExecutor;
     private final LauncherApps mLauncherApps;
     private final BubbleIconFactory mIconFactory;
     private final SystemUiProxy mSystemUiProxy;
@@ -198,7 +197,6 @@
         if (sBubbleBarEnabled) {
             mSystemUiProxy.setBubblesListener(this);
         }
-        mMainExecutor = MAIN_EXECUTOR;
         mLauncherApps = context.getSystemService(LauncherApps.class);
         mIconFactory = new BubbleIconFactory(context,
                 context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size),
@@ -241,7 +239,7 @@
     private void createAndAddOverflowIfNeeded() {
         if (mOverflowBubble == null) {
             BubbleBarOverflow overflow = createOverflow(mContext);
-            mMainExecutor.execute(() -> {
+            MAIN_EXECUTOR.execute(() -> {
                 // we're on the main executor now, so check that the overflow hasn't been created
                 // again to avoid races.
                 if (mOverflowBubble == null) {
@@ -303,12 +301,12 @@
                     }
                     viewUpdate.currentBubbles = currentBubbles;
                 }
-                mMainExecutor.execute(() -> applyViewChanges(viewUpdate));
+                MAIN_EXECUTOR.execute(() -> applyViewChanges(viewUpdate));
             });
         } else {
             // No bubbles to load, immediately apply the changes.
             BUBBLE_STATE_EXECUTOR.execute(
-                    () -> mMainExecutor.execute(() -> applyViewChanges(viewUpdate)));
+                    () -> MAIN_EXECUTOR.execute(() -> applyViewChanges(viewUpdate)));
         }
     }
 
@@ -496,7 +494,7 @@
 
     @Override
     public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
-        mMainExecutor.execute(
+        MAIN_EXECUTOR.execute(
                 () -> mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation));
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index e3a2bab..2168f7a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -1118,7 +1118,7 @@
     }
 
     @Override
-    protected void collectStateHandlers(List<StateHandler> out) {
+    public void collectStateHandlers(List<StateHandler<LauncherState>> out) {
         super.collectStateHandlers(out);
         out.add(getDepthController());
         out.add(new RecentsViewStateController(this));
diff --git a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
index 9e896fd..89fbf4a 100644
--- a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -154,7 +154,8 @@
 
     @Override
     public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
-        final StateManager<RecentsState> stateManager = getCreatedContainer().getStateManager();
+        final StateManager<RecentsState, RecentsActivity> stateManager =
+                getCreatedContainer().getStateManager();
         if (stateManager.getState() == HOME) {
             exitRunnable.run();
             notifyRecentsOfOrientation(deviceState);
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 08c2e1c..c428827 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -36,6 +36,7 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Flags;
+import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.LauncherInitListener;
 import com.android.launcher3.LauncherState;
@@ -242,7 +243,8 @@
 
     @Override
     public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
-        final StateManager<LauncherState> stateManager = getCreatedContainer().getStateManager();
+        final StateManager<LauncherState, Launcher> stateManager =
+                getCreatedContainer().getStateManager();
         stateManager.addStateListener(
                 new StateManager.StateListener<LauncherState>() {
                     @Override
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 26a7c2f..7da92bc 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -228,8 +228,9 @@
         }
         BaseActivityInterface<?, T> activityInterface =
                 mOverviewComponentObserver.getActivityInterface();
-        RecentsView visibleRecentsView = activityInterface.getVisibleRecentsView();
-        RecentsView createdRecentsView;
+
+        RecentsView<?, ?> visibleRecentsView = activityInterface.getVisibleRecentsView();
+        RecentsView<?, ?> createdRecentsView;
 
         Log.d(TAG, "executeCommand: " + cmd
                 + " - visibleRecentsView: " + visibleRecentsView);
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 3df62da..13e9844 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -115,7 +115,7 @@
     private TISBindHelper mTISBindHelper;
     private @Nullable FallbackTaskbarUIController mTaskbarUIController;
 
-    private StateManager<RecentsState> mStateManager;
+    private StateManager<RecentsState, RecentsActivity> mStateManager;
 
     // Strong refs to runners which are cleared when the activity is destroyed
     private RemoteAnimationFactory mActivityLaunchAnimationRunner;
@@ -386,6 +386,11 @@
         }
     }
 
+    @Override
+    public boolean shouldAnimateStateChange() {
+        return false;
+    }
+
     /**
      * Initialize/update the device profile.
      */
@@ -462,12 +467,12 @@
             };
 
     @Override
-    protected void collectStateHandlers(List<StateHandler> out) {
+    public void collectStateHandlers(List<StateHandler<RecentsState>> out) {
         out.add(new FallbackRecentsStateController(this));
     }
 
     @Override
-    public StateManager<RecentsState> getStateManager() {
+    public StateManager<RecentsState, RecentsActivity> getStateManager() {
         return mStateManager;
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 8bcdaa3..7adce74 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -45,6 +45,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED;
 
 import android.app.ActivityTaskManager;
 import android.content.Context;
@@ -386,7 +387,7 @@
         boolean canStartWithNavHidden = (mSystemUiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0
                 || (mSystemUiStateFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0
                 || mRotationTouchHelper.isTaskListFrozen();
-        return canStartWithNavHidden && canStartTrackpadGesture();
+        return canStartWithNavHidden && canStartAnyGesture();
     }
 
     /**
@@ -395,14 +396,24 @@
      * mode.
      */
     public boolean canStartTrackpadGesture() {
-        return (mSystemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0
-                && (mSystemUiStateFlags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING) == 0
-                && (mSystemUiStateFlags & SYSUI_STATE_QUICK_SETTINGS_EXPANDED) == 0
-                && (mSystemUiStateFlags & SYSUI_STATE_MAGNIFICATION_OVERLAP) == 0
-                && ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0
-                        || (mSystemUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0)
-                && (mSystemUiStateFlags & SYSUI_STATE_DEVICE_DREAMING) == 0
-                && (mSystemUiStateFlags & SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION) == 0;
+        boolean trackpadGesturesEnabled =
+                (mSystemUiStateFlags & SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED) == 0;
+        return trackpadGesturesEnabled && canStartAnyGesture();
+    }
+
+    /**
+     * Common logic to determine if either trackpad or finger gesture can be started
+     */
+    private boolean canStartAnyGesture() {
+        boolean homeOrOverviewEnabled = (mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0
+                || (mSystemUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0;
+        long gestureDisablingStates = SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
+                        | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
+                        | SYSUI_STATE_QUICK_SETTINGS_EXPANDED
+                        | SYSUI_STATE_MAGNIFICATION_OVERLAP
+                        | SYSUI_STATE_DEVICE_DREAMING
+                        | SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
+        return (gestureDisablingStates & mSystemUiStateFlags) == 0 && homeOrOverviewEnabled;
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 485d6c4..f4a2738 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -93,7 +93,7 @@
     }
 
     @Override
-    public StateManager<RecentsState> getStateManager() {
+    public StateManager<RecentsState, RecentsActivity> getStateManager() {
         return mContainer.getStateManager();
     }
 
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
index 4f1dbbe..36ea926 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -59,6 +59,7 @@
 
     @Nullable private TutorialType[] mTutorialSteps;
     private GestureSandboxFragment mCurrentFragment;
+    private GestureSandboxFragment mPendingFragment;
 
     private int mCurrentStep;
     private int mNumSteps;
@@ -176,16 +177,26 @@
                     && getResources().getConfiguration().orientation
                     == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 
-            showFragment(showRotationPrompt
+            GestureSandboxFragment fragment = showRotationPrompt
                     ? new RotationPromptFragment()
-                    : mCurrentFragment.canRecreateFragment()
-                            ? mCurrentFragment.recreateFragment() : mCurrentFragment);
+                    : mCurrentFragment.canRecreateFragment() || mPendingFragment == null
+                            ? mCurrentFragment.recreateFragment()
+                            : mPendingFragment.recreateFragment();
+            showFragment(fragment == null ? mCurrentFragment : fragment);
+
         } else {
             setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
         }
     }
 
     private void showFragment(@NonNull GestureSandboxFragment fragment) {
+        // Store the current fragment in mPendingFragment so that it can be recreated after the
+        // new fragment is shown.
+        if (mCurrentFragment.canRecreateFragment()) {
+            mPendingFragment = mCurrentFragment;
+        } else {
+            mPendingFragment = null;
+        }
         mCurrentFragment = fragment;
         getSupportFragmentManager().beginTransaction()
                 .replace(R.id.gesture_tutorial_fragment_container, mCurrentFragment)
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
index c9647f5..c7c04ed 100644
--- a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -39,6 +39,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.touch.AllAppsSwipeController;
 import com.android.quickstep.DeviceConfigWrapper;
@@ -163,7 +164,8 @@
                     recentsOrientedState.getContainerInterface().getCreatedContainer();
             if (container != null) {
                 RecentsView recentsView = container.getOverviewPanel();
-                StateManager<LauncherState> stateManager = recentsView.getStateManager();
+                StateManager<LauncherState, StatefulActivity<LauncherState>> stateManager =
+                        recentsView.getStateManager();
                 if (stateManager.isInStableState(LauncherState.BACKGROUND_APP)
                         && stateManager.isInTransition()) {
 
diff --git a/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java b/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java
index 132d1c1..492c801 100644
--- a/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java
+++ b/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java
@@ -62,7 +62,7 @@
      * {@link WorkspaceRevealAnim}.
      */
     public void animateWithVelocity(float velocity) {
-        StateManager<LauncherState> stateManager = mLauncher.getStateManager();
+        StateManager<LauncherState, Launcher> stateManager = mLauncher.getStateManager();
         LauncherState startState = stateManager.getState();
         if (startState != OVERVIEW) {
             Log.e(TAG, "animateFromOverviewToHome: unexpected start state " + startState);
diff --git a/quickstep/src/com/android/quickstep/util/PhoneSplitToConfirmTimings.java b/quickstep/src/com/android/quickstep/util/PhoneSplitToConfirmTimings.java
index 3d9e09e..cbe0f19 100644
--- a/quickstep/src/com/android/quickstep/util/PhoneSplitToConfirmTimings.java
+++ b/quickstep/src/com/android/quickstep/util/PhoneSplitToConfirmTimings.java
@@ -27,6 +27,8 @@
     public int getPlaceholderIconFadeInEnd() { return 133; }
     public int getStagedRectSlideStart() { return 0; }
     public int getStagedRectSlideEnd() { return 333; }
+    public int getBackingScrimFadeInStart() { return 0; }
+    public int getBackingScrimFadeInEnd() { return 266; }
 
     public int getDuration() { return PHONE_CONFIRM_DURATION; }
 }
diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
index 2a27dea..cfe5b2c 100644
--- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
+++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
@@ -27,6 +27,7 @@
 import static com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_SEEN;
 import static com.android.launcher3.util.OnboardingPrefs.HOTSEAT_DISCOVERY_TIP_COUNT;
 
+import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
@@ -48,7 +49,7 @@
      * Sets up the initial onboarding behavior for the launcher
      */
     public static void setup(QuickstepLauncher launcher) {
-        StateManager<LauncherState> stateManager = launcher.getStateManager();
+        StateManager<LauncherState, Launcher> stateManager = launcher.getStateManager();
         if (!HOME_BOUNCE_SEEN.get(launcher)) {
             stateManager.addStateListener(new StateListener<LauncherState>() {
                 @Override
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 8243ede..7ea04b1 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -281,64 +281,45 @@
     }
 
     /**
-     * Creates and returns a view to fade in at .4 animation progress and adds it to the provided
-     * [pendingAnimation]. Assumes that animation will be the final split placeholder launch anim.
-     *
-     * [secondPlaceholderEndingBounds] refers to the second placeholder view that gets added on
-     * screen, not the logical second app. For landscape it's the left app and for portrait the top
-     * one.
+     * Creates and returns a fullscreen scrim to fade in behind the split confirm animation, and
+     * adds it to the provided [pendingAnimation].
      */
-    fun addDividerPlaceholderViewToAnim(
+    fun addScrimBehindAnim(
         pendingAnimation: PendingAnimation,
         container: RecentsViewContainer,
-        secondPlaceholderEndingBounds: Rect,
         context: Context
     ): View {
-        val mSplitDividerPlaceholderView = View(context)
+        val scrim = View(context)
         val recentsView = container.getOverviewPanel<RecentsView<*, *>>()
-        val dp: com.android.launcher3.DeviceProfile = container.getDeviceProfile()
+        val dp: DeviceProfile = container.getDeviceProfile()
         // Add it before/under the most recently added first floating taskView
         val firstAddedSplitViewIndex: Int =
             container
                 .getDragLayer()
                 .indexOfChild(recentsView.splitSelectController.firstFloatingTaskView)
-        container.getDragLayer().addView(mSplitDividerPlaceholderView, firstAddedSplitViewIndex)
-        val lp = mSplitDividerPlaceholderView.layoutParams as InsettableFrameLayout.LayoutParams
+        container.getDragLayer().addView(scrim, firstAddedSplitViewIndex)
+        // Make the scrim fullscreen
+        val lp = scrim.layoutParams as InsettableFrameLayout.LayoutParams
         lp.topMargin = 0
+        lp.height = dp.heightPx
+        lp.width = dp.widthPx
 
-        if (dp.isLeftRightSplit) {
-            lp.height = secondPlaceholderEndingBounds.height()
-            lp.width =
-                container
-                    .asContext()
-                    .resources
-                    .getDimensionPixelSize(R.dimen.split_divider_handle_region_height)
-            mSplitDividerPlaceholderView.translationX =
-                secondPlaceholderEndingBounds.right - lp.width / 2f
-            mSplitDividerPlaceholderView.translationY = 0f
-        } else {
-            lp.height =
-                container
-                    .asContext()
-                    .resources
-                    .getDimensionPixelSize(R.dimen.split_divider_handle_region_height)
-            lp.width = secondPlaceholderEndingBounds.width()
-            mSplitDividerPlaceholderView.translationY =
-                secondPlaceholderEndingBounds.top - lp.height / 2f
-            mSplitDividerPlaceholderView.translationX = 0f
-        }
-
-        mSplitDividerPlaceholderView.alpha = 0f
-        mSplitDividerPlaceholderView.setBackgroundColor(
+        scrim.alpha = 0f
+        scrim.setBackgroundColor(
             container.asContext().resources.getColor(R.color.taskbar_background_dark)
         )
-        val timings = AnimUtils.getDeviceSplitToConfirmTimings(dp.isTablet)
+        val timings = AnimUtils.getDeviceSplitToConfirmTimings(dp.isTablet) as SplitToConfirmTimings
         pendingAnimation.setViewAlpha(
-            mSplitDividerPlaceholderView,
+            scrim,
             1f,
-            Interpolators.clampToProgress(timings.stagedRectScaleXInterpolator, 0.4f, 1f)
+            Interpolators.clampToProgress(
+                timings.backingScrimFadeInterpolator,
+                timings.backingScrimFadeInStartOffset,
+                timings.backingScrimFadeInEndOffset
+            )
         )
-        return mSplitDividerPlaceholderView
+
+        return scrim
     }
 
     /** Does not play any animation if user is not currently in split selection state. */
@@ -510,7 +491,7 @@
         apps: Array<RemoteAnimationTarget>?,
         wallpapers: Array<RemoteAnimationTarget>?,
         nonApps: Array<RemoteAnimationTarget>?,
-        stateManager: StateManager<*>,
+        stateManager: StateManager<*, *>,
         depthController: DepthController?,
         info: TransitionInfo?,
         t: Transaction?,
@@ -589,7 +570,7 @@
     @VisibleForTesting
     fun composeRecentsSplitLaunchAnimator(
         launchingTaskView: GroupedTaskView,
-        stateManager: StateManager<*>,
+        stateManager: StateManager<*, *>,
         depthController: DepthController?,
         info: TransitionInfo,
         t: Transaction,
@@ -617,7 +598,7 @@
         apps: Array<RemoteAnimationTarget>,
         wallpapers: Array<RemoteAnimationTarget>,
         nonApps: Array<RemoteAnimationTarget>,
-        stateManager: StateManager<*>,
+        stateManager: StateManager<*, *>,
         depthController: DepthController?,
         finishCallback: Runnable
     ) {
diff --git a/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java b/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java
index d1ec2b6..7557ad1 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java
@@ -17,6 +17,7 @@
 package com.android.quickstep.util;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.app.animation.Interpolators.LINEAR;
 
 import android.view.animation.Interpolator;
 
@@ -31,6 +32,8 @@
     abstract public int getPlaceholderIconFadeInEnd();
     abstract public int getStagedRectSlideStart();
     abstract public int getStagedRectSlideEnd();
+    abstract public int getBackingScrimFadeInStart();
+    abstract public int getBackingScrimFadeInEnd();
 
     // Common timings
     public int getInstructionsFadeStart() { return 0; }
@@ -39,6 +42,7 @@
     public Interpolator getStagedRectYInterpolator() { return EMPHASIZED; }
     public Interpolator getStagedRectScaleXInterpolator() { return EMPHASIZED; }
     public Interpolator getStagedRectScaleYInterpolator() { return EMPHASIZED; }
+    public Interpolator getBackingScrimFadeInterpolator() { return LINEAR; }
 
     abstract public int getDuration();
 
@@ -48,4 +52,10 @@
     public float getInstructionsFadeEndOffset() {
         return (float) getInstructionsFadeEnd() / getDuration();
     }
+    public float getBackingScrimFadeInStartOffset() {
+        return (float) getBackingScrimFadeInStart() / getDuration();
+    }
+    public float getBackingScrimFadeInEndOffset() {
+        return (float) getBackingScrimFadeInEnd() / getDuration();
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
index 2396512..4962367 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
@@ -163,9 +163,8 @@
                 new RectF(firstTaskStartingBounds), firstTaskEndingBounds,
                 false /* fadeWithThumbnail */, true /* isStagedTask */);
 
-        View mSplitDividerPlaceholderView = recentsView.getSplitSelectController()
-                .getSplitAnimationController().addDividerPlaceholderViewToAnim(pendingAnimation,
-                        mLauncher, secondTaskEndingBounds, view.getContext());
+        View backingScrim = recentsView.getSplitSelectController().getSplitAnimationController()
+                .addScrimBehindAnim(pendingAnimation, mLauncher, view.getContext());
 
         FloatingTaskView secondFloatingTaskView = FloatingTaskView.getFloatingTaskView(mLauncher,
                 view, bitmap, icon, secondTaskStartingBounds);
@@ -197,7 +196,7 @@
             private void cleanUp() {
                 mLauncher.getDragLayer().removeView(firstFloatingTaskView);
                 mLauncher.getDragLayer().removeView(secondFloatingTaskView);
-                mLauncher.getDragLayer().removeView(mSplitDividerPlaceholderView);
+                mLauncher.getDragLayer().removeView(backingScrim);
                 mController.getSplitAnimationController().removeSplitInstructionsView(mLauncher);
                 mController.resetState();
             }
diff --git a/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java b/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java
index 3756b4a..d74473c 100644
--- a/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java
+++ b/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java
@@ -27,6 +27,8 @@
     public int getPlaceholderIconFadeInEnd() { return 250; }
     public int getStagedRectSlideStart() { return 0; }
     public int getStagedRectSlideEnd() { return 500; }
+    public int getBackingScrimFadeInStart() { return 0; }
+    public int getBackingScrimFadeInEnd() { return 400; }
 
     public int getDuration() { return TABLET_CONFIRM_DURATION; }
 }
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 5284b44..e48a7c6 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -37,6 +37,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.desktop.DesktopRecentsTransitionController;
@@ -104,7 +105,7 @@
     }
 
     @Override
-    public StateManager<LauncherState> getStateManager() {
+    public StateManager<LauncherState, Launcher> getStateManager() {
         return mContainer.getStateManager();
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 9096b75..4f802c9 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -152,6 +152,7 @@
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.touch.OverScroll;
@@ -738,7 +739,11 @@
     private int mSplitHiddenTaskViewIndex = -1;
     @Nullable
     private FloatingTaskView mSecondFloatingTaskView;
-    private View mSplitDividerPlaceholderView;
+    /**
+     * A fullscreen scrim that goes behind the splitscreen animation to hide color conflicts and
+     * possible flickers. Removed after tasks + divider finish animating in.
+     */
+    private View mSplitScrim;
 
     /**
      * The task to be removed and immediately re-added. Should not be added to task pool.
@@ -2487,7 +2492,9 @@
     /** Returns whether user can start home based on state in {@link OverviewCommandHelper}. */
     protected abstract boolean canStartHomeSafely();
 
-    public abstract StateManager<STATE_TYPE> getStateManager();
+    /** Returns the state manager used in RecentsView **/
+    public abstract StateManager<STATE_TYPE,
+            ? extends StatefulContainer<STATE_TYPE>> getStateManager();
 
     public void reset() {
         setCurrentTask(-1);
@@ -4926,9 +4933,8 @@
                 mSplitSelectStateController.getActiveSplitStagePosition(), firstTaskEndingBounds,
                 secondTaskEndingBounds);
 
-        mSplitDividerPlaceholderView = mSplitSelectStateController
-                .getSplitAnimationController().addDividerPlaceholderViewToAnim(pendingAnimation,
-                        mContainer, secondTaskEndingBounds, getContext());
+        mSplitScrim = mSplitSelectStateController.getSplitAnimationController()
+                .addScrimBehindAnim(pendingAnimation, mContainer, getContext());
         FloatingTaskView firstFloatingTaskView =
                 mSplitSelectStateController.getFirstFloatingTaskView();
         firstFloatingTaskView.getBoundsOnScreen(firstTaskStartingBounds);
@@ -4983,7 +4989,7 @@
             safeRemoveDragLayerView(mSplitSelectStateController.getFirstFloatingTaskView());
             safeRemoveDragLayerView(mSecondFloatingTaskView);
             safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
-            safeRemoveDragLayerView(mSplitDividerPlaceholderView);
+            safeRemoveDragLayerView(mSplitScrim);
             mSecondFloatingTaskView = null;
             mSplitSelectSource = null;
             mSplitSelectStateController.getSplitAnimationController()
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 5e79743..9c1aaa6 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -63,6 +63,7 @@
 import com.android.launcher3.testing.TestLogging
 import com.android.launcher3.testing.shared.TestProtocol
 import com.android.launcher3.util.CancellableTask
+import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.MultiPropertyFactory
 import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE
@@ -76,6 +77,7 @@
 import com.android.launcher3.util.TransformingTouchDelegate
 import com.android.launcher3.util.ViewPool
 import com.android.launcher3.util.rects.set
+import com.android.launcher3.views.ActivityContext
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.RemoteAnimationTargets
 import com.android.quickstep.TaskAnimationManager
@@ -1574,7 +1576,20 @@
 
         @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
         open fun computeWindowCornerRadius(context: Context): Float {
-            return QuickStepContract.getWindowCornerRadius(context)
+            val activityContext: ActivityContext? = ActivityContext.lookupContextNoThrow(context)
+
+            // The corner radius is fixed to match when Taskbar is persistent mode
+            return if (
+                activityContext != null &&
+                    activityContext.deviceProfile?.isTaskbarPresent == true &&
+                    DisplayController.isTransientTaskbar(context)
+            ) {
+                context.resources
+                    .getDimensionPixelSize(R.dimen.persistent_taskbar_corner_radius)
+                    .toFloat()
+            } else {
+                QuickStepContract.getWindowCornerRadius(context)
+            }
         }
 
         /** Sets the progress in range [0, 1] */
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
new file mode 100644
index 0000000..c09dcf2
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
@@ -0,0 +1,185 @@
+/*
+ * 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.launcher3.taskbar.allapps
+
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Process
+import androidx.test.annotation.UiThreadTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.BubbleTextView
+import com.android.launcher3.appprediction.PredictionRowView
+import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.notification.NotificationKeyData
+import com.android.launcher3.taskbar.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.overlay.TaskbarOverlayController
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.android.launcher3.util.PackageUserKey
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
+class TaskbarAllAppsControllerTest {
+
+    @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule()
+
+    @InjectController lateinit var allAppsController: TaskbarAllAppsController
+    @InjectController lateinit var overlayController: TaskbarOverlayController
+
+    @Test
+    @UiThreadTest
+    fun testToggle_once_showsAllApps() {
+        allAppsController.toggle()
+        assertThat(allAppsController.isOpen).isTrue()
+    }
+
+    @Test
+    @UiThreadTest
+    fun testToggle_twice_closesAllApps() {
+        allAppsController.toggle()
+        allAppsController.toggle()
+        assertThat(allAppsController.isOpen).isFalse()
+    }
+
+    @Test
+    @UiThreadTest
+    fun testToggle_taskbarRecreated_allAppsReopened() {
+        allAppsController.toggle()
+        taskbarUnitTestRule.recreateTaskbar()
+        assertThat(allAppsController.isOpen).isTrue()
+    }
+
+    @Test
+    @UiThreadTest
+    fun testSetApps_beforeOpened_cachesInfo() {
+        allAppsController.setApps(TEST_APPS, 0, emptyMap())
+        allAppsController.toggle()
+
+        val overlayContext = overlayController.requestWindow()
+        assertThat(overlayContext.appsView.appsStore.apps).isEqualTo(TEST_APPS)
+    }
+
+    @Test
+    @UiThreadTest
+    fun testSetApps_afterOpened_updatesStore() {
+        allAppsController.toggle()
+        allAppsController.setApps(TEST_APPS, 0, emptyMap())
+
+        val overlayContext = overlayController.requestWindow()
+        assertThat(overlayContext.appsView.appsStore.apps).isEqualTo(TEST_APPS)
+    }
+
+    @Test
+    @UiThreadTest
+    fun testSetPredictedApps_beforeOpened_cachesInfo() {
+        allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+        allAppsController.toggle()
+
+        val predictedApps =
+            overlayController
+                .requestWindow()
+                .appsView
+                .floatingHeaderView
+                .findFixedRowByType(PredictionRowView::class.java)
+                .predictedApps
+        assertThat(predictedApps).isEqualTo(TEST_PREDICTED_APPS)
+    }
+
+    @Test
+    @UiThreadTest
+    fun testSetPredictedApps_afterOpened_cachesInfo() {
+        allAppsController.toggle()
+        allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+
+        val predictedApps =
+            overlayController
+                .requestWindow()
+                .appsView
+                .floatingHeaderView
+                .findFixedRowByType(PredictionRowView::class.java)
+                .predictedApps
+        assertThat(predictedApps).isEqualTo(TEST_PREDICTED_APPS)
+    }
+
+    @Test
+    fun testUpdateNotificationDots_appInfo_hasDot() {
+        getInstrumentation().runOnMainSync {
+            allAppsController.setApps(TEST_APPS, 0, emptyMap())
+            allAppsController.toggle()
+            taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted(
+                PackageUserKey.fromItemInfo(TEST_APPS[0]),
+                NotificationKeyData("key"),
+            )
+        }
+
+        // Ensure the recycler view fully inflates before trying to grab an icon.
+        getInstrumentation().runOnMainSync {
+            val btv =
+                overlayController
+                    .requestWindow()
+                    .appsView
+                    .activeRecyclerView
+                    .findViewHolderForAdapterPosition(0)
+                    ?.itemView as? BubbleTextView
+            assertThat(btv?.hasDot()).isTrue()
+        }
+    }
+
+    @Test
+    @UiThreadTest
+    fun testUpdateNotificationDots_predictedApp_hasDot() {
+        allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+        allAppsController.toggle()
+
+        taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted(
+            PackageUserKey.fromItemInfo(TEST_PREDICTED_APPS[0]),
+            NotificationKeyData("key"),
+        )
+
+        val predictionRowView =
+            overlayController
+                .requestWindow()
+                .appsView
+                .floatingHeaderView
+                .findFixedRowByType(PredictionRowView::class.java)
+        val btv = predictionRowView.getChildAt(0) as BubbleTextView
+        assertThat(btv.hasDot()).isTrue()
+    }
+
+    private companion object {
+        private val TEST_APPS =
+            Array(16) {
+                AppInfo(
+                    ComponentName(
+                        getInstrumentation().context,
+                        "com.android.launcher3.tests.Activity$it",
+                    ),
+                    "Test App $it",
+                    Process.myUserHandle(),
+                    Intent(),
+                )
+            }
+
+        private val TEST_PREDICTED_APPS = TEST_APPS.take(4).map { WorkspaceItemInfo(it) }
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index 250dc7b..d40f8ab 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -78,7 +78,7 @@
     private val mockSplitSourceDrawable: Drawable = mock()
     private val mockSplitSourceView: View = mock()
 
-    private val stateManager: StateManager<*> = mock()
+    private val stateManager: StateManager<*, *> = mock()
     private val depthController: DepthController = mock()
     private val transitionInfo: TransitionInfo = mock()
     private val transaction: Transaction = mock()
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index aa08ca4..bab84ef 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -31,6 +31,7 @@
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.statehandlers.DepthController
 import com.android.launcher3.statemanager.StateManager
+import com.android.launcher3.statemanager.StatefulActivity
 import com.android.launcher3.util.ComponentKey
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.quickstep.RecentsModel
@@ -61,7 +62,7 @@
     private val depthController: DepthController = mock()
     private val statsLogManager: StatsLogManager = mock()
     private val statsLogger: StatsLogger = mock()
-    private val stateManager: StateManager<LauncherState> = mock()
+    private val stateManager: StateManager<LauncherState, StatefulActivity<LauncherState>> = mock()
     private val handler: Handler = mock()
     private val context: RecentsViewContainer = mock()
     private val recentsModel: RecentsModel = mock()
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
index f3115c6..04012c0 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
@@ -37,7 +37,7 @@
     lateinit var stateListener: StateManager.StateListener<RecentsState>
 
     private val recentsActivity: RecentsActivity = mock()
-    private val stateManager: StateManager<RecentsState> = mock()
+    private val stateManager: StateManager<RecentsState, RecentsActivity> = mock()
 
     @Before
     override fun setup() {
diff --git a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt b/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
index 5157c71..80fbce7 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
@@ -9,8 +9,19 @@
 import com.android.launcher3.util.DisplayController.CHANGE_ROTATION
 import com.android.launcher3.util.DisplayController.Info
 import com.android.launcher3.util.NavigationMode
-import com.android.launcher3.util.window.WindowManagerProxy
 import com.android.quickstep.util.GestureExclusionManager
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED
+import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -28,7 +39,6 @@
 class RecentsAnimationDeviceStateTest {
 
     @Mock private lateinit var exclusionManager: GestureExclusionManager
-    @Mock private lateinit var windowManagerProxy: WindowManagerProxy
     @Mock private lateinit var info: Info
 
     private val context = ApplicationProvider.getApplicationContext() as Context
@@ -108,4 +118,88 @@
 
         verifyZeroInteractions(exclusionManager)
     }
+
+    @Test
+    fun trackpadGesturesNotAllowedForSelectedStates() {
+        val disablingStates = GESTURE_DISABLING_SYSUI_STATES +
+                SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED
+
+        allSysUiStates().forEach { state ->
+            val canStartGesture = !disablingStates.contains(state)
+            underTest.setSystemUiFlags(state)
+            assertThat(underTest.canStartTrackpadGesture()).isEqualTo(canStartGesture)
+        }
+    }
+
+    @Test
+    fun trackpadGesturesNotAllowedIfHomeAndOverviewIsDisabled() {
+        val stateToExpectedResult = mapOf(
+            SYSUI_STATE_HOME_DISABLED to true,
+            SYSUI_STATE_OVERVIEW_DISABLED to true,
+            DEFAULT_STATE
+                .enable(SYSUI_STATE_OVERVIEW_DISABLED)
+                .enable(SYSUI_STATE_HOME_DISABLED) to false
+        )
+
+        stateToExpectedResult.forEach { (state, allowed) ->
+            underTest.setSystemUiFlags(state)
+            assertThat(underTest.canStartTrackpadGesture()).isEqualTo(allowed)
+        }
+    }
+
+    @Test
+    fun systemGesturesNotAllowedForSelectedStates() {
+        val disablingStates = GESTURE_DISABLING_SYSUI_STATES + SYSUI_STATE_NAV_BAR_HIDDEN
+
+        allSysUiStates().forEach { state ->
+            val canStartGesture = !disablingStates.contains(state)
+            underTest.setSystemUiFlags(state)
+            assertThat(underTest.canStartSystemGesture()).isEqualTo(canStartGesture)
+        }
+    }
+
+    @Test
+    fun systemGesturesNotAllowedWhenGestureStateDisabledAndNavBarVisible() {
+        val stateToExpectedResult = mapOf(
+            DEFAULT_STATE
+                .enable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY)
+                .disable(SYSUI_STATE_NAV_BAR_HIDDEN) to true,
+            DEFAULT_STATE
+                .enable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY)
+                .enable(SYSUI_STATE_NAV_BAR_HIDDEN) to true,
+            DEFAULT_STATE
+                .disable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY)
+                .disable(SYSUI_STATE_NAV_BAR_HIDDEN) to true,
+            DEFAULT_STATE
+                .disable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY)
+                .enable(SYSUI_STATE_NAV_BAR_HIDDEN) to false,
+        )
+
+        stateToExpectedResult.forEach {(state, gestureAllowed) ->
+            underTest.setSystemUiFlags(state)
+            assertThat(underTest.canStartSystemGesture()).isEqualTo(gestureAllowed)
+        }
+    }
+
+    private fun allSysUiStates(): List<Long> {
+        // SYSUI_STATES_* are binary flags
+        return (0..SYSUI_STATES_COUNT).map { 1L shl it }
+    }
+
+    companion object {
+        private val GESTURE_DISABLING_SYSUI_STATES = listOf(
+            SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED,
+            SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING,
+            SYSUI_STATE_QUICK_SETTINGS_EXPANDED,
+            SYSUI_STATE_MAGNIFICATION_OVERLAP,
+            SYSUI_STATE_DEVICE_DREAMING,
+            SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION,
+        )
+        private const val SYSUI_STATES_COUNT = 33
+        private const val DEFAULT_STATE = 0L
+    }
+
+    private fun Long.enable(state: Long) = this or state
+
+    private fun Long.disable(state: Long) = this and state.inv()
 }
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index aa83c01..05724e2 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -448,6 +448,9 @@
     <!-- Size of the maximum radius for the enforced rounded rectangles. -->
     <dimen name="enforced_rounded_corner_max_radius">16dp</dimen>
 
+    <!-- Size of the radius for the rounded corners of Persistent Taskbar. -->
+    <dimen name="persistent_taskbar_corner_radius">16dp</dimen>
+
     <!-- Base Swipe Detector, speed in dp/s -->
     <dimen name="base_swift_detector_fling_release_velocity">1dp</dimen>
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 6c9d4c0..4e566ab 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -312,7 +312,7 @@
 
     private static boolean sIsNewProcess = true;
 
-    private StateManager<LauncherState> mStateManager;
+    private StateManager<LauncherState, Launcher> mStateManager;
 
     private static final int ON_ACTIVITY_RESULT_ANIMATION_DELAY = 500;
 
@@ -2775,7 +2775,7 @@
     }
 
     @Override
-    protected void collectStateHandlers(List<StateHandler> out) {
+     public void collectStateHandlers(List<StateHandler<LauncherState>> out) {
         out.add(getAllAppsController());
         out.add(getWorkspace());
     }
@@ -2996,7 +2996,7 @@
     }
 
     @Override
-    public StateManager<LauncherState> getStateManager() {
+    public StateManager<LauncherState, Launcher> getStateManager() {
         return mStateManager;
     }
 
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 47a7115..ca1b2a9 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -480,7 +480,7 @@
                 }
             }
 
-            if (!removedIds.isEmpty()) {
+            if (!removedIds.isEmpty() && !isAppArchived) {
                 taskController.deleteAndBindComponentsRemoved(
                         ItemInfoMatcher.ofItemIds(removedIds),
                         "removed because install session failed");
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index d2d56f2..102189b 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -440,7 +440,7 @@
      */
     public void onBackInvoked(Launcher launcher) {
         if (this != NORMAL) {
-            StateManager<LauncherState> lsm = launcher.getStateManager();
+            StateManager<LauncherState, Launcher> lsm = launcher.getStateManager();
             LauncherState lastState = lsm.getLastState();
             lsm.goToState(lastState, forEndCallback(this::onBackAnimationCompleted));
         }
@@ -461,7 +461,7 @@
      */
     public void onBackProgressed(
             Launcher launcher, @FloatRange(from = 0.0, to = 1.0) float backProgress) {
-        StateManager<LauncherState> lsm = launcher.getStateManager();
+        StateManager<LauncherState, Launcher> lsm = launcher.getStateManager();
         LauncherState toState = lsm.getLastState();
         lsm.onBackProgressed(toState, backProgress);
     }
@@ -471,7 +471,7 @@
      * predictive back gesture.
      */
     public void onBackCancelled(Launcher launcher) {
-        StateManager<LauncherState> lsm = launcher.getStateManager();
+        StateManager<LauncherState, Launcher> lsm = launcher.getStateManager();
         LauncherState toState = lsm.getLastState();
         lsm.onBackCancelled(toState);
     }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 584d089..e601a3e 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -526,7 +526,7 @@
         }
 
         updateChildrenLayersEnabled();
-        StateManager<LauncherState> stateManager = mLauncher.getStateManager();
+        StateManager<LauncherState, Launcher> stateManager = mLauncher.getStateManager();
         stateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
             @Override
             public void onStateTransitionComplete(LauncherState finalState) {
diff --git a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
index 63a168e..1e0f289 100644
--- a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
@@ -56,7 +56,7 @@
             return false;
         }
         Launcher launcher = mActivityContext;
-        StateManager<LauncherState> manager = launcher.getStateManager();
+        StateManager<LauncherState, Launcher> manager = launcher.getStateManager();
         if (manager.isInTransition() && manager.getTargetState() != null) {
             return manager.getTargetState().shouldFloatingSearchBarUsePillWhenUnfocused(launcher);
         }
@@ -70,7 +70,7 @@
             return super.getFloatingSearchBarRestingMarginBottom();
         }
         Launcher launcher = mActivityContext;
-        StateManager<LauncherState> stateManager = launcher.getStateManager();
+        StateManager<LauncherState, Launcher> stateManager = launcher.getStateManager();
 
         // We want to rest at the current state's resting position, unless we are in transition and
         // the target state's resting position is higher (that way if we are closing the keyboard,
@@ -95,7 +95,7 @@
             return super.getFloatingSearchBarRestingMarginStart();
         }
 
-        StateManager<LauncherState> stateManager = mActivityContext.getStateManager();
+        StateManager<LauncherState, Launcher> stateManager = mActivityContext.getStateManager();
 
         // Special case to not expand the search bar when exiting All Apps on phones.
         if (stateManager.getCurrentStableState() == LauncherState.ALL_APPS
@@ -117,7 +117,7 @@
             return super.getFloatingSearchBarRestingMarginEnd();
         }
 
-        StateManager<LauncherState> stateManager = mActivityContext.getStateManager();
+        StateManager<LauncherState, Launcher> stateManager = mActivityContext.getStateManager();
 
         // Special case to not expand the search bar when exiting All Apps on phones.
         if (stateManager.getCurrentStableState() == LauncherState.ALL_APPS
diff --git a/src/com/android/launcher3/model/FirstScreenBroadcast.java b/src/com/android/launcher3/model/FirstScreenBroadcast.java
index cc20cd1..f443f81 100644
--- a/src/com/android/launcher3/model/FirstScreenBroadcast.java
+++ b/src/com/android/launcher3/model/FirstScreenBroadcast.java
@@ -96,7 +96,7 @@
                 .collect(groupingBy(SessionInfo::getInstallerPackageName,
                         mapping(SessionInfo::getAppPackageName, Collectors.toSet())))
                 .forEach((installer, packages) ->
-                    sendBroadcastToInstaller(context, installer, packages, firstScreenItems));
+                        sendBroadcastToInstaller(context, installer, packages, firstScreenItems));
     }
 
     /**
diff --git a/src/com/android/launcher3/model/FirstScreenBroadcastHelper.kt b/src/com/android/launcher3/model/FirstScreenBroadcastHelper.kt
new file mode 100644
index 0000000..aa62c32
--- /dev/null
+++ b/src/com/android/launcher3/model/FirstScreenBroadcastHelper.kt
@@ -0,0 +1,387 @@
+/*
+ * 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.launcher3.model
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageInstaller.SessionInfo
+import android.os.Process
+import android.util.Log
+import androidx.annotation.AnyThread
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+import com.android.launcher3.model.data.CollectionInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.pm.InstallSessionHelper
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.PackageManagerHelper
+import com.android.launcher3.util.PackageUserKey
+
+/**
+ * Helper class to send broadcasts to package installers that have:
+ * - Pending Items on first screen
+ * - Installed/Archived Items on first screen
+ * - Installed/Archived Widgets on every screen
+ *
+ * The packages are broken down by: folder items, workspace items, hotseat items, and widgets.
+ * Package installers only receive data for items that they are installing or have installed.
+ */
+object FirstScreenBroadcastHelper {
+    @VisibleForTesting const val MAX_BROADCAST_SIZE = 70
+
+    private const val TAG = "FirstScreenBroadcastHelper"
+    private const val DEBUG = true
+    private const val ACTION_FIRST_SCREEN_ACTIVE_INSTALLS =
+        "com.android.launcher3.action.FIRST_SCREEN_ACTIVE_INSTALLS"
+    // String retained as "folderItem" for back-compatibility reasons.
+    private const val PENDING_COLLECTION_ITEM_EXTRA = "folderItem"
+    private const val PENDING_WORKSPACE_ITEM_EXTRA = "workspaceItem"
+    private const val PENDING_HOTSEAT_ITEM_EXTRA = "hotseatItem"
+    private const val PENDING_WIDGET_ITEM_EXTRA = "widgetItem"
+    // Extras containing all installed items, including Archived Apps.
+    private const val INSTALLED_WORKSPACE_ITEMS_EXTRA = "workspaceInstalledItems"
+    private const val INSTALLED_HOTSEAT_ITEMS_EXTRA = "hotseatInstalledItems"
+    // This includes installed widgets on all screens, not just first.
+    private const val ALL_INSTALLED_WIDGETS_ITEM_EXTRA = "widgetInstalledItems"
+    private const val VERIFICATION_TOKEN_EXTRA = "verificationToken"
+
+    /**
+     * Return list of [FirstScreenBroadcastModel] for each installer and their
+     * installing/installed/archived items. If the FirstScreenBroadcastModel data is greater in size
+     * than [MAX_BROADCAST_SIZE], then we will truncate the data until it meets the size limit to
+     * avoid overloading the broadcast.
+     *
+     * @param packageManagerHelper helper for querying PackageManager
+     * @param firstScreenItems every ItemInfo on first screen
+     * @param userKeyToSessionMap map of pending SessionInfo's for installing items
+     * @param allWidgets list of all Widgets added to every screen
+     */
+    @WorkerThread
+    @JvmStatic
+    fun createModelsForFirstScreenBroadcast(
+        packageManagerHelper: PackageManagerHelper,
+        firstScreenItems: List<ItemInfo>,
+        userKeyToSessionMap: Map<PackageUserKey, SessionInfo>,
+        allWidgets: List<LauncherAppWidgetInfo>
+    ): List<FirstScreenBroadcastModel> {
+
+        // installers for installing items
+        val pendingItemInstallerMap: Map<String, MutableSet<String>> =
+            createPendingItemsMap(userKeyToSessionMap)
+        val installingPackages = pendingItemInstallerMap.values.flatten().toSet()
+
+        // installers for installed items on first screen
+        val installedItemInstallerMap: Map<String, MutableSet<ItemInfo>> =
+            createInstalledItemsMap(firstScreenItems, installingPackages, packageManagerHelper)
+
+        // installers for widgets on all screens
+        val allInstalledWidgetsMap: Map<String, MutableSet<LauncherAppWidgetInfo>> =
+            createAllInstalledWidgetsMap(allWidgets, installingPackages, packageManagerHelper)
+
+        val allInstallers: Set<String> =
+            pendingItemInstallerMap.keys +
+                installedItemInstallerMap.keys +
+                allInstalledWidgetsMap.keys
+        val models = mutableListOf<FirstScreenBroadcastModel>()
+        // create broadcast for each installer, with extras for each item category
+        allInstallers.forEach { installer ->
+            val installingItems = pendingItemInstallerMap[installer]
+            val broadcastModel =
+                FirstScreenBroadcastModel(installerPackage = installer).apply {
+                    addPendingItems(installingItems, firstScreenItems)
+                    addInstalledItems(installer, installedItemInstallerMap)
+                    addAllScreenWidgets(installer, allInstalledWidgetsMap)
+                }
+            broadcastModel.truncateModelForBroadcast()
+            models.add(broadcastModel)
+        }
+        return models
+    }
+
+    /** From the model data, create Intents to send broadcasts and fire them. */
+    @WorkerThread
+    @JvmStatic
+    fun sendBroadcastsForModels(context: Context, models: List<FirstScreenBroadcastModel>) {
+        for (model in models) {
+            model.printDebugInfo()
+            val intent =
+                Intent(ACTION_FIRST_SCREEN_ACTIVE_INSTALLS)
+                    .setPackage(model.installerPackage)
+                    .putExtra(
+                        VERIFICATION_TOKEN_EXTRA,
+                        PendingIntent.getActivity(
+                            context,
+                            0 /* requestCode */,
+                            Intent(),
+                            PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+                        )
+                    )
+                    .putStringArrayListExtra(
+                        PENDING_COLLECTION_ITEM_EXTRA,
+                        ArrayList(model.pendingCollectionItems)
+                    )
+                    .putStringArrayListExtra(
+                        PENDING_WORKSPACE_ITEM_EXTRA,
+                        ArrayList(model.pendingWorkspaceItems)
+                    )
+                    .putStringArrayListExtra(
+                        PENDING_HOTSEAT_ITEM_EXTRA,
+                        ArrayList(model.pendingHotseatItems)
+                    )
+                    .putStringArrayListExtra(
+                        PENDING_WIDGET_ITEM_EXTRA,
+                        ArrayList(model.pendingWidgetItems)
+                    )
+                    .putStringArrayListExtra(
+                        INSTALLED_WORKSPACE_ITEMS_EXTRA,
+                        ArrayList(model.installedWorkspaceItems)
+                    )
+                    .putStringArrayListExtra(
+                        INSTALLED_HOTSEAT_ITEMS_EXTRA,
+                        ArrayList(model.installedHotseatItems)
+                    )
+                    .putStringArrayListExtra(
+                        ALL_INSTALLED_WIDGETS_ITEM_EXTRA,
+                        ArrayList(
+                            model.firstScreenInstalledWidgets +
+                                model.secondaryScreenInstalledWidgets
+                        )
+                    )
+            context.sendBroadcast(intent)
+        }
+    }
+
+    /** Maps Installer packages to Set of app packages from install sessions */
+    private fun createPendingItemsMap(
+        userKeyToSessionMap: Map<PackageUserKey, SessionInfo>
+    ): Map<String, MutableSet<String>> {
+        val myUser = Process.myUserHandle()
+        val result = mutableMapOf<String, MutableSet<String>>()
+        userKeyToSessionMap.forEach { entry ->
+            if (!myUser.equals(InstallSessionHelper.getUserHandle(entry.value))) return@forEach
+            val installer = entry.value.installerPackageName
+            val appPackage = entry.value.appPackageName
+            if (installer.isNullOrEmpty() || appPackage.isNullOrEmpty()) return@forEach
+            result.getOrPut(installer) { mutableSetOf() }.add(appPackage)
+        }
+        return result
+    }
+
+    /**
+     * Maps Installer packages to Set of ItemInfo from first screen. Filter out installing packages.
+     */
+    private fun createInstalledItemsMap(
+        firstScreenItems: List<ItemInfo>,
+        installingPackages: Set<String>,
+        packageManagerHelper: PackageManagerHelper
+    ): Map<String, MutableSet<ItemInfo>> {
+        val result = mutableMapOf<String, MutableSet<ItemInfo>>()
+        firstScreenItems.forEach { item ->
+            val appPackage = getPackageName(item) ?: return@forEach
+            if (installingPackages.contains(appPackage)) return@forEach
+            val installer = packageManagerHelper.getAppInstallerPackage(appPackage)
+            if (installer.isNullOrEmpty()) return@forEach
+            result.getOrPut(installer) { mutableSetOf() }.add(item)
+        }
+        return result
+    }
+
+    /**
+     * Maps Installer packages to Set of AppWidget packages installed on all screens. Filter out
+     * installing packages.
+     */
+    private fun createAllInstalledWidgetsMap(
+        allWidgets: List<LauncherAppWidgetInfo>,
+        installingPackages: Set<String>,
+        packageManagerHelper: PackageManagerHelper
+    ): Map<String, MutableSet<LauncherAppWidgetInfo>> {
+        val result = mutableMapOf<String, MutableSet<LauncherAppWidgetInfo>>()
+        allWidgets
+            .sortedBy { widget -> widget.screenId }
+            .forEach { widget ->
+                val appPackage = getPackageName(widget) ?: return@forEach
+                if (installingPackages.contains(appPackage)) return@forEach
+                val installer = packageManagerHelper.getAppInstallerPackage(appPackage)
+                if (installer.isNullOrEmpty()) return@forEach
+                result.getOrPut(installer) { mutableSetOf() }.add(widget)
+            }
+        return result
+    }
+
+    /**
+     * Add first screen Pending Items from Map to [FirstScreenBroadcastModel] for given installer
+     */
+    private fun FirstScreenBroadcastModel.addPendingItems(
+        installingItems: Set<String>?,
+        firstScreenItems: List<ItemInfo>
+    ) {
+        if (installingItems == null) return
+        for (info in firstScreenItems) {
+            addCollectionItems(info, installingItems)
+            val packageName = getPackageName(info) ?: continue
+            if (!installingItems.contains(packageName)) continue
+            when {
+                info is LauncherAppWidgetInfo -> pendingWidgetItems.add(packageName)
+                info.container == CONTAINER_HOTSEAT -> pendingHotseatItems.add(packageName)
+                info.container == CONTAINER_DESKTOP -> pendingWorkspaceItems.add(packageName)
+            }
+        }
+    }
+
+    /**
+     * Add first screen installed Items from Map to [FirstScreenBroadcastModel] for given installer
+     */
+    private fun FirstScreenBroadcastModel.addInstalledItems(
+        installer: String,
+        installedItemInstallerMap: Map<String, Set<ItemInfo>>,
+    ) {
+        installedItemInstallerMap[installer]?.forEach { info ->
+            val packageName: String = getPackageName(info) ?: return@forEach
+            when (info.container) {
+                CONTAINER_HOTSEAT -> installedHotseatItems.add(packageName)
+                CONTAINER_DESKTOP -> installedWorkspaceItems.add(packageName)
+            }
+        }
+    }
+
+    /** Add Widgets on every screen from Map to [FirstScreenBroadcastModel] for given installer */
+    private fun FirstScreenBroadcastModel.addAllScreenWidgets(
+        installer: String,
+        allInstalledWidgetsMap: Map<String, Set<LauncherAppWidgetInfo>>
+    ) {
+        allInstalledWidgetsMap[installer]?.forEach { widget ->
+            val packageName: String = getPackageName(widget) ?: return@forEach
+            if (widget.screenId == 0) {
+                firstScreenInstalledWidgets.add(packageName)
+            } else {
+                secondaryScreenInstalledWidgets.add(packageName)
+            }
+        }
+    }
+
+    private fun FirstScreenBroadcastModel.addCollectionItems(
+        info: ItemInfo,
+        installingPackages: Set<String>
+    ) {
+        if (info !is CollectionInfo) return
+        pendingCollectionItems.addAll(
+            cloneOnMainThread(info.getAppContents())
+                .mapNotNull { getPackageName(it) }
+                .filter { installingPackages.contains(it) }
+        )
+    }
+
+    /**
+     * Creates a copy of [FirstScreenBroadcastModel] with items truncated to meet
+     * [MAX_BROADCAST_SIZE] in a prioritized order.
+     */
+    @VisibleForTesting
+    fun FirstScreenBroadcastModel.truncateModelForBroadcast() {
+        val totalItemCount = getTotalItemCount()
+        if (totalItemCount <= MAX_BROADCAST_SIZE) return
+        var extraItemCount = totalItemCount - MAX_BROADCAST_SIZE
+
+        while (extraItemCount > 0) {
+            // In this order, remove items until we meet the max size limit.
+            when {
+                pendingCollectionItems.isNotEmpty() ->
+                    pendingCollectionItems.apply { remove(last()) }
+                pendingHotseatItems.isNotEmpty() -> pendingHotseatItems.apply { remove(last()) }
+                installedHotseatItems.isNotEmpty() -> installedHotseatItems.apply { remove(last()) }
+                secondaryScreenInstalledWidgets.isNotEmpty() ->
+                    secondaryScreenInstalledWidgets.apply { remove(last()) }
+                pendingWidgetItems.isNotEmpty() -> pendingWidgetItems.apply { remove(last()) }
+                firstScreenInstalledWidgets.isNotEmpty() ->
+                    firstScreenInstalledWidgets.apply { remove(last()) }
+                pendingWorkspaceItems.isNotEmpty() -> pendingWorkspaceItems.apply { remove(last()) }
+                installedWorkspaceItems.isNotEmpty() ->
+                    installedWorkspaceItems.apply { remove(last()) }
+            }
+            extraItemCount--
+        }
+    }
+
+    /** Returns count of all Items held by [FirstScreenBroadcastModel]. */
+    @VisibleForTesting
+    fun FirstScreenBroadcastModel.getTotalItemCount() =
+        pendingCollectionItems.size +
+            pendingWorkspaceItems.size +
+            pendingHotseatItems.size +
+            pendingWidgetItems.size +
+            installedWorkspaceItems.size +
+            installedHotseatItems.size +
+            firstScreenInstalledWidgets.size +
+            secondaryScreenInstalledWidgets.size
+
+    private fun FirstScreenBroadcastModel.printDebugInfo() {
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                "Sending First Screen Broadcast for installer=$installerPackage" +
+                    ", total packages=${getTotalItemCount()}"
+            )
+            pendingCollectionItems.forEach {
+                Log.d(TAG, "$installerPackage:Pending Collection item:$it")
+            }
+            pendingWorkspaceItems.forEach {
+                Log.d(TAG, "$installerPackage:Pending Workspace item:$it")
+            }
+            pendingHotseatItems.forEach { Log.d(TAG, "$installerPackage:Pending Hotseat item:$it") }
+            pendingWidgetItems.forEach { Log.d(TAG, "$installerPackage:Pending Widget item:$it") }
+            installedWorkspaceItems.forEach {
+                Log.d(TAG, "$installerPackage:Installed Workspace item:$it")
+            }
+            installedHotseatItems.forEach {
+                Log.d(TAG, "$installerPackage:Installed Hotseat item:$it")
+            }
+            firstScreenInstalledWidgets.forEach {
+                Log.d(TAG, "$installerPackage:Installed Widget item (first screen):$it")
+            }
+            secondaryScreenInstalledWidgets.forEach {
+                Log.d(TAG, "$installerPackage:Installed Widget item (secondary screens):$it")
+            }
+        }
+    }
+
+    private fun getPackageName(info: ItemInfo): String? {
+        var packageName: String? = null
+        if (info is LauncherAppWidgetInfo) {
+            info.providerName?.let { packageName = info.providerName.packageName }
+        } else if (info.targetComponent != null) {
+            packageName = info.targetComponent?.packageName
+        }
+        return packageName
+    }
+
+    /**
+     * Clone the provided list on UI thread. This is used for [FolderInfo.getContents] which is
+     * always modified on UI thread.
+     */
+    @AnyThread
+    private fun cloneOnMainThread(list: ArrayList<WorkspaceItemInfo>): List<WorkspaceItemInfo> {
+        return try {
+            return Executors.MAIN_EXECUTOR.submit<ArrayList<WorkspaceItemInfo>> { ArrayList(list) }
+                .get()
+        } catch (e: Exception) {
+            emptyList()
+        }
+    }
+}
diff --git a/src/com/android/launcher3/model/FirstScreenBroadcastModel.kt b/src/com/android/launcher3/model/FirstScreenBroadcastModel.kt
new file mode 100644
index 0000000..ba5c526
--- /dev/null
+++ b/src/com/android/launcher3/model/FirstScreenBroadcastModel.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.launcher3.model
+
+/** Data model for the information used for [FirstScreenBroadcastHelper] Broadcast Extras */
+data class FirstScreenBroadcastModel(
+    // Package name of the installer for all items
+    val installerPackage: String,
+    // Installing items in Folders
+    val pendingCollectionItems: MutableSet<String> = mutableSetOf(),
+    // Installing items on first screen
+    val pendingWorkspaceItems: MutableSet<String> = mutableSetOf(),
+    // Installing items on hotseat
+    val pendingHotseatItems: MutableSet<String> = mutableSetOf(),
+    // Installing widgets on first screen
+    val pendingWidgetItems: MutableSet<String> = mutableSetOf(),
+    // Installed/Archived Items on first screen
+    val installedWorkspaceItems: MutableSet<String> = mutableSetOf(),
+    // Installed/Archived items on hotseat
+    val installedHotseatItems: MutableSet<String> = mutableSetOf(),
+    // Installed/Archived Widgets on first screen
+    val firstScreenInstalledWidgets: MutableSet<String> = mutableSetOf(),
+    // Installed Archived Widgets on secondary screens
+    val secondaryScreenInstalledWidgets: MutableSet<String> = mutableSetOf()
+)
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 0d40a24..dc6968c 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -47,6 +47,7 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.LongSparseArray;
@@ -195,17 +196,32 @@
     }
 
     private void sendFirstScreenActiveInstallsBroadcast() {
-        ArrayList<ItemInfo> firstScreenItems = new ArrayList<>();
-        ArrayList<ItemInfo> allItems = mBgDataModel.getAllWorkspaceItems();
-
         // Screen set is never empty
         IntArray allScreens = mBgDataModel.collectWorkspaceScreens();
         final int firstScreen = allScreens.get(0);
         IntSet firstScreens = IntSet.wrap(firstScreen);
 
+        ArrayList<ItemInfo> allItems = mBgDataModel.getAllWorkspaceItems();
+        ArrayList<ItemInfo> firstScreenItems = new ArrayList<>();
         filterCurrentWorkspaceItems(firstScreens, allItems, firstScreenItems,
                 new ArrayList<>() /* otherScreenItems are ignored */);
-        mFirstScreenBroadcast.sendBroadcasts(mApp.getContext(), firstScreenItems);
+        final int launcherBroadcastInstalledApps = Settings.Secure.getInt(
+                mApp.getContext().getContentResolver(),
+                "launcher_broadcast_installed_apps",
+                /* def= */ 0);
+        if (launcherBroadcastInstalledApps == 1) {
+            List<FirstScreenBroadcastModel> broadcastModels =
+                    FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+                            mPmHelper,
+                            firstScreenItems,
+                            mInstallingPkgsCached,
+                            mBgDataModel.appWidgets
+                    );
+            logASplit("Sending first screen broadcast with additional archiving Extras");
+            FirstScreenBroadcastHelper.sendBroadcastsForModels(mApp.getContext(), broadcastModels);
+        } else {
+            mFirstScreenBroadcast.sendBroadcasts(mApp.getContext(), firstScreenItems);
+        }
     }
 
     public void run() {
@@ -249,7 +265,7 @@
             mModelDelegate.workspaceLoadComplete();
             // Notify the installer packages of packages with active installs on the first screen.
             sendFirstScreenActiveInstallsBroadcast();
-            logASplit("sendFirstScreenActiveInstallsBroadcast");
+            logASplit("sendFirstScreenBroadcast");
 
             // Take a break
             waitForIdle();
diff --git a/src/com/android/launcher3/notification/NotificationKeyData.java b/src/com/android/launcher3/notification/NotificationKeyData.java
index 4115b3d..e38824c 100644
--- a/src/com/android/launcher3/notification/NotificationKeyData.java
+++ b/src/com/android/launcher3/notification/NotificationKeyData.java
@@ -22,6 +22,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Utilities;
 
@@ -38,6 +39,11 @@
     public final String[] personKeysFromNotification;
     public int count;
 
+    @VisibleForTesting
+    public NotificationKeyData(String notificationKey) {
+        this(notificationKey, null, 1, new String[]{});
+    }
+
     private NotificationKeyData(String notificationKey, String shortcutId, int count,
             String[] personKeysFromNotification) {
         this.notificationKey = notificationKey;
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index a4ff29f..21897bf 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -418,9 +418,7 @@
         DeviceGridState deviceGridState = new DeviceGridState(context);
         FileLog.d(TAG, "restore initiated from backup: DeviceGridState=" + deviceGridState);
         LauncherPrefs.get(context).putSync(RESTORE_DEVICE.to(deviceGridState.getDeviceType()));
-        if (enableLauncherBrMetricsFixed()) {
-            LauncherPrefs.get(context).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(true));
-        }
+        LauncherPrefs.get(context).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(true));
     }
 
     @WorkerThread
diff --git a/src/com/android/launcher3/statemanager/BaseState.java b/src/com/android/launcher3/statemanager/BaseState.java
index a01d402..b81729a 100644
--- a/src/com/android/launcher3/statemanager/BaseState.java
+++ b/src/com/android/launcher3/statemanager/BaseState.java
@@ -21,7 +21,7 @@
 import com.android.launcher3.views.ActivityContext;
 
 /**
- * Interface representing a state of a StatefulActivity
+ * Interface representing a state of a StatefulContainer
  */
 public interface BaseState<T extends BaseState> {
 
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index eea1a7d..ac07c0f 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -26,6 +26,7 @@
 import android.animation.Animator.AnimatorListener;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
+import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.Log;
@@ -47,8 +48,11 @@
 /**
  * Class to manage transitions between different states for a StatefulActivity based on different
  * states
+ * @param STATE_TYPE Basestate used by the state manager
+ * @param STATEFUL_CONTAINER container object used to manage state
  */
-public class StateManager<STATE_TYPE extends BaseState<STATE_TYPE>> {
+public class StateManager<STATE_TYPE extends BaseState<STATE_TYPE>,
+        STATEFUL_CONTAINER extends Context & StatefulContainer<STATE_TYPE>> {
 
     public static final String TAG = "StateManager";
     // b/279059025, b/325463989
@@ -56,7 +60,7 @@
 
     private final AnimationState mConfig = new AnimationState();
     private final Handler mUiHandler;
-    private final StatefulActivity<STATE_TYPE> mActivity;
+    private final STATEFUL_CONTAINER mStatefulContainer;
     private final ArrayList<StateListener<STATE_TYPE>> mListeners = new ArrayList<>();
     private final STATE_TYPE mBaseState;
 
@@ -71,12 +75,12 @@
 
     private STATE_TYPE mRestState;
 
-    public StateManager(StatefulActivity<STATE_TYPE> l, STATE_TYPE baseState) {
+    public StateManager(STATEFUL_CONTAINER container, STATE_TYPE baseState) {
         mUiHandler = new Handler(Looper.getMainLooper());
-        mActivity = l;
+        mStatefulContainer = container;
         mBaseState = baseState;
         mState = mLastStableState = mCurrentStableState = baseState;
-        mAtomicAnimationFactory = l.createAtomicAnimationFactory();
+        mAtomicAnimationFactory = container.createAtomicAnimationFactory();
     }
 
     public STATE_TYPE getState() {
@@ -109,10 +113,10 @@
         writer.println(prefix + "\tisInTransition:" + isInTransition());
     }
 
-    public StateHandler[] getStateHandlers() {
+    public StateHandler<STATE_TYPE>[] getStateHandlers() {
         if (mStateHandlers == null) {
-            ArrayList<StateHandler> handlers = new ArrayList<>();
-            mActivity.collectStateHandlers(handlers);
+            ArrayList<StateHandler<STATE_TYPE>> handlers = new ArrayList<>();
+            mStatefulContainer.collectStateHandlers(handlers);
             mStateHandlers = handlers.toArray(new StateHandler[handlers.size()]);
         }
         return mStateHandlers;
@@ -130,7 +134,7 @@
      * Returns true if the state changes should be animated.
      */
     public boolean shouldAnimateStateChange() {
-        return !mActivity.isForceInvisible() && mActivity.isStarted();
+        return mStatefulContainer.shouldAnimateStateChange();
     }
 
     /**
@@ -245,7 +249,7 @@
         }
 
         animated &= areAnimatorsEnabled();
-        if (mActivity.isInState(state)) {
+        if (mStatefulContainer.isInState(state)) {
             if (mConfig.currentAnimation == null) {
                 // Run any queued runnable
                 if (listener != null) {
@@ -302,8 +306,8 @@
         // Since state mBaseState can be reached from multiple states, just assume that the
         // transition plays in reverse and use the same duration as previous state.
         mConfig.duration = state == mBaseState
-                ? fromState.getTransitionDuration(mActivity, false /* isToState */)
-                : state.getTransitionDuration(mActivity, true /* isToState */);
+                ? fromState.getTransitionDuration(mStatefulContainer, false /* isToState */)
+                : state.getTransitionDuration(mStatefulContainer, true /* isToState */);
         prepareForAtomicAnimation(fromState, state, mConfig);
         AnimatorSet animation = createAnimationToNewWorkspaceInternal(state).buildAnim();
         if (listener != null) {
@@ -336,7 +340,7 @@
         PendingAnimation builder = new PendingAnimation(config.duration);
         prepareForAtomicAnimation(fromState, toState, config);
 
-        for (StateHandler handler : mActivity.getStateManager().getStateHandlers()) {
+        for (StateHandler handler : mStatefulContainer.getStateManager().getStateHandlers()) {
             handler.setStateWithAnimation(toState, config, builder);
         }
         return builder.buildAnim();
@@ -402,7 +406,7 @@
 
     private void onStateTransitionStart(STATE_TYPE state) {
         mState = state;
-        mActivity.onStateSetStart(mState);
+        mStatefulContainer.onStateSetStart(mState);
 
         if (DEBUG) {
             Log.d(TAG, "onStateTransitionStart - state: " + state);
@@ -419,7 +423,7 @@
             mCurrentStableState = state;
         }
 
-        mActivity.onStateSetEnd(state);
+        mStatefulContainer.onStateSetEnd(state);
         if (state == mBaseState) {
             setRestState(null);
         }
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index 30ba703..28f2def 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -18,7 +18,6 @@
 import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
 
-import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS;
 import static com.android.launcher3.LauncherState.FLAG_NON_INTERACTIVE;
 
 import android.content.res.Configuration;
@@ -29,11 +28,9 @@
 
 import androidx.annotation.CallSuper;
 
-import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.LauncherRootView;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.util.window.WindowManagerProxy;
 import com.android.launcher3.views.BaseDragLayer;
@@ -45,7 +42,7 @@
  * @param <STATE_TYPE> Type of state object
  */
 public abstract class StatefulActivity<STATE_TYPE extends BaseState<STATE_TYPE>>
-        extends BaseDraggingActivity {
+        extends BaseDraggingActivity implements StatefulContainer<STATE_TYPE> {
 
     public final Handler mHandler = new Handler();
     private final Runnable mHandleDeferredResume = this::handleDeferredResume;
@@ -67,19 +64,9 @@
     /**
      * Create handlers to control the property changes for this activity
      */
-    protected abstract void collectStateHandlers(List<StateHandler> out);
 
-    /**
-     * Returns true if the activity is in the provided state
-     */
-    public boolean isInState(STATE_TYPE state) {
-        return getStateManager().getState() == state;
-    }
-
-    /**
-     * Returns the state manager for this activity
-     */
-    public abstract StateManager<STATE_TYPE> getStateManager();
+    @Override
+    public abstract void collectStateHandlers(List<StateHandler<STATE_TYPE>> out);
 
     protected void inflateRootView(int layoutId) {
         mRootView = (LauncherRootView) LayoutInflater.from(this).inflate(layoutId, null);
@@ -106,22 +93,12 @@
         if (mDeferredResumePending) {
             handleDeferredResume();
         }
-
-        if (state.hasFlag(FLAG_CLOSE_POPUPS)) {
-            AbstractFloatingView.closeAllOpenViews(this, !state.hasFlag(FLAG_NON_INTERACTIVE));
-        }
+        StatefulContainer.super.onStateSetStart(state);
     }
 
-    /**
-     * Called when transition to state ends
-     */
-    public void onStateSetEnd(STATE_TYPE state) { }
-
-    /**
-     * Creates a factory for atomic state animations
-     */
-    public AtomicAnimationFactory<STATE_TYPE> createAtomicAnimationFactory() {
-        return new AtomicAnimationFactory(0);
+    @Override
+    public boolean shouldAnimateStateChange() {
+        return !isForceInvisible() && isStarted();
     }
 
     @Override
diff --git a/src/com/android/launcher3/statemanager/StatefulContainer.java b/src/com/android/launcher3/statemanager/StatefulContainer.java
new file mode 100644
index 0000000..0cf0a27
--- /dev/null
+++ b/src/com/android/launcher3/statemanager/StatefulContainer.java
@@ -0,0 +1,84 @@
+/*
+ * 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.launcher3.statemanager;
+
+
+import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS;
+import static com.android.launcher3.statemanager.BaseState.FLAG_NON_INTERACTIVE;
+
+import androidx.annotation.CallSuper;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.views.ActivityContext;
+
+import java.util.List;
+
+/**
+ * Interface for a container that can be managed by a state manager.
+ *
+ * @param <STATE_TYPE> The type of state that the container can be in.
+ */
+public interface StatefulContainer<STATE_TYPE extends BaseState<STATE_TYPE>> extends
+        ActivityContext {
+
+    /**
+     * Creates a factory for atomic state animations
+     */
+    default StateManager.AtomicAnimationFactory<STATE_TYPE> createAtomicAnimationFactory() {
+        return new StateManager.AtomicAnimationFactory<>(0);
+    }
+
+    /**
+     * Create handlers to control the property changes for this activity
+     */
+    void collectStateHandlers(List<StateManager.StateHandler<STATE_TYPE>> out);
+
+    /**
+     * Retrieves state manager for given container
+     */
+    StateManager<STATE_TYPE, ?> getStateManager();
+
+    /**
+     * Called when transition to state ends
+     * @param state current state of State_Type
+     */
+    default void onStateSetEnd(STATE_TYPE state) { }
+
+    /**
+     * Called when transition to state starts
+     * @param state current state of State_Type
+     */
+    @CallSuper
+    default void onStateSetStart(STATE_TYPE state) {
+        if (state.hasFlag(FLAG_CLOSE_POPUPS)) {
+            AbstractFloatingView.closeAllOpenViews(this, !state.hasFlag(FLAG_NON_INTERACTIVE));
+        }
+    }
+
+    /**
+     * Returns true if the activity is in the provided state
+     * @param state current state of State_Type
+     */
+    default boolean isInState(STATE_TYPE state) {
+        return getStateManager().getState() == state;
+    }
+
+    /**
+     * Returns true if state change should transition with animation
+     */
+    boolean shouldAnimateStateChange();
+}
diff --git a/src/com/android/launcher3/util/CannedAnimationCoordinator.kt b/src/com/android/launcher3/util/CannedAnimationCoordinator.kt
index 85f81f5..749caac 100644
--- a/src/com/android/launcher3/util/CannedAnimationCoordinator.kt
+++ b/src/com/android/launcher3/util/CannedAnimationCoordinator.kt
@@ -113,10 +113,9 @@
         // flags accordingly
         animationController =
             controller.apply {
-                activity.stateManager.setCurrentAnimation(
-                    this,
-                    USER_CONTROLLED or HANDLE_STATE_APPLY
-                )
+                activity
+                    .stateManager
+                    .setCurrentAnimation(this, USER_CONTROLLED or HANDLE_STATE_APPLY)
             }
         recreateAnimation(provider)
     }
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index f7c4df4..8c5a76e 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -16,9 +16,10 @@
 
 package com.android.launcher3.util;
 
-import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
 import static android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI;
 
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
+
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
 import android.content.Context;
@@ -151,6 +152,18 @@
     }
 
     /**
+     * Returns the installing app package for the given package
+     */
+    public String getAppInstallerPackage(@NonNull final String packageName) {
+        try {
+            return mPm.getInstallSourceInfo(packageName).getInstallingPackageName();
+        } catch (NameNotFoundException e) {
+            Log.e(TAG, "Failed to get installer package for app package:" + packageName, e);
+            return null;
+        }
+    }
+
+    /**
      * Returns the application info for the provided package or null
      */
     @Nullable
diff --git a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index 3405635..8770859 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -57,7 +57,7 @@
  *
  * For an implementation that mocks InvariantDeviceProfile, use [FakeInvariantDeviceProfileTest]
  */
-@AllowedDevices(allowed = [DeviceProduct.CF_PHONE, DeviceProduct.HOST_SIDE_X86_64])
+@AllowedDevices(allowed = [DeviceProduct.CF_PHONE, DeviceProduct.ROBOLECTRIC])
 @IgnoreLimit(ignoreLimit = BuildConfig.IS_STUDIO_BUILD)
 abstract class AbstractDeviceProfileTest {
     protected val testContext: Context = InstrumentationRegistry.getInstrumentation().context
@@ -341,4 +341,9 @@
     protected fun Int.dpToPx(): Int {
         return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics)
     }
+
+    protected fun String.xmlToId(): Int {
+        val context = InstrumentationRegistry.getInstrumentation().context
+        return context.resources.getIdentifier(this, "xml", context.packageName)
+    }
 }
diff --git a/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
similarity index 93%
rename from tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
index c99da96..3dca35e 100644
--- a/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
@@ -22,7 +22,6 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.AbstractDeviceProfileTest
 import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
-import com.android.launcher3.tests.R as TestR
 import com.android.launcher3.util.TestResourceHelper
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -45,7 +44,7 @@
     fun parseValidFile() {
         val allAppsSpecs =
             ResponsiveSpecsProvider.create(
-                TestResourceHelper(context, TestR.xml.valid_all_apps_file),
+                TestResourceHelper(context, "valid_all_apps_file".xmlToId()),
                 ResponsiveSpecType.AllApps
             )
 
@@ -114,7 +113,7 @@
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_missingTag_throwsError() {
         ResponsiveSpecsProvider.create(
-            TestResourceHelper(context, TestR.xml.invalid_all_apps_file_case_1),
+            TestResourceHelper(context, "invalid_all_apps_file_case_1".xmlToId()),
             ResponsiveSpecType.AllApps
         )
     }
@@ -122,7 +121,7 @@
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
         ResponsiveSpecsProvider.create(
-            TestResourceHelper(context, TestR.xml.invalid_all_apps_file_case_2),
+            TestResourceHelper(context, "invalid_all_apps_file_case_2".xmlToId()),
             ResponsiveSpecType.AllApps
         )
     }
@@ -130,7 +129,7 @@
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_valueBiggerThan1_throwsError() {
         ResponsiveSpecsProvider.create(
-            TestResourceHelper(context, TestR.xml.invalid_all_apps_file_case_3),
+            TestResourceHelper(context, "invalid_all_apps_file_case_3".xmlToId()),
             ResponsiveSpecType.AllApps
         )
     }
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
similarity index 94%
rename from tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
index 1cc5ed2..8346492 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
@@ -23,7 +23,6 @@
 import com.android.launcher3.AbstractDeviceProfileTest
 import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
 import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
-import com.android.launcher3.tests.R as TestR
 import com.android.launcher3.util.TestResourceHelper
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -52,7 +51,7 @@
 
         val workspaceSpecs =
             ResponsiveSpecsProvider.create(
-                TestResourceHelper(context, TestR.xml.valid_workspace_file),
+                TestResourceHelper(context, "valid_workspace_file".xmlToId()),
                 ResponsiveSpecType.Workspace
             )
         val widthSpec =
@@ -62,7 +61,7 @@
 
         val allAppsSpecs =
             ResponsiveSpecsProvider.create(
-                TestResourceHelper(context, TestR.xml.valid_all_apps_file),
+                TestResourceHelper(context, "valid_all_apps_file".xmlToId()),
                 ResponsiveSpecType.AllApps
             )
 
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedFolderSpecTest.kt
similarity index 95%
rename from tests/src/com/android/launcher3/responsive/CalculatedFolderSpecTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedFolderSpecTest.kt
index c4e2d2a..46d6cb9 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedFolderSpecTest.kt
@@ -23,7 +23,6 @@
 import com.android.launcher3.AbstractDeviceProfileTest
 import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
 import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
-import com.android.launcher3.tests.R
 import com.android.launcher3.util.TestResourceHelper
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -47,12 +46,12 @@
         val columns = 6
 
         // Loading workspace specs
-        val resourceHelperWorkspace = TestResourceHelper(context, R.xml.valid_workspace_file)
+        val resourceHelperWorkspace = TestResourceHelper(context, "valid_workspace_file".xmlToId())
         val workspaceSpecs =
             ResponsiveSpecsProvider.create(resourceHelperWorkspace, ResponsiveSpecType.Workspace)
 
         // Loading folders specs
-        val resourceHelperFolder = TestResourceHelper(context, R.xml.valid_folders_specs)
+        val resourceHelperFolder = TestResourceHelper(context, "valid_folders_specs".xmlToId())
         val folderSpecs =
             ResponsiveSpecsProvider.create(resourceHelperFolder, ResponsiveSpecType.Folder)
         val specs = folderSpecs.getSpecsByAspectRatio(aspectRatio)
@@ -123,12 +122,12 @@
         val rows = 5
 
         // Loading workspace specs
-        val resourceHelperWorkspace = TestResourceHelper(context, R.xml.valid_workspace_file)
+        val resourceHelperWorkspace = TestResourceHelper(context, "valid_workspace_file".xmlToId())
         val workspaceSpecs =
             ResponsiveSpecsProvider.create(resourceHelperWorkspace, ResponsiveSpecType.Workspace)
 
         // Loading folders specs
-        val resourceHelperFolder = TestResourceHelper(context, R.xml.valid_folders_specs)
+        val resourceHelperFolder = TestResourceHelper(context, "valid_folders_specs".xmlToId())
         val folderSpecs =
             ResponsiveSpecsProvider.create(resourceHelperFolder, ResponsiveSpecType.Folder)
         val specs = folderSpecs.getSpecsByAspectRatio(aspectRatio)
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt
similarity index 95%
rename from tests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt
index 1a564ac..5ab44f3 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt
@@ -22,7 +22,6 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.AbstractDeviceProfileTest
 import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
-import com.android.launcher3.tests.R as TestR
 import com.android.launcher3.util.TestResourceHelper
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -47,7 +46,7 @@
         val availableHeight = deviceSpec.naturalSize.second
 
         val hotseatSpecsProvider =
-            HotseatSpecsProvider.create(TestResourceHelper(context, TestR.xml.valid_hotseat_file))
+            HotseatSpecsProvider.create(TestResourceHelper(context, "valid_hotseat_file".xmlToId()))
         val heightSpec =
             hotseatSpecsProvider.getCalculatedSpec(
                 aspectRatio,
@@ -73,7 +72,7 @@
         val availableHeight = deviceSpec.naturalSize.second
 
         val hotseatSpecsProvider =
-            HotseatSpecsProvider.create(TestResourceHelper(context, TestR.xml.valid_hotseat_file))
+            HotseatSpecsProvider.create(TestResourceHelper(context, "valid_hotseat_file".xmlToId()))
         val heightSpec =
             hotseatSpecsProvider.getCalculatedSpec(
                 aspectRatio,
@@ -99,7 +98,7 @@
 
         val hotseatSpecsProvider =
             HotseatSpecsProvider.create(
-                TestResourceHelper(context, TestR.xml.valid_hotseat_land_file)
+                TestResourceHelper(context, "valid_hotseat_land_file".xmlToId())
             )
         val widthSpec =
             hotseatSpecsProvider.getCalculatedSpec(aspectRatio, DimensionType.WIDTH, availableWidth)
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
similarity index 95%
rename from tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
index 0c5d347..dea98b6 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
@@ -23,7 +23,6 @@
 import com.android.launcher3.AbstractDeviceProfileTest
 import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
 import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
-import com.android.launcher3.tests.R as TestR
 import com.android.launcher3.util.TestResourceHelper
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -53,7 +52,7 @@
 
         val workspaceSpecs =
             ResponsiveSpecsProvider.create(
-                TestResourceHelper(context, TestR.xml.valid_workspace_file),
+                TestResourceHelper(context, "valid_workspace_file".xmlToId()),
                 ResponsiveSpecType.Workspace
             )
         val widthSpec =
@@ -96,7 +95,7 @@
 
         val workspaceSpecs =
             ResponsiveSpecsProvider.create(
-                TestResourceHelper(context, TestR.xml.valid_workspace_file),
+                TestResourceHelper(context, "valid_workspace_file".xmlToId()),
                 ResponsiveSpecType.Workspace
             )
         val widthSpec =
@@ -138,7 +137,7 @@
         val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 640
         val workspaceSpecs =
             ResponsiveSpecsProvider.create(
-                TestResourceHelper(context, TestR.xml.valid_workspace_unsorted_file),
+                TestResourceHelper(context, "valid_workspace_unsorted_file".xmlToId()),
                 ResponsiveSpecType.Workspace
             )
         val widthSpec =
diff --git a/tests/src/com/android/launcher3/responsive/FolderSpecTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/FolderSpecTest.kt
similarity index 91%
rename from tests/src/com/android/launcher3/responsive/FolderSpecTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/FolderSpecTest.kt
index 5cfa49f..d2b264b 100644
--- a/tests/src/com/android/launcher3/responsive/FolderSpecTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/FolderSpecTest.kt
@@ -23,7 +23,6 @@
 import com.android.launcher3.AbstractDeviceProfileTest
 import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
 import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
-import com.android.launcher3.tests.R
 import com.android.launcher3.util.TestResourceHelper
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -44,7 +43,7 @@
 
     @Test
     fun parseValidFile() {
-        val resourceHelper = TestResourceHelper(context, R.xml.valid_folders_specs)
+        val resourceHelper = TestResourceHelper(context, "valid_folders_specs".xmlToId())
         val folderSpecs = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
         val specs = folderSpecs.getSpecsByAspectRatio(aspectRatio)
 
@@ -92,25 +91,25 @@
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_missingTag_throwsError() {
-        val resourceHelper = TestResourceHelper(context, R.xml.invalid_folders_specs_1)
+        val resourceHelper = TestResourceHelper(context, "invalid_folders_specs_1".xmlToId())
         ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
-        val resourceHelper = TestResourceHelper(context, R.xml.invalid_folders_specs_2)
+        val resourceHelper = TestResourceHelper(context, "invalid_folders_specs_2".xmlToId())
         ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_valueBiggerThan1_throwsError() {
-        val resourceHelper = TestResourceHelper(context, R.xml.invalid_folders_specs_3)
+        val resourceHelper = TestResourceHelper(context, "invalid_folders_specs_3".xmlToId())
         ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_missingSpecs_throwsError() {
-        val resourceHelper = TestResourceHelper(context, R.xml.invalid_folders_specs_4)
+        val resourceHelper = TestResourceHelper(context, "invalid_folders_specs_4".xmlToId())
         ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
     }
 
@@ -132,7 +131,7 @@
         val calculatedWorkspaceSpec =
             CalculatedResponsiveSpec(aspectRatio, availableSpace, cells, workspaceSpec)
 
-        val resourceHelper = TestResourceHelper(context, R.xml.invalid_folders_specs_5)
+        val resourceHelper = TestResourceHelper(context, "invalid_folders_specs_5".xmlToId())
         val folderSpecs = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
         folderSpecs.getCalculatedSpec(
             aspectRatio,
@@ -161,7 +160,7 @@
         val calculatedWorkspaceSpec =
             CalculatedResponsiveSpec(aspectRatio, availableSpace, cells, workspaceSpec)
 
-        val resourceHelper = TestResourceHelper(context, R.xml.invalid_folders_specs_5)
+        val resourceHelper = TestResourceHelper(context, "invalid_folders_specs_5".xmlToId())
         val folderSpecs = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
         folderSpecs.getCalculatedSpec(
             aspectRatio,
@@ -190,7 +189,7 @@
         val calculatedWorkspaceSpec =
             CalculatedResponsiveSpec(aspectRatio, availableSpace, cells, workspaceSpec)
 
-        val resourceHelper = TestResourceHelper(context, R.xml.valid_folders_specs)
+        val resourceHelper = TestResourceHelper(context, "valid_folders_specs".xmlToId())
         val folderSpecs = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
         val calculatedWidthSpec =
             folderSpecs.getCalculatedSpec(
@@ -227,7 +226,7 @@
         val calculatedWorkspaceSpec =
             CalculatedResponsiveSpec(aspectRatio, availableSpace, cells, workspaceSpec)
 
-        val resourceHelper = TestResourceHelper(context, R.xml.valid_folders_specs)
+        val resourceHelper = TestResourceHelper(context, "valid_folders_specs".xmlToId())
         val folderSpecs = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
         folderSpecs.getCalculatedSpec(
             aspectRatio,
@@ -256,7 +255,7 @@
         val calculatedWorkspaceSpec =
             CalculatedResponsiveSpec(aspectRatio, availableSpace, cells, workspaceSpec)
 
-        val resourceHelper = TestResourceHelper(context, R.xml.valid_folders_specs)
+        val resourceHelper = TestResourceHelper(context, "valid_folders_specs".xmlToId())
         val folderSpecs = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
         val calculatedHeightSpec =
             folderSpecs.getCalculatedSpec(
@@ -293,7 +292,7 @@
         val calculatedWorkspaceSpec =
             CalculatedResponsiveSpec(aspectRatio, availableSpace, cells, workspaceSpec)
 
-        val resourceHelper = TestResourceHelper(context, R.xml.valid_folders_specs)
+        val resourceHelper = TestResourceHelper(context, "valid_folders_specs".xmlToId())
         val folderSpecs = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
         folderSpecs.getCalculatedSpec(
             aspectRatio,
diff --git a/tests/src/com/android/launcher3/responsive/HotseatSpecsProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/HotseatSpecsProviderTest.kt
similarity index 93%
rename from tests/src/com/android/launcher3/responsive/HotseatSpecsProviderTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/HotseatSpecsProviderTest.kt
index 78cb1ac..58324e1 100644
--- a/tests/src/com/android/launcher3/responsive/HotseatSpecsProviderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/HotseatSpecsProviderTest.kt
@@ -22,7 +22,6 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.AbstractDeviceProfileTest
 import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
-import com.android.launcher3.tests.R as TestR
 import com.android.launcher3.util.TestResourceHelper
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -44,7 +43,7 @@
     @Test
     fun parseValidFile() {
         val hotseatSpecsProvider =
-            HotseatSpecsProvider.create(TestResourceHelper(context, TestR.xml.valid_hotseat_file))
+            HotseatSpecsProvider.create(TestResourceHelper(context, "valid_hotseat_file".xmlToId()))
         val specs = hotseatSpecsProvider.getSpecsByAspectRatio(aspectRatio)
 
         val expectedHeightSpecs =
@@ -76,7 +75,7 @@
     fun parseValidLandscapeFile() {
         val hotseatSpecsProvider =
             HotseatSpecsProvider.create(
-                TestResourceHelper(context, TestR.xml.valid_hotseat_land_file)
+                TestResourceHelper(context, "valid_hotseat_land_file".xmlToId())
             )
         val specs = hotseatSpecsProvider.getSpecsByAspectRatio(aspectRatio)
         assertThat(specs.heightSpecs.size).isEqualTo(0)
@@ -107,14 +106,14 @@
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_spaceIsNotFixedSize_throwsError() {
         HotseatSpecsProvider.create(
-            TestResourceHelper(context, TestR.xml.invalid_hotseat_file_case_1)
+            TestResourceHelper(context, "invalid_hotseat_file_case_1".xmlToId())
         )
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_invalidFixedSize_throwsError() {
         HotseatSpecsProvider.create(
-            TestResourceHelper(context, TestR.xml.invalid_hotseat_file_case_2)
+            TestResourceHelper(context, "invalid_hotseat_file_case_2".xmlToId())
         )
     }
 }
diff --git a/tests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt
similarity index 92%
rename from tests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt
index 50cd358..11161bd 100644
--- a/tests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt
@@ -22,7 +22,6 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.AbstractDeviceProfileTest
 import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
-import com.android.launcher3.tests.R as TestR
 import com.android.launcher3.util.TestResourceHelper
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -43,7 +42,7 @@
 
     @Test
     fun parseValidFile() {
-        val testResourceHelper = TestResourceHelper(context, TestR.xml.valid_cell_specs_file)
+        val testResourceHelper = TestResourceHelper(context, "valid_cell_specs_file".xmlToId())
         val provider = ResponsiveCellSpecsProvider.create(testResourceHelper)
 
         // Validate Portrait
@@ -98,21 +97,21 @@
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_IsNotFixedSizeOrMatchWorkspace_throwsError() {
         ResponsiveCellSpecsProvider.create(
-            TestResourceHelper(context, TestR.xml.invalid_cell_specs_1)
+            TestResourceHelper(context, "invalid_cell_specs_1".xmlToId())
         )
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_dimensionTypeIsNotHeight_throwsError() {
         ResponsiveCellSpecsProvider.create(
-            TestResourceHelper(context, TestR.xml.invalid_cell_specs_2)
+            TestResourceHelper(context, "invalid_cell_specs_2".xmlToId())
         )
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_invalidFixedSize_throwsError() {
         ResponsiveCellSpecsProvider.create(
-            TestResourceHelper(context, TestR.xml.invalid_cell_specs_3)
+            TestResourceHelper(context, "invalid_cell_specs_3".xmlToId())
         )
     }
 }
diff --git a/tests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt
similarity index 93%
rename from tests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt
index 54a1dc5..c74f7a8 100644
--- a/tests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt
@@ -23,7 +23,6 @@
 import com.android.launcher3.AbstractDeviceProfileTest
 import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
 import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
-import com.android.launcher3.tests.R
 import com.android.launcher3.util.TestResourceHelper
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -44,7 +43,7 @@
 
     @Test
     fun parseValidFile() {
-        val resourceHelper = TestResourceHelper(context, R.xml.valid_responsive_spec_unsorted)
+        val resourceHelper = TestResourceHelper(context, "valid_responsive_spec_unsorted".xmlToId())
         val provider = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
 
         // Validate Portrait
@@ -111,44 +110,44 @@
 
     @Test(expected = IllegalStateException::class)
     fun parseValidFile_invalidAspectRatio_throwsError() {
-        val resourceHelper = TestResourceHelper(context, R.xml.valid_responsive_spec_unsorted)
+        val resourceHelper = TestResourceHelper(context, "valid_responsive_spec_unsorted".xmlToId())
         val provider = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
         provider.getSpecsByAspectRatio(0f)
     }
 
     @Test(expected = InvalidResponsiveGridSpec::class)
     fun parseInvalidFile_missingGroups_throwsError() {
-        val resourceHelper = TestResourceHelper(context, R.xml.invalid_responsive_spec_1)
+        val resourceHelper = TestResourceHelper(context, "invalid_responsive_spec_1".xmlToId())
         ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
     }
 
     @Test(expected = InvalidResponsiveGridSpec::class)
     fun parseInvalidFile_partialGroups_throwsError() {
-        val resourceHelper = TestResourceHelper(context, R.xml.invalid_responsive_spec_2)
+        val resourceHelper = TestResourceHelper(context, "invalid_responsive_spec_2".xmlToId())
         ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_invalidAspectRatio_throwsError() {
-        val resourceHelper = TestResourceHelper(context, R.xml.invalid_responsive_spec_3)
+        val resourceHelper = TestResourceHelper(context, "invalid_responsive_spec_3".xmlToId())
         ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_invalidRemainderSpace_throwsError() {
-        val resourceHelper = TestResourceHelper(context, R.xml.invalid_responsive_spec_4)
+        val resourceHelper = TestResourceHelper(context, "invalid_responsive_spec_4".xmlToId())
         ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_invalidAvailableSpace_throwsError() {
-        val resourceHelper = TestResourceHelper(context, R.xml.invalid_responsive_spec_5)
+        val resourceHelper = TestResourceHelper(context, "invalid_responsive_spec_5".xmlToId())
         ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_invalidFixedSize_throwsError() {
-        val resourceHelper = TestResourceHelper(context, R.xml.invalid_responsive_spec_6)
+        val resourceHelper = TestResourceHelper(context, "invalid_responsive_spec_6".xmlToId())
         ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
     }
 
diff --git a/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
similarity index 94%
rename from tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
index 17b0ee4..bc133ba 100644
--- a/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
@@ -22,7 +22,6 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.AbstractDeviceProfileTest
 import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
-import com.android.launcher3.tests.R as TestR
 import com.android.launcher3.util.TestResourceHelper
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -45,7 +44,7 @@
     fun parseValidFile() {
         val workspaceSpecs =
             ResponsiveSpecsProvider.create(
-                TestResourceHelper(context, TestR.xml.valid_workspace_file),
+                TestResourceHelper(context, "valid_workspace_file".xmlToId()),
                 ResponsiveSpecType.Workspace
             )
 
@@ -169,7 +168,7 @@
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_missingTag_throwsError() {
         ResponsiveSpecsProvider.create(
-            TestResourceHelper(context, TestR.xml.invalid_workspace_file_case_1),
+            TestResourceHelper(context, "invalid_workspace_file_case_1".xmlToId()),
             ResponsiveSpecType.Workspace
         )
     }
@@ -177,7 +176,7 @@
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
         ResponsiveSpecsProvider.create(
-            TestResourceHelper(context, TestR.xml.invalid_workspace_file_case_2),
+            TestResourceHelper(context, "invalid_workspace_file_case_2".xmlToId()),
             ResponsiveSpecType.Workspace
         )
     }
@@ -185,7 +184,7 @@
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_valueBiggerThan1_throwsError() {
         ResponsiveSpecsProvider.create(
-            TestResourceHelper(context, TestR.xml.invalid_workspace_file_case_3),
+            TestResourceHelper(context, "invalid_workspace_file_case_3".xmlToId()),
             ResponsiveSpecType.Workspace
         )
     }
@@ -193,7 +192,7 @@
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_matchWorkspace_true_throwsError() {
         ResponsiveSpecsProvider.create(
-            TestResourceHelper(context, TestR.xml.invalid_workspace_file_case_4),
+            TestResourceHelper(context, "invalid_workspace_file_case_4".xmlToId()),
             ResponsiveSpecType.Workspace
         )
     }
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/TestResourceHelper.kt b/tests/multivalentTests/src/com/android/launcher3/util/TestResourceHelper.kt
new file mode 100644
index 0000000..d95c6f8
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/TestResourceHelper.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.launcher3.util
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.R
+import kotlin.IntArray
+
+class TestResourceHelper(private val context: Context, specsFileId: Int) :
+    ResourceHelper(context, specsFileId) {
+
+    val responsiveStyleables = listOf(
+            R.styleable.SizeSpec,
+            R.styleable.WorkspaceSpec,
+            R.styleable.FolderSpec,
+            R.styleable.AllAppsSpec,
+            R.styleable.ResponsiveSpecGroup
+    )
+
+    override fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray {
+        val clone =
+                if (responsiveStyleables.any { styleId.contentEquals(it) }) {
+                    convertStyleId(styleId)
+                } else {
+                    styleId.clone()
+                }
+
+        return context.obtainStyledAttributes(attrs, clone)
+    }
+
+    private fun convertStyleId(styleableArr: IntArray): IntArray {
+        val targetContextRes = getInstrumentation().targetContext.resources
+        val context = getInstrumentation().context
+        return styleableArr
+            .map { attrId -> targetContextRes.getResourceName(attrId).split(":").last() }
+            .map { attrName ->
+                // Get required attr from context instead of targetContext
+                context.resources.getIdentifier(attrName, null, context.packageName)
+            }
+            .toIntArray()
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt b/tests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt
new file mode 100644
index 0000000..aadf72e
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt
@@ -0,0 +1,386 @@
+/*
+ * 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.launcher3.model
+
+import android.app.PendingIntent
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.PackageInstaller.SessionInfo
+import android.os.UserHandle
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+import com.android.launcher3.model.FirstScreenBroadcastHelper.MAX_BROADCAST_SIZE
+import com.android.launcher3.model.FirstScreenBroadcastHelper.getTotalItemCount
+import com.android.launcher3.model.FirstScreenBroadcastHelper.truncateModelForBroadcast
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.util.PackageManagerHelper
+import com.android.launcher3.util.PackageUserKey
+import junit.framework.Assert.assertEquals
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class FirstScreenBroadcastHelperTest {
+    private val context = spy(InstrumentationRegistry.getInstrumentation().targetContext)
+    private val mockPmHelper = mock<PackageManagerHelper>()
+    private val expectedAppPackage = "appPackageExpected"
+    private val expectedComponentName = ComponentName(expectedAppPackage, "expectedClass")
+    private val expectedInstallerPackage = "installerPackage"
+    private val expectedIntent =
+        Intent().apply {
+            component = expectedComponentName
+            setPackage(expectedAppPackage)
+        }
+    private val unexpectedAppPackage = "appPackageUnexpected"
+    private val unexpectedComponentName = ComponentName(expectedAppPackage, "unexpectedClass")
+    private val firstScreenItems =
+        listOf(
+            WorkspaceItemInfo().apply {
+                container = CONTAINER_DESKTOP
+                intent = expectedIntent
+            },
+            WorkspaceItemInfo().apply {
+                container = CONTAINER_HOTSEAT
+                intent = expectedIntent
+            },
+            LauncherAppWidgetInfo().apply { providerName = expectedComponentName }
+        )
+
+    @Test
+    fun `Broadcast Models are created with Pending Items from first screen`() {
+        // Given
+        val sessionInfoExpected =
+            SessionInfo().apply {
+                installerPackageName = expectedInstallerPackage
+                appPackageName = expectedAppPackage
+            }
+        val sessionInfoUnexpected =
+            SessionInfo().apply {
+                installerPackageName = expectedInstallerPackage
+                appPackageName = unexpectedAppPackage
+            }
+        val sessionInfoMap: HashMap<PackageUserKey, SessionInfo> =
+            hashMapOf(
+                PackageUserKey(unexpectedAppPackage, UserHandle(0)) to sessionInfoExpected,
+                PackageUserKey(expectedAppPackage, UserHandle(0)) to sessionInfoUnexpected
+            )
+
+        // When
+        val actualResult =
+            FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+                packageManagerHelper = mockPmHelper,
+                firstScreenItems = firstScreenItems,
+                userKeyToSessionMap = sessionInfoMap,
+                allWidgets = listOf()
+            )
+
+        // Then
+        val expectedResult =
+            listOf(
+                FirstScreenBroadcastModel(
+                    installerPackage = expectedInstallerPackage,
+                    pendingWorkspaceItems = mutableSetOf(expectedAppPackage),
+                    pendingHotseatItems = mutableSetOf(expectedAppPackage),
+                    pendingWidgetItems = mutableSetOf(expectedAppPackage)
+                )
+            )
+
+        assertEquals(expectedResult, actualResult)
+    }
+
+    @Test
+    fun `Broadcast Models are created with Installed Items from first screen`() {
+        // Given
+        whenever(mockPmHelper.getAppInstallerPackage(expectedAppPackage))
+            .thenReturn(expectedInstallerPackage)
+
+        // When
+        val actualResult =
+            FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+                packageManagerHelper = mockPmHelper,
+                firstScreenItems = firstScreenItems,
+                userKeyToSessionMap = hashMapOf(),
+                allWidgets =
+                    listOf(
+                        LauncherAppWidgetInfo().apply {
+                            providerName = expectedComponentName
+                            screenId = 0
+                        }
+                    )
+            )
+
+        // Then
+        val expectedResult =
+            listOf(
+                FirstScreenBroadcastModel(
+                    installerPackage = expectedInstallerPackage,
+                    installedHotseatItems = mutableSetOf(expectedAppPackage),
+                    installedWorkspaceItems = mutableSetOf(expectedAppPackage),
+                    firstScreenInstalledWidgets = mutableSetOf(expectedAppPackage)
+                )
+            )
+        assertEquals(expectedResult, actualResult)
+    }
+
+    @Test
+    fun `Broadcast Models are created with Installed Widgets from every screen`() {
+        // Given
+        val expectedAppPackage2 = "appPackageExpected2"
+        val expectedComponentName2 = ComponentName(expectedAppPackage2, "expectedClass2")
+        whenever(mockPmHelper.getAppInstallerPackage(expectedAppPackage))
+            .thenReturn(expectedInstallerPackage)
+        whenever(mockPmHelper.getAppInstallerPackage(expectedAppPackage2))
+            .thenReturn(expectedInstallerPackage)
+
+        // When
+        val actualResult =
+            FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+                packageManagerHelper = mockPmHelper,
+                firstScreenItems = listOf(),
+                userKeyToSessionMap = hashMapOf(),
+                allWidgets =
+                    listOf(
+                        LauncherAppWidgetInfo().apply {
+                            providerName = expectedComponentName
+                            screenId = 0
+                        },
+                        LauncherAppWidgetInfo().apply {
+                            providerName = expectedComponentName2
+                            screenId = 1
+                        },
+                        LauncherAppWidgetInfo().apply {
+                            providerName = unexpectedComponentName
+                            screenId = 0
+                        }
+                    )
+            )
+
+        // Then
+        val expectedResult =
+            listOf(
+                FirstScreenBroadcastModel(
+                    installerPackage = expectedInstallerPackage,
+                    installedHotseatItems = mutableSetOf(),
+                    installedWorkspaceItems = mutableSetOf(),
+                    firstScreenInstalledWidgets = mutableSetOf(expectedAppPackage),
+                    secondaryScreenInstalledWidgets = mutableSetOf(expectedAppPackage2)
+                )
+            )
+        assertEquals(expectedResult, actualResult)
+    }
+
+    @Test
+    fun `Broadcast Models are created with Pending Items in Collections from the first screen`() {
+        // Given
+        val sessionInfoExpected =
+            SessionInfo().apply {
+                installerPackageName = expectedInstallerPackage
+                appPackageName = expectedAppPackage
+            }
+        val sessionInfoUnexpected =
+            SessionInfo().apply {
+                installerPackageName = expectedInstallerPackage
+                appPackageName = unexpectedAppPackage
+            }
+        val sessionInfoMap: HashMap<PackageUserKey, SessionInfo> =
+            hashMapOf(
+                PackageUserKey(unexpectedAppPackage, UserHandle(0)) to sessionInfoExpected,
+                PackageUserKey(expectedAppPackage, UserHandle(0)) to sessionInfoUnexpected,
+            )
+        val expectedItemInfo = WorkspaceItemInfo().apply { intent = expectedIntent }
+        val expectedFolderInfo = FolderInfo().apply { add(expectedItemInfo) }
+        val firstScreenItems = listOf(expectedFolderInfo)
+
+        // When
+        val actualResult =
+            FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+                packageManagerHelper = mockPmHelper,
+                firstScreenItems = firstScreenItems,
+                userKeyToSessionMap = sessionInfoMap,
+                allWidgets = listOf()
+            )
+
+        // Then
+        val expectedResult =
+            listOf(
+                FirstScreenBroadcastModel(
+                    installerPackage = expectedInstallerPackage,
+                    pendingCollectionItems = mutableSetOf(expectedAppPackage)
+                )
+            )
+        assertEquals(expectedResult, actualResult)
+    }
+
+    @Test
+    fun `Models with too many items get truncated to max Broadcast size`() {
+        // given
+        val broadcastModel =
+            FirstScreenBroadcastModel(
+                installerPackage = expectedInstallerPackage,
+                pendingCollectionItems =
+                    mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+                pendingWorkspaceItems =
+                    mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+                pendingHotseatItems =
+                    mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+                pendingWidgetItems =
+                    mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+                installedWorkspaceItems =
+                    mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+                installedHotseatItems =
+                    mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+                firstScreenInstalledWidgets =
+                    mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+                secondaryScreenInstalledWidgets =
+                    mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } }
+            )
+
+        // When
+        broadcastModel.truncateModelForBroadcast()
+
+        // Then
+        assertEquals(MAX_BROADCAST_SIZE, broadcastModel.getTotalItemCount())
+    }
+
+    @Test
+    fun `Broadcast truncates installed Hotseat items before other installed items`() {
+        // Given
+        val broadcastModel =
+            FirstScreenBroadcastModel(
+                installerPackage = expectedInstallerPackage,
+                installedWorkspaceItems =
+                    mutableSetOf<String>().apply { repeat(50) { add(it.toString()) } },
+                firstScreenInstalledWidgets =
+                    mutableSetOf<String>().apply { repeat(10) { add(it.toString()) } },
+                secondaryScreenInstalledWidgets =
+                    mutableSetOf<String>().apply { repeat(10) { add((it + 10).toString()) } },
+                installedHotseatItems =
+                    mutableSetOf<String>().apply { repeat(10) { add(it.toString()) } },
+            )
+
+        // When
+        broadcastModel.truncateModelForBroadcast()
+
+        // Then
+        assertEquals(MAX_BROADCAST_SIZE, broadcastModel.getTotalItemCount())
+        assertEquals(50, broadcastModel.installedWorkspaceItems.size)
+        assertEquals(10, broadcastModel.firstScreenInstalledWidgets.size)
+        assertEquals(10, broadcastModel.secondaryScreenInstalledWidgets.size)
+        assertEquals(0, broadcastModel.installedHotseatItems.size)
+    }
+
+    @Test
+    fun `Broadcast truncates Widgets before the rest of the first screen items`() {
+        // Given
+        val broadcastModel =
+            FirstScreenBroadcastModel(
+                installerPackage = expectedInstallerPackage,
+                installedWorkspaceItems =
+                    mutableSetOf<String>().apply { repeat(70) { add(it.toString()) } },
+                firstScreenInstalledWidgets =
+                    mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+                secondaryScreenInstalledWidgets =
+                    mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+            )
+
+        // When
+        broadcastModel.truncateModelForBroadcast()
+
+        // Then
+        assertEquals(MAX_BROADCAST_SIZE, broadcastModel.getTotalItemCount())
+        assertEquals(70, broadcastModel.installedWorkspaceItems.size)
+        assertEquals(0, broadcastModel.firstScreenInstalledWidgets.size)
+        assertEquals(0, broadcastModel.secondaryScreenInstalledWidgets.size)
+    }
+
+    @Test
+    fun `Broadcasts are correctly formed with Extras for each Installer`() {
+        // Given
+        val broadcastModels: List<FirstScreenBroadcastModel> =
+            listOf(
+                FirstScreenBroadcastModel(
+                    installerPackage = expectedInstallerPackage,
+                    pendingCollectionItems = mutableSetOf("pendingCollectionItem"),
+                    pendingWorkspaceItems = mutableSetOf("pendingWorkspaceItem"),
+                    pendingHotseatItems = mutableSetOf("pendingHotseatItems"),
+                    pendingWidgetItems = mutableSetOf("pendingWidgetItems"),
+                    installedWorkspaceItems = mutableSetOf("installedWorkspaceItems"),
+                    installedHotseatItems = mutableSetOf("installedHotseatItems"),
+                    firstScreenInstalledWidgets = mutableSetOf("firstScreenInstalledWidgetItems"),
+                    secondaryScreenInstalledWidgets = mutableSetOf("secondaryInstalledWidgetItems")
+                )
+            )
+        val expectedPendingIntent =
+            PendingIntent.getActivity(
+                context,
+                0 /* requestCode */,
+                Intent(),
+                PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+            )
+
+        // When
+        FirstScreenBroadcastHelper.sendBroadcastsForModels(context, broadcastModels)
+
+        // Then
+        val argumentCaptor = ArgumentCaptor.forClass(Intent::class.java)
+        verify(context).sendBroadcast(argumentCaptor.capture())
+
+        assertEquals(
+            "com.android.launcher3.action.FIRST_SCREEN_ACTIVE_INSTALLS",
+            argumentCaptor.value.action
+        )
+        assertEquals(expectedInstallerPackage, argumentCaptor.value.`package`)
+        assertEquals(
+            expectedPendingIntent,
+            argumentCaptor.value.getParcelableExtra("verificationToken")
+        )
+        assertEquals(
+            arrayListOf("pendingCollectionItem"),
+            argumentCaptor.value.getStringArrayListExtra("folderItem")
+        )
+        assertEquals(
+            arrayListOf("pendingWorkspaceItem"),
+            argumentCaptor.value.getStringArrayListExtra("workspaceItem")
+        )
+        assertEquals(
+            arrayListOf("pendingHotseatItems"),
+            argumentCaptor.value.getStringArrayListExtra("hotseatItem")
+        )
+        assertEquals(
+            arrayListOf("pendingWidgetItems"),
+            argumentCaptor.value.getStringArrayListExtra("widgetItem")
+        )
+        assertEquals(
+            arrayListOf("installedWorkspaceItems"),
+            argumentCaptor.value.getStringArrayListExtra("workspaceInstalledItems")
+        )
+        assertEquals(
+            arrayListOf("installedHotseatItems"),
+            argumentCaptor.value.getStringArrayListExtra("hotseatInstalledItems")
+        )
+        assertEquals(
+            arrayListOf("firstScreenInstalledWidgetItems", "secondaryInstalledWidgetItems"),
+            argumentCaptor.value.getStringArrayListExtra("widgetInstalledItems")
+        )
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java
index 4a67600..342eedf 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java
@@ -21,9 +21,8 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.Launcher;
-import com.android.launcher3.util.rule.ScreenRecordRule.KeepRecordOnSuccess;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
+import com.android.launcher3.Launcher;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -32,7 +31,6 @@
 @RunWith(AndroidJUnit4.class)
 public class TaplTestsLauncher3Test extends AbstractLauncherUiTest<Launcher> {
 
-    @KeepRecordOnSuccess
     @ScreenRecord // b/322823478
     @Test
     public void testDevicePressMenu() throws Exception {
diff --git a/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java b/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
index 69b42cb..b2e413d 100644
--- a/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
+++ b/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
@@ -45,7 +45,6 @@
 import com.android.launcher3.allapps.WorkProfileManager;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.util.TestUtil;
-import com.android.launcher3.util.rule.ScreenRecordRule.KeepRecordOnSuccess;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 
@@ -196,7 +195,6 @@
 
     }
 
-    @KeepRecordOnSuccess
     @ScreenRecord // b/322823478
     @Test
     public void testEdu() {
diff --git a/tests/src/com/android/launcher3/util/TestResourceHelper.kt b/tests/src/com/android/launcher3/util/TestResourceHelper.kt
deleted file mode 100644
index b4d3ba8..0000000
--- a/tests/src/com/android/launcher3/util/TestResourceHelper.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util
-
-import android.content.Context
-import android.content.res.TypedArray
-import android.util.AttributeSet
-import com.android.launcher3.R
-import com.android.launcher3.tests.R as TestR
-import kotlin.IntArray
-
-class TestResourceHelper(private val context: Context, specsFileId: Int) :
-    ResourceHelper(context, specsFileId) {
-    override fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray {
-        val clone =
-            when {
-                styleId.contentEquals(R.styleable.SizeSpec) -> TestR.styleable.SizeSpec
-                styleId.contentEquals(R.styleable.WorkspaceSpec) -> TestR.styleable.WorkspaceSpec
-                styleId.contentEquals(R.styleable.FolderSpec) -> TestR.styleable.FolderSpec
-                styleId.contentEquals(R.styleable.AllAppsSpec) -> TestR.styleable.AllAppsSpec
-                styleId.contentEquals(R.styleable.ResponsiveSpecGroup) ->
-                    TestR.styleable.ResponsiveSpecGroup
-                else -> styleId.clone()
-            }
-
-        return context.obtainStyledAttributes(attrs, clone)
-    }
-}
diff --git a/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java b/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java
index ff96afb..7a5cf2c 100644
--- a/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java
+++ b/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java
@@ -49,9 +49,6 @@
             return base;
         }
 
-        final Boolean keepRecordOnSuccess = description.getAnnotation(KeepRecordOnSuccess.class)
-                != null;
-
         return new Statement() {
             @Override
             public void evaluate() throws Throwable {
@@ -73,7 +70,7 @@
                     device.executeShellCommand("kill -INT " + screenRecordPid);
                     Log.e(TAG, "Screenrecord captured at: " + outputFile);
                     output.close();
-                    if (success && !keepRecordOnSuccess) {
+                    if (success) {
                         automation.executeShellCommand("rm " + outputFile);
                     }
                 }
@@ -88,14 +85,4 @@
     @Target(ElementType.METHOD)
     public @interface ScreenRecord {
     }
-
-
-    /**
-     * Interface to indicate that we should keep the screen record even if the test succeeds, use
-     * sparingly since it uses a lof of memory.
-     */
-    @Retention(RetentionPolicy.RUNTIME)
-    @Target(ElementType.METHOD)
-    public @interface KeepRecordOnSuccess {
-    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
index 4be46ab..c58d16e 100644
--- a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
+++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
@@ -17,10 +17,12 @@
 
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 
-import android.text.TextUtils;
+import android.graphics.Rect;
 import android.widget.TextView;
 
 import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.Direction;
 import androidx.test.uiautomator.UiObject2;
 
 import java.util.ArrayList;
@@ -34,10 +36,12 @@
     // This particular ID change should happen with caution
     private static final String SEARCH_CONTAINER_RES_ID = "search_results_list_view";
     protected final LauncherInstrumentation mLauncher;
+    private final UiObject2 mSearchContainer;
 
     SearchResultFromQsb(LauncherInstrumentation launcher) {
         mLauncher = launcher;
         mLauncher.waitForLauncherObject("search_container_all_apps");
+        mSearchContainer = mLauncher.waitForSystemLauncherObject(SEARCH_CONTAINER_RES_ID);
     }
 
     /** Find the app from search results with app name. */
@@ -52,18 +56,9 @@
 
     /** Find the web suggestion from search suggestion's title text */
     public SearchWebSuggestion findWebSuggestion(String text) {
-        ArrayList<UiObject2> webSuggestions =
-                new ArrayList<>(mLauncher.waitForObjectsInContainer(
-                        mLauncher.waitForSystemLauncherObject(SEARCH_CONTAINER_RES_ID),
-                        By.clazz(TextView.class)));
-        for (UiObject2 uiObject: webSuggestions) {
-            String currentString = uiObject.getText();
-            if (currentString.equals(text)) {
-                return createWebSuggestion(uiObject);
-            }
-        }
-        mLauncher.fail("Web suggestion title: " + text + " not found");
-        return null;
+        UiObject2 webSuggestion = mLauncher.waitForObjectInContainer(mSearchContainer,
+                getSearchResultSelector(text));
+        return createWebSuggestion(webSuggestion);
     }
 
     protected SearchWebSuggestion createWebSuggestion(UiObject2 webSuggestion) {
@@ -73,13 +68,25 @@
     /** Find the total amount of views being displayed and return the size */
     public int getSearchResultItemSize() {
         ArrayList<UiObject2> searchResultItems =
-                new ArrayList<>(mLauncher.waitForObjectsInContainer(
-                        mLauncher.waitForSystemLauncherObject(SEARCH_CONTAINER_RES_ID),
+                new ArrayList<>(mLauncher.waitForObjectsInContainer(mSearchContainer,
                         By.clazz(TextView.class)));
         return searchResultItems.size();
     }
 
     /**
+     * Scroll down to make next page search results rendered.
+     */
+    public void getNextPageSearchResults() {
+        final int searchContainerHeight = mLauncher.getVisibleBounds(mSearchContainer).height();
+        // Start scrolling from center of the screen to top of the screen.
+        mLauncher.scroll(mSearchContainer,
+                Direction.DOWN,
+                new Rect(0, 0, 0, searchContainerHeight / 2),
+                /* steps= */ 10,
+                /*slowDown= */ false);
+    }
+
+    /**
      * Taps outside bottom sheet to dismiss and return to workspace. Available on tablets only.
      * @param tapRight Tap on the right of bottom sheet if true, or left otherwise.
      */
@@ -121,25 +128,13 @@
 
     /** Verify a tile is present by checking its title and subtitle. */
     public void verifyTileIsPresent(String title, String subtitle) {
-        ArrayList<UiObject2> searchResults =
-                new ArrayList<>(mLauncher.waitForObjectsInContainer(
-                        mLauncher.waitForSystemLauncherObject(SEARCH_CONTAINER_RES_ID),
-                        By.clazz(TextView.class)));
-        boolean foundTitle = false;
-        boolean foundSubtitle = false;
-        for (UiObject2 uiObject: searchResults) {
-            String currentString = uiObject.getText();
-            if (TextUtils.equals(currentString, title)) {
-                foundTitle = true;
-            } else if (TextUtils.equals(currentString, subtitle)) {
-                foundSubtitle = true;
-            }
-        }
-        if (!foundTitle) {
-            mLauncher.fail("Tile not found for title: " + title);
-        }
-        if (!foundSubtitle) {
-            mLauncher.fail("Tile not found for subtitle: " + subtitle);
-        }
+        mLauncher.waitForObjectInContainer(mSearchContainer,
+                getSearchResultSelector(title));
+        mLauncher.waitForObjectInContainer(mSearchContainer,
+                getSearchResultSelector(subtitle));
+    }
+
+    private BySelector getSearchResultSelector(String text) {
+        return By.clazz(TextView.class).text(text);
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 4fa93ef..9ac6768 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -444,6 +444,8 @@
             Runnable expectLongClickEvents) {
         try (LauncherInstrumentation.Closable c = launcher.addContextLayer(
                 "uninstalling app icon")) {
+
+            final String appNameToUninstall = homeAppIcon.getAppName();
             dragIconToWorkspace(
                     launcher,
                     homeAppIcon,
@@ -468,7 +470,10 @@
 
             try (LauncherInstrumentation.Closable c1 = launcher.addContextLayer(
                     "uninstalled app by dragging to the drop bar")) {
-                return new Workspace(launcher);
+                final Workspace newWorkspace = new Workspace(launcher);
+                launcher.waitUntilLauncherObjectGone(
+                        AppIcon.getAppIconSelector(appNameToUninstall));
+                return newWorkspace;
             }
         }
     }