Merge "Add split instructions to stage split from keyboard shortcut" into main
diff --git a/aconfig/launcher_search.aconfig b/aconfig/launcher_search.aconfig
index b846604..4e16e7f 100644
--- a/aconfig/launcher_search.aconfig
+++ b/aconfig/launcher_search.aconfig
@@ -34,3 +34,10 @@
     description: "This flag disables accessibility drag for Private Space Apps."
     bug: "289223923"
 }
+
+flag {
+    name: "private_space_restrict_item_drag"
+    namespace: "launcher_search"
+    description: "This flag disables drag and drop for Private Space Items."
+    bug: "289223923"
+}
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 5caf004..f15d12b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -22,6 +22,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
@@ -47,7 +48,8 @@
 public final class KeyboardQuickSwitchController implements
         TaskbarControllers.LoggableTaskbarController {
 
-    static final int MAX_TASKS = 6;
+    @VisibleForTesting
+    public static final int MAX_TASKS = 6;
 
     @NonNull private final ControllerCallbacks mControllerCallbacks = new ControllerCallbacks();
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 5819bb3..6dc7db7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -71,6 +71,7 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Executors.SimpleThreadFactory;
 import com.android.quickstep.SystemUiProxy;
+import com.android.wm.shell.Flags;
 import com.android.wm.shell.bubbles.IBubblesListener;
 import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
 import com.android.wm.shell.common.bubbles.BubbleInfo;
@@ -101,8 +102,8 @@
      *
      * @see #onTaskbarRecreated()
      */
-    private static boolean sBubbleBarEnabled =
-            SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
+    private static boolean sBubbleBarEnabled = Flags.enableBubbleBar()
+            || SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
 
     /** Whether showing bubbles in the launcher bubble bar is enabled. */
     public static boolean isBubbleBarEnabled() {
@@ -111,8 +112,10 @@
 
     /** Re-reads the value of the flag from SystemProperties when taskbar is recreated. */
     public static void onTaskbarRecreated() {
-        sBubbleBarEnabled = SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
+        sBubbleBarEnabled = Flags.enableBubbleBar()
+                || SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
     }
+
     private static final int MASK_HIDE_BUBBLE_BAR = SYSUI_STATE_BOUNCER_SHOWING
             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
index 22f24f1..039c0a0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
@@ -28,10 +28,8 @@
 import android.util.Pair;
 import android.view.View;
 import android.widget.RemoteViews;
-import android.widget.Toast;
 import android.window.SplashScreen;
 
-import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.ItemInfo;
@@ -63,8 +61,7 @@
             // Log metric
             StatsLogManager.StatsLogger logger = mLauncher.getStatsLogManager().logger();
             logger.log(LAUNCHER_SPLIT_WIDGET_ATTEMPT);
-            Toast.makeText(hostView.getContext(), R.string.split_widgets_not_supported,
-                    Toast.LENGTH_SHORT).show();
+            mLauncher.handleIncorrectSplitTargetSelection();
             return true;
         }
         Pair<Intent, ActivityOptions> options = remoteResponse.getLaunchOptions(view);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 32d10b0..ac0e53e 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -36,6 +36,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
+import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
 import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
 import static com.android.launcher3.popup.QuickstepSystemShortcut.getSplitSelectShortcutByPosition;
@@ -676,7 +677,7 @@
                     splitSelectSource.alreadyRunningTaskId = taskWasFound
                             ? foundTask.key.id
                             : INVALID_TASK_ID;
-                    if (FeatureFlags.enableSplitContextually()) {
+                    if (enableSplitContextually()) {
                         startSplitToHome(splitSelectSource);
                     } else {
                         recentsView.initiateSplitSelect(splitSelectSource);
@@ -761,7 +762,7 @@
 
         super.onPause();
 
-        if (FeatureFlags.enableSplitContextually()) {
+        if (enableSplitContextually()) {
             // If Launcher pauses before both split apps are selected, exit split screen.
             if (!mSplitSelectStateController.isBothSplitAppsConfirmed() &&
                     !mSplitSelectStateController.isLaunchingFirstAppFullscreen()) {
@@ -1352,6 +1353,15 @@
         return (mTaskbarUIController != null && mTaskbarUIController.hasBubbles());
     }
 
+    @Override
+    public boolean handleIncorrectSplitTargetSelection() {
+        if (enableSplitContextually() && !mSplitSelectStateController.isSplitSelectActive()) {
+            return false;
+        }
+        mSplitSelectStateController.getSplitInstructionsView().goBoing();
+        return true;
+    }
+
     private static final class LauncherTaskViewController extends
             TaskViewTouchController<Launcher> {
 
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 60d0e2b..6698600 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -22,7 +22,9 @@
 import static android.widget.Toast.LENGTH_SHORT;
 
 import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
+import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.app.animation.Interpolators.DECELERATE;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.app.animation.Interpolators.OVERSHOOT_1_2;
 import static com.android.launcher3.BaseActivity.EVENT_DESTROYED;
 import static com.android.launcher3.BaseActivity.EVENT_STARTED;
@@ -134,6 +136,7 @@
 import com.android.quickstep.util.SurfaceTransactionApplier;
 import com.android.quickstep.util.SwipePipToHomeAnimator;
 import com.android.quickstep.util.TaskViewSimulator;
+import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
@@ -167,6 +170,9 @@
 
     private static final ArrayList<String> STATE_NAMES = new ArrayList<>();
 
+    // Fraction of the scroll and transform animation in which the current task fades out
+    private static final float KQS_TASK_FADE_ANIMATION_FRACTION = 0.4f;
+
     protected final BaseActivityInterface<S, T> mActivityInterface;
     protected final InputConsumerProxy mInputConsumerProxy;
     protected final ActivityInitListener mActivityInitListener;
@@ -900,7 +906,10 @@
             return;
         }
         mLauncherTransitionController.setProgress(
-                Math.max(mCurrentShift.value, getScaleProgressDueToScroll()), mDragLengthFactor);
+                // Immediately finish the grid transition
+                isKeyboardTaskFocusPending()
+                        ? 1f : Math.max(mCurrentShift.value, getScaleProgressDueToScroll()),
+                mDragLengthFactor);
     }
 
     /**
@@ -1349,7 +1358,9 @@
         }
         Interpolator interpolator;
         S state = mActivityInterface.stateFromGestureEndTarget(endTarget);
-        if (state.displayOverviewTasksAsGrid(mDp)) {
+        if (isKeyboardTaskFocusPending()) {
+            interpolator = EMPHASIZED;
+        } else if (state.displayOverviewTasksAsGrid(mDp)) {
             interpolator = ACCELERATE_DECELERATE;
         } else if (endTarget == RECENTS) {
             interpolator = OVERSHOOT_1_2;
@@ -1653,7 +1664,8 @@
             animatorSet.play(windowAnim);
             if (mRecentsView != null) {
                 mRecentsView.onPrepareGestureEndAnimation(
-                        animatorSet, mGestureState.getEndTarget(),
+                        mGestureState.isHandlingAtomicEvent() ? null : animatorSet,
+                        mGestureState.getEndTarget(),
                         getRemoteTaskViewSimulators());
             }
             animatorSet.setDuration(duration).setInterpolator(interpolator);
@@ -2225,6 +2237,14 @@
         }
     }
 
+    private boolean shouldLinkRecentsViewScroll() {
+        return mRecentsViewScrollLinked && !isKeyboardTaskFocusPending();
+    }
+
+    private boolean isKeyboardTaskFocusPending() {
+        return mRecentsView != null && mRecentsView.isKeyboardTaskFocusPending();
+    }
+
     private void onRecentsViewScroll() {
         if (moveWindowWithRecentsScroll()) {
             onCurrentShiftUpdated();
@@ -2457,6 +2477,44 @@
         mActivityInitListener.register();
     }
 
+    private boolean shouldFadeOutTargetsForKeyboardQuickSwitch(
+            TransformParams transformParams,
+            TaskViewSimulator taskViewSimulator,
+            float progress) {
+        RemoteAnimationTargets targets = transformParams.getTargetSet();
+        boolean fadeAppTargets = isKeyboardTaskFocusPending()
+                && targets != null
+                && targets.apps != null
+                && targets.apps.length > 0;
+        float fadeProgress = Utilities.mapBoundToRange(
+                progress,
+                /* lowerBound= */ 0f,
+                /* upperBound= */ KQS_TASK_FADE_ANIMATION_FRACTION,
+                /* toMin= */ 0f,
+                /* toMax= */ 1f,
+                LINEAR);
+        if (!fadeAppTargets || Float.compare(fadeProgress, 1f) == 0) {
+            return false;
+        }
+        SurfaceTransaction surfaceTransaction =
+                transformParams.createSurfaceParams(taskViewSimulator);
+        SurfaceControl.Transaction transaction = surfaceTransaction.getTransaction();
+
+        for (RemoteAnimationTarget app : targets.apps) {
+            transaction.setAlpha(app.leash, 1f - fadeProgress);
+            transaction.setPosition(app.leash,
+                    /* x= */ app.startBounds.left
+                            + (mActivity.getDeviceProfile().overviewPageSpacing
+                            * (mRecentsView.isRtl() ? fadeProgress : -fadeProgress)),
+                    /* y= */ 0f);
+            transaction.setScale(app.leash, 1f, 1f);
+            taskViewSimulator.taskPrimaryTranslation.value =
+                    mRecentsView.getScrollOffsetForKeyboardTaskFocus();
+            taskViewSimulator.apply(transformParams, surfaceTransaction);
+        }
+        return true;
+    }
+
     /**
      * Applies the transform on the recents animation
      */
@@ -2466,7 +2524,7 @@
         //    swipe-to-icon animation is handled by RectFSpringAnim anim
         boolean notSwipingToHome = mRecentsAnimationTargets != null
                 && mGestureState.getEndTarget() != HOME;
-        boolean setRecentsScroll = mRecentsViewScrollLinked && mRecentsView != null;
+        boolean setRecentsScroll = shouldLinkRecentsViewScroll() && mRecentsView != null;
         float progress = Math.max(mCurrentShift.value, getScaleProgressDueToScroll());
         int scrollOffset = setRecentsScroll ? mRecentsView.getScrollOffset() : 0;
         if (!mStartMovingTasks && (progress > 0 || scrollOffset != 0)) {
@@ -2485,7 +2543,12 @@
                 if (setRecentsScroll) {
                     taskViewSimulator.setScroll(scrollOffset);
                 }
-                taskViewSimulator.apply(remoteHandle.getTransformParams());
+                TransformParams transformParams = remoteHandle.getTransformParams();
+                if (shouldFadeOutTargetsForKeyboardQuickSwitch(
+                        transformParams, taskViewSimulator, progress)) {
+                    continue;
+                }
+                taskViewSimulator.apply(transformParams);
             }
         }
     }
