Merge "Only add accessibility actions in Overview if not in select mode" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index ee7e975..878aa6e 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -519,4 +519,11 @@
   namespace: "launcher"
   description: "Enable MSDL feedback for Launcher interactions"
   bug: "377496684"
+}
+
+flag {
+    name: "taskbar_recents_layout_transition"
+    namespace: "launcher"
+    description: "Enable Taskbar LayoutTransition for Recent Apps"
+    bug: "343521765"
 }
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index bd2c7cc..dc0f899 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -55,6 +55,7 @@
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
+import com.android.systemui.animation.back.FlingOnBackAnimationCallback;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -374,12 +375,12 @@
     /**
      * Animation callback for different predictive back animation states for the widget picker.
      */
-    private class BackAnimationCallback implements OnBackAnimationCallback {
+    private class BackAnimationCallback extends FlingOnBackAnimationCallback {
         @Nullable
         OnBackAnimationCallback mActiveOnBackAnimationCallback;
 
         @Override
-        public void onBackStarted(@NonNull BackEvent backEvent) {
+        public void onBackStartedCompat(@NonNull BackEvent backEvent) {
             if (mActiveOnBackAnimationCallback != null) {
                 mActiveOnBackAnimationCallback.onBackCancelled();
             }
@@ -390,7 +391,7 @@
         }
 
         @Override
-        public void onBackInvoked() {
+        public void onBackInvokedCompat() {
             if (mActiveOnBackAnimationCallback == null) {
                 return;
             }
@@ -399,7 +400,7 @@
         }
 
         @Override
-        public void onBackProgressed(@NonNull BackEvent backEvent) {
+        public void onBackProgressedCompat(@NonNull BackEvent backEvent) {
             if (mActiveOnBackAnimationCallback == null) {
                 return;
             }
@@ -407,7 +408,7 @@
         }
 
         @Override
-        public void onBackCancelled() {
+        public void onBackCancelledCompat() {
             if (mActiveOnBackAnimationCallback == null) {
                 return;
             }
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 09dbeb6..b1cb2c6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -372,6 +372,11 @@
                             * mTaskbarInAppDisplayProgress.value);
             mControllers.navbarButtonsViewController
                     .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress);
+
+            if (isBubbleBarEnabled()) {
+                mControllers.bubbleControllers.ifPresent(
+                        c -> c.bubbleStashController.setInAppDisplayOverrideProgress(progress));
+            }
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
index 831faa1..d9589bb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
@@ -176,12 +176,18 @@
                 bubbleBarTranslationYForTaskbar
             }
 
-    /** Translation Y to align the bubble bar with the hotseat. */
+    /** Translation Y to align the bubble bar with the taskbar. */
     val bubbleBarTranslationYForTaskbar: Float
 
-    /** Return translation Y to align the bubble bar with the taskbar. */
+    /** Return translation Y to align the bubble bar with the hotseat. */
     val bubbleBarTranslationYForHotseat: Float
 
+    /**
+     * Show bubble bar is if it were in-app while launcher state is still on home. Set as a progress
+     * value between 0 and 1: 0 - use home layout, 1 - use in-app layout.
+     */
+    var inAppDisplayOverrideProgress: Float
+
     /** Dumps the state of BubbleStashController. */
     fun dump(pw: PrintWriter) {
         pw.println("Bubble stash controller state:")
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
index c117ad4..9a68335 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
@@ -22,6 +22,8 @@
 import android.graphics.Rect
 import android.view.MotionEvent
 import android.view.View
+import com.android.app.animation.Interpolators
+import com.android.launcher3.Utilities
 import com.android.launcher3.anim.AnimatedFloat
 import com.android.launcher3.taskbar.TaskbarInsetsController
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
@@ -97,6 +99,34 @@
             return -hotseatVerticalCenter + bubbleBarHeight / 2
         }
 
+    override val bubbleBarTranslationY: Float
+        get() =
+            if (inAppDisplayOverrideProgress > 0f && launcherState == BubbleLauncherState.HOME) {
+                Utilities.mapToRange(
+                    inAppDisplayOverrideProgress,
+                    /* fromMin = */ 0f,
+                    /* fromMax = */ 1f,
+                    bubbleBarTranslationYForHotseat,
+                    bubbleBarTranslationYForTaskbar,
+                    Interpolators.LINEAR,
+                )
+            } else {
+                super.bubbleBarTranslationY
+            }
+
+    override var inAppDisplayOverrideProgress: Float = 0f
+        set(value) {
+            if (field == value) return
+            field = value
+            if (launcherState == BubbleLauncherState.HOME) {
+                bubbleBarViewController.bubbleBarTranslationY.updateValue(bubbleBarTranslationY)
+                if (value == 0f || value == 1f) {
+                    // Update insets only when we reach the end values
+                    taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+                }
+            }
+        }
+
     override fun init(
         taskbarInsetsController: TaskbarInsetsController,
         bubbleBarViewController: BubbleBarViewController,
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
index fbeecaa..71303f8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
@@ -126,6 +126,9 @@
     override val bubbleBarTranslationYForTaskbar: Float =
         -taskbarHotseatDimensionsProvider.getTaskbarBottomSpace().toFloat()
 
+    /** Not supported in transient mode */
+    override var inAppDisplayOverrideProgress: Float = 0f
+
     /** Check if we have handle view controller */
     override val hasHandleView: Boolean
         get() = bubbleStashedHandleViewController != null
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index fe68ebc..34d9a68 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -190,6 +190,7 @@
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.animation.back.FlingOnBackAnimationCallback;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.unfold.RemoteUnfoldSharedComponent;
@@ -900,12 +901,12 @@
     protected void registerBackDispatcher() {
         getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
                 OnBackInvokedDispatcher.PRIORITY_DEFAULT,
-                new OnBackAnimationCallback() {
+                new FlingOnBackAnimationCallback() {
 
                     @Nullable OnBackAnimationCallback mActiveOnBackAnimationCallback;
 
                     @Override
-                    public void onBackStarted(@NonNull BackEvent backEvent) {
+                    public void onBackStartedCompat(@NonNull BackEvent backEvent) {
                         if (mActiveOnBackAnimationCallback != null) {
                             mActiveOnBackAnimationCallback.onBackCancelled();
                         }
@@ -915,7 +916,7 @@
 
                     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
                     @Override
-                    public void onBackInvoked() {
+                    public void onBackInvokedCompat() {
                         // Recreate mActiveOnBackAnimationCallback if necessary to avoid NPE
                         // because:
                         // 1. b/260636433: In 3-button-navigation mode, onBackStarted() is not
@@ -931,7 +932,7 @@
                     }
 
                     @Override
-                    public void onBackProgressed(@NonNull BackEvent backEvent) {
+                    public void onBackProgressedCompat(@NonNull BackEvent backEvent) {
                         if (!FeatureFlags.IS_STUDIO_BUILD
                                 && mActiveOnBackAnimationCallback == null) {
                             return;
@@ -940,7 +941,7 @@
                     }
 
                     @Override
-                    public void onBackCancelled() {
+                    public void onBackCancelledCompat() {
                         if (!FeatureFlags.IS_STUDIO_BUILD
                                 && mActiveOnBackAnimationCallback == null) {
                             return;
diff --git a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
index db02f55..f719bed 100644
--- a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
+++ b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
@@ -217,6 +217,11 @@
         animation.addListener(
             AnimatorListeners.forEndCallback(
                 Runnable {
+                    // The workspace might stay at a transparent state when the animation is
+                    // cancelled, and the alpha will not be recovered (this doesn't apply to scales
+                    // somehow). Resetting the alpha for the workspace here.
+                    workspace.alpha = 1.0F
+
                     workspace.setLayerType(View.LAYER_TYPE_NONE, null)
                     hotseat.setLayerType(View.LAYER_TYPE_NONE, null)
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 9a8041b..839c42e 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -3607,7 +3607,8 @@
      * @param dismissingForSplitSelection task dismiss animation is used for entering split
      *                                    selection state from app icon
      */
-    public void createTaskDismissAnimation(PendingAnimation anim, TaskView dismissedTaskView,
+    public void createTaskDismissAnimation(PendingAnimation anim,
+            @Nullable TaskView dismissedTaskView,
             boolean animateTaskView, boolean shouldRemoveTask, long duration,
             boolean dismissingForSplitSelection) {
         if (mPendingAnimation != null) {
@@ -3622,7 +3623,8 @@
         boolean showAsGrid = showAsGrid();
         int taskCount = getTaskViewCount();
         int dismissedIndex = indexOfChild(dismissedTaskView);
-        int dismissedTaskViewId = dismissedTaskView.getTaskViewId();
+        int dismissedTaskViewId =
+                dismissedTaskView != null ? dismissedTaskView.getTaskViewId() : INVALID_TASK_ID;
 
         // Grid specific properties.
         boolean isFocusedTaskDismissed = false;
@@ -3637,10 +3639,18 @@
         int scrollDiffPerPage = 0;
         // Non-grid specific properties.
         boolean needsCurveUpdates = false;
+        boolean areAllDesktopTasksDismissed = false;
 
         if (showAsGrid) {
-            dismissedTaskWidth = dismissedTaskView.getLayoutParams().width + mPageSpacing;
-            isFocusedTaskDismissed = dismissedTaskViewId == mFocusedTaskViewId;
+            if (dismissedTaskView != null) {
+                dismissedTaskWidth = dismissedTaskView.getLayoutParams().width + mPageSpacing;
+            }
+            isFocusedTaskDismissed = dismissedTaskViewId != INVALID_TASK_ID
+                    && dismissedTaskViewId == mFocusedTaskViewId;
+            if (dismissingForSplitSelection && getTaskViewAt(
+                    mCurrentPage) instanceof DesktopTaskView) {
+                areAllDesktopTasksDismissed = true;
+            }
             if (isFocusedTaskDismissed) {
                 if (isSplitSelectionActive()) {
                     isStagingFocusedTask = true;
@@ -3812,19 +3822,23 @@
         SplitAnimationTimings splitTimings =
                 AnimUtils.getDeviceOverviewToSplitTimings(mContainer.getDeviceProfile().isTablet);
 
-        int distanceFromDismissedTask = 0;
+        int distanceFromDismissedTask = 1;
+        int stagingTranslation = 0;
+        if (isStagingFocusedTask || areAllDesktopTasksDismissed) {
+            int nextSnappedPage = isStagingFocusedTask
+                    ? indexOfChild(mUtils.getFirstSmallTaskView(getTaskViews()))
+                    : mUtils.getDesktopTaskViewCount(getTaskViews());
+            stagingTranslation = getPagedOrientationHandler().getPrimaryScroll(this)
+                    - getScrollForPage(nextSnappedPage);
+        }
         for (int i = 0; i < count; i++) {
             View child = getChildAt(i);
             if (child == dismissedTaskView) {
-                if (animateTaskView) {
-                    if (dismissingForSplitSelection) {
-                        createInitialSplitSelectAnimation(anim);
-                    } else {
-                        addDismissedTaskAnimations(dismissedTaskView, duration, anim);
-                    }
+                if (animateTaskView && !dismissingForSplitSelection) {
+                    addDismissedTaskAnimations(dismissedTaskView, duration, anim);
                 }
             } else if (!showAsGrid || (enableLargeDesktopWindowingTile()
-                    && dismissedTaskView.isLargeTile()
+                    && dismissedTaskView != null && dismissedTaskView.isLargeTile()
                     && nextFocusedTaskView == null && !dismissingForSplitSelection)) {
                 int offset = getOffsetToDismissedTask(scrollDiffPerPage, dismissedIndex, taskCount);
                 int scrollDiff = newScroll[i] - oldScroll[i] + offset;
@@ -3838,19 +3852,9 @@
                     needsCurveUpdates = true;
                 }
             } else if (child instanceof TaskView taskView) {
-                if (isFocusedTaskDismissed) {
-                    if (nextFocusedTaskView != null &&
-                            !isSameGridRow(taskView, nextFocusedTaskView)) {
-                        continue;
-                    }
-                } else if (i < dismissedIndex || !isSameGridRow(taskView, dismissedTaskView)) {
-                    continue;
-                }
-
                 // Animate task with index >= dismissed index and in the same row as the
                 // dismissed index or next focused index. Offset successive task dismissal
                 // durations for a staggered effect.
-                distanceFromDismissedTask++;
                 int staggerColumn = isStagingFocusedTask
                         ? (int) Math.ceil(distanceFromDismissedTask / 2f)
                         : distanceFromDismissedTask;
@@ -3877,16 +3881,15 @@
                         : dismissTranslationInterpolationEnd;
                 Interpolator dismissInterpolator = isStagingFocusedTask ? OVERSHOOT_0_75 : LINEAR;
 
+                float primaryTranslation = 0;
                 if (taskView == nextFocusedTaskView) {
                     // Enlarge the task to be focused next, and translate into focus position.
                     float scale = mTaskWidth / (float) mLastComputedGridTaskSize.width();
                     anim.setFloat(taskView, TaskView.DISMISS_SCALE, scale,
                             clampToProgress(LINEAR, animationStartProgress,
                                     dismissTranslationInterpolationEnd));
-                    anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
-                            mIsRtl ? dismissedTaskWidth : -dismissedTaskWidth,
-                            clampToProgress(LINEAR, animationStartProgress,
-                                    dismissTranslationInterpolationEnd));
+                    primaryTranslation += mIsRtl ? dismissedTaskWidth : -dismissedTaskWidth;
+                    animationEndProgress = dismissTranslationInterpolationEnd;
                     float secondaryTranslation = -mTaskGridVerticalDiff;
                     if (!nextFocusedTaskFromTop) {
                         secondaryTranslation -= mTopBottomRowHeightDiff;
@@ -3896,25 +3899,27 @@
                                     dismissTranslationInterpolationEnd));
                     anim.add(taskView.getFocusTransitionScaleAndDimOutAnimator(),
                             clampToProgress(LINEAR, 0f, ANIMATION_DISMISS_PROGRESS_MIDPOINT));
-                } else {
-                    float primaryTranslation =
+                } else if ((isFocusedTaskDismissed && nextFocusedTaskView != null && isSameGridRow(
+                        taskView, nextFocusedTaskView))
+                        || (!isFocusedTaskDismissed && i >= dismissedIndex && isSameGridRow(
+                        taskView, dismissedTaskView))) {
+                    primaryTranslation +=
                             nextFocusedTaskView != null ? nextFocusedTaskWidth : dismissedTaskWidth;
-                    if (isStagingFocusedTask) {
-                        // Moves less if focused task is not in scroll position.
-                        int focusedTaskScroll = getScrollForPage(dismissedIndex);
-                        int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
-                        int focusedTaskScrollDiff = primaryScroll - focusedTaskScroll;
-                        primaryTranslation +=
-                                mIsRtl ? focusedTaskScrollDiff : -focusedTaskScrollDiff;
-                    }
+                }
+                primaryTranslation += mIsRtl ? stagingTranslation : -stagingTranslation;
 
+                if (primaryTranslation != 0) {
                     anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
                             mIsRtl ? primaryTranslation : -primaryTranslation,
                             clampToProgress(dismissInterpolator, animationStartProgress,
                                     animationEndProgress));
+                    distanceFromDismissedTask++;
                 }
             }
         }
+        if (dismissingForSplitSelection) {
+            createInitialSplitSelectAnimation(anim);
+        }
 
         if (needsCurveUpdates) {
             anim.addOnFrameCallback(this::updateCurveProperties);
@@ -3923,7 +3928,7 @@
         // Add a tiny bit of translation Z, so that it draws on top of other views. This is relevant
         // (e.g.) when we dismiss a task by sliding it upward: if there is a row of icons above, we
         // want the dragged task to stay above all other views.
-        if (animateTaskView) {
+        if (animateTaskView && dismissedTaskView != null) {
             dismissedTaskView.setTranslationZ(0.1f);
         }
 
@@ -3935,7 +3940,8 @@
         mPendingAnimation.addEndListener(new Consumer<>() {
             @Override
             public void accept(Boolean success) {
-                if (mEnableDrawingLiveTile && dismissedTaskView.isRunningTask() && success) {
+                if (mEnableDrawingLiveTile && dismissedTaskView != null
+                        && dismissedTaskView.isRunningTask() && success) {
                     finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
                             () -> onEnd(true));
                 } else {
@@ -3951,7 +3957,7 @@
 
                 if (success) {
                     mAnyTaskHasBeenDismissed = true;
-                    if (shouldRemoveTask) {
+                    if (shouldRemoveTask && dismissedTaskView != null) {
                         if (dismissedTaskView.isRunningTask()) {
                             finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
                                     () -> removeTaskInternal(dismissedTaskView));
@@ -5080,9 +5086,10 @@
         if (enableLargeDesktopWindowingTile()) {
             for (TaskView taskView : getTaskViews()) {
                 if (taskView instanceof DesktopTaskView) {
+                    // Correcting the animation for split mode since we hide DW in split.
                     builder.addFloat(taskView.getSplitAlphaProperty(),
                             MULTI_PROPERTY_VALUE, 1f, 0f,
-                            deskTopFadeInterPolator);
+                            clampToProgress(deskTopFadeInterPolator, 0f, 0.1f));
                 }
             }
         }
@@ -5140,7 +5147,15 @@
                     true /* dismissingForSplitSelection*/);
         } else {
             // Splitting from Home
-            createInitialSplitSelectAnimation(builder);
+            TaskView currentPageTaskView = getTaskViewAt(mCurrentPage);
+            // When current page is a Desktop task it needs special handling to
+            // display correct animation in split mode
+            if (currentPageTaskView instanceof DesktopTaskView) {
+                createTaskDismissAnimation(builder, null, true, false, duration,
+                        true /* dismissingForSplitSelection*/);
+            } else {
+                createInitialSplitSelectAnimation(builder);
+            }
         }
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
index 00b42bc..e3d41e7 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
@@ -38,6 +38,7 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
 import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.never
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
@@ -228,6 +229,90 @@
             .isEqualTo(TASK_BAR_TRANSLATION_Y)
     }
 
+    @Test
+    fun inAppDisplayOverrideProgress_onHome_updatesTranslationFromHomeToInApp() {
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
+
+        assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
+            .isEqualTo(HOTSEAT_TRANSLATION_Y)
+
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f
+
+        val middleBetweenHotseatAndTaskbar = (HOTSEAT_TRANSLATION_Y + TASK_BAR_TRANSLATION_Y) / 2f
+        assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
+            .isWithin(0.1f)
+            .of(middleBetweenHotseatAndTaskbar)
+
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f
+
+        assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
+            .isEqualTo(TASK_BAR_TRANSLATION_Y)
+    }
+
+    @Test
+    fun inAppDisplayOverrideProgress_onHome_updatesInsetsWhenProgressReachesOne() {
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
+        // Reset invocations to track only changes from in-app display override
+        clearInvocations(taskbarInsetsController)
+
+        // Insets are not updated for values between 0 and 1
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f
+        verify(taskbarInsetsController, never()).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+
+        // Update insets when progress reaches 1
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    @Test
+    fun inAppDisplayOverrideProgress_onHome_updatesInsetsWhenProgressReachesZero() {
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f
+        // Reset invocations to track only changes from in-app display override
+        clearInvocations(taskbarInsetsController)
+
+        // Insets are not updated for values between 0 and 1
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f
+        verify(taskbarInsetsController, never()).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+
+        // Update insets when progress reaches 0
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 0f
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    @Test
+    fun inAppDisplayProgressUpdate_inApp_noTranslationUpdate() {
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP
+
+        assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
+            .isEqualTo(TASK_BAR_TRANSLATION_Y)
+
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f
+
+        assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
+            .isEqualTo(TASK_BAR_TRANSLATION_Y)
+    }
+
+    @Test
+    fun inAppDisplayOverrideProgress_inApp_noInsetsUpdate() {
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP
+
+        // Reset invocations to track only changes from in-app display override
+        clearInvocations(taskbarInsetsController)
+
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 0f
+
+        // Never triggers an update to insets
+        verify(taskbarInsetsController, never()).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
     private fun advanceTimeBy(advanceMs: Long) {
         // Advance animator for on-device tests
         getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(advanceMs) }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
similarity index 73%
rename from quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
rename to quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
index e981570..5b46dc8 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
@@ -15,7 +15,9 @@
  */
 package com.android.quickstep;
 
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.launcher3.util.TestUtil.resolveSystemAppInfo;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -25,10 +27,13 @@
 import android.app.usage.UsageStatsManager;
 import android.content.Intent;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.BaseLauncherActivityTest;
 import com.android.quickstep.views.DigitalWellBeingToast;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskContainer;
@@ -41,30 +46,31 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4.class)
-public class TaplDigitalWellBeingToastTest extends AbstractQuickStepTest {
-    private static final String CALCULATOR_PACKAGE =
-            resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
+public class DigitalWellBeingToastTest extends BaseLauncherActivityTest<QuickstepLauncher> {
+
+    public final String calculatorPackage =
+            resolveSystemAppInfo(Intent.CATEGORY_APP_CALCULATOR).packageName;
 
     @Test
-    public void testToast() throws Exception {
-        startAppFast(CALCULATOR_PACKAGE);
+    public void testToast() {
+        startAppFast(calculatorPackage);
 
         final UsageStatsManager usageStatsManager =
-                mTargetContext.getSystemService(UsageStatsManager.class);
+                targetContext().getSystemService(UsageStatsManager.class);
         final int observerId = 0;
 
         try {
-            final String[] packages = new String[]{CALCULATOR_PACKAGE};
+            final String[] packages = new String[]{calculatorPackage};
 
             // Set time limit for app.
             runWithShellPermission(() ->
                     usageStatsManager.registerAppUsageLimitObserver(observerId, packages,
                             Duration.ofSeconds(600), Duration.ofSeconds(300),
-                            PendingIntent.getActivity(mTargetContext, -1, new Intent()
-                                            .setPackage(mTargetContext.getPackageName()),
+                            PendingIntent.getActivity(targetContext(), -1, new Intent()
+                                            .setPackage(targetContext().getPackageName()),
                                     PendingIntent.FLAG_MUTABLE)));
 
-            mLauncher.goHome();
+            loadLauncherSync();
             final DigitalWellBeingToast toast = getToast();
 
             waitForLauncherCondition("Toast is not visible", launcher -> toast.getHasLimit());
@@ -74,7 +80,7 @@
             runWithShellPermission(
                     () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId));
 
-            mLauncher.goHome();
+            goToState(LauncherState.NORMAL);
             assertFalse("Toast is visible", getToast().getHasLimit());
         } finally {
             runWithShellPermission(
@@ -83,12 +89,12 @@
     }
 
     private DigitalWellBeingToast getToast() {
-        mLauncher.getWorkspace().switchToOverview();
+        goToState(LauncherState.OVERVIEW);
         final TaskView task = getOnceNotNull("No latest task", launcher -> getLatestTask(launcher));
 
         return getFromLauncher(launcher -> {
             TaskContainer taskContainer = task.getTaskContainers().get(0);
-            assertTrue("Latest task is not Calculator", CALCULATOR_PACKAGE.equals(
+            assertTrue("Latest task is not Calculator", calculatorPackage.equals(
                     taskContainer.getTask().getTopComponent().getPackageName()));
             return taskContainer.getDigitalWellBeingToast();
         });
@@ -105,6 +111,5 @@
         } finally {
             getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
         }
-
     }
 }
diff --git a/res/drawable/bg_widgets_header_states_two_pane.xml b/res/drawable/bg_widgets_header_states_two_pane.xml
index 5f4b8c6..1ec41a9 100644
--- a/res/drawable/bg_widgets_header_states_two_pane.xml
+++ b/res/drawable/bg_widgets_header_states_two_pane.xml
@@ -14,18 +14,16 @@
      limitations under the License.
 -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <item android:state_expanded="true">
-        <shape android:shape="rectangle">
-            <solid android:color="?attr/widgetPickerHeaderBackgroundColor" />
-            <corners android:radius="@dimen/widget_list_top_bottom_corner_radius" />
-        </shape>
+    <item android:state_expanded="true" android:state_focused="false">
+        <ripple android:color="@color/accent_ripple_color">
+            <item android:drawable="@drawable/bg_widgets_header_two_pane_expanded_unfocused" />
+        </ripple>
     </item>
-
-    <item android:state_expanded="false">
-        <shape android:shape="rectangle">
-            <solid android:color="@android:color/transparent" />
-            <corners android:radius="@dimen/widget_list_top_bottom_corner_radius" />
-        </shape>
+    <item android:state_expanded="false" android:state_focused="false">
+        <ripple android:color="@color/accent_ripple_color">
+            <item android:drawable="@drawable/bg_widgets_header_two_pane_unexpanded_unfocused" />
+        </ripple>
     </item>
+    <item android:drawable="@drawable/bg_widgets_header_two_pane_expanded_focused" android:state_expanded="true" android:state_focused="true" />
+    <item android:drawable="@drawable/bg_widgets_header_two_pane_unexpanded_focused" android:state_expanded="false" android:state_focused="true" />
 </selector>
diff --git a/res/drawable/bg_widgets_header_two_pane.xml b/res/drawable/bg_widgets_header_two_pane.xml
index ca3feef..e237002 100644
--- a/res/drawable/bg_widgets_header_two_pane.xml
+++ b/res/drawable/bg_widgets_header_two_pane.xml
@@ -14,13 +14,10 @@
      limitations under the License.
 -->
 <inset xmlns:android="http://schemas.android.com/apk/res/android"
-    android:insetTop="@dimen/widget_list_entry_spacing" >
-    <ripple
-        android:color="@color/accent_ripple_color"
-        android:paddingTop="@dimen/widget_list_header_view_vertical_padding"
-        android:paddingBottom="@dimen/widget_list_header_view_vertical_padding" >
-        <item android:id="@android:id/mask"
-            android:drawable="@drawable/bg_widgets_header_states_two_pane" />
+    android:insetTop="@dimen/widget_list_entry_spacing">
+    <layer-list
+        android:paddingBottom="@dimen/widget_list_header_view_vertical_padding"
+        android:paddingTop="@dimen/widget_list_header_view_vertical_padding">
         <item android:drawable="@drawable/bg_widgets_header_states_two_pane" />
-    </ripple>
-</inset>
+    </layer-list>
+</inset>
\ No newline at end of file
diff --git a/res/drawable/bg_widgets_header_two_pane_expanded_focused.xml b/res/drawable/bg_widgets_header_two_pane_expanded_focused.xml
new file mode 100644
index 0000000..0ee3d14
--- /dev/null
+++ b/res/drawable/bg_widgets_header_two_pane_expanded_focused.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- Draw the focus ring -->
+    <item>
+        <shape>
+            <corners android:radius="@dimen/widget_focus_ring_corner_radius" />
+            <stroke
+                android:width="@dimen/widget_header_focus_ring_width"
+                android:color="?attr/widgetPickerTabBackgroundSelected" />
+        </shape>
+    </item>
+
+    <!-- Draw the background with padding to make it spaced within the focus ring. -->
+    <item
+        android:bottom="@dimen/widget_header_background_border"
+        android:end="@dimen/widget_header_background_border"
+        android:start="@dimen/widget_header_background_border"
+        android:top="@dimen/widget_header_background_border">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/widget_list_top_bottom_corner_radius" />
+            <solid android:color="?attr/widgetPickerHeaderBackgroundColor" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/bg_widgets_header_two_pane_expanded_unfocused.xml b/res/drawable/bg_widgets_header_two_pane_expanded_unfocused.xml
new file mode 100644
index 0000000..9028ebe
--- /dev/null
+++ b/res/drawable/bg_widgets_header_two_pane_expanded_unfocused.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
+    android:paddingBottom="@dimen/widget_list_header_view_vertical_padding"
+    android:paddingTop="@dimen/widget_list_header_view_vertical_padding">
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/widget_list_top_bottom_corner_radius" />
+            <solid android:color="?attr/widgetPickerHeaderBackgroundColor" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/bg_widgets_header_two_pane_unexpanded_focused.xml b/res/drawable/bg_widgets_header_two_pane_unexpanded_focused.xml
new file mode 100644
index 0000000..12dc907
--- /dev/null
+++ b/res/drawable/bg_widgets_header_two_pane_unexpanded_focused.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- Draw the focus ring and a transparent background -->
+    <item>
+        <shape>
+            <corners android:radius="@dimen/widget_focus_ring_corner_radius" />
+            <solid android:color="@android:color/transparent" />
+            <stroke
+                android:width="@dimen/widget_header_focus_ring_width"
+                android:color="?attr/widgetPickerTabBackgroundSelected" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/bg_widgets_header_two_pane_unexpanded_unfocused.xml b/res/drawable/bg_widgets_header_two_pane_unexpanded_unfocused.xml
new file mode 100644
index 0000000..ba26f9f
--- /dev/null
+++ b/res/drawable/bg_widgets_header_two_pane_unexpanded_unfocused.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/widget_list_top_bottom_corner_radius" />
+            <solid android:color="@android:color/transparent" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index d1dde3f..c69778a 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -228,6 +228,10 @@
     <!-- Bottom margin for the search and recommended widgets container with work profile -->
     <dimen name="search_and_recommended_widgets_container_small_bottom_margin">10dp</dimen>
 
+    <dimen name="widget_header_focus_ring_width">3dp</dimen>
+    <dimen name="widget_focus_ring_corner_radius">28dp</dimen>
+    <dimen name="widget_header_background_border">5dp</dimen>
+
     <dimen name="widget_list_top_bottom_corner_radius">28dp</dimen>
     <dimen name="widget_list_content_corner_radius">4dp</dimen>
     <!-- Button that expands the widget apps list in the widget picker. -->
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 27602af..6468f74 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -186,18 +186,20 @@
      */
     public void adjustForBubbleBar(boolean isBubbleBarVisible) {
         DeviceProfile dp = mActivity.getDeviceProfile();
+        float adjustedBorderSpace = dp.getHotseatAdjustedBorderSpaceForBubbleBar(getContext());
+        boolean adjustmentRequired = Float.compare(adjustedBorderSpace, 0f) != 0;
 
         ShortcutAndWidgetContainer icons = getShortcutsAndWidgets();
-        AnimatorSet animatorSet = new AnimatorSet();
-
         // update the translation provider for future layout passes of hotseat icons.
-        if (isBubbleBarVisible) {
+        if (adjustmentRequired && isBubbleBarVisible) {
             icons.setTranslationProvider(
                     cellX -> dp.getHotseatAdjustedTranslation(getContext(), cellX));
         } else {
             icons.setTranslationProvider(null);
         }
+        if (!adjustmentRequired) return;
 
+        AnimatorSet animatorSet = new AnimatorSet();
         for (int i = 0; i < icons.getChildCount(); i++) {
             View child = icons.getChildAt(i);
             float tx = isBubbleBarVisible ? dp.getHotseatAdjustedTranslation(getContext(), i) : 0;
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index be2ba57..6145077 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -96,7 +96,6 @@
 import static com.android.launcher3.popup.SystemShortcut.INSTALL;
 import static com.android.launcher3.popup.SystemShortcut.WIDGETS;
 import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
-import static com.android.launcher3.states.RotationHelper.REQUEST_FIXED_LANDSCAPE;
 import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
 import static com.android.launcher3.testing.shared.TestProtocol.LAUNCHER_ACTIVITY_STOPPED_MESSAGE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -782,12 +781,9 @@
         if (!com.android.launcher3.Flags.oneGridSpecs()) {
             return;
         }
-        if (Objects.requireNonNull(mDeviceProfile.inv).isFixedLandscapeMode) {
-            // Set rotation fixed
-            getRotationHelper().setStateHandlerRequest(REQUEST_FIXED_LANDSCAPE);
-        } else {
-            getRotationHelper().setStateHandlerRequest(REQUEST_NONE);
-        }
+        getRotationHelper().setFixedLandscape(
+                Objects.requireNonNull(mDeviceProfile.inv).isFixedLandscapeMode
+        );
     }
 
     public void onAssistantVisibilityChanged(float visibility) {
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index bd604eb..cfb238b 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -43,7 +43,6 @@
 import androidx.annotation.FloatRange;
 import androidx.annotation.Nullable;
 
-import com.android.app.animation.Interpolators;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.Launcher;
@@ -277,10 +276,9 @@
             return;
         }
 
-        float deceleratedProgress = Interpolators.BACK_GESTURE.getInterpolation(backProgress);
         float scaleProgress = ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE
                 + (1 - ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE)
-                * (1 - deceleratedProgress);
+                * (1 - backProgress);
 
         mAllAppScale.updateValue(scaleProgress);
     }
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index 94c36c0..3000b25 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.graphics;
 
+import static android.content.res.Configuration.UI_MODE_NIGHT_NO;
+import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
@@ -25,6 +27,7 @@
 import android.app.WallpaperColors;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
+import android.content.res.Configuration;
 import android.database.Cursor;
 import android.hardware.display.DisplayManager;
 import android.os.Bundle;
@@ -84,6 +87,7 @@
     private static final String KEY_COLORS = "wallpaper_colors";
     private static final String KEY_COLOR_RESOURCE_IDS = "color_resource_ids";
     private static final String KEY_COLOR_VALUES = "color_values";
+    private static final String KEY_DARK_MODE = "use_dark_mode";
 
     private Context mContext;
     private final IBinder mHostToken;
@@ -95,6 +99,7 @@
     private final Display mDisplay;
     private final WallpaperColors mWallpaperColors;
     private SparseIntArray mPreviewColorOverride;
+    @Nullable private Boolean mDarkMode;
     private final RunnableList mLifeCycleTracker;
 
     private final SurfaceControlViewHost mSurfaceControlViewHost;
@@ -235,6 +240,8 @@
     }
 
     private void updateColorOverrides(Bundle bundle) {
+        mDarkMode =
+                bundle.containsKey(KEY_DARK_MODE) ? bundle.getBoolean(KEY_DARK_MODE) : null;
         int[] ids = bundle.getIntArray(KEY_COLOR_RESOURCE_IDS);
         int[] colors = bundle.getIntArray(KEY_COLOR_VALUES);
         if (ids != null && colors != null) {
@@ -253,6 +260,18 @@
      */
     private Context getPreviewContext() {
         Context context = mContext.createDisplayContext(mDisplay);
+        if (mDarkMode != null) {
+            Configuration configuration = new Configuration(
+                    context.getResources().getConfiguration());
+            if (mDarkMode) {
+                configuration.uiMode &= ~UI_MODE_NIGHT_NO;
+                configuration.uiMode |= UI_MODE_NIGHT_YES;
+            } else {
+                configuration.uiMode &= ~UI_MODE_NIGHT_YES;
+                configuration.uiMode |= UI_MODE_NIGHT_NO;
+            }
+            context = context.createConfigurationContext(configuration);
+        }
         if (Flags.newCustomizationPickerUi()) {
             if (mPreviewColorOverride != null) {
                 LocalColorExtractor.newInstance(context)
diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
index 2d6be7e..617cac7 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -17,7 +17,6 @@
 package com.android.launcher3.model;
 
 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
-import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE;
 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
@@ -118,13 +117,14 @@
             @NonNull DeviceGridState srcDeviceState,
             @NonNull DeviceGridState destDeviceState,
             @NonNull DatabaseHelper target,
-            @NonNull SQLiteDatabase source) {
+            @NonNull SQLiteDatabase source,
+            boolean isDestNewDb) {
 
         if (!needsToMigrate(srcDeviceState, destDeviceState)) {
             return true;
         }
 
-        if (LauncherPrefs.get(context).get(IS_FIRST_LOAD_AFTER_RESTORE)
+        if (isDestNewDb
                 && srcDeviceState.getColumns().equals(destDeviceState.getColumns())
                 && srcDeviceState.getRows() < destDeviceState.getRows()) {
             // Only use this strategy when comparing the previous grid to the new grid and the
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
index 07316ef..c856d4b 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -46,6 +46,7 @@
         destDeviceState: DeviceGridState,
         target: DatabaseHelper,
         source: SQLiteDatabase,
+        isDestNewDb: Boolean,
     ) {
         if (!GridSizeMigrationDBController.needsToMigrate(srcDeviceState, destDeviceState)) {
             return
@@ -57,7 +58,7 @@
         // This is a special case where if the grid is the same amount of columns but a larger
         // amount of rows we simply copy over the source grid to the destination grid, rather
         // than undergoing the general grid migration.
-        if (shouldMigrateToStrictlyTallerGrid(isFirstLoad, srcDeviceState, destDeviceState)) {
+        if (shouldMigrateToStrictlyTallerGrid(isDestNewDb, srcDeviceState, destDeviceState)) {
             GridSizeMigrationDBController.copyCurrentGridToNewGrid(
                 context,
                 destDeviceState,
@@ -335,11 +336,11 @@
 
     /** Only migrate the grid in this manner if the target grid is taller and not wider. */
     private fun shouldMigrateToStrictlyTallerGrid(
-        isFirstLoad: Boolean,
+        isDestNewDb: Boolean,
         srcDeviceState: DeviceGridState,
         destDeviceState: DeviceGridState,
     ): Boolean {
-        return isFirstLoad &&
+        return isDestNewDb &&
             srcDeviceState.columns == destDeviceState.columns &&
             srcDeviceState.rows < destDeviceState.rows
     }
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index 094798b..6ff8547 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -89,6 +89,7 @@
 import java.io.File;
 import java.io.InputStream;
 import java.io.StringReader;
+import java.util.List;
 
 /**
  * Utility class which maintains an instance of Launcher database and provides utility methods
@@ -368,6 +369,13 @@
 
         InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
         DatabaseHelper oldHelper = mOpenHelper;
+
+        // We save the existing db's before creating the destination db helper so we know what logic
+        // to run in grid migration based on if that grid already existed before migration or not.
+        List<String> existingDBs = LauncherFiles.GRID_DB_FILES.stream()
+                .filter(dbName -> mContext.getDatabasePath(dbName).exists())
+                .toList();
+
         mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
                 : createDatabaseHelper(true, new DeviceGridState(idp).getDbFile());
         try {
@@ -376,9 +384,11 @@
             // This is the state we want to migrate to that is given by the idp
             DeviceGridState destDeviceState = new DeviceGridState(idp);
 
+            boolean isDestNewDb = !existingDBs.contains(destDeviceState.getDbFile());
+
             GridSizeMigrationLogic gridSizeMigrationLogic = new GridSizeMigrationLogic();
             gridSizeMigrationLogic.migrateGrid(mContext, srcDeviceState, destDeviceState,
-                    mOpenHelper, oldHelper.getWritableDatabase());
+                    mOpenHelper, oldHelper.getWritableDatabase(), isDestNewDb);
         } catch (Exception e) {
             resetLauncherDb(restoreEventLogger);
             throw new Exception("Failed to migrate grid", e);
@@ -441,6 +451,13 @@
             return false;
         }
         DatabaseHelper oldHelper = mOpenHelper;
+
+        // We save the existing db's before creating the destination db helper so we know what logic
+        // to run in grid migration based on if that grid already existed before migration or not.
+        List<String> existingDBs = LauncherFiles.GRID_DB_FILES.stream()
+                .filter(dbName -> mContext.getDatabasePath(dbName).exists())
+                .toList();
+
         mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
                 : createDatabaseHelper(true /* forMigration */, targetDbName);
         try {
@@ -448,8 +465,11 @@
             DeviceGridState srcDeviceState = new DeviceGridState(mContext);
             // This is the state we want to migrate to that is given by the idp
             DeviceGridState destDeviceState = new DeviceGridState(idp);
+
+            boolean isDestNewDb = !existingDBs.contains(destDeviceState.getDbFile());
+
             return GridSizeMigrationDBController.migrateGridIfNeeded(mContext, srcDeviceState,
-                    destDeviceState, mOpenHelper, oldHelper.getWritableDatabase());
+                    destDeviceState, mOpenHelper, oldHelper.getWritableDatabase(), isDestNewDb);
         } catch (Exception e) {
             FileLog.e(TAG, "Failed to migrate grid", e);
             return false;
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index 76f8dd3..5851f62 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -294,6 +294,9 @@
                 case NOTIFICATION_DOTS_PREFERENCE_KEY:
                     return BuildConfig.NOTIFICATION_DOTS_ENABLED;
                 case ALLOW_ROTATION_PREFERENCE_KEY:
+                    if (Flags.oneGridSpecs()) {
+                        return false;
+                    }
                     if (info.isTablet(info.realBounds)) {
                         // Launcher supports rotation by default. No need to show this setting.
                         return false;
diff --git a/src/com/android/launcher3/states/RotationHelper.java b/src/com/android/launcher3/states/RotationHelper.java
index 1f397d1..7d7ccd3 100644
--- a/src/com/android/launcher3/states/RotationHelper.java
+++ b/src/com/android/launcher3/states/RotationHelper.java
@@ -63,7 +63,8 @@
     public static final int REQUEST_NONE = 0;
     public static final int REQUEST_ROTATE = 1;
     public static final int REQUEST_LOCK = 2;
-    public static final int REQUEST_FIXED_LANDSCAPE = 3;
+
+    private boolean mIsFixedLandscape = false;
 
     @NonNull
     private final BaseActivity mActivity;
@@ -165,6 +166,18 @@
         notifyChange();
     }
 
+    public boolean isFixedLandscape() {
+        return mIsFixedLandscape;
+    }
+
+    /**
+     * If fixedLandscape is true then the Launcher become landscape until set false..
+     */
+    public void setFixedLandscape(boolean fixedLandscape) {
+        mIsFixedLandscape = fixedLandscape;
+        notifyChange();
+    }
+
     // Used by tests only.
     public void forceAllowRotationForTesting(boolean allowRotation) {
         if (mDestroyed) return;
@@ -197,9 +210,7 @@
         }
 
         final int activityFlags;
-        if (mStateHandlerRequest == REQUEST_FIXED_LANDSCAPE) {
-            activityFlags = SCREEN_ORIENTATION_USER_LANDSCAPE;
-        } else if (mStateHandlerRequest != REQUEST_NONE) {
+        if (mStateHandlerRequest != REQUEST_NONE) {
             activityFlags = mStateHandlerRequest == REQUEST_LOCK ?
                     SCREEN_ORIENTATION_LOCKED : SCREEN_ORIENTATION_UNSPECIFIED;
         } else if (mCurrentTransitionRequest != REQUEST_NONE) {
@@ -207,6 +218,8 @@
                     SCREEN_ORIENTATION_LOCKED : SCREEN_ORIENTATION_UNSPECIFIED;
         } else if (mCurrentStateRequest == REQUEST_LOCK) {
             activityFlags = SCREEN_ORIENTATION_LOCKED;
+        } else if (mIsFixedLandscape) {
+            activityFlags = SCREEN_ORIENTATION_USER_LANDSCAPE;
         } else if (mIgnoreAutoRotateSettings || mCurrentStateRequest == REQUEST_ROTATE
                 || mHomeRotationEnabled || mForceAllowRotationForTesting) {
             activityFlags = SCREEN_ORIENTATION_UNSPECIFIED;
diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index 85aad89..65d02d0 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -298,8 +298,7 @@
     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void onBackProgressed(BackEvent backEvent) {
         final float progress = backEvent.getProgress();
-        float deceleratedProgress = Interpolators.BACK_GESTURE.getInterpolation(progress);
-        mSwipeToDismissProgress.updateValue(deceleratedProgress);
+        mSwipeToDismissProgress.updateValue(progress);
     }
 
     /**
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java b/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java
index ce682f1..eb25acf 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java
@@ -29,7 +29,10 @@
 import android.app.blob.BlobHandle;
 import android.app.blob.BlobStoreManager;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
 import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.os.AsyncTask;
@@ -231,6 +234,15 @@
                 .adoptShellPermissionIdentity(Manifest.permission.WRITE_SECURE_SETTINGS);
     }
 
+    /**
+     * Returns the activity info corresponding to the system app for the provided category
+     */
+    public static ActivityInfo resolveSystemAppInfo(String category) {
+        return getInstrumentation().getTargetContext().getPackageManager().resolveActivity(
+                new Intent(Intent.ACTION_MAIN).addCategory(category),
+                PackageManager.MATCH_SYSTEM_ONLY).activityInfo;
+    }
+
     /** Interface to indicate a runnable which can throw any exception. */
     public interface UncheckedRunnable {
         /** Method to run the task */
diff --git a/tests/src/com/android/launcher3/LauncherIntentTest.java b/tests/src/com/android/launcher3/LauncherIntentTest.java
index 3e16713..a3d9614 100644
--- a/tests/src/com/android/launcher3/LauncherIntentTest.java
+++ b/tests/src/com/android/launcher3/LauncherIntentTest.java
@@ -27,17 +27,23 @@
 
 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
 import com.android.launcher3.allapps.SearchRecyclerView;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.util.BaseLauncherActivityTest;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @LargeTest
 @RunWith(AndroidJUnit4.class)
-public class LauncherIntentTest extends AbstractLauncherUiTest<Launcher> {
+public class LauncherIntentTest extends BaseLauncherActivityTest<Launcher> {
 
     public final Intent allAppsIntent = new Intent(Intent.ACTION_ALL_APPS);
 
+    @Before
+    public void setUp() {
+        loadLauncherSync();
+    }
+
     @Test
     public void testAllAppsIntent() {
         // Try executing ALL_APPS intent
@@ -45,7 +51,6 @@
         // A-Z view with Main adapter should be loaded
         assertOnMainAdapterAToZView();
 
-
         // Try Moving to search view now
         moveToSearchView();
         // Try executing ALL_APPS intent
@@ -63,12 +68,14 @@
         // Search view should be in focus
         waitForLauncherCondition("Search view is not in focus.",
                 launcher -> launcher.getAppsView().getSearchView().hasFocus());
-        mLauncher.pressAndHoldKeyCode(KeyEvent.KEYCODE_C, 0);
+
+        injectKeyEvent(KeyEvent.KEYCODE_C, true);
         // Upon key press, search recycler view should be loaded
         waitForLauncherCondition("Search view not active.",
                 launcher -> launcher.getAppsView().getActiveRecyclerView()
                         instanceof SearchRecyclerView);
-        mLauncher.unpressKeyCode(KeyEvent.KEYCODE_C, 0);
+
+        injectKeyEvent(KeyEvent.KEYCODE_C, false);
     }
 
     // Checks if main adapter view is selected, search bar is out of focus and scroller is at start.
diff --git a/tests/src/com/android/launcher3/allapps/KeyboardFocusTest.java b/tests/src/com/android/launcher3/allapps/KeyboardFocusTest.java
new file mode 100644
index 0000000..44df5b8
--- /dev/null
+++ b/tests/src/com/android/launcher3/allapps/KeyboardFocusTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.view.KeyEvent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.util.BaseLauncherActivityTest;
+import com.android.launcher3.views.ActivityContext;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class KeyboardFocusTest extends BaseLauncherActivityTest<Launcher> {
+
+    @Test
+    public void testAllAppsFocusApp() {
+        loadLauncherSync();
+        goToState(LauncherState.ALL_APPS);
+        freezeAllApps();
+
+        injectKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, true);
+        injectKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, false);
+        waitForLauncherCondition("No focused child", launcher ->
+                launcher.getAppsView().getActiveRecyclerView().getApps().getFocusedChild()
+                        != null);
+    }
+
+    @Test
+    public void testAllAppsExitSearchAndFocusApp() {
+        loadLauncherSync();
+        goToState(LauncherState.ALL_APPS);
+        freezeAllApps();
+
+        executeOnLauncher(launcher -> launcher.getAppsView().getSearchView().requestFocus());
+        waitForLauncherCondition("Search view does not have focus.",
+                launcher -> launcher.getAppsView().getSearchView().hasFocus());
+
+        injectKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, true);
+        injectKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, false);
+        waitForLauncherCondition("No focused child", launcher ->
+                launcher.getAppsView().getActiveRecyclerView().getApps().getFocusedChild()
+                        != null);
+    }
+
+    @Test
+    public void testAllAppsExitSearchAndFocusSearchResults() {
+        loadLauncherSync();
+        goToState(LauncherState.ALL_APPS);
+        freezeAllApps();
+
+        executeOnLauncher(launcher -> launcher.getAppsView().getSearchView().requestFocus());
+        waitForLauncherCondition("Search view does not have focus.",
+                launcher -> launcher.getAppsView().getSearchView().hasFocus());
+
+        injectKeyEvent(KeyEvent.KEYCODE_C, true);
+        waitForLauncherCondition("Search view not active.",
+                launcher -> launcher.getAppsView().getActiveRecyclerView()
+                        instanceof SearchRecyclerView);
+        injectKeyEvent(KeyEvent.KEYCODE_C, false);
+
+        executeOnLauncher(launcher -> launcher.getAppsView().getSearchUiManager().getEditText()
+                .hideKeyboard(/* clearFocus= */ false));
+        waitForLauncherCondition("Keyboard still visible.",
+                ActivityContext::isSoftwareKeyboardHidden);
+
+        injectKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, true);
+        injectKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, false);
+        waitForLauncherCondition("No focused child", launcher ->
+                launcher.getAppsView().getActiveRecyclerView().getApps().getFocusedChild()
+                        != null);
+    }
+}
diff --git a/tests/src/com/android/launcher3/allapps/TaplKeyboardFocusTest.java b/tests/src/com/android/launcher3/allapps/TaplKeyboardFocusTest.java
deleted file mode 100644
index 4e627a9..0000000
--- a/tests/src/com/android/launcher3/allapps/TaplKeyboardFocusTest.java
+++ /dev/null
@@ -1,108 +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.allapps;
-
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import android.view.KeyEvent;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.tapl.HomeAllApps;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
-import com.android.launcher3.util.rule.TestStabilityRule;
-import com.android.launcher3.views.ActivityContext;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class TaplKeyboardFocusTest extends AbstractLauncherUiTest<Launcher> {
-
-    @Test
-    public void testAllAppsFocusApp() {
-        final HomeAllApps allApps = mLauncher.goHome().switchToAllApps();
-        assertTrue("Launcher internal state is not All Apps",
-                isInState(() -> LauncherState.ALL_APPS));
-        allApps.freeze();
-        try {
-            mLauncher.pressAndHoldKeyCode(KeyEvent.KEYCODE_DPAD_DOWN, 0);
-            executeOnLauncher(launcher -> assertNotNull("No focused child.",
-                    launcher.getAppsView().getActiveRecyclerView().getApps().getFocusedChild()));
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-
-    @Test
-    public void testAllAppsExitSearchAndFocusApp() {
-        final HomeAllApps allApps = mLauncher.goHome().switchToAllApps();
-        assertTrue("Launcher internal state is not All Apps",
-                isInState(() -> LauncherState.ALL_APPS));
-        allApps.freeze();
-        try {
-            executeOnLauncher(launcher -> launcher.getAppsView().getSearchView().requestFocus());
-            waitForLauncherCondition("Search view does not have focus.",
-                    launcher -> launcher.getAppsView().getSearchView().hasFocus());
-
-            mLauncher.pressAndHoldKeyCode(KeyEvent.KEYCODE_DPAD_DOWN, 0);
-            executeOnLauncher(launcher -> assertNotNull("No focused child.",
-                    launcher.getAppsView().getActiveRecyclerView().getApps().getFocusedChild()));
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-
-    @Test
-    public void testAllAppsExitSearchAndFocusSearchResults() {
-        final HomeAllApps allApps = mLauncher.goHome().switchToAllApps();
-        assertTrue("Launcher internal state is not All Apps",
-                isInState(() -> LauncherState.ALL_APPS));
-        allApps.freeze();
-        try {
-            executeOnLauncher(launcher -> launcher.getAppsView().getSearchView().requestFocus());
-            waitForLauncherCondition("Search view does not have focus.",
-                    launcher -> launcher.getAppsView().getSearchView().hasFocus());
-
-            mLauncher.pressAndHoldKeyCode(KeyEvent.KEYCODE_C, 0);
-            waitForLauncherCondition("Search view not active.",
-                    launcher -> launcher.getAppsView().getActiveRecyclerView()
-                            instanceof SearchRecyclerView);
-            mLauncher.unpressKeyCode(KeyEvent.KEYCODE_C, 0);
-
-            executeOnLauncher(launcher -> launcher.getAppsView().getSearchUiManager().getEditText()
-                    .hideKeyboard(/* clearFocus= */ false));
-            waitForLauncherCondition("Keyboard still visible.",
-                    ActivityContext::isSoftwareKeyboardHidden);
-
-            mLauncher.pressAndHoldKeyCode(KeyEvent.KEYCODE_DPAD_DOWN, 0);
-            mLauncher.unpressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN, 0);
-            waitForLauncherCondition("No focused child", launcher ->
-                    launcher.getAppsView().getActiveRecyclerView().getApps().getFocusedChild()
-                            != null);
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-}
diff --git a/tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java b/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
similarity index 73%
rename from tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java
rename to tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
index 1500538..34b292c 100644
--- a/tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java
+++ b/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
@@ -1,18 +1,19 @@
 /*
  * Copyright (C) 2019 The Android Open Source Project
  *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
  *
- * http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.compat;
 
 import static com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING;
@@ -27,32 +28,31 @@
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.text.TextUtils;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.tapl.AllApps;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.util.BaseLauncherActivityTest;
 import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
 import com.android.launcher3.util.TestUtil;
-import com.android.launcher3.util.rule.ViewCaptureRule;
 
 import org.junit.After;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.UUID;
 
-
 /**
  * Test to verify promise icon flow.
  */
 @LargeTest
 @RunWith(AndroidJUnit4.class)
-public class TaplPromiseIconUiTest extends AbstractLauncherUiTest<Launcher> {
+public class PromiseIconUiTest extends BaseLauncherActivityTest<Launcher> {
 
     @Rule
     public final CheckFlagsRule mCheckFlagsRule =
@@ -64,19 +64,17 @@
 
     private int mSessionId = -1;
 
-    @Override
+    @Before
     public void setUp() throws Exception {
-        super.setUp();
-        mDevice.pressHome();
-        waitForLauncherCondition("Launcher didn't start", launcher -> launcher != null);
-        waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
+        loadLauncherSync();
+        goToState(LauncherState.NORMAL);
         mSessionId = -1;
     }
 
     @After
     public void tearDown() throws IOException {
         if (mSessionId > -1) {
-            mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
+            targetContext().getPackageManager().getPackageInstaller().abandonSession(mSessionId);
         }
         TestUtil.uninstallDummyApp();
     }
@@ -90,7 +88,7 @@
         params.setAppLabel(label);
         params.setAppIcon(icon);
         params.setInstallReason(PackageManager.INSTALL_REASON_USER);
-        return mTargetContext.getPackageManager().getPackageInstaller().createSession(params);
+        return targetContext().getPackageManager().getPackageInstaller().createSession(params);
     }
 
     @Test
@@ -108,7 +106,7 @@
                 launcher.getWorkspace().getFirstMatch(findPromiseApp) != null);
 
         // Remove session
-        mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
+        targetContext().getPackageManager().getPackageInstaller().abandonSession(mSessionId);
         mSessionId = -1;
 
         // Verify promise icon is removed
@@ -117,7 +115,6 @@
     }
 
     @Test
-    @ViewCaptureRule.MayProduceNoFrames
     public void testPromiseIcon_notAddedFromIneligibleSession() throws Throwable {
         final String appLabel = "Test Promise App " + UUID.randomUUID().toString();
         final ItemOperator findPromiseApp = (info, view) ->
@@ -138,7 +135,8 @@
     @RequiresFlagsEnabled(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING)
     public void testPromiseIcon_addedArchivedApp() throws Throwable {
         installDummyAppAndWaitForUIUpdate();
-        assertThat(mDevice.executeShellCommand(String.format("pm archive %s", DUMMY_PACKAGE)))
+        assertThat(executeShellCommand(
+                String.format("pm archive %s", DUMMY_PACKAGE)))
                 .isEqualTo("Success\n");
 
         // Create and add test session
@@ -148,28 +146,19 @@
         // Verify promise icon is added to all apps view. The icon may not be added to the
         // workspace even if there might be no icon present for archived app. But icon will
         // always be in all apps view. In case an icon is not added, an exception would be thrown.
-        final AllApps allApps = mLauncher.getWorkspace().switchToAllApps();
+        goToState(LauncherState.ALL_APPS);
 
         // Wait for the promise icon to be added.
         waitForLauncherCondition(
                 DUMMY_PACKAGE + " app was not found on all apps after being archived",
-                launcher -> {
-                    try {
-                        allApps.getAppIcon(DUMMY_LABEL);
-                    } catch (Throwable t) {
-                        return false;
-                    }
-                    return true;
-                });
-
-        // Remove session
-        mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
-        mSessionId = -1;
+                launcher -> Arrays.stream(launcher.getAppsView().getAppsStore().getApps())
+                        .filter(info -> DUMMY_LABEL.equals(info.title.toString()))
+                        .findAny()
+                        .isPresent());
     }
 
     private void installDummyAppAndWaitForUIUpdate() throws IOException {
         TestUtil.installDummyApp();
-        mLauncher.waitForModelQueueCleared();
-        mLauncher.waitForLauncherInitialized();
+        loadLauncherSync();
     }
 }
diff --git a/tests/src/com/android/launcher3/model/GridMigrationTest.kt b/tests/src/com/android/launcher3/model/GridMigrationTest.kt
index 666ec16..379e98d 100644
--- a/tests/src/com/android/launcher3/model/GridMigrationTest.kt
+++ b/tests/src/com/android/launcher3/model/GridMigrationTest.kt
@@ -101,6 +101,7 @@
                 dst.gridState,
                 dst.dbHelper,
                 src.dbHelper.readableDatabase,
+                false,
             )
         } else {
             GridSizeMigrationDBController.migrateGridIfNeeded(
@@ -109,6 +110,7 @@
                 dst.gridState,
                 dst.dbHelper,
                 src.dbHelper.readableDatabase,
+                false,
             )
         }
     }
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 1fbdceb..a273648 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -21,6 +21,7 @@
 
 import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.TestUtil.resolveSystemAppInfo;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -31,7 +32,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.ActivityInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.graphics.Point;
@@ -627,13 +627,6 @@
                 launcherInstrumentation);
     }
 
-    public static ActivityInfo resolveSystemAppInfo(String category) {
-        return getInstrumentation().getContext().getPackageManager().resolveActivity(
-                new Intent(Intent.ACTION_MAIN).addCategory(category),
-                PackageManager.MATCH_SYSTEM_ONLY).
-                activityInfo;
-    }
-
 
     public static String resolveSystemApp(String category) {
         return resolveSystemAppInfo(category).packageName;
diff --git a/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt b/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt
new file mode 100644
index 0000000..bacce40
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.util
+
+import android.content.Context
+import android.content.Intent
+import android.os.SystemClock
+import android.view.InputDevice
+import android.view.KeyCharacterMap
+import android.view.KeyEvent
+import android.view.MotionEvent
+import androidx.lifecycle.Lifecycle.State.RESUMED
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ActivityScenario.ActivityAction
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.Launcher
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherState
+import com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST
+import com.android.launcher3.tapl.TestHelpers
+import com.android.launcher3.util.ModelTestExtensions.loadModelSync
+import com.android.launcher3.util.Wait.atMost
+import java.util.function.Function
+import java.util.function.Supplier
+import org.junit.After
+
+/**
+ * Base class for tests which use Launcher activity with some utility methods.
+ *
+ * This should instead be a rule, but is kept as a base class for easier migration from TAPL
+ */
+open class BaseLauncherActivityTest<LAUNCHER_TYPE : Launcher> {
+
+    private var currentScenario: ActivityScenario<LAUNCHER_TYPE>? = null
+
+    val scenario: ActivityScenario<LAUNCHER_TYPE>
+        get() =
+            currentScenario
+                ?: ActivityScenario.launch<LAUNCHER_TYPE>(
+                        TestHelpers.getHomeIntentInPackage(targetContext()),
+                        null,
+                    )
+                    .also { currentScenario = it }
+
+    @After
+    fun closeCurrentActivity() {
+        currentScenario?.close()
+        currentScenario = null
+    }
+
+    protected fun loadLauncherSync() {
+        LauncherAppState.getInstance(targetContext()).model.loadModelSync()
+        scenario.moveToState(RESUMED)
+    }
+
+    protected fun targetContext(): Context = getInstrumentation().targetContext
+
+    protected fun goToState(state: LauncherState) = executeOnLauncher {
+        it.stateManager.goToState(state, 0)
+    }
+
+    protected fun executeOnLauncher(f: ActivityAction<LAUNCHER_TYPE>) = scenario.onActivity(f)
+
+    protected fun <T> getFromLauncher(f: Function<in LAUNCHER_TYPE, out T?>): T? {
+        var result: T? = null
+        executeOnLauncher { result = f.apply(it) }
+        return result
+    }
+
+    protected fun isInState(state: Supplier<LauncherState>): Boolean =
+        getFromLauncher { it.stateManager.state == state.get() }!!
+
+    protected fun waitForState(message: String, state: Supplier<LauncherState>) =
+        waitForLauncherCondition(message) { it.stateManager.currentStableState === state.get() }
+
+    protected fun waitForLauncherCondition(
+        message: String,
+        condition: Function<LAUNCHER_TYPE, Boolean>,
+    ) = atMost(message, { getFromLauncher(condition)!! })
+
+    protected fun <T> getOnceNotNull(message: String, f: Function<LAUNCHER_TYPE, T?>): T? {
+        var output: T? = null
+        atMost(
+            message,
+            {
+                val fromLauncher = getFromLauncher<T>(f)
+                output = fromLauncher
+                fromLauncher != null
+            },
+        )
+        return output
+    }
+
+    protected fun getAllAppsScroll(launcher: LAUNCHER_TYPE) =
+        launcher.appsView.activeRecyclerView.computeVerticalScrollOffset()
+
+    @JvmOverloads
+    protected fun injectKeyEvent(keyCode: Int, actionDown: Boolean, metaState: Int = 0) {
+        val eventTime = SystemClock.uptimeMillis()
+        val event =
+            KeyEvent.obtain(
+                eventTime,
+                eventTime,
+                if (actionDown) KeyEvent.ACTION_DOWN else MotionEvent.ACTION_UP,
+                keyCode,
+                /* repeat= */ 0,
+                metaState,
+                KeyCharacterMap.VIRTUAL_KEYBOARD,
+                /* scancode= */ 0,
+                /* flags= */ 0,
+                InputDevice.SOURCE_KEYBOARD,
+                /* characters =*/ null,
+            )
+        executeOnLauncher { it.dispatchKeyEvent(event) }
+        event.recycle()
+    }
+
+    fun startAppFast(packageName: String) {
+        val intent = targetContext().packageManager.getLaunchIntentForPackage(packageName)!!
+        intent.addCategory(Intent.CATEGORY_LAUNCHER)
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+        targetContext().startActivity(intent)
+        UiDevice.getInstance(getInstrumentation()).waitForIdle()
+    }
+
+    fun freezeAllApps() = executeOnLauncher {
+        it.appsView.appsStore.enableDeferUpdates(DEFER_UPDATES_TEST)
+    }
+
+    fun executeShellCommand(cmd: String) =
+        UiDevice.getInstance(getInstrumentation()).executeShellCommand(cmd)
+}