@@ -2493,7 +2556,7 @@
     // Scaling of RecentsView during quick switch based on amount of recents scroll
     private float getScaleProgressDueToScroll() {
         if (mActivity == null || !mActivity.getDeviceProfile().isTablet || mRecentsView == null
-                || !mRecentsViewScrollLinked) {
+                || !shouldLinkRecentsViewScroll()) {
             return 0;
         }
 
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index ca82a2d..b89d20c 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -178,7 +178,7 @@
     public abstract <T extends RecentsView> T getVisibleRecentsView();
 
     @UiThread
-    public abstract boolean switchToRecentsIfVisible(Runnable onCompleteCallback);
+    public abstract boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener);
 
     public abstract Rect getOverviewWindowBounds(
             Rect homeBounds, RemoteAnimationTarget target);
@@ -520,7 +520,7 @@
             // Since we are changing the start position of the UI, reapply the state, at the end
             controller.setEndAction(() -> mActivity.getStateManager().goToState(
                     controller.getInterpolatedProgress() > 0.5 ? mTargetState : mBackgroundState,
-                    false));
+                    /* animated= */ false));
 
             RecentsView recentsView = mActivity.getOverviewPanel();
             AnimatorControllerWithResistance controllerWithResistance =
diff --git a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
index 693d6ae..27e8726 100644
--- a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -122,7 +122,7 @@
     }
 
     @Override
-    public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
+    public boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener) {
         return false;
     }
 
diff --git a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
index e77a450..b42eb06 100644
--- a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -371,8 +371,8 @@
                     if (mSpringAnim != null) {
                         mSpringAnim.onTargetPositionChanged();
                     }
-                    mOnFinishCallback = data.getParcelable(EXTRA_ON_FINISH_CALLBACK);
                 }
+                mOnFinishCallback = data.getParcelable(EXTRA_ON_FINISH_CALLBACK);
                 maybeSendEndMessage();
             } catch (Exception e) {
                 // Ignore
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 70b9183..97c48e6 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.LauncherState.FLOATING_SEARCH_BAR;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 
@@ -212,7 +211,7 @@
     }
 
     @Override
-    public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
+    public boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener) {
         Launcher launcher = getVisibleLauncher();
         if (launcher == null) {
             return false;
@@ -227,7 +226,7 @@
         closeOverlay();
         launcher.getStateManager().goToState(OVERVIEW,
                 launcher.getStateManager().shouldAnimateStateChange(),
-                onCompleteCallback == null ? null : forEndCallback(onCompleteCallback));
+                animatorListener);
         return true;
     }
 
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 315316d..e448a14 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -15,9 +15,12 @@
  */
 package com.android.quickstep;
 
+import static com.android.launcher3.PagedView.INVALID_PAGE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.content.Intent;
 import android.graphics.PointF;
 import android.os.SystemClock;
@@ -25,7 +28,6 @@
 import android.view.View;
 
 import androidx.annotation.BinderThread;
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
@@ -75,7 +77,7 @@
      * do not lose the focus across multiple calls of
      * {@link OverviewCommandHelper#executeCommand(CommandInfo)} for the same command
      */
-    private int mTaskFocusIndexOverride = -1;
+    private int mKeyboardTaskFocusIndex = -1;
 
     /**
      * Whether we should incoming toggle commands while a previous toggle command is still ongoing.
@@ -195,9 +197,11 @@
         }
         BaseActivityInterface<?, T> activityInterface =
                 mOverviewComponentObserver.getActivityInterface();
-        RecentsView recents = activityInterface.getVisibleRecentsView();
-        if (recents == null) {
+        RecentsView visibleRecentsView = activityInterface.getVisibleRecentsView();
+        RecentsView createdRecentsView;
+        if (visibleRecentsView == null) {
             T activity = activityInterface.getCreatedActivity();
+            createdRecentsView = activity == null ? null : activity.getOverviewPanel();
             DeviceProfile dp = activity == null ? null : activity.getDeviceProfile();
             TaskbarUIController uiController = activityInterface.getTaskbarController();
             boolean allowQuickSwitch = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
@@ -209,8 +213,8 @@
                 if (!allowQuickSwitch) {
                     return true;
                 }
-                mTaskFocusIndexOverride = uiController.launchFocusedTask();
-                if (mTaskFocusIndexOverride == -1) {
+                mKeyboardTaskFocusIndex = uiController.launchFocusedTask();
+                if (mKeyboardTaskFocusIndex == -1) {
                     return true;
                 }
             }
@@ -224,34 +228,47 @@
                 return true;
             }
         } else {
+            createdRecentsView = visibleRecentsView;
             switch (cmd.type) {
                 case TYPE_SHOW:
                     // already visible
                     return true;
                 case TYPE_HIDE: {
-                    mTaskFocusIndexOverride = -1;
-                    int currentPage = recents.getNextPage();
-                    TaskView tv = (currentPage >= 0 && currentPage < recents.getTaskViewCount())
-                            ? (TaskView) recents.getPageAt(currentPage)
+                    mKeyboardTaskFocusIndex = INVALID_PAGE;
+                    int currentPage = visibleRecentsView.getNextPage();
+                    TaskView tv = (currentPage >= 0
+                            && currentPage < visibleRecentsView.getTaskViewCount())
+                            ? (TaskView) visibleRecentsView.getPageAt(currentPage)
                             : null;
-                    return launchTask(recents, tv, cmd);
+                    return launchTask(visibleRecentsView, tv, cmd);
                 }
                 case TYPE_TOGGLE:
-                    return launchTask(recents, getNextTask(recents), cmd);
+                    return launchTask(visibleRecentsView, getNextTask(visibleRecentsView), cmd);
                 case TYPE_HOME:
-                    recents.startHome();
+                    visibleRecentsView.startHome();
                     return true;
             }
         }
 
-        final Runnable completeCallback = () -> {
-            RecentsView rv = activityInterface.getVisibleRecentsView();
-            if (rv != null && (cmd.type == TYPE_KEYBOARD_INPUT || cmd.type == TYPE_HIDE)) {
-                updateRecentsViewFocus(rv);
+        if (createdRecentsView != null) {
+            createdRecentsView.setKeyboardTaskFocusIndex(mKeyboardTaskFocusIndex);
+        }
+        // Handle recents view focus when launching from home
+        Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() {
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                super.onAnimationStart(animation);
+                updateRecentsViewFocus(cmd);
             }
-            scheduleNextTask(cmd);
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                onRecentsViewFocusUpdated(cmd);
+                scheduleNextTask(cmd);
+            }
         };
-        if (activityInterface.switchToRecentsIfVisible(completeCallback)) {
+        if (activityInterface.switchToRecentsIfVisible(animatorListener)) {
             // If successfully switched, wait until animation finishes
             return false;
         }
@@ -276,6 +293,7 @@
             @Override
             public void onRecentsAnimationStart(RecentsAnimationController controller,
                     RecentsAnimationTargets targets) {
+                updateRecentsViewFocus(cmd);
                 activityInterface.runOnInitBackgroundStateUI(() ->
                         interactionHandler.onGestureEnded(0, new PointF()));
                 cmd.removeListener(this);
@@ -290,14 +308,12 @@
                 if (createdActivity == null) {
                     return;
                 }
-                RecentsView createdRecents = createdActivity.getOverviewPanel();
-                if (createdRecents != null) {
-                    createdRecents.onRecentsAnimationComplete();
+                if (createdRecentsView != null) {
+                    createdRecentsView.onRecentsAnimationComplete();
                 }
             }
         };
 
-        RecentsView<?, ?> visibleRecentsView = activityInterface.getVisibleRecentsView();
         if (visibleRecentsView != null) {
             visibleRecentsView.moveRunningTaskToFront();
         }
@@ -317,7 +333,6 @@
             interactionHandler.onGestureStarted(false /*isLikelyToStartNewTask*/);
             cmd.mActiveCallbacks.addListener(recentAnimListener);
         }
-
         Trace.beginAsyncSection(TRANSITION_NAME, 0);
         return false;
     }
@@ -325,47 +340,58 @@
     private void onTransitionComplete(CommandInfo cmd, AbsSwipeUpHandler handler) {
         cmd.removeListener(handler);
         Trace.endAsyncSection(TRANSITION_NAME, 0);
-
-        RecentsView rv =
-                mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
-        if (rv != null && (cmd.type == TYPE_KEYBOARD_INPUT || cmd.type == TYPE_HIDE)) {
-            updateRecentsViewFocus(rv);
-        }
+        onRecentsViewFocusUpdated(cmd);
         scheduleNextTask(cmd);
     }
 
-    private void updateRecentsViewFocus(@NonNull RecentsView rv) {
+    private void updateRecentsViewFocus(CommandInfo cmd) {
+        RecentsView recentsView =
+                mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
+        if (recentsView == null || (cmd.type != TYPE_KEYBOARD_INPUT && cmd.type != TYPE_HIDE)) {
+            return;
+        }
         // When the overview is launched via alt tab (cmd type is TYPE_KEYBOARD_INPUT),
         // the touch mode somehow is not change to false by the Android framework.
         // The subsequent tab to go through tasks in overview can only be dispatched to
         // focuses views, while focus can only be requested in
         // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note,
         // here we launch overview with live tile.
-        rv.getViewRootImpl().touchModeChanged(false);
+        recentsView.getViewRootImpl().touchModeChanged(false);
         // Ensure that recents view has focus so that it receives the followup key inputs
-        TaskView taskView = rv.getTaskViewAt(mTaskFocusIndexOverride);
-        if (taskView != null) {
-            requestFocus(taskView);
+        if (requestFocus(recentsView.getTaskViewAt(mKeyboardTaskFocusIndex))) {
             return;
         }
-        taskView = rv.getNextTaskView();
-        if (taskView != null) {
-            requestFocus(taskView);
+        if (requestFocus(recentsView.getNextTaskView())) {
             return;
         }
-        taskView = rv.getTaskViewAt(0);
-        if (taskView != null) {
-            requestFocus(taskView);
+        if (requestFocus(recentsView.getTaskViewAt(0))) {
             return;
         }
-        requestFocus(rv);
+        requestFocus(recentsView);
     }
 
-    private void requestFocus(@NonNull View view) {
-        view.post(() -> {
-            view.requestFocus();
-            view.requestAccessibilityFocus();
+    private void onRecentsViewFocusUpdated(CommandInfo cmd) {
+        RecentsView recentsView =
+                mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
+        if (recentsView == null
+                || cmd.type != TYPE_HIDE
+                || mKeyboardTaskFocusIndex == INVALID_PAGE) {
+            return;
+        }
+        recentsView.setKeyboardTaskFocusIndex(INVALID_PAGE);
+        recentsView.setCurrentPage(mKeyboardTaskFocusIndex);
+        mKeyboardTaskFocusIndex = INVALID_PAGE;
+    }
+
+    private boolean requestFocus(@Nullable View taskView) {
+        if (taskView == null) {
+            return false;
+        }
+        taskView.post(() -> {
+            taskView.requestFocus();
+            taskView.requestAccessibilityFocus();
         });
+        return true;
     }
 
     public void dump(PrintWriter pw) {
@@ -374,7 +400,7 @@
         if (!mPendingCommands.isEmpty()) {
             pw.println("    pendingCommandType=" + mPendingCommands.get(0).type);
         }
-        pw.println("  mTaskFocusIndexOverride=" + mTaskFocusIndexOverride);
+        pw.println("  mKeyboardTaskFocusIndex=" + mKeyboardTaskFocusIndex);
         pw.println("  mWaitForToggleCommandComplete=" + mWaitForToggleCommandComplete);
     }
 
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 4f885af..a1a3145 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -66,6 +66,8 @@
     private Runnable mLiveTileCleanUpHandler;
     private Context mCtx;
 
+    private boolean mRecentsAnimationStartPending = false;
+
     private final TaskStackChangeListener mLiveTileRestartListener = new TaskStackChangeListener() {
         @Override
         public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
@@ -100,6 +102,10 @@
                 .startRecentsActivity(intent, 0, null, null, null));
     }
 
+    public boolean isRecentsAnimationStartPending() {
+        return mRecentsAnimationStartPending;
+    }
+
     /**
      * Starts a new recents animation for the activity with the given {@param intent}.
      */
@@ -109,6 +115,7 @@
         ActiveGestureLog.INSTANCE.addLog(
                 /* event= */ "startRecentsAnimation",
                 /* gestureEvent= */ START_RECENTS_ANIMATION);
+        mRecentsAnimationStartPending = true;
         // Notify if recents animation is still running
         if (mController != null) {
             String msg = "New recents animation started before old animation completed";
@@ -138,6 +145,7 @@
             @Override
             public void onRecentsAnimationStart(RecentsAnimationController controller,
                     RecentsAnimationTargets targets) {
+                mRecentsAnimationStartPending = false;
                 if (mCallbacks == null) {
                     // It's possible for the recents animation to have finished and be cleaned up
                     // by the time we process the start callback, and in that case, just we can skip
@@ -178,11 +186,13 @@
 
             @Override
             public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
+                mRecentsAnimationStartPending = false;
                 cleanUpRecentsAnimation(newCallbacks);
             }
 
             @Override
             public void onRecentsAnimationFinished(RecentsAnimationController controller) {
+                mRecentsAnimationStartPending = false;
                 cleanUpRecentsAnimation(newCallbacks);
             }
 
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 55a7985..5228420 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -24,6 +24,7 @@
 import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.launcher3.Flags.enableCursorHoverStates;
+import static com.android.launcher3.Flags.useActivityOverlay;
 import static com.android.launcher3.Launcher.INTENT_ACTION_ALL_APPS_TOGGLE;
 import static com.android.launcher3.LauncherPrefs.backedUpItem;
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
@@ -39,6 +40,7 @@
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENTS_ANIMATION_START_PENDING;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER;
@@ -697,7 +699,7 @@
     private void onInputEvent(InputEvent ev) {
         if (!(ev instanceof MotionEvent)) {
             ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
-                    .append("Cannot process input event, received unknown event ")
+                    .append("Cannot process input event: received unknown event ")
                     .append(ev.toString()));
             return;
         }
@@ -710,22 +712,32 @@
         if (!isUserUnlocked || (mDeviceState.isButtonNavMode()
                 && !isTrackpadMotionEvent(event))) {
             ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
-                    .append("Cannot process input event, ")
+                    .append("Cannot process input event: ")
                     .append(!isUserUnlocked
                             ? "user is locked"
                             : "using 3-button nav and event is not a trackpad event"));
             return;
         }
 
-        SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent");
-
         final int action = event.getActionMasked();
         // Note this will create a new consumer every mouse click, as after ACTION_UP from the click
         // an ACTION_HOVER_ENTER will fire as well.
         boolean isHoverActionWithoutConsumer = enableCursorHoverStates()
                 && isHoverActionWithoutConsumer(event);
+        if (mTaskAnimationManager.isRecentsAnimationStartPending()
+                && (action == ACTION_DOWN || isHoverActionWithoutConsumer)) {
+            ActiveGestureLog.INSTANCE.addLog(
+                    new CompoundString("TIS.onInputEvent: ")
+                            .append("Cannot process input event: a recents animation has been ")
+                            .append("requested, but hasn't started."),
+                    RECENTS_ANIMATION_START_PENDING);
+            return;
+        }
+
+        SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent");
+
         CompoundString reasonString = action == ACTION_DOWN
-                ? new CompoundString("onMotionEvent: ") : CompoundString.NO_OP;
+                ? new CompoundString("TIS.onMotionEvent: ") : CompoundString.NO_OP;
         if (action == ACTION_DOWN || isHoverActionWithoutConsumer) {
             mRotationTouchHelper.setOrientationTransformIfNeeded(event);
 
@@ -1148,6 +1160,14 @@
         boolean launcherResumedThroughShellTransition =
                 gestureState.getActivityInterface().isResumed()
                         && !previousGestureState.isRecentsAnimationRunning();
+        // If a task fragment within Launcher is resumed
+        boolean launcherChildActivityResumed = useActivityOverlay()
+                && runningTask != null
+                && runningTask.isHomeTask()
+                && mOverviewComponentObserver.isHomeAndOverviewSame()
+                && !launcherResumedThroughShellTransition
+                && !previousGestureState.isRecentsAnimationRunning();
+
         if (gestureState.getActivityInterface().isInLiveTileMode()) {
             return createOverviewInputConsumer(
                     previousGestureState,
@@ -1174,9 +1194,11 @@
                                             ? "launcher resumed through a shell transition"
                                             : "forceOverviewInputConsumer == true"))
                             .append(", trying to use overview input consumer"));
-        } else if (mDeviceState.isGestureBlockedTask(runningTask)) {
+        } else if (mDeviceState.isGestureBlockedTask(runningTask) || launcherChildActivityResumed) {
             return getDefaultInputConsumer(reasonString.append(SUBSTRING_PREFIX)
-                    .append("is gesture-blocked task, trying to use default input consumer"));
+                    .append(launcherChildActivityResumed
+                            ? "is launcher child-task, trying to use default input consumer"
+                            : "is gesture-blocked task, trying to use default input consumer"));
         } else {
             reasonString.append(SUBSTRING_PREFIX)
                     .append("using OtherActivityInputConsumer");
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
index 4359294..e3772bd 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
+++ b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
@@ -39,7 +39,7 @@
         SET_ON_PAGE_TRANSITION_END_CALLBACK, CANCEL_CURRENT_ANIMATION, CLEANUP_SCREENSHOT,
         SCROLLER_ANIMATION_ABORTED, TASK_APPEARED, EXPECTING_TASK_APPEARED,
         FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER, LAUNCHER_DESTROYED, RECENT_TASKS_MISSING,
-        INVALID_VELOCITY_ON_SWIPE_UP,
+        INVALID_VELOCITY_ON_SWIPE_UP, RECENTS_ANIMATION_START_PENDING,
 
         /**
          * These GestureEvents are specifically associated to state flags that get set in
@@ -273,6 +273,14 @@
                 case START_RECENTS_ANIMATION:
                     lastStartRecentAnimationEventEntryTime = eventEntry.getTime();
                     break;
+                case RECENTS_ANIMATION_START_PENDING:
+                    errorDetected |= printErrorIfTrue(
+                            true,
+                            prefix,
+                            /* errorMessage= */ "new gesture attempted while a requested recents"
+                                    + " animation is still pending.",
+                            writer);
+                    break;
                 case EXPECTING_TASK_APPEARED:
                 case MOTION_DOWN:
                 case SET_END_TARGET:
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 065a9c5..0bb6b23 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -40,6 +40,7 @@
 import android.view.RemoteAnimationTarget;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Utilities;
@@ -344,6 +345,14 @@
      * Applies the target to the previously set parameters
      */
     public void apply(TransformParams params) {
+        apply(params, null);
+    }
+
+    /**
+     * Applies the target to the previously set parameters, optionally with an overridden
+     * surface transaction
+     */
+    public void apply(TransformParams params, @Nullable SurfaceTransaction surfaceTransaction) {
         if (mDp == null || mThumbnailPosition.isEmpty()) {
             return;
         }
@@ -404,7 +413,8 @@
         mTempRectF.roundOut(mTmpCropRect);
 
         params.setProgress(1f - fullScreenProgress);
-        params.applySurfaceParams(params.createSurfaceParams(this));
+        params.applySurfaceParams(surfaceTransaction == null
+                ? params.createSurfaceParams(this) : surfaceTransaction);
 
         if (!DEBUG) {
             return;
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.java b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
index d2b0540..0a261ef 100644
--- a/quickstep/src/com/android/quickstep/views/IconAppChipView.java
+++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
@@ -86,6 +86,7 @@
     private final int mMinIconBackgroundHeight;
     private final int mMaxIconBackgroundCornerRadius;
     private final float mMinIconBackgroundCornerRadius;
+    private AnimatorSet mAnimator;
 
     private int mMaxWidth = Integer.MAX_VALUE;
 
@@ -316,11 +317,13 @@
     }
 
     protected void revealAnim(boolean isRevealing) {
+        cancelInProgressAnimations();
+
         if (isRevealing) {
             boolean isRtl = isLayoutRtl();
             bringToFront();
             ((AnimatedVectorDrawable) mIconArrowView.getDrawable()).start();
-            AnimatorSet anim = new AnimatorSet();
+            mAnimator = new AnimatorSet();
             float backgroundScaleY = mMaxIconBackgroundHeight / (float) mMinIconBackgroundHeight;
             float maxCornerSize = Math.min(mMaxIconBackgroundHeight / 2f,
                     mMaxIconBackgroundCornerRadius);
@@ -341,7 +344,7 @@
                             mIconTextMaxWidth + maxCornerSize);
                 }
             });
-            anim.playTogether(
+            mAnimator.playTogether(
                     expandedTextRevealAnim,
                     ObjectAnimator.ofFloat(mIconViewBackgroundCornersStart, SCALE_Y,
                             backgroundScaleY),
@@ -367,9 +370,9 @@
                     ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 1),
                     ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X,
                             isRtl ? -arrowTranslationX : arrowTranslationX));
-            anim.setDuration(MENU_BACKGROUND_REVEAL_DURATION);
-            anim.setInterpolator(EMPHASIZED);
-            anim.start();
+            mAnimator.setDuration(MENU_BACKGROUND_REVEAL_DURATION);
+            mAnimator.setInterpolator(EMPHASIZED);
+            mAnimator.start();
         } else {
             ((AnimatedVectorDrawable) mIconArrowView.getDrawable()).reverse();
             float maxCornerSize = Math.min(mMaxIconBackgroundHeight / 2f,
@@ -386,8 +389,8 @@
                             mIconTextExpandedView.getHeight() / 2f, 0);
                 }
             });
-            AnimatorSet anim = new AnimatorSet();
-            anim.playTogether(
+            mAnimator = new AnimatorSet();
+            mAnimator.playTogether(
                     expandedTextClipAnim,
                     ObjectAnimator.ofFloat(mIconViewBackgroundCornersStart, SCALE_X, 1),
                     ObjectAnimator.ofFloat(mIconViewBackgroundCornersStart, SCALE_Y, 1),
@@ -403,9 +406,9 @@
                     ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 1),
                     ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 0),
                     ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0));
-            anim.setDuration(MENU_BACKGROUND_HIDE_DURATION);
-            anim.setInterpolator(EMPHASIZED);
-            anim.start();
+            mAnimator.setDuration(MENU_BACKGROUND_HIDE_DURATION);
+            mAnimator.setInterpolator(EMPHASIZED);
+            mAnimator.start();
         }
     }
 
@@ -425,6 +428,16 @@
         mIconTextExpandedView.setAlpha(0);
         mIconArrowView.setTranslationX(0);
         ((AnimatedVectorDrawable) mIconArrowView.getDrawable()).reset();
+        mAnimator = null;
+    }
+
+    private void cancelInProgressAnimations() {
+        // We null the `AnimatorSet` because it holds references to the `Animators` which aren't
+        // expecting to be mutable and will cause a crash if they are re-used.
+        if (mAnimator != null && mAnimator.isStarted()) {
+            mAnimator.cancel();
+            mAnimator = null;
+        }
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 66d651e..d10541a 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -542,6 +542,9 @@
     private int mOverScrollShift = 0;
     private long mScrollLastHapticTimestamp;
 
+    private int mKeyboardTaskFocusSnapAnimationDuration;
+    private int mKeyboardTaskFocusIndex = INVALID_PAGE;
+
     /**
      * TODO: Call reloadIdNeeded in onTaskStackChanged.
      */
@@ -1704,6 +1707,7 @@
         // Removing views sets the currentPage to 0, so we save this and restore it after
         // the new set of views are added
         int previousCurrentPage = mCurrentPage;
+        int previousFocusedPage = indexOfChild(getFocusedChild());
         removeAllViews();
 
         // If we are entering Overview as a result of initiating a split from somewhere else
@@ -1833,6 +1837,8 @@
                     targetPage = indexOfChild(currentTaskView);
                 }
             }
+        } else if (previousFocusedPage != INVALID_PAGE) {
+            targetPage = previousFocusedPage;
         } else {
             // Set the current page to the running task, but not if settling on new task.
             if (hasAnyValidTaskIds(runningTaskId)) {
@@ -2914,7 +2920,7 @@
         int focusedTaskShift = 0;
         int focusedTaskWidthAndSpacing = 0;
         int snappedTaskRowWidth = 0;
-        int snappedPage = getNextPage();
+        int snappedPage = isKeyboardTaskFocusPending() ? mKeyboardTaskFocusIndex : getNextPage();
         TaskView snappedTaskView = getTaskViewAt(snappedPage);
         TaskView homeTaskView = getHomeTaskView();
         TaskView nextFocusedTaskView = null;
@@ -5577,6 +5583,19 @@
     }
 
     /**
+     * Returns how many pixels the running task is offset on the currently laid out dominant axis
+     * specifically during a Keyboard task focus.
+     */
+    public int getScrollOffsetForKeyboardTaskFocus() {
+        if (!isKeyboardTaskFocusPending()) {
+            return getScrollOffset(getRunningTaskIndex());
+        }
+        return getPagedOrientationHandler().getPrimaryScroll(this)
+                - getScrollForPage(mKeyboardTaskFocusIndex)
+                + getScrollOffset(getRunningTaskIndex());
+    }
+
+    /**
      * Sets whether or not we should clamp the scroll offset.
      * This is used to avoid x-axis movement when swiping up transient taskbar.
      * Should only be set at the beginning and end of the gesture, otherwise a jump may occur.
@@ -5609,14 +5628,14 @@
         if (pageIndex == -1) {
             return 0;
         }
-
-        int overScrollShift = getOverScrollShift();
-        if (mAdjacentPageHorizontalOffset > 0) {
-            // Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so
-            // that the page can move freely given there's no visual indication why it shouldn't.
-            overScrollShift = (int) Utilities.mapRange(mAdjacentPageHorizontalOffset,
-                    overScrollShift, getUndampedOverScrollShift());
-        }
+        // Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so that
+        // the page can move freely given there's no visual indication why it shouldn't.
+        int overScrollShift = mAdjacentPageHorizontalOffset > 0
+                        ? (int) Utilities.mapRange(
+                                mAdjacentPageHorizontalOffset,
+                                getOverScrollShift(),
+                                getUndampedOverScrollShift())
+                        : getOverScrollShift();
         return getScrollForPage(pageIndex) - getPagedOrientationHandler().getPrimaryScroll(this)
                 + overScrollShift + getOffsetFromScrollPosition(pageIndex);
     }
@@ -6025,11 +6044,50 @@
         dispatchScrollChanged();
     }
 
+    /**
+     * Prepares this RecentsView to scroll properly for an upcoming child view focus request from
+     * keyboard quick switching
+     */
+    public void setKeyboardTaskFocusIndex(int taskIndex) {
+        mKeyboardTaskFocusIndex = taskIndex;
+    }
+
+    /** Returns whether this RecentsView will be scrolling to a child view for a focus request */
+    public boolean isKeyboardTaskFocusPending() {
+        return mKeyboardTaskFocusIndex != INVALID_PAGE;
+    }
+
+    private boolean isKeyboardTaskFocusPendingForChild(View child) {
+        return isKeyboardTaskFocusPending() && mKeyboardTaskFocusIndex == indexOfChild(child);
+    }
+
     @Override
-    protected boolean shouldHandleRequestChildFocus() {
-        // If we are already scrolling to a task view, then the focus request has already been
-        // handled
-        return mScroller.isFinished();
+    protected int getSnapAnimationDuration() {
+        return isKeyboardTaskFocusPending()
+                ? mKeyboardTaskFocusSnapAnimationDuration : super.getSnapAnimationDuration();
+    }
+
+    @Override
+    protected void onVelocityValuesUpdated() {
+        super.onVelocityValuesUpdated();
+        mKeyboardTaskFocusSnapAnimationDuration =
+                getResources().getInteger(R.integer.config_keyboardTaskFocusSnapAnimationDuration);
+    }
+
+    @Override
+    protected boolean shouldHandleRequestChildFocus(View child) {
+        // If we are already scrolling to a task view and we aren't focusing to this child from
+        // keyboard quick switch, then the focus request has already been handled
+        return mScroller.isFinished() || isKeyboardTaskFocusPendingForChild(child);
+    }
+
+    @Override
+    public void requestChildFocus(View child, View focused) {
+        if (isKeyboardTaskFocusPendingForChild(child)) {
+            updateGridProperties();
+            updateScrollSynchronously();
+        }
+        super.requestChildFocus(child, focused);
     }
 
     private void dispatchScrollChanged() {
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index d779276..cf50835 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -24,6 +24,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Outline;
 import android.graphics.Rect;
@@ -69,6 +70,8 @@
     private TextView mTaskName;
     @Nullable
     private AnimatorSet mOpenCloseAnimator;
+    @Nullable
+    private ValueAnimator mRevealAnimator;
     @Nullable private Runnable mOnClosingStartCallback;
     private TaskView mTaskView;
     private TaskIdAttributeContainer mTaskContainer;
@@ -290,13 +293,18 @@
 
     private void animateOpenOrClosed(boolean closing) {
         if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
-            mOpenCloseAnimator.end();
+            mOpenCloseAnimator.cancel();
         }
         mOpenCloseAnimator = new AnimatorSet();
-
-        final Animator revealAnimator = createOpenCloseOutlineProvider()
-                .createRevealAnimator(this, closing);
-        revealAnimator.setInterpolator(enableOverviewIconMenu() ? Interpolators.EMPHASIZED
+        // If we're opening, we just start from the beginning as a new `TaskMenuView` is created
+        // each time we do the open animation so there will never be a partial value here.
+        float revealAnimationStartProgress = 0f;
+        if (closing && mRevealAnimator != null) {
+            revealAnimationStartProgress = 1f - mRevealAnimator.getAnimatedFraction();
+        }
+        mRevealAnimator = createOpenCloseOutlineProvider()
+                .createRevealAnimator(this, closing, revealAnimationStartProgress);
+        mRevealAnimator.setInterpolator(enableOverviewIconMenu() ? Interpolators.EMPHASIZED
                 : Interpolators.DECELERATE);
 
         if (enableOverviewIconMenu()) {
@@ -349,7 +357,7 @@
             mOpenCloseAnimator.playTogether(translationXAnim, menuTranslationXAnim);
         }
 
-        mOpenCloseAnimator.playTogether(revealAnimator,
+        mOpenCloseAnimator.playTogether(mRevealAnimator,
                 ObjectAnimator.ofFloat(
                         mTaskContainer.getThumbnailView(), DIM_ALPHA,
                         closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA),
@@ -378,6 +386,7 @@
         mIsOpen = false;
         resetOverviewIconMenu();
         mActivity.getDragLayer().removeView(this);
+        mRevealAnimator = null;
     }
 
     private void resetOverviewIconMenu() {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 11e721e..b971f67 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -1137,9 +1137,8 @@
         DeviceProfile dp = mActivity.getDeviceProfile();
         if (enableOverviewIconMenu() && iconView instanceof IconAppChipView) {
             ((IconAppChipView) iconView).revealAnim(/* isRevealing= */ true);
-            return TaskMenuView.showForTask(menuContainer, () -> {
-                ((IconAppChipView) iconView).revealAnim(/* isRevealing= */ false);
-            });
+            return TaskMenuView.showForTask(menuContainer,
+                    () -> ((IconAppChipView) iconView).revealAnim(/* isRevealing= */ false));
         } else if (dp.isTablet) {
             int alignedOptionIndex = 0;
             if (getRecentsView().isOnGridBottomRow(menuContainer.getTaskView()) && dp.isLandscape) {
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
index 7191f70..36c591e 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
@@ -22,6 +22,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.tapl.KeyboardQuickSwitch;
+import com.android.launcher3.taskbar.KeyboardQuickSwitchController;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 
 import org.junit.Assume;
@@ -49,7 +50,7 @@
         DISMISS(0),
         LAUNCH_LAST_APP(0),
         LAUNCH_SELECTED_APP(1),
-        LAUNCH_OVERVIEW(5);
+        LAUNCH_OVERVIEW(KeyboardQuickSwitchController.MAX_TASKS - 1);
 
         private final int mNumAdditionalRunningTasks;
 
@@ -196,7 +197,9 @@
                 if (!testSurface.mInitialFocusAtZero) {
                     kqs.moveFocusBackward();
                 }
-                kqs.launchFocusedOverviewTask();
+                kqs.launchFocusedOverviewTask()
+                        // Check that the correct task was focused
+                        .launchFocusedTaskByEnterKey(CALCULATOR_APP_PACKAGE);
                 break;
             default:
                 throw new IllegalStateException("Cannot run test case: " + testCase);
diff --git a/res/values-sw600dp/config.xml b/res/values-sw600dp/config.xml
index e718d9c..bddfcfc 100644
--- a/res/values-sw600dp/config.xml
+++ b/res/values-sw600dp/config.xml
@@ -18,6 +18,9 @@
     <!-- The duration of the PagedView page snap animation -->
     <integer name="config_pageSnapAnimationDuration">550</integer>
 
+    <!-- The duration of the PagedView page snap animation -->
+    <integer name="config_keyboardTaskFocusSnapAnimationDuration">400</integer>
+
     <!-- The duration of the Widget picker opening and closing animation -->
     <integer name="config_bottomSheetOpenDuration">500</integer>
     <integer name="config_bottomSheetCloseDuration">500</integer>
diff --git a/res/values/config.xml b/res/values/config.xml
index 29c4e66..1b74238 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -43,6 +43,9 @@
     <!-- The duration of the PagedView page snap animation -->
     <integer name="config_pageSnapAnimationDuration">750</integer>
 
+    <!-- The duration of the PagedView page snap animation -->
+    <integer name="config_keyboardTaskFocusSnapAnimationDuration">750</integer>
+
     <!-- View tag key used to store SpringAnimation data. -->
     <item type="id" name="spring_animation_tag" />
 
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 3016559..3aa4a77 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -56,7 +56,7 @@
     <!-- App Widget resize frame -->
     <dimen name="widget_handle_margin">13dp</dimen>
     <dimen name="resize_frame_background_padding">24dp</dimen>
-    <dimen name="resize_frame_margin">22dp</dimen>
+    <dimen name="resize_frame_margin">23dp</dimen>
     <dimen name="resize_frame_invalid_drag_across_two_panel_opacity_margin">24dp</dimen>
 
     <!-- App widget reconfigure button -->
@@ -196,6 +196,8 @@
     <dimen name="widget_list_header_view_vertical_padding">20dp</dimen>
     <dimen name="widget_list_entry_spacing">2dp</dimen>
     <dimen name="widget_list_horizontal_margin">16dp</dimen>
+    <!-- Margin applied to the recycler view with search bar & the list of widget apps below it. -->
+    <dimen name="widget_list_left_pane_horizontal_margin">0dp</dimen>
     <dimen name="widget_list_horizontal_margin_two_pane">24dp</dimen>
 
     <dimen name="widget_preview_shadow_blur">0.5dp</dimen>
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 496cb4e..798b238 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -411,6 +411,8 @@
     private final SettingsCache.OnChangeListener mNaturalScrollingChangedListener =
             enabled -> mIsNaturalScrollingEnabled = enabled;
 
+    private int mActivityStopCount; // Used only by tests
+
     public static Launcher getLauncher(Context context) {
         return fromContext(context);
     }
@@ -1052,10 +1054,18 @@
         mAppWidgetHolder.setActivityStarted(false);
         NotificationListener.removeNotificationsChangedListener(getPopupDataProvider());
         FloatingIconView.resetIconLoadResult();
+        ++mActivityStopCount;
         AccessibilityManagerCompat.sendTestProtocolEventToTest(
                 this, LAUNCHER_ACTIVITY_STOPPED_MESSAGE);
     }
 
+    /** Return activity stop count and reset it. Used only by tests. */
+    public int getAndResetActivityStopCount() {
+        final int activityStopCount = mActivityStopCount;
+        mActivityStopCount = 0;
+        return activityStopCount;
+    }
+
     @Override
     protected void onStart() {
         TraceHelper.INSTANCE.beginSection(ON_START_EVT);
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 43dca5c..ca83245 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -637,6 +637,11 @@
         mMinFlingVelocity = res.getDimensionPixelSize(R.dimen.min_fling_velocity);
         mMinSnapVelocity = res.getDimensionPixelSize(R.dimen.min_page_snap_velocity);
         mPageSnapAnimationDuration = res.getInteger(R.integer.config_pageSnapAnimationDuration);
+        onVelocityValuesUpdated();
+    }
+
+    protected void onVelocityValuesUpdated() {
+        // Overridden in RecentsView
     }
 
     @Override
@@ -1582,7 +1587,7 @@
     @Override
     public void requestChildFocus(View child, View focused) {
         super.requestChildFocus(child, focused);
-        if (!shouldHandleRequestChildFocus()) {
+        if (!shouldHandleRequestChildFocus(child)) {
             return;
         }
         // In case the device is controlled by a controller, mCurrentPage isn't updated properly
@@ -1598,7 +1603,7 @@
         }
     }
 
-    protected boolean shouldHandleRequestChildFocus() {
+    protected boolean shouldHandleRequestChildFocus(View child) {
         return true;
     }
 
@@ -1652,7 +1657,7 @@
     }
 
     protected void snapToDestination() {
-        snapToPage(getDestinationPage(), mPageSnapAnimationDuration);
+        snapToPage(getDestinationPage(), getSnapAnimationDuration());
     }
 
     // We want the duration of the page snap animation to be influenced by the distance that
@@ -1676,7 +1681,7 @@
         if (Math.abs(velocity) < mMinFlingVelocity) {
             // If the velocity is low enough, then treat this more as an automatic page advance
             // as opposed to an apparent physical response to flinging
-            return snapToPage(whichPage, mPageSnapAnimationDuration);
+            return snapToPage(whichPage, getSnapAnimationDuration());
         }
 
         // Here we compute a "distance" that will be used in the computation of the overall
@@ -1698,12 +1703,16 @@
         return snapToPage(whichPage, delta, duration);
     }
 
+    protected int getSnapAnimationDuration() {
+        return mPageSnapAnimationDuration;
+    }
+
     public boolean snapToPage(int whichPage) {
-        return snapToPage(whichPage, mPageSnapAnimationDuration);
+        return snapToPage(whichPage, getSnapAnimationDuration());
     }
 
     public boolean snapToPageImmediately(int whichPage) {
-        return snapToPage(whichPage, mPageSnapAnimationDuration, true);
+        return snapToPage(whichPage, getSnapAnimationDuration(), true);
     }
 
     public boolean snapToPage(int whichPage, int duration) {
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index 557ec48..6422943 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER;
 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_PRIVATE_SPACE_INSTALL_APP;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
@@ -109,7 +110,7 @@
         itemInfo.bitmap = bitmapInfo;
         itemInfo.contentDescription = context.getResources().getString(
                 com.android.launcher3.R.string.ps_add_button_content_description);
-        itemInfo.runtimeStatusFlags |= FLAG_PRIVATE_SPACE_INSTALL_APP;
+        itemInfo.runtimeStatusFlags |= FLAG_PRIVATE_SPACE_INSTALL_APP | FLAG_NOT_PINNABLE;
 
         BaseAllAppsAdapter.AdapterItem item = new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_ICON);
         item.itemInfo = itemInfo;
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index 4427a49..f9d047b 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -20,6 +20,7 @@
 import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.text.style.SuggestionSpan;
+import android.util.Log;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.View.OnFocusChangeListener;
@@ -42,6 +43,7 @@
         implements TextWatcher, OnEditorActionListener, ExtendedEditText.OnBackKeyListener,
         OnFocusChangeListener {
 
+    private static final String TAG = "AllAppsSearchBarController";
     protected ActivityContext mLauncher;
     protected SearchCallback<AdapterItem> mCallback;
     protected ExtendedEditText mInput;
@@ -122,6 +124,7 @@
     public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
 
         if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_GO) {
+            Log.i(TAG, "User tapped ime search button");
             // selectFocusedView should return SearchTargetEvent that is passed onto onClick
             return mLauncher.getAppsView().getMainAdapterProvider().launchHighlightedItem();
         }
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 3ccde0a..b6e5977 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.dragndrop;
 
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE;
+
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
@@ -29,8 +31,10 @@
 import com.android.app.animation.Interpolators;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
+import com.android.launcher3.Flags;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.views.ActivityContext;
@@ -221,6 +225,12 @@
         }
     }
 
+    protected boolean isItemPinnable() {
+        return !Flags.privateSpaceRestrictItemDrag()
+                || !(mDragObject.dragInfo instanceof ItemInfoWithIcon itemInfoWithIcon)
+                || (itemInfoWithIcon.runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0;
+    }
+
     public Optional<InstanceId> getLogInstanceId() {
         return Optional.ofNullable(mDragObject)
                 .map(dragObject -> dragObject.logInstanceId);
diff --git a/src/com/android/launcher3/dragndrop/LauncherDragController.java b/src/com/android/launcher3/dragndrop/LauncherDragController.java
index da6f446..f3708a2 100644
--- a/src/com/android/launcher3/dragndrop/LauncherDragController.java
+++ b/src/com/android/launcher3/dragndrop/LauncherDragController.java
@@ -149,9 +149,10 @@
 
         handleMoveEvent(mLastTouch.x, mLastTouch.y);
 
-        if (!mActivity.isTouchInProgress() && options.simulatedDndStartPoint == null) {
+        if (!isItemPinnable()
+                || (!mActivity.isTouchInProgress() && options.simulatedDndStartPoint == null)) {
             // If it is an internal drag and the touch is already complete, cancel immediately
-            MAIN_EXECUTOR.submit(this::cancelDrag);
+            MAIN_EXECUTOR.post(this::cancelDrag);
         }
         return dragView;
     }
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 7ae70e0..2f3f029 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -800,6 +800,14 @@
             return;
         }
 
+        int size = getIconsInReadingOrder().size();
+        if (size <= 1) {
+            Log.d(TAG, "Couldn't animate folder closed because there's " + size + " icons");
+            closeComplete(false);
+            post(this::announceAccessibilityChanges);
+            return;
+        }
+
         mContent.completePendingPageChanges();
         mContent.snapToPageImmediately(mContent.getDestinationPage());
 
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index 5e86bd6..96a8da9 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -33,6 +33,7 @@
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemFactory;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -102,6 +103,11 @@
                             Objects.requireNonNull(item.getIntent()))) {
                         continue;
                     }
+
+                    if (item instanceof ItemInfoWithIcon
+                            && ((ItemInfoWithIcon) item).isArchived()) {
+                        continue;
+                    }
                 }
 
                 if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
diff --git a/src/com/android/launcher3/model/ItemInstallQueue.java b/src/com/android/launcher3/model/ItemInstallQueue.java
index 9a3abd4..59f453a 100644
--- a/src/com/android/launcher3/model/ItemInstallQueue.java
+++ b/src/com/android/launcher3/model/ItemInstallQueue.java
@@ -18,10 +18,12 @@
 
 import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
 
+import static com.android.launcher3.Flags.enableSupportForArchiving;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
 import static com.android.launcher3.model.data.AppInfo.makeLaunchIntent;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.appwidget.AppWidgetManager;
@@ -276,6 +278,7 @@
             return intent;
         }
 
+        @SuppressWarnings("NewApi")
         public Pair<ItemInfo, Object> getItemInfo(Context context) {
             switch (itemType) {
                 case ITEM_TYPE_APPLICATION: {
@@ -297,6 +300,9 @@
                     } else {
                         lai = laiList.get(0);
                         si.intent = makeLaunchIntent(lai);
+                        if (enableSupportForArchiving() && lai.getActivityInfo().isArchived) {
+                            si.runtimeStatusFlags |= FLAG_ARCHIVED;
+                        }
                     }
                     LauncherAppState.getInstance(context).getIconCache()
                             .getTitleAndIcon(si, () -> lai, usePackageIcon, false);
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 8708d5a..032de31 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -28,9 +28,13 @@
 
 import androidx.annotation.AnyThread;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.UserBadgeDrawable;
+import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
@@ -166,4 +170,14 @@
     public List<UserHandle> getUserProfiles() {
         return List.copyOf(mUserToSerialMap.keySet());
     }
+
+    /**
+     * Get a non-themed {@link UserBadgeDrawable} based on the provided {@link UserHandle}.
+     */
+    @Nullable
+    public static UserBadgeDrawable getBadgeDrawable(Context context, UserHandle userHandle) {
+        return (UserBadgeDrawable) BitmapInfo.LOW_RES_INFO.withFlags(UserCache.getInstance(context)
+                        .getUserInfo(userHandle).applyBitmapInfoFlags(FlagOp.NO_OP))
+                .getBadgeDrawable(context, false /* isThemed */);
+    }
 }
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index c9c5fd3..1c9db17 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_SHORTCUTS;
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE;
 import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
@@ -44,6 +45,7 @@
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.DropTarget.DragObject;
+import com.android.launcher3.Flags;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
@@ -53,6 +55,7 @@
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
@@ -204,17 +207,21 @@
                 .collect(Collectors.toList());
         container = (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
                 R.layout.popup_container, launcher.getDragLayer(), false);
-        container.configureForLauncher(launcher);
+        container.configureForLauncher(launcher, item);
         container.populateAndShowRows(icon, deepShortcutCount, systemShortcuts);
         launcher.refreshAndBindWidgetsForPackageUser(PackageUserKey.fromItemInfo(item));
         container.requestFocus();
         return container;
     }
 
-    private void configureForLauncher(Launcher launcher) {
+    private void configureForLauncher(Launcher launcher, ItemInfo itemInfo) {
         addOnAttachStateChangeListener(new LauncherPopupLiveUpdateHandler(
                 launcher, (PopupContainerWithArrow<Launcher>) this));
-        mPopupItemDragHandler = new LauncherPopupItemDragHandler(launcher, this);
+        if (!Flags.privateSpaceRestrictItemDrag()
+                || !(itemInfo instanceof ItemInfoWithIcon itemInfoWithIcon)
+                || (itemInfoWithIcon.runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0) {
+            mPopupItemDragHandler = new LauncherPopupItemDragHandler(launcher, this);
+        }
         mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(launcher);
         launcher.getDragController().addDragListener(this);
     }
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java
index 8d1d96b..79b25a4 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.secondarydisplay;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
@@ -129,6 +131,10 @@
         dragView.show(mLastTouch.x, mLastTouch.y);
         mDistanceSinceScroll = 0;
 
+        if (!isItemPinnable()) {
+            MAIN_EXECUTOR.post(this:: cancelDrag);
+        }
+
         if (!mIsInPreDrag) {
             callOnDragStart();
         } else if (mOptions.preDragCondition != null) {
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 315b5e3..27a153b 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -150,6 +150,12 @@
                 }, this::getCurrentActivity);
             }
 
+            case TestProtocol.REQUEST_CELL_LAYOUT_BOARDER_HEIGHT: {
+                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                        mDeviceProfile.cellLayoutBorderSpacePx.y);
+                return response;
+            }
+
             case TestProtocol.REQUEST_SYSTEM_GESTURE_REGION: {
                 return getUIProperty(Bundle::putParcelable, activity -> {
                     WindowInsetsCompat insets = WindowInsetsCompat.toWindowInsetsCompat(
@@ -208,6 +214,16 @@
                                 .forceAllowRotationForTesting(Boolean.parseBoolean(arg)));
                 return response;
 
+            case TestProtocol.REQUEST_GET_AND_RESET_ACTIVITY_STOP_COUNT: {
+                final Bundle bundle = getLauncherUIProperty(Bundle::putInt,
+                        launcher -> launcher.getAndResetActivityStopCount());
+                if (bundle != null) return bundle;
+
+                // If Launcher activity wasn't created, 'it' was stopped 0 times.
+                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, 0);
+                return response;
+            }
+
             case TestProtocol.REQUEST_WORKSPACE_CELL_LAYOUT_SIZE:
                 return getLauncherUIProperty(Bundle::putIntArray, launcher -> {
                     final Workspace<?> workspace = launcher.getWorkspace();
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 2b5aaf5..50d8886 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.util;
 
+import static com.android.launcher3.Flags.enableSupportForArchiving;
+
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
 import android.content.Context;
@@ -112,8 +114,7 @@
             @NonNull final UserHandle user, final int flags) {
         try {
             ApplicationInfo info = mLauncherApps.getApplicationInfo(packageName, flags, user);
-            return (info.flags & ApplicationInfo.FLAG_INSTALLED) == 0 || !info.enabled
-                    ? null : info;
+            return !isPackageInstalledOrArchived(info) || !info.enabled ? null : info;
         } catch (PackageManager.NameNotFoundException e) {
             return null;
         }
@@ -253,4 +254,11 @@
         }
         return 100;
     }
+
+    /** Returns true in case app is installed on the device or in archived state. */
+    @SuppressWarnings("NewApi")
+    private boolean isPackageInstalledOrArchived(ApplicationInfo info) {
+        return (info.flags & ApplicationInfo.FLAG_INSTALLED) != 0 || (
+                enableSupportForArchiving() && info.isArchived);
+    }
 }
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index c5317e3..230a651 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -155,6 +155,19 @@
     }
 
     /**
+     * Handle user tapping on unsupported target when in split selection mode.
+     * See {@link #isSplitSelectionActive()}
+     *
+     * @return {@code true} if this method will handle the incorrect target selection,
+     *         {@code false} if it could not be handled or if not possible to handle based on
+     *         current split state
+     */
+    default boolean handleIncorrectSplitTargetSelection() {
+        // Overridden
+        return false;
+    }
+
+    /**
      * The root view to support drag-and-drop and popup support.
      */
     BaseDragLayer getDragLayer();
diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java
index 76b6fde..c60e1a4 100644
--- a/src/com/android/launcher3/views/FloatingSurfaceView.java
+++ b/src/com/android/launcher3/views/FloatingSurfaceView.java
@@ -175,7 +175,6 @@
 
             if (!mTmpPosition.equals(mIconPosition)) {
                 mIconPosition.set(mTmpPosition);
-                sendIconInfo();
 
                 LayoutParams lp = (LayoutParams) mSurfaceView.getLayoutParams();
                 lp.width = Math.round(mIconPosition.width());
@@ -184,6 +183,9 @@
                 lp.topMargin = Math.round(mIconPosition.top);
             }
         }
+
+        sendIconInfo();
+
         if (mIcon != null && iconChanged && !mIconBounds.isEmpty()) {
             // Record the icon display
             setCurrentIconVisible(true);
@@ -197,7 +199,7 @@
     }
 
     private void sendIconInfo() {
-        if (mContract != null && !mIconPosition.isEmpty()) {
+        if (mContract != null) {
             mContract.sendEndPosition(mIconPosition, mLauncher, mSurfaceView.getSurfaceControl());
         }
     }
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 9de7f62..145ad80 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -72,14 +72,21 @@
 
     public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mContentHorizontalMargin = getResources().getDimensionPixelSize(
-                R.dimen.widget_list_horizontal_margin);
+        mContentHorizontalMargin = getWidgetListHorizontalMargin();
         mWidgetCellHorizontalPadding = getResources().getDimensionPixelSize(
                 R.dimen.widget_cell_horizontal_padding);
         mNavBarScrimPaint = new Paint();
         mNavBarScrimPaint.setColor(Themes.getNavBarScrimColor(mActivityContext));
     }
 
+    /**
+     * Returns the margins to be applied to the left and right of the widget apps list.
+     */
+    protected int getWidgetListHorizontalMargin() {
+        return getResources().getDimensionPixelSize(
+                R.dimen.widget_list_horizontal_margin);
+    }
+
     protected int getScrimColor(Context context) {
         return context.getResources().getColor(R.color.widgets_picker_scrim);
     }
@@ -142,8 +149,7 @@
     @Override
     public void setInsets(Rect insets) {
         mInsets.set(insets);
-        @Px int contentHorizontalMargin = getResources().getDimensionPixelSize(
-                R.dimen.widget_list_horizontal_margin);
+        @Px int contentHorizontalMargin = getWidgetListHorizontalMargin();
         if (contentHorizontalMargin != mContentHorizontalMargin) {
             onContentHorizontalMarginChanged(contentHorizontalMargin);
             mContentHorizontalMargin = contentHorizontalMargin;
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index e9a590b..17d9276 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -182,7 +182,8 @@
                 .stream()
                 .anyMatch(user -> mUserCache.getUserInfo(user).isWork());
         mWorkWidgetsFilter = entry -> mHasWorkProfile
-                && mUserCache.getUserInfo(entry.mPkgItem.user).isWork();
+                && mUserCache.getUserInfo(entry.mPkgItem.user).isWork()
+                && !mUserManagerState.isUserQuiet(entry.mPkgItem.user);
         mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY));
         mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK));
         mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH));
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index 6656237..26c04f5 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -303,6 +303,12 @@
     }
 
     @Override
+    protected int getWidgetListHorizontalMargin() {
+        return getResources().getDimensionPixelSize(
+                R.dimen.widget_list_left_pane_horizontal_margin);
+    }
+
+    @Override
     protected boolean isTwoPane() {
         return true;
     }
diff --git a/tests/Android.bp b/tests/Android.bp
index 310e418..ed8609e 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -176,6 +176,7 @@
     name: "launcher-testing-shared",
     srcs: [
         "multivalentTests/shared/com/android/launcher3/testing/shared/**/*.java",
+        "multivalentTests/shared/com/android/launcher3/testing/shared/**/*.kt"
     ],
     resource_dirs: [],
     manifest: "multivalentTests/shared/AndroidManifest.xml",
diff --git a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 3e188e6..187daca 100644
--- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -121,6 +121,7 @@
     public static final String REQUEST_IS_TABLET = "is-tablet";
     public static final String REQUEST_NUM_ALL_APPS_COLUMNS = "num-all-apps-columns";
     public static final String REQUEST_IS_TWO_PANELS = "is-two-panel";
+    public static final String REQUEST_CELL_LAYOUT_BOARDER_HEIGHT = "cell-layout-boarder-height";
     public static final String REQUEST_START_DRAG_THRESHOLD = "start-drag-threshold";
     public static final String REQUEST_SHELL_DRAG_READY = "shell-drag-ready";
     public static final String REQUEST_GET_ACTIVITIES_CREATED_COUNT =
@@ -177,6 +178,9 @@
     public static final String REQUEST_UNSTASH_BUBBLE_BAR_IF_STASHED =
             "unstash-bubble-bar-if-stashed";
 
+    public static final String REQUEST_GET_AND_RESET_ACTIVITY_STOP_COUNT =
+            "get-and-reset-activity-stops";
+
     /** Logs {@link Log#d(String, String)} if {@link #sDebugTracing} is true. */
     public static void testLogD(String tag, String message) {
         if (!sDebugTracing) {
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java
index dbbdcf5..62f2259 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java
@@ -66,7 +66,7 @@
     }
 
     public CellLayoutBoard(int width, int height) {
-        mWidget = new char[width + 1][height + 1];
+        mWidget = new char[width][height];
         this.mWidth = width;
         this.mHeight = height;
         for (int x = 0; x < mWidget.length; x++) {
@@ -371,8 +371,8 @@
         s.append("\n");
         maxX = Math.min(maxX, mWidget.length);
         maxY = Math.min(maxY, mWidget[0].length);
-        for (int y = 0; y <= maxY; y++) {
-            for (int x = 0; x <= maxX; x++) {
+        for (int y = 0; y < maxY; y++) {
+            for (int x = 0; x < maxX; x++) {
                 s.append(mWidget[x][y]);
             }
             s.append('\n');
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
index 770024f..fcfb3db 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
@@ -27,7 +27,7 @@
      *   usually less than 100.
      * @return a randomly generated board filled with icons and widgets.
      */
-    open fun generateBoard(width: Int, height: Int, remainingEmptySpaces: Int): CellLayoutBoard? {
+    open fun generateBoard(width: Int, height: Int, remainingEmptySpaces: Int): CellLayoutBoard {
         val cellLayoutBoard = CellLayoutBoard(width, height)
         return fillBoard(cellLayoutBoard, Rect(0, 0, width, height), remainingEmptySpaces)
     }
@@ -39,8 +39,8 @@
     ): CellLayoutBoard {
         var remainingEmptySpaces = remainingEmptySpacesArg
         if (area.height() * area.width() <= 0) return board
-        val width = getRandom(1, area.width() - 1)
-        val height = getRandom(1, area.height() - 1)
+        val width = getRandom(1, area.width())
+        val height = getRandom(1, area.height())
         val x = area.left + getRandom(0, area.width() - width)
         val y = area.top + getRandom(0, area.height() - height)
         if (remainingEmptySpaces > 0) {
diff --git a/tests/multivalentTests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/multivalentTests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index f0d2e20..dcf56f8 100644
--- a/tests/multivalentTests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -599,6 +599,9 @@
         Wait.atMost("Launcher activity didn't stop",
                 () -> !launcherInstrumentation.isLauncherActivityStarted(),
                 DEFAULT_ACTIVITY_TIMEOUT, launcherInstrumentation);
+
+        // Reset activity stop count.
+        launcherInstrumentation.getAndResetActivityStopCount();
     }
 
     public static ActivityInfo resolveSystemAppInfo(String category) {
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/rule/TestIsolationRule.java b/tests/multivalentTests/src/com/android/launcher3/util/rule/TestIsolationRule.java
index 2b45902..e98dcf4 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/rule/TestIsolationRule.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/rule/TestIsolationRule.java
@@ -44,11 +44,15 @@
         return new Statement() {
             @Override
             public void evaluate() throws Throwable {
-                base.evaluate();
-                // Make sure that Launcher workspace looks correct.
+                // Reset activity stop count.
+                mLauncher.getAndResetActivityStopCount();
 
+                base.evaluate();
+
+                // Make sure that Launcher workspace looks correct.
                 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).pressHome();
                 AbstractLauncherUiTest.checkDetectedLeaks(mLauncher, mRequireOneActiveActivity);
+                mLauncher.assertNoUnexpectedStops();
             }
         };
     }
diff --git a/tests/multivalentTests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/multivalentTests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 1bc489c..d337b91 100644
--- a/tests/multivalentTests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/multivalentTests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -19,9 +19,11 @@
 import static android.view.KeyEvent.KEYCODE_ESCAPE;
 
 import static com.android.launcher3.tapl.LauncherInstrumentation.TASKBAR_RES_ID;
+import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 
 import android.graphics.Rect;
+import android.view.KeyEvent;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -47,6 +49,10 @@
             "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
     private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile(
             "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
+    private static final Pattern EVENT_ENTER_DOWN = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_ENTER");
+    private static final Pattern EVENT_ENTER_UP = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ENTER");
 
     private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
 
@@ -414,6 +420,31 @@
         }
     }
 
+    /**
+     * Presses the enter key to launch the focused task
+     * <p>
+     * If no task is focused, this will fail.
+     */
+    public LaunchedAppState launchFocusedTaskByEnterKey(@NonNull String expectedPackageName) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ENTER_DOWN);
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ENTER_UP);
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
+
+            mLauncher.executeAndWaitForLauncherStop(
+                    () -> mLauncher.assertTrue(
+                            "Failed to press enter",
+                            mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_ENTER)),
+                    "pressing enter");
+            mLauncher.assertAppLaunched(expectedPackageName);
+
+            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                    "pressed enter")) {
+                return new LaunchedAppState(mLauncher);
+            }
+        }
+    }
+
     private void verifyActionsViewVisibility() {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to assert overview actions view visibility")) {
diff --git a/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index fef93b7..90ca990 100644
--- a/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -207,6 +207,7 @@
 
     private TrackpadGestureType mTrackpadGestureType = TrackpadGestureType.NONE;
     private int mPointerCount = 0;
+    private final boolean mIsLauncherTest;
 
     private static Pattern getKeyEventPattern(String action, String keyCode) {
         return Pattern.compile("Key event: KeyEvent.*action=" + action + ".*keyCode=" + keyCode);
@@ -243,6 +244,7 @@
      */
     @Deprecated
     public LauncherInstrumentation(Instrumentation instrumentation, boolean isLauncherTest) {
+        mIsLauncherTest = isLauncherTest;
         mInstrumentation = instrumentation;
         mDevice = UiDevice.getInstance(instrumentation);
 
@@ -405,11 +407,21 @@
                 .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    int getCellLayoutBoarderHeight() {
+        return getTestInfo(TestProtocol.REQUEST_CELL_LAYOUT_BOARDER_HEIGHT)
+                .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
     int getFocusedTaskHeightForTablet() {
         return getTestInfo(TestProtocol.REQUEST_GET_FOCUSED_TASK_HEIGHT_FOR_TABLET).getInt(
                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    public int getAndResetActivityStopCount() {
+        return getTestInfo(TestProtocol.REQUEST_GET_AND_RESET_ACTIVITY_STOP_COUNT).getInt(
+                TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
     Rect getGridTaskRectForTablet() {
         return ((Rect) getTestInfo(TestProtocol.REQUEST_GET_GRID_TASK_SIZE_RECT_FOR_TABLET)
                 .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD));
@@ -623,10 +635,19 @@
 
     public void onTestStart() {
         mTestStartTime = System.currentTimeMillis();
+        assertNoUnexpectedStops();
     }
 
     public void onTestFinish() {
         mTestStartTime = -1;
+        assertNoUnexpectedStops();
+    }
+
+    /** Verify that the activity stop count is zero. */
+    public void assertNoUnexpectedStops() {
+        if (mIsLauncherTest) {
+            assertEquals("Unexpected activity stops", 0, getAndResetActivityStopCount());
+        }
     }
 
     private String formatSystemHealthMessage(String message) {
@@ -1000,6 +1021,9 @@
                 event -> TestProtocol.LAUNCHER_ACTIVITY_STOPPED_MESSAGE
                         .equals(event.getClassName().toString()),
                 () -> "Launcher activity didn't stop", actionName);
+
+        // Reset activity stop count.
+        getAndResetActivityStopCount();
     }
 
     /**
@@ -2252,6 +2276,7 @@
     }
 
     public Closable eventsCheck() {
+        assertNoUnexpectedStops();
         Assert.assertTrue("Nested event checking", mEventChecker == null);
         disableSensorRotation();
         final Integer initialPid = getPid();
@@ -2259,6 +2284,7 @@
         if (eventChecker.start()) mEventChecker = eventChecker;
 
         return () -> {
+            assertNoUnexpectedStops();
             if (initialPid != null && initialPid.intValue() != getPid()) {
                 if (mOnLauncherCrashed != null) mOnLauncherCrashed.run();
                 checkForAnomaly();
diff --git a/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java b/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java
index d0573e0..3895302 100644
--- a/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java
+++ b/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java
@@ -57,7 +57,8 @@
             Rect originalWidgetSize = widget.getVisibleBounds();
             Point targetStart = bottomResizeHandle.getVisibleCenter();
             Point targetDest = bottomResizeHandle.getVisibleCenter();
-            targetDest.offset(0, originalWidgetSize.height());
+            targetDest.offset(0,
+                    originalWidgetSize.height() + mLauncher.getCellLayoutBoarderHeight());
 
             final long downTime = SystemClock.uptimeMillis();
             mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetStart,
diff --git a/tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java b/tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java
index 27a2c75..ba74244 100644
--- a/tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java
+++ b/tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java
@@ -62,5 +62,6 @@
         } finally {
             allApps.unfreeze();
         }
+        mLauncher.goHome();
     }
 }
diff --git a/tests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt b/tests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
new file mode 100644
index 0000000..13dfd5e
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt
@@ -0,0 +1,179 @@
+/*
+ * 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.celllayout
+
+import android.content.Context
+import android.graphics.Point
+import android.util.Log
+import android.view.View
+import androidx.core.view.get
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.CellLayout
+import com.android.launcher3.celllayout.board.CellLayoutBoard
+import com.android.launcher3.celllayout.board.IconPoint
+import com.android.launcher3.celllayout.board.PermutedBoardComparator
+import com.android.launcher3.celllayout.board.WidgetRect
+import com.android.launcher3.celllayout.testgenerator.RandomBoardGenerator
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.views.DoubleShadowBubbleTextView
+import java.util.Random
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+private class HotseatReorderTestCase(
+    val startBoard: CellLayoutBoard,
+    val endBoard: CellLayoutBoard
+) {
+    override fun toString(): String {
+        return "$startBoard#endBoard:\n$endBoard"
+    }
+}
+
+class HotseatReorderUnitTest {
+
+    private val applicationContext: Context =
+        ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+
+    @JvmField @Rule var cellLayoutBuilder = UnitTestCellLayoutBuilderRule()
+
+    /**
+     * This test generates random CellLayout configurations and then try to reorder it and makes
+     * sure the result is a valid board meaning it didn't remove any widget or icon.
+     */
+    @Test
+    fun generateValidTests() {
+        val generator = Random(Companion.SEED.toLong())
+        for (i in 0 until Companion.TOTAL_OF_CASES_GENERATED) {
+            // Using a new seed so that we can replicate the same test cases.
+            val seed = generator.nextInt()
+            Log.d(Companion.TAG, "Seed = $seed")
+
+            val testCase: HotseatReorderTestCase =
+                generateRandomTestCase(RandomBoardGenerator(Random(seed.toLong())))
+            Log.d(Companion.TAG, "testCase = $testCase")
+
+            Assert.assertTrue(
+                "invalid case $i",
+                PermutedBoardComparator().compare(testCase.startBoard, testCase.endBoard) == 0
+            )
+        }
+    }
+
+    private fun addViewInCellLayout(
+        cellLayout: CellLayout,
+        cellX: Int,
+        cellY: Int,
+        spanX: Int,
+        spanY: Int,
+        isWidget: Boolean
+    ) {
+        val cell =
+            if (isWidget) View(applicationContext)
+            else DoubleShadowBubbleTextView(applicationContext)
+        cell.layoutParams = CellLayoutLayoutParams(cellX, cellY, spanX, spanY)
+        cellLayout.addViewToCellLayout(
+            cell,
+            -1,
+            cell.id,
+            cell.layoutParams as CellLayoutLayoutParams,
+            true
+        )
+    }
+
+    private fun solve(board: CellLayoutBoard): CellLayout {
+        val cl = cellLayoutBuilder.createCellLayout(board.width, board.height, false)
+        // The views have to be sorted or the result can vary
+        board.icons
+            .map(IconPoint::getCoord)
+            .sortedWith(
+                Comparator.comparing { p: Any -> (p as Point).x }
+                    .thenComparing { p: Any -> (p as Point).y }
+            )
+            .forEach { p ->
+                addViewInCellLayout(
+                    cellLayout = cl,
+                    cellX = p.x,
+                    cellY = p.y,
+                    spanX = 1,
+                    spanY = 1,
+                    isWidget = false
+                )
+            }
+        board.widgets
+            .sortedWith(
+                Comparator.comparing(WidgetRect::getCellX).thenComparing(WidgetRect::getCellY)
+            )
+            .forEach { widget ->
+                addViewInCellLayout(
+                    cl,
+                    widget.cellX,
+                    widget.cellY,
+                    widget.spanX,
+                    widget.spanY,
+                    isWidget = true
+                )
+            }
+        if (cl.makeSpaceForHotseatMigration(true)) {
+            commitTempPosition(cl)
+        }
+        return cl
+    }
+
+    private fun commitTempPosition(cellLayout: CellLayout) {
+        val count = cellLayout.shortcutsAndWidgets.childCount
+        for (i in 0 until count) {
+            val params = cellLayout.shortcutsAndWidgets[i].layoutParams as CellLayoutLayoutParams
+            params.cellX = params.tmpCellX
+            params.cellY = params.tmpCellY
+        }
+    }
+
+    private fun boardFromCellLayout(cellLayout: CellLayout): CellLayoutBoard {
+        val views = mutableListOf<View>()
+        for (i in 0 until cellLayout.shortcutsAndWidgets.childCount) {
+            views.add(cellLayout.shortcutsAndWidgets.getChildAt(i))
+        }
+        return CellLayoutTestUtils.viewsToBoard(views, cellLayout.countX, cellLayout.countY)
+    }
+
+    private fun generateRandomTestCase(
+        boardGenerator: RandomBoardGenerator
+    ): HotseatReorderTestCase {
+        val width: Int = boardGenerator.getRandom(3, Companion.MAX_BOARD_SIZE)
+        val height: Int = boardGenerator.getRandom(3, Companion.MAX_BOARD_SIZE)
+        val targetWidth: Int = boardGenerator.getRandom(1, width - 2)
+        val targetHeight: Int = boardGenerator.getRandom(1, height - 2)
+        val board: CellLayoutBoard =
+            boardGenerator.generateBoard(width, height, targetWidth * targetHeight)
+        val finishBoard: CellLayoutBoard = boardFromCellLayout(solve(board))
+        return HotseatReorderTestCase(board, finishBoard)
+    }
+
+    companion object {
+        private const val MAX_BOARD_SIZE = 13
+
+        /**
+         * There is nothing special about this numbers, the random seed is just to be able to
+         * reproduce the test cases and the height and width is a random number similar to what
+         * users expect on their devices
+         */
+        private const val SEED = -194162315
+        private const val TOTAL_OF_CASES_GENERATED = 300
+        private const val TAG = "HotseatReorderUnitTest"
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
index d96287f..7aa26a1 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
@@ -161,11 +161,12 @@
 
         public int getWidgetId() throws InterruptedException {
             Intent intent = blockingGetExtraIntent();
-            assertNotNull(intent);
-            assertEquals(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE, intent.getAction());
+            assertNotNull("Null EXTRA_INTENT", intent);
+            assertEquals("Intent action is not ACTION_APPWIDGET_CONFIGURE",
+                    AppWidgetManager.ACTION_APPWIDGET_CONFIGURE, intent.getAction());
             int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                     LauncherAppWidgetInfo.NO_ID);
-            assertNotSame(widgetId, LauncherAppWidgetInfo.NO_ID);
+            assertNotSame("Widget id is NO_ID", widgetId, LauncherAppWidgetInfo.NO_ID);
             return widgetId;
         }
     }
diff --git a/tests/src/com/android/launcher3/util/PackageManagerHelperTest.java b/tests/src/com/android/launcher3/util/PackageManagerHelperTest.java
new file mode 100644
index 0000000..d1da5f4
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/PackageManagerHelperTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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 static com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link PackageManagerHelper}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class PackageManagerHelperTest {
+    @Rule
+    public ExpectedException exception = ExpectedException.none();
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    private static final String TEST_PACKAGE = "com.android.test.package";
+    private static final int TEST_USER = 2;
+
+    private Context mContext;
+    private LauncherApps mLauncherApps;
+    private PackageManagerHelper mPackageManagerHelper;
+
+    @Before
+    public void setup() {
+        mContext = mock(Context.class);
+        mLauncherApps = mock(LauncherApps.class);
+        when(mContext.getSystemService(eq(LauncherApps.class))).thenReturn(mLauncherApps);
+        mPackageManagerHelper = new PackageManagerHelper(mContext);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING)
+    public void getApplicationInfo_archivedApp_appInfoIsNotNull()
+            throws PackageManager.NameNotFoundException {
+        ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.isArchived = true;
+        when(mLauncherApps.getApplicationInfo(TEST_PACKAGE, 0 /* flags */,
+                UserHandle.of(TEST_USER)))
+                .thenReturn(applicationInfo);
+
+        assertThat(mPackageManagerHelper.getApplicationInfo(TEST_PACKAGE, UserHandle.of(TEST_USER),
+                0 /* flags */))
+                .isNotNull();
+    }
+}