Merge "Make DesktopTaskView's icon handling more similar to standard TaskView" into main
diff --git a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
index 694475a..84c2ed2 100644
--- a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -28,14 +28,15 @@
 import android.text.TextPaint;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.ColorInt;
 import androidx.core.content.ContextCompat;
 
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.FloatingHeaderRow;
 import com.android.launcher3.allapps.FloatingHeaderView;
-import com.android.launcher3.util.Themes;
 
 /**
  * A view which shows a horizontal divider
@@ -54,6 +55,7 @@
 
     private final @ColorInt int mStrokeColor;
     private final @ColorInt int mAllAppsLabelTextColor;
+    private final AccessibilityManager mAccessibilityManager;
 
     private Layout mAllAppsLabelLayout;
     private boolean mShowAllAppsLabel;
@@ -87,7 +89,8 @@
         mAllAppsLabelTextColor = ContextCompat.getColor(context,
                 R.color.material_color_on_surface_variant);
 
-        mShowAllAppsLabel = !ALL_APPS_VISITED_COUNT.hasReachedMax(context);
+        mAccessibilityManager = AccessibilityManager.getInstance(context);
+        setShowAllAppsLabel(!ALL_APPS_VISITED_COUNT.hasReachedMax(context));
     }
 
     public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
@@ -99,6 +102,9 @@
 
     /** {@code true} if all apps label should be shown in place of divider. */
     public void setShowAllAppsLabel(boolean showAllAppsLabel) {
+        if (mAccessibilityManager.isEnabled() && !Utilities.isRunningInTestHarness()) {
+            showAllAppsLabel = true;
+        }
         if (showAllAppsLabel != mShowAllAppsLabel) {
             mShowAllAppsLabel = showAllAppsLabel;
             updateDividerType();
@@ -148,6 +154,7 @@
             mDividerType = dividerType;
             int topPadding;
             int bottomPadding;
+            setContentDescription(null);
             switch (dividerType) {
                 case LINE:
                     topPadding = 0;
@@ -161,6 +168,7 @@
                     bottomPadding = getResources()
                             .getDimensionPixelSize(R.dimen.all_apps_label_bottom_padding);
                     mPaint.setColor(mAllAppsLabelTextColor);
+                    setContentDescription(mAllAppsLabelLayout.getText());
                     break;
                 case NONE:
                 default:
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 6163dad..95c4e25 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -153,6 +153,16 @@
                 context.deviceProfile.widthPx,
                 windowLayoutParams.height
             )
+
+            // if there's an animating bubble add it to the touch region so that it's clickable
+            val animatingBubbleBounds =
+                controllers.bubbleControllers
+                    .getOrNull()
+                    ?.bubbleBarViewController
+                    ?.animatingBubbleBounds
+            if (animatingBubbleBounds != null) {
+                defaultTouchableRegion.op(animatingBubbleBounds, Region.Op.UNION)
+            }
         }
 
         // Pre-calculate insets for different providers across different rotations for this gravity
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index e47640b..3196bfb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -62,6 +62,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks;
 import com.android.launcher3.taskbar.unfold.NonDestroyableScopedUnfoldTransitionProgressProvider;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.DisplayController;
@@ -70,7 +71,6 @@
 import com.android.quickstep.AllAppsActionManager;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.TouchInteractionService;
 import com.android.quickstep.util.AssistUtils;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
@@ -209,15 +209,18 @@
 
     @SuppressLint("WrongConstant")
     public TaskbarManager(
-            TouchInteractionService service, AllAppsActionManager allAppsActionManager) {
+            Context context,
+            AllAppsActionManager allAppsActionManager,
+            TaskbarNavButtonCallbacks navCallbacks) {
+
         Display display =
-                service.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY);
-        mContext = service.createWindowContext(display,
+                context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY);
+        mContext = context.createWindowContext(display,
                 ENABLE_TASKBAR_NAVBAR_UNIFICATION ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL,
                 null);
         mAllAppsActionManager = allAppsActionManager;
         mNavigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
-                ? service.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
+                ? context.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
                 : null;
         if (enableTaskbarNoRecreate()) {
             mWindowManager = mContext.getSystemService(WindowManager.class);
@@ -234,8 +237,11 @@
                 }
             };
         }
-        mNavButtonController = new TaskbarNavButtonController(service,
-                SystemUiProxy.INSTANCE.get(mContext), new Handler(),
+        mNavButtonController = new TaskbarNavButtonController(
+                context,
+                navCallbacks,
+                SystemUiProxy.INSTANCE.get(mContext),
+                new Handler(),
                 AssistUtils.newInstance(mContext));
         mComponentCallbacks = new ComponentCallbacks() {
             private Configuration mOldConfig = mContext.getResources().getConfiguration();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index 03f55ca..ade4649 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -31,6 +31,7 @@
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 
+import android.content.Context;
 import android.os.Bundle;
 import android.os.Handler;
 import android.util.Log;
@@ -47,10 +48,8 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.quickstep.LauncherActivityInterface;
-import com.android.quickstep.OverviewCommandHelper;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskUtils;
-import com.android.quickstep.TouchInteractionService;
 import com.android.quickstep.util.AssistUtils;
 
 import java.io.PrintWriter;
@@ -106,7 +105,8 @@
     private static final int SCREEN_UNPIN_COMBO = BUTTON_BACK | BUTTON_RECENTS;
     private int mLongPressedButtons = 0;
 
-    private final TouchInteractionService mService;
+    private final Context mContext;
+    private final TaskbarNavButtonCallbacks mCallbacks;
     private final SystemUiProxy mSystemUiProxy;
     private final Handler mHandler;
     private final AssistUtils mAssistUtils;
@@ -114,9 +114,14 @@
 
     private final Runnable mResetLongPress = this::resetScreenUnpin;
 
-    public TaskbarNavButtonController(TouchInteractionService service,
-            SystemUiProxy systemUiProxy, Handler handler, AssistUtils assistUtils) {
-        mService = service;
+    public TaskbarNavButtonController(
+            Context context,
+            TaskbarNavButtonCallbacks callbacks,
+            SystemUiProxy systemUiProxy,
+            Handler handler,
+            AssistUtils assistUtils) {
+        mContext = context;
+        mCallbacks = callbacks;
         mSystemUiProxy = systemUiProxy;
         mHandler = handler;
         mAssistUtils = assistUtils;
@@ -286,7 +291,7 @@
             desktopVisibilityController.onHomeActionTriggered();
         }
 
-        mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_HOME);
+        mCallbacks.onNavigateHome();
     }
 
     private void navigateToOverview() {
@@ -295,7 +300,7 @@
         }
         TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle");
         TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
-        mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_TOGGLE);
+        mCallbacks.onToggleOverview();
     }
 
     private void executeBack() {
@@ -310,7 +315,7 @@
         if (longClick) {
             mSystemUiProxy.notifyAccessibilityButtonLongClicked();
         } else {
-            mSystemUiProxy.notifyAccessibilityButtonClicked(mService.getDisplayId());
+            mSystemUiProxy.notifyAccessibilityButtonClicked(mContext.getDisplayId());
         }
     }
 
@@ -333,4 +338,13 @@
     private void showNotifications() {
         mSystemUiProxy.toggleNotificationPanel();
     }
+
+    /** Callbacks for navigation buttons on Taskbar. */
+    public interface TaskbarNavButtonCallbacks {
+        /** Callback invoked when the home button is pressed. */
+        default void onNavigateHome() {}
+
+        /** Callback invoked when the overview button is pressed. */
+        default void onToggleOverview() {}
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 110c30f..5234936 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -104,6 +104,8 @@
      * updates the bounds and accounts for translation.
      */
     private final Rect mBubbleBarBounds = new Rect();
+    /** The bounds of the animating bubble in the coordinate space of the BubbleBarView. */
+    private final Rect mAnimatingBubbleBounds = new Rect();
     // The amount the bubbles overlap when they are stacked in the bubble bar
     private final float mIconOverlapAmount;
     // The spacing between the bubbles when bubble bar is expanded
@@ -120,6 +122,7 @@
     // Whether the bar is expanded (i.e. the bubble activity is being displayed).
     private boolean mIsBarExpanded = false;
     // The currently selected bubble view.
+    @Nullable
     private BubbleView mSelectedBubbleView;
     private BubbleBarLocation mBubbleBarLocation = BubbleBarLocation.DEFAULT;
     // The click listener when the bubble bar is collapsed.
@@ -205,6 +208,7 @@
                 // If the bar was just collapsed and the overflow was the last bubble that was
                 // selected, set the first bubble as selected.
                 if (!mIsBarExpanded && mUpdateSelectedBubbleAfterCollapse != null
+                        && mSelectedBubbleView != null
                         && mSelectedBubbleView.getBubble() instanceof BubbleBarOverflow) {
                     BubbleView firstBubble = (BubbleView) getChildAt(0);
                     mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey());
@@ -458,6 +462,30 @@
         return mBubbleBarBounds;
     }
 
+    /** Returns the bounds of the animating bubble, or {@code null} if no bubble is animating. */
+    @Nullable
+    public Rect getAnimatingBubbleBounds() {
+        if (mIsAnimatingNewBubble) {
+            return mAnimatingBubbleBounds;
+        }
+        return null;
+    }
+
+    /**
+     * Updates the animating bubble bounds. This should be called when the bubble is fully animated
+     * in so that we can include it in taskbar touchable region.
+     *
+     * <p>The bounds are adjusted to the coordinate space of BubbleBarView so that it can be used
+     * by taskbar.
+     */
+    public void updateAnimatingBubbleBounds(int left, int top, int width, int height) {
+        Rect bubbleBarBounds = getBubbleBarBounds();
+        mAnimatingBubbleBounds.left = bubbleBarBounds.left + left;
+        mAnimatingBubbleBounds.top = bubbleBarBounds.top + top;
+        mAnimatingBubbleBounds.right = mAnimatingBubbleBounds.left + width;
+        mAnimatingBubbleBounds.bottom = mAnimatingBubbleBounds.top + height;
+    }
+
     /**
      * Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height
      * respectively. If the value is not in range of 0 to 1 it will be normalized.
@@ -534,6 +562,10 @@
     @Override
     public void removeView(View view) {
         super.removeView(view);
+        if (view == mSelectedBubbleView) {
+            mSelectedBubbleView = null;
+            mBubbleBarBackground.showArrow(false);
+        }
         updateWidth();
     }
 
@@ -575,6 +607,8 @@
         final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
         final boolean animate = getVisibility() == VISIBLE;
         final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
+        // elevation state is opposite to widthState - when expanded all icons are flat
+        float elevationState = (1 - widthState);
         for (int i = 0; i < bubbleCount; i++) {
             BubbleView bv = (BubbleView) getChildAt(i);
             bv.setTranslationY(ty);
@@ -593,17 +627,20 @@
                 expandedX = i * (mIconSize + mExpandedBarIconsSpacing);
                 collapsedX = i == 0 ? 0 : mIconOverlapAmount;
             }
-
+            if (bv == mDraggedBubbleView) {
+                // if bubble is dragged set the elevation to bubble drag elevation
+                bv.setZ(mDragElevation);
+            } else {
+                // otherwise slowly animate elevation while keeping correct Z ordering
+                float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i;
+                bv.setZ(fullElevationForChild * elevationState);
+            }
             if (mIsBarExpanded) {
                 // If bar is on the right, account for bubble bar expanding and shifting left
                 final float expandedBarShift = onLeft ? 0 : currentWidth - expandedWidth;
                 // where the bubble will end up when the animation ends
                 final float targetX = expandedX + expandedBarShift;
                 bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
-                // if we're fully expanded, set the z level to 0 or to bubble elevation if dragged
-                if (widthState == 1f) {
-                    bv.setZ(bv == mDraggedBubbleView ? mBubbleElevation : 0);
-                }
                 // When we're expanded, we're not stacked so we're not behind the stack
                 bv.setBehindStack(false, animate);
                 bv.setAlpha(1);
@@ -612,7 +649,6 @@
                 final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth;
                 final float targetX = collapsedX + collapsedBarShift;
                 bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
-                bv.setZ((MAX_BUBBLES * mBubbleElevation) - i);
                 // If we're not the first bubble we're behind the stack
                 bv.setBehindStack(i > 0, animate);
                 // If we're fully collapsed, hide all bubbles except for the first 2. If there are
@@ -689,8 +725,12 @@
      * Sets which bubble view should be shown as selected.
      */
     public void setSelectedBubble(BubbleView view) {
+        BubbleView previouslySelectedBubble = mSelectedBubbleView;
         mSelectedBubbleView = view;
-        updateArrowForSelected(/* shouldAnimate= */ true);
+        mBubbleBarBackground.showArrow(view != null);
+        // TODO: (b/283309949) remove animation should be implemented first, so than arrow
+        //  animation is adjusted, skip animation for now
+        updateArrowForSelected(previouslySelectedBubble != null);
     }
 
     /**
@@ -715,6 +755,10 @@
         // Find the center of the bubble when it's expanded, set the arrow position to it.
         final float tx = arrowPositionForSelectedWhenExpanded();
         final float currentArrowPosition = mBubbleBarBackground.getArrowPositionX();
+        if (tx == currentArrowPosition) {
+            // arrow position remains unchanged
+            return;
+        }
         if (shouldAnimate && currentArrowPosition > expandedWidth()) {
             Log.d(TAG, "arrow out of bounds of expanded view, skip animation");
             shouldAnimate = false;
@@ -838,10 +882,15 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
-        if (!mIsBarExpanded) {
+        if (!mIsBarExpanded && !mIsAnimatingNewBubble) {
             // When the bar is collapsed, all taps on it should expand it.
             return true;
         }
         return super.onInterceptTouchEvent(ev);
     }
+
+    /** Whether a new bubble is currently animating. */
+    public boolean isAnimatingNewBubble() {
+        return mIsAnimatingNewBubble;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 0e62eaf..3c46f32 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -138,6 +138,15 @@
         if (bubble == null) {
             Log.e(TAG, "bubble click listener, bubble was null");
         }
+
+        if (mBarView.isAnimatingNewBubble()) {
+            mBubbleBarViewAnimator.onBubbleClickedWhileAnimating();
+            mBubbleStashController.showBubbleBarImmediate();
+            setExpanded(true);
+            mBubbleBarController.showAndSelectBubble(bubble);
+            return;
+        }
+
         final String currentlySelected = mBubbleBarController.getSelectedBubbleKey();
         if (mBarView.isExpanded() && Objects.equals(bubble.getKey(), currentlySelected)) {
             // Tapping the currently selected bubble while expanded collapses the view.
@@ -213,6 +222,12 @@
         return mBarView.getBubbleBarBounds();
     }
 
+    /** The bounds of the animating bubble, or {@code null} if no bubble is animating. */
+    @Nullable
+    public Rect getAnimatingBubbleBounds() {
+        return mBarView.getAnimatingBubbleBounds();
+    }
+
     /** The horizontal margin of the bubble bar from the edge of the screen. */
     public int getHorizontalMargin() {
         return mBarView.getHorizontalMargin();
@@ -373,7 +388,7 @@
 
             boolean isInApp = mTaskbarStashController.isInApp();
             // only animate the new bubble if we're in an app and not auto expanding
-            if (b instanceof BubbleBarBubble && isInApp && !isExpanding) {
+            if (b instanceof BubbleBarBubble && isInApp && !isExpanding && !isExpanded()) {
                 mBubbleBarViewAnimator.animateBubbleInForStashed((BubbleBarBubble) b);
             }
         } else {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 76d86de..bea0af8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -347,7 +347,7 @@
                 hotseatCellHeight - mUnstashedHeight) / 2;
     }
 
-    float getBubbleBarTranslationY() {
+    public float getBubbleBarTranslationY() {
         // If we're on home, adjust the translation so the bubble bar aligns with hotseat.
         // Otherwise we're either showing in an app or in overview. In either case adjust it so
         // the bubble bar aligns with the taskbar.
@@ -374,4 +374,19 @@
     public PhysicsAnimator<View> getStashedHandlePhysicsAnimator() {
         return mHandleViewController.getPhysicsAnimator();
     }
+
+    /** Notifies taskbar that it should update its touchable region. */
+    public void updateTaskbarTouchRegion() {
+        mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
+    }
+
+    /** Shows the bubble bar immediately without animation. */
+    public void showBubbleBarImmediate() {
+        mHandleViewController.setTranslationYForSwipe(0);
+        mIconTranslationYForStash.updateValue(getBubbleBarTranslationY());
+        mIconAlphaForStash.setValue(1);
+        mIconScaleForStash.updateValue(1);
+        mIsStashed = false;
+        onIsStashedChanged();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 2d8983f..da36944 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -35,6 +35,8 @@
     private val scheduler: Scheduler = HandlerScheduler(bubbleBarView)
 ) {
 
+    private var animatingBubble: AnimatingBubble? = null
+
     private companion object {
         /** The time to show the flyout. */
         const val FLYOUT_DELAY_MS: Long = 2500
@@ -54,26 +56,40 @@
         const val BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y = -20f
     }
 
+    /** Wrapper around the animating bubble with its show and hide animations. */
+    private data class AnimatingBubble(
+        val bubbleView: BubbleView,
+        val showAnimation: Runnable,
+        val hideAnimation: Runnable
+    )
+
     /** An interface for scheduling jobs. */
     interface Scheduler {
 
         /** Schedule the given [block] to run. */
-        fun post(block: () -> Unit)
+        fun post(block: Runnable)
 
         /** Schedule the given [block] to start with a delay of [delayMillis]. */
-        fun postDelayed(delayMillis: Long, block: () -> Unit)
+        fun postDelayed(delayMillis: Long, block: Runnable)
+
+        /** Cancel the given [block] if it hasn't started yet. */
+        fun cancel(block: Runnable)
     }
 
     /** A [Scheduler] that uses a Handler to run jobs. */
     private class HandlerScheduler(private val view: View) : Scheduler {
 
-        override fun post(block: () -> Unit) {
+        override fun post(block: Runnable) {
             view.post(block)
         }
 
-        override fun postDelayed(delayMillis: Long, block: () -> Unit) {
+        override fun postDelayed(delayMillis: Long, block: Runnable) {
             view.postDelayed(block, delayMillis)
         }
+
+        override fun cancel(block: Runnable) {
+            view.removeCallbacks(block)
+        }
     }
 
     private val springConfig =
@@ -91,6 +107,7 @@
         // and the second part hides it after a delay.
         val showAnimation = buildShowAnimation(bubbleView, b.key)
         val hideAnimation = buildHideAnimation(bubbleView)
+        animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
         scheduler.post(showAnimation)
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
@@ -113,7 +130,7 @@
     private fun buildShowAnimation(
         bubbleView: BubbleView,
         key: String,
-    ): () -> Unit = {
+    ) = Runnable {
         bubbleBarView.prepareForAnimatingBubbleWhileStashed(key)
         // calculate the initial translation x the bubble should have in order to align it with the
         // stash handle.
@@ -140,7 +157,7 @@
 
                     // map the path [0, BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y] to [0,1]
                     val fraction = ty / BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y
-                    target.alpha = 1 - fraction / 2
+                    target.alpha = 1 - fraction
                 }
                 ty >= totalTranslationY -> {
                     // this is the second leg of the animation. the handle should be completely
@@ -173,6 +190,16 @@
                 }
             }
         }
+        animator.addEndListener { _, _, _, _, _, _, _ ->
+            // the bubble is now fully settled in. make it touchable
+            bubbleBarView.updateAnimatingBubbleBounds(
+                bubbleView.left,
+                bubbleView.top,
+                bubbleView.width,
+                bubbleView.height
+            )
+            bubbleStashController.updateTaskbarTouchRegion()
+        }
         animator.start()
     }
 
@@ -189,7 +216,7 @@
      * 1. In the second part the bubble is fully hidden and the handle animates in.
      * 1. The third part is the overshoot. The handle is made fully visible.
      */
-    private fun buildHideAnimation(bubbleView: BubbleView): () -> Unit = {
+    private fun buildHideAnimation(bubbleView: BubbleView) = Runnable {
         // this is the total distance that both the stashed handle and the bubble will be traveling
         val totalTranslationY =
             BUBBLE_ANIMATION_BUBBLE_TRANSLATION_Y + BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y
@@ -230,6 +257,7 @@
             }
         }
         animator.addEndListener { _, _, _, _, _, _, _ ->
+            animatingBubble = null
             bubbleView.alpha = 0f
             bubbleView.translationY = 0f
             bubbleView.scaleY = 1f
@@ -237,9 +265,18 @@
                 bubbleBarView.alpha = 0f
             }
             bubbleBarView.onAnimatingBubbleCompleted()
+            bubbleStashController.updateTaskbarTouchRegion()
         }
         animator.start()
     }
+
+    /** Handles clicking on the animating bubble while the animation is still playing. */
+    fun onBubbleClickedWhileAnimating() {
+        val hideAnimation = animatingBubble?.hideAnimation ?: return
+        scheduler.cancel(hideAnimation)
+        bubbleBarView.onAnimatingBubbleCompleted()
+        animatingBubble = null
+    }
 }
 
 /** The X position in screen coordinates of the center of the bubble. */
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index 79b09fd..225b127 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -220,6 +220,11 @@
                 }
             });
         }
+
+        @Override
+        public void setTriggerBack(boolean triggerBack) {
+            // TODO(b/261654570): track touch from the Launcher process.
+        }
     }
 
     private static class RemoteAnimationRunnerStub extends IRemoteAnimationRunner.Stub {
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index a842b51..832f4e1 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -94,6 +94,7 @@
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarManager;
+import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -470,6 +471,18 @@
 
     private final ScreenOnTracker.ScreenOnListener mScreenOnListener = this::onScreenOnChanged;
 
+    private final TaskbarNavButtonCallbacks mNavCallbacks = new TaskbarNavButtonCallbacks() {
+        @Override
+        public void onNavigateHome() {
+            mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HOME);
+        }
+
+        @Override
+        public void onToggleOverview() {
+            mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_TOGGLE);
+        }
+    };
+
     private ActivityManagerWrapper mAM;
     private OverviewCommandHelper mOverviewCommandHelper;
     private OverviewComponentObserver mOverviewComponentObserver;
@@ -500,7 +513,7 @@
         mDeviceState = new RecentsAnimationDeviceState(this, true);
         mAllAppsActionManager = new AllAppsActionManager(
                 this, UI_HELPER_EXECUTOR, this::createAllAppsPendingIntent);
-        mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager);
+        mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager, mNavCallbacks);
         mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
         mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer();
 
diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
index 9df568e..2a27dea 100644
--- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
+++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
@@ -136,21 +136,16 @@
             });
         }
 
-        if (!ALL_APPS_VISITED_COUNT.hasReachedMax(launcher)) {
+        if (!Utilities.isRunningInTestHarness()) {
             launcher.getStateManager().addStateListener(new StateListener<LauncherState>() {
                 @Override
                 public void onStateTransitionComplete(LauncherState finalState) {
                     if (finalState == ALL_APPS) {
                         ALL_APPS_VISITED_COUNT.increment(launcher);
-                        return;
                     }
-
-                    boolean hasReachedMaxCount = ALL_APPS_VISITED_COUNT.hasReachedMax(launcher);
-                    launcher.getAppsView().getFloatingHeaderView().findFixedRowByType(
-                            AppsDividerView.class).setShowAllAppsLabel(!hasReachedMaxCount);
-                    if (hasReachedMaxCount) {
-                        launcher.getStateManager().removeStateListener(this);
-                    }
+                    launcher.getAppsView().getFloatingHeaderView()
+                            .findFixedRowByType(AppsDividerView.class)
+                            .setShowAllAppsLabel(!ALL_APPS_VISITED_COUNT.hasReachedMax(launcher));
                 }
             });
         }
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index b62057f..a0ec525 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -38,7 +38,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.desktop.DesktopRecentsTransitionController;
@@ -292,16 +291,7 @@
 
     @Override
     protected void setThumbnailOrientation(RecentsOrientedState orientationState) {
-        DeviceProfile deviceProfile = mContainer.getDeviceProfile();
-        int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
-
-        LayoutParams snapshotParams = (LayoutParams) mTaskThumbnailViewDeprecated.getLayoutParams();
-        snapshotParams.topMargin = thumbnailTopMargin;
-
-        for (int i = 0; i < mSnapshotViewMap.size(); i++) {
-            TaskThumbnailViewDeprecated thumbnailView = mSnapshotViewMap.valueAt(i);
-            thumbnailView.setLayoutParams(snapshotParams);
-        }
+        // no-op
     }
 
     @Override
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
index 58be345..0f06d98 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
@@ -12,10 +12,10 @@
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_IME_SWITCH;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.SCREEN_PIN_LONG_PRESS_THRESHOLD;
-import static com.android.quickstep.OverviewCommandHelper.TYPE_HOME;
-import static com.android.quickstep.OverviewCommandHelper.TYPE_TOGGLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
@@ -31,7 +31,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.logging.StatsLogManager;
-import com.android.quickstep.OverviewCommandHelper;
+import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TouchInteractionService;
 import com.android.quickstep.util.AssistUtils;
@@ -52,8 +52,6 @@
     @Mock
     TouchInteractionService mockService;
     @Mock
-    OverviewCommandHelper mockCommandHelper;
-    @Mock
     Handler mockHandler;
     @Mock
     AssistUtils mockAssistUtils;
@@ -68,13 +66,26 @@
     @Mock
     View mockView;
 
+    private int mHomePressCount;
+    private int mOverviewToggleCount;
+    private final TaskbarNavButtonCallbacks mCallbacks = new TaskbarNavButtonCallbacks() {
+        @Override
+        public void onNavigateHome() {
+            mHomePressCount++;
+        }
+
+        @Override
+        public void onToggleOverview() {
+            mOverviewToggleCount++;
+        }
+    };
+
     private TaskbarNavButtonController mNavButtonController;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
         when(mockService.getDisplayId()).thenReturn(DISPLAY_ID);
-        when(mockService.getOverviewCommandHelper()).thenReturn(mockCommandHelper);
         when(mockService.getApplicationContext())
                 .thenReturn(InstrumentationRegistry.getInstrumentation().getTargetContext()
                         .getApplicationContext());
@@ -82,8 +93,12 @@
         when(mockTaskbarControllers.getTaskbarActivityContext())
                 .thenReturn(mockTaskbarActivityContext);
         doReturn(mockStatsLogManager).when(mockTaskbarActivityContext).getStatsLogManager();
-        mNavButtonController = new TaskbarNavButtonController(mockService,
-                mockSystemUiProxy, mockHandler, mockAssistUtils);
+        mNavButtonController = new TaskbarNavButtonController(
+                mockService,
+                mCallbacks,
+                mockSystemUiProxy,
+                mockHandler,
+                mockAssistUtils);
     }
 
     @Test
@@ -154,20 +169,20 @@
     @Test
     public void testPressHome() {
         mNavButtonController.onButtonClick(BUTTON_HOME, mockView);
-        verify(mockCommandHelper, times(1)).addCommand(TYPE_HOME);
+        assertThat(mHomePressCount).isEqualTo(1);
     }
 
     @Test
     public void testPressRecents() {
         mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView);
-        verify(mockCommandHelper, times(1)).addCommand(TYPE_TOGGLE);
+        assertThat(mOverviewToggleCount).isEqualTo(1);
     }
 
     @Test
-    public void testPressRecentsWithScreenPinned() {
+    public void testPressRecentsWithScreenPinned_noNavigationToOverview() {
         mNavButtonController.updateSysuiFlags(SYSUI_STATE_SCREEN_PINNING);
         mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView);
-        verify(mockCommandHelper, times(0)).addCommand(TYPE_TOGGLE);
+        assertThat(mOverviewToggleCount).isEqualTo(0);
     }
 
     @Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index d90e048..3d8484d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -130,18 +130,90 @@
         assertThat(handle.translationY).isEqualTo(0)
     }
 
-    private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
+    @Test
+    fun animateBubbleInForStashed_tapAnimatingBubble() {
+        lateinit var overflowView: BubbleView
+        lateinit var bubbleView: BubbleView
+        lateinit var bubble: BubbleBarBubble
+        val bubbleBarView = BubbleBarView(context)
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
+            val inflater = LayoutInflater.from(context)
 
-        var delayedBlock: (() -> Unit)? = null
-            private set
+            val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
+            overflowView =
+                inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
+            overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
+            bubbleBarView.addView(overflowView)
 
-        override fun post(block: () -> Unit) {
-            block.invoke()
+            val bubbleInfo = BubbleInfo("key", 0, null, null, 0, context.packageName, null, false)
+            bubbleView =
+                inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
+            bubble =
+                BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
+            bubbleView.setBubble(bubble)
+            bubbleBarView.addView(bubbleView)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+        val bubbleStashController = mock<BubbleStashController>()
+        whenever(bubbleStashController.isStashed).thenReturn(true)
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble)
         }
 
-        override fun postDelayed(delayMillis: Long, block: () -> Unit) {
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY).isEqualTo(-70)
+        assertThat(overflowView.visibility).isEqualTo(INVISIBLE)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleView.alpha).isEqualTo(1)
+        assertThat(bubbleView.translationY).isEqualTo(-20)
+        assertThat(bubbleView.scaleY).isEqualTo(1)
+
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        animator.onBubbleClickedWhileAnimating()
+
+        assertThat(animatorScheduler.delayedBlock).isNull()
+        assertThat(overflowView.visibility).isEqualTo(VISIBLE)
+        assertThat(overflowView.alpha).isEqualTo(1)
+        assertThat(bubbleView.alpha).isEqualTo(1)
+        assertThat(bubbleView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.background).isNotNull()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+    }
+
+    private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
+
+        var delayedBlock: Runnable? = null
+            private set
+
+        override fun post(block: Runnable) {
+            block.run()
+        }
+
+        override fun postDelayed(delayMillis: Long, block: Runnable) {
             check(delayedBlock == null) { "there is already a pending block waiting to run" }
             delayedBlock = block
         }
+
+        override fun cancel(block: Runnable) {
+            check(delayedBlock == block) { "the pending block does not match the canceled block" }
+            delayedBlock = null
+        }
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java
index ca5fb70..fc757b4 100644
--- a/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java
@@ -55,7 +55,7 @@
                 "com.android.launcher3.testcomponent.BaseTestingActivity");
         mLauncherLayout = TestUtil.setLauncherDefaultLayout(mTargetContext, layoutBuilder);
         AbstractLauncherUiTest.initialize(this);
-        startAppFastInFullscreen(CALCULATOR_APP_PACKAGE);
+        startAppFast(CALCULATOR_APP_PACKAGE);
         mLauncher.enableBlockTimeout(true);
         mLauncher.showTaskbarIfHidden();
     }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
index edc0a6f..7708233 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
@@ -123,6 +123,21 @@
         psContainer.verifyInstalledAppIsPresent(INSTALLED_APP_NAME);
     }
 
+    @Test
+    @ScreenRecordRule.ScreenRecord // b/334946529
+    public void testPrivateSpaceAppLongPressUninstallMenu() throws IOException {
+        // Ensure that the App is not installed in main user otherwise, it may not be found in
+        // PS container.
+        TestUtil.uninstallDummyApp();
+        // Install the app in Private Profile
+        TestUtil.installDummyAppForUser(mProfileUserId);
+        waitForLauncherUIUpdate();
+        // Scroll to the bottom of All Apps
+        executeOnLauncher(launcher -> launcher.getAppsView().resetAndScrollToPrivateSpaceHeader());
+        // Get the "uninstall" menu item.
+        mLauncher.getAllApps().getAppIcon(INSTALLED_APP_NAME).openMenu().getMenuItem("Uninstall");
+    }
+
     private void waitForPrivateSpaceSetup() {
         waitForLauncherCondition("Private Profile not setup",
                 launcher -> launcher.getAppsView().hasPrivateProfile(),
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
index 7ab72f2..43ebb17 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
@@ -65,7 +65,7 @@
     public void setUp() throws Exception {
         Assume.assumeTrue(mLauncher.isTablet());
         super.setUp();
-        startAppFastInFullscreen(CALCULATOR_APP_PACKAGE);
+        startAppFast(CALCULATOR_APP_PACKAGE);
         startTestActivity(2);
     }
 
diff --git a/res/layout/private_space_header.xml b/res/layout/private_space_header.xml
index 84554e4..cefe394 100644
--- a/res/layout/private_space_header.xml
+++ b/res/layout/private_space_header.xml
@@ -25,7 +25,9 @@
         android:clipToOutline="true"
         android:gravity="center_vertical"
         android:textDirection="locale"
-        android:orientation="horizontal">
+        android:orientation="horizontal"
+        android:contentDescription="@string/ps_container_lock_button_content_description"
+        android:importantForAccessibility="yes">
 
     <LinearLayout
         android:id="@+id/settingsAndLockGroup"
@@ -49,7 +51,7 @@
             android:background="@drawable/ps_lock_background"
             android:gravity="center_vertical"
             android:layout_marginEnd="@dimen/ps_lock_button_margin_end"
-            android:contentDescription="@string/ps_container_lock_unlock_button">
+            android:contentDescription="@string/ps_container_lock_button_content_description">
             <ImageView
                 android:id="@+id/lock_icon"
                 android:layout_width="@dimen/ps_lock_icon_size"
@@ -95,6 +97,7 @@
         android:gravity="center_vertical"
         android:layout_marginStart="@dimen/ps_header_layout_margin"
         android:text="@string/ps_container_title"
-        android:theme="@style/PrivateSpaceHeaderTextStyle"/>
+        android:theme="@style/PrivateSpaceHeaderTextStyle"
+        android:importantForAccessibility="no"/>
 
 </RelativeLayout>
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index f16c69b..31def04 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -494,7 +494,7 @@
 
     <!-- Private Space parameters -->
     <dimen name="ps_container_corner_radius">24dp</dimen>
-    <dimen name="ps_header_height">64dp</dimen>
+    <dimen name="ps_header_height">72dp</dimen>
     <dimen name="ps_header_relative_layout_height">48dp</dimen>
     <dimen name="ps_header_image_height">48dp</dimen>
     <dimen name="ps_header_text_height">24dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index e1c7d64..da5b709 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -487,8 +487,10 @@
     <string name="ps_container_title">Private</string>
     <!-- Description for Private Space Settings button -->
     <string name="ps_container_settings">Private Space Settings</string>
-    <!-- Description for Private Space Lock/Unlock button -->
-    <string name="ps_container_lock_unlock_button">Lock/Unlock Private Space</string>
+    <!-- Description for Private Space Unlock button -->
+    <string name="ps_container_unlock_button_content_description">Private, unlocked.</string>
+    <!-- Description for Private Space Lock button -->
+    <string name="ps_container_lock_button_content_description">Private, locked.</string>
     <string name="ps_container_lock_title">Lock</string>
     <!-- Description for Private Space Transition button -->
     <string name="ps_container_transition">Private Space Transitioning</string>
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 1c23644..cc9f08e 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -485,7 +485,10 @@
         super.onVisibilityAggregated(isVisible);
     }
 
-    protected boolean isPageInTransition() {
+    /**
+     * Returns true if the page is in the middle of transition to another page
+     */
+    public boolean isPageInTransition() {
         return mIsPageInTransition;
     }
 
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index 7e975df..551fa94 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -112,6 +112,8 @@
     private Runnable mOnPSHeaderAdded;
     @Nullable
     private RelativeLayout mPSHeader;
+    private final String mLockedStateContentDesc;
+    private final String mUnLockedStateContentDesc;
 
     public PrivateProfileManager(UserManager userManager,
             ActivityAllAppsContainerView<?> allApps,
@@ -125,6 +127,10 @@
         UI_HELPER_EXECUTOR.post(() -> initializeInBackgroundThread(appContext));
         mPsHeaderHeight = mAllApps.getContext().getResources().getDimensionPixelSize(
                 R.dimen.ps_header_height);
+        mLockedStateContentDesc = mAllApps.getContext()
+                .getString(R.string.ps_container_lock_button_content_description);
+        mUnLockedStateContentDesc = mAllApps.getContext()
+                .getString(R.string.ps_container_unlock_button_content_description);
     }
 
     /** Adds Private Space Header to the layout. */
@@ -398,11 +404,13 @@
                 lockText.setVisibility(VISIBLE);
                 lockButton.setVisibility(VISIBLE);
                 lockButton.setOnClickListener(view -> lockingAction(/* lock */ true));
+                lockButton.setContentDescription(mUnLockedStateContentDesc);
             }
             case STATE_DISABLED -> {
                 lockText.setVisibility(GONE);
                 lockButton.setVisibility(VISIBLE);
                 lockButton.setOnClickListener(view -> lockingAction(/* lock */ false));
+                lockButton.setContentDescription(mLockedStateContentDesc);
             }
             default -> lockButton.setVisibility(GONE);
         }
@@ -412,9 +420,14 @@
         if (getCurrentState() == STATE_DISABLED) {
             header.setOnClickListener(view -> lockingAction(/* lock */ false));
             header.setClickable(true);
+            // Add header as accessibility target when disabled.
+            header.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+            header.setContentDescription(mLockedStateContentDesc);
         } else {
             header.setOnClickListener(null);
             header.setClickable(false);
+            // Remove header from accessibility target when enabled.
+            header.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
         }
     }
 
diff --git a/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
index 456cde8..480e8f3 100644
--- a/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
+++ b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
@@ -246,7 +246,8 @@
 
     protected void setCurrentItem(T item) {
         mCurrentItem = item;
-        mShift = 0;
+        // Set it to end value directly to skip the animation for outline
+        mShift = Flags.enableFocusOutline() ? 1 : 0;
         mTargetItem = null;
     }
 
diff --git a/src/com/android/launcher3/keyboard/ViewGroupFocusHelper.java b/src/com/android/launcher3/keyboard/ViewGroupFocusHelper.java
index 4653bf1..21d157a 100644
--- a/src/com/android/launcher3/keyboard/ViewGroupFocusHelper.java
+++ b/src/com/android/launcher3/keyboard/ViewGroupFocusHelper.java
@@ -19,6 +19,7 @@
 import android.graphics.Rect;
 import android.view.View;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.PagedView;
 
 /**
@@ -35,6 +36,28 @@
     }
 
     @Override
+    protected boolean shouldDraw(View item) {
+        if (Flags.enableFocusOutline()) {
+            // Not draw outline in page transition because the outline just remains fully
+            // persistent during the transition and does not look smooth
+            return super.shouldDraw(item) && !isInPageTransition(item);
+        } else {
+            return super.shouldDraw(item);
+        }
+    }
+
+    private boolean isInPageTransition(View view) {
+        if (view == null || !(view.getParent() instanceof View)) {
+            return false;
+        }
+        boolean isInTransition = false;
+        if (view instanceof PagedView) {
+            isInTransition = ((PagedView<?>) view).isPageInTransition();
+        }
+        return isInTransition || isInPageTransition((View) view.getParent());
+    }
+
+    @Override
     public void viewToRect(View v, Rect outRect) {
         // Using FocusedRect here allows views to provide their custom rect for drawing outline,
         // e.g. making the Rect bigger than the content to leave some padding between view and
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 28eeb10..b3fcefa 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
+import static com.android.launcher3.widget.picker.WidgetsListAdapter.VIEW_TYPE_WIDGETS_LIST;
 
 import android.animation.Animator;
 import android.content.Context;
@@ -1094,10 +1095,35 @@
                 default:
                     break;
             }
-            mWidgetsListItemAnimator = new DefaultItemAnimator();
+            mWidgetsListItemAnimator = new DefaultItemAnimator() {
+                @Override
+                public boolean animateChange(RecyclerView.ViewHolder oldHolder,
+                        RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft,
+                        int toTop) {
+                    // As we expand an item, the content / widgets list that appears (with add
+                    // event) also gets change events as its previews load asynchronously. The
+                    // super implementation of animateChange cancels the animations on it - breaking
+                    // the "add animation". Instead, here, we skip "change" animation for content
+                    // list - because we want it to either appear or disappear. And, the previews
+                    // themselves have their own animation when loaded, so, we don't need change
+                    // animations for them anyway. Below, we do-nothing.
+                    if (oldHolder.getItemViewType() == VIEW_TYPE_WIDGETS_LIST) {
+                        dispatchChangeStarting(oldHolder, true);
+                        dispatchChangeFinished(oldHolder, true);
+                        return true;
+                    }
+                    return super.animateChange(oldHolder, newHolder, fromLeft, fromTop, toLeft,
+                            toTop);
+                }
+            };
             // Disable change animations because it disrupts the item focus upon adapter item
             // change.
             mWidgetsListItemAnimator.setSupportsChangeAnimations(false);
+            // Make the moves a bit faster, so that the amount of time for which user sees the
+            // bottom-sheet background before "add" animation starts is less making it smoother.
+            mWidgetsListItemAnimator.setChangeDuration(90);
+            mWidgetsListItemAnimator.setMoveDuration(90);
+            mWidgetsListItemAnimator.setAddDuration(300);
         }
 
         private int getEmptySpaceHeight() {
diff --git a/tests/src/com/android/launcher3/dragging/TaplDragTest.java b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
index d64d096..1c41ded 100644
--- a/tests/src/com/android/launcher3/dragging/TaplDragTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
@@ -21,8 +21,6 @@
 import static com.android.launcher3.util.TestConstants.AppNames.PHOTOS_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.STORE_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -43,8 +41,6 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.util.TestUtil;
-import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
-import com.android.launcher3.util.rule.TestStabilityRule;
 
 import org.junit.Test;
 
@@ -67,9 +63,6 @@
      */
     @Test
     @PortraitLandscape
-    @ScreenRecord
-    // Staging; will be promoted to presubmit if stable
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
     @PlatinumTest(focusArea = "launcher")
     public void testDragToFolder() {
         // TODO: add the use case to drag an icon to an existing folder. Currently it either fails
diff --git a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
index c2e73fc..6bbcf85 100644
--- a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
+++ b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -24,18 +24,29 @@
 import android.content.pm.PackageInstaller
 import android.content.pm.ShortcutInfo
 import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
 import android.util.LongSparseArray
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.Utilities
 import com.android.launcher3.Utilities.EMPTY_PERSON_ARRAY
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError.Companion.MISSING_INFO
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError.Companion.MISSING_WIDGET_PROVIDER
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError.Companion.PROFILE_DELETED
 import com.android.launcher3.model.data.FolderInfo
 import com.android.launcher3.model.data.IconRequestInfo
 import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
+import com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_UI_NOT_READY
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.shortcuts.ShortcutKey
@@ -43,11 +54,14 @@
 import com.android.launcher3.util.PackageManagerHelper
 import com.android.launcher3.util.PackageUserKey
 import com.android.launcher3.util.UserIconInfo
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
 import com.android.launcher3.widget.WidgetInflater
+import com.android.launcher3.widget.WidgetSections
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Before
 import org.junit.Test
+import org.mockito.ArgumentCaptor
 import org.mockito.Mock
 import org.mockito.Mockito.RETURNS_DEEP_STUBS
 import org.mockito.Mockito.mock
@@ -56,8 +70,10 @@
 import org.mockito.kotlin.any
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
 
 class WorkspaceItemProcessorTest {
 
@@ -73,29 +89,31 @@
     @Mock private lateinit var mockUserManagerState: UserManagerState
     @Mock private lateinit var mockWidgetInflater: WidgetInflater
 
-    private lateinit var intent: Intent
-    private lateinit var userHandle: UserHandle
-    private lateinit var iconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>>
-    private lateinit var componentName: ComponentName
-    private lateinit var unlockedUsersArray: LongSparseArray<Boolean>
-    private lateinit var keyToPinnedShortcutsMap: MutableMap<ShortcutKey, ShortcutInfo>
-    private lateinit var installingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo>
-    private lateinit var allDeepShortcuts: MutableList<ShortcutInfo>
+    private var intent: Intent = Intent()
+    private var mUserHandle: UserHandle = UserHandle(0)
+    private var mIconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>> = mutableListOf()
+    private var mComponentName: ComponentName = ComponentName("package", "class")
+    private var mUnlockedUsersArray: LongSparseArray<Boolean> = LongSparseArray()
+    private var mKeyToPinnedShortcutsMap: MutableMap<ShortcutKey, ShortcutInfo> = mutableMapOf()
+    private var mInstallingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo> = hashMapOf()
+    private var mAllDeepShortcuts: MutableList<ShortcutInfo> = mutableListOf()
+    private var mWidgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?> =
+        mutableMapOf()
+    private var mPendingPackages: MutableSet<PackageUserKey> = mutableSetOf()
 
     private lateinit var itemProcessorUnderTest: WorkspaceItemProcessor
 
     @Before
     fun setup() {
-        userHandle = UserHandle(0)
+        mUserHandle = UserHandle(0)
         mockIconRequestInfo = mock<IconRequestInfo<WorkspaceItemInfo>>()
-        iconRequestInfos = mutableListOf(mockIconRequestInfo)
         mockWorkspaceInfo = mock<WorkspaceItemInfo>()
         mockBgDataModel = mock<BgDataModel>()
-        componentName = ComponentName("package", "class")
-        unlockedUsersArray = LongSparseArray<Boolean>(1).apply { put(101, true) }
+        mComponentName = ComponentName("package", "class")
+        mUnlockedUsersArray = LongSparseArray<Boolean>(1).apply { put(101, true) }
         intent =
             Intent().apply {
-                component = componentName
+                component = mComponentName
                 `package` = "pkg"
                 putExtra(ShortcutKey.EXTRA_SHORTCUT_ID, "")
             }
@@ -112,17 +130,17 @@
             }
         mockPmHelper =
             mock<PackageManagerHelper>().apply {
-                whenever(getAppLaunchIntent(componentName.packageName, userHandle))
+                whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
                     .thenReturn(intent)
             }
         mockLauncherApps =
             mock<LauncherApps>().apply {
-                whenever(isPackageEnabled("package", userHandle)).thenReturn(true)
-                whenever(isActivityEnabled(componentName, userHandle)).thenReturn(true)
+                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
+                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(true)
             }
         mockCursor =
             mock(LoaderCursor::class.java, RETURNS_DEEP_STUBS).apply {
-                user = userHandle
+                user = mUserHandle
                 itemType = ITEM_TYPE_APPLICATION
                 id = 1
                 restoreFlag = 1
@@ -137,15 +155,18 @@
         mockUserCache =
             mock<UserCache>().apply {
                 val userIconInfo =
-                    mock<UserIconInfo>().apply() { whenever(isPrivate).thenReturn(false) }
+                    mock<UserIconInfo>().apply { whenever(isPrivate).thenReturn(false) }
                 whenever(getUserInfo(any())).thenReturn(userIconInfo)
             }
 
         mockUserManagerState = mock<UserManagerState>()
         mockWidgetInflater = mock<WidgetInflater>()
-        keyToPinnedShortcutsMap = mutableMapOf()
-        installingPkgs = hashMapOf()
-        allDeepShortcuts = mutableListOf()
+        mKeyToPinnedShortcutsMap = mutableMapOf()
+        mInstallingPkgs = hashMapOf()
+        mAllDeepShortcuts = mutableListOf()
+        mWidgetProvidersMap = mutableMapOf()
+        mIconRequestInfos = mutableListOf()
+        mPendingPackages = mutableSetOf()
     }
 
     /**
@@ -159,18 +180,18 @@
         userCache: UserCache = mockUserCache,
         userManagerState: UserManagerState = mockUserManagerState,
         launcherApps: LauncherApps = mockLauncherApps,
-        shortcutKeyToPinnedShortcuts: Map<ShortcutKey, ShortcutInfo> = keyToPinnedShortcutsMap,
+        shortcutKeyToPinnedShortcuts: Map<ShortcutKey, ShortcutInfo> = mKeyToPinnedShortcutsMap,
         app: LauncherAppState = mockAppState,
         bgDataModel: BgDataModel = mockBgDataModel,
-        widgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?> = mutableMapOf(),
+        widgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?> = mWidgetProvidersMap,
         widgetInflater: WidgetInflater = mockWidgetInflater,
         pmHelper: PackageManagerHelper = mockPmHelper,
-        iconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>> = mutableListOf(),
+        iconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>> = mIconRequestInfos,
         isSdCardReady: Boolean = false,
-        pendingPackages: MutableSet<PackageUserKey> = mutableSetOf(),
-        unlockedUsers: LongSparseArray<Boolean> = unlockedUsersArray,
-        installingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo> = hashMapOf(),
-        allDeepShortcuts: MutableList<ShortcutInfo> = mutableListOf()
+        pendingPackages: MutableSet<PackageUserKey> = mPendingPackages,
+        unlockedUsers: LongSparseArray<Boolean> = mUnlockedUsersArray,
+        installingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo> = mInstallingPkgs,
+        allDeepShortcuts: MutableList<ShortcutInfo> = mAllDeepShortcuts
     ) =
         WorkspaceItemProcessor(
             c = cursor,
@@ -241,8 +262,8 @@
     fun `When app has empty String target package then mark deleted`() {
 
         // Given
-        componentName = ComponentName("", "")
-        intent.component = componentName
+        mComponentName = ComponentName("", "")
+        intent.component = mComponentName
         intent.`package` = ""
 
         // When
@@ -267,7 +288,7 @@
             .isEqualTo(0)
         // currently gets marked restored twice, although markRestore() has check for restoreFlag
         verify(mockCursor, times(2)).markRestored()
-        assertThat(iconRequestInfos).containsExactly(mockIconRequestInfo)
+        assertThat(mIconRequestInfos).containsExactly(mockIconRequestInfo)
         verify(mockCursor).checkAndAddItem(mockWorkspaceInfo, mockBgDataModel, null)
     }
 
@@ -277,12 +298,12 @@
         // Given
         mockLauncherApps =
             mock<LauncherApps>().apply {
-                whenever(isPackageEnabled("package", userHandle)).thenReturn(true)
-                whenever(isActivityEnabled(componentName, userHandle)).thenReturn(false)
+                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
+                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(false)
             }
         mockPmHelper =
             mock<PackageManagerHelper>().apply {
-                whenever(getAppLaunchIntent(componentName.packageName, userHandle))
+                whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
                     .thenReturn(intent)
             }
 
@@ -295,7 +316,7 @@
             .that(mockCursor.restoreFlag)
             .isEqualTo(0)
         verify(mockCursor.updater().put(Favorites.INTENT, intent.toUri(0))).commit()
-        assertThat(iconRequestInfos).containsExactly(mockIconRequestInfo)
+        assertThat(mIconRequestInfos).containsExactly(mockIconRequestInfo)
         verify(mockCursor).checkAndAddItem(mockWorkspaceInfo, mockBgDataModel, null)
     }
 
@@ -305,12 +326,13 @@
         // Given
         mockLauncherApps =
             mock<LauncherApps>().apply {
-                whenever(isPackageEnabled("package", userHandle)).thenReturn(true)
-                whenever(isActivityEnabled(componentName, userHandle)).thenReturn(false)
+                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
+                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(false)
             }
         mockPmHelper =
             mock<PackageManagerHelper>().apply {
-                whenever(getAppLaunchIntent(componentName.packageName, userHandle)).thenReturn(null)
+                whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
+                    .thenReturn(null)
             }
 
         // When
@@ -349,20 +371,20 @@
                 whenever(persons).thenReturn(EMPTY_PERSON_ARRAY)
             }
         val shortcutKey = ShortcutKey.fromIntent(intent, mockCursor.user)
-        keyToPinnedShortcutsMap[shortcutKey] = expectedShortcutInfo
-        iconRequestInfos = mutableListOf()
+        mKeyToPinnedShortcutsMap[shortcutKey] = expectedShortcutInfo
+        mIconRequestInfos = mutableListOf()
 
         // When
         itemProcessorUnderTest =
-            createWorkspaceItemProcessorUnderTest(allDeepShortcuts = allDeepShortcuts)
+            createWorkspaceItemProcessorUnderTest(allDeepShortcuts = mAllDeepShortcuts)
         itemProcessorUnderTest.processItem()
 
         // Then
         assertWithMessage("item restoreFlag should be set to 0")
             .that(mockCursor.restoreFlag)
             .isEqualTo(0)
-        assertThat(iconRequestInfos).isEmpty()
-        assertThat(allDeepShortcuts).containsExactly(expectedShortcutInfo)
+        assertThat(mIconRequestInfos).isEmpty()
+        assertThat(mAllDeepShortcuts).containsExactly(expectedShortcutInfo)
         verify(mockCursor).markRestored()
         verify(mockCursor).checkAndAddItem(any(), any(), anyOrNull())
     }
@@ -372,8 +394,8 @@
 
         // Given
         mockCursor.itemType = ITEM_TYPE_DEEP_SHORTCUT
-        iconRequestInfos = mutableListOf()
-        keyToPinnedShortcutsMap = hashMapOf()
+        mIconRequestInfos = mutableListOf()
+        mKeyToPinnedShortcutsMap = hashMapOf()
 
         // When
         itemProcessorUnderTest = createWorkspaceItemProcessorUnderTest()
@@ -383,7 +405,7 @@
         assertWithMessage("item restoreFlag should be set to 0")
             .that(mockCursor.restoreFlag)
             .isEqualTo(0)
-        assertThat(iconRequestInfos).isEmpty()
+        assertThat(mIconRequestInfos).isEmpty()
         verify(mockCursor, times(0)).checkAndAddItem(any(), any(), anyOrNull())
         verify(mockCursor)
             .markDeleted(
@@ -408,25 +430,25 @@
                 whenever(disabledReason).thenReturn(0)
                 whenever(persons).thenReturn(EMPTY_PERSON_ARRAY)
             }
-        iconRequestInfos = mutableListOf()
+        mIconRequestInfos = mutableListOf()
         // Make sure shortcuts map has expected key from expected package
-        intent.`package` = componentName.packageName
+        intent.`package` = mComponentName.packageName
         val shortcutKey = ShortcutKey.fromIntent(intent, mockCursor.user)
-        keyToPinnedShortcutsMap[shortcutKey] = expectedShortcutInfo
+        mKeyToPinnedShortcutsMap[shortcutKey] = expectedShortcutInfo
         // set intent package back to null to test scenario
         intent.`package` = null
 
         // When
         itemProcessorUnderTest =
-            createWorkspaceItemProcessorUnderTest(allDeepShortcuts = allDeepShortcuts)
+            createWorkspaceItemProcessorUnderTest(allDeepShortcuts = mAllDeepShortcuts)
         itemProcessorUnderTest.processItem()
 
         // Then
         assertWithMessage("item restoreFlag should be set to 0")
             .that(mockCursor.restoreFlag)
             .isEqualTo(0)
-        assertThat(iconRequestInfos).isEmpty()
-        assertThat(allDeepShortcuts).containsExactly(expectedShortcutInfo)
+        assertThat(mIconRequestInfos).isEmpty()
+        assertThat(mAllDeepShortcuts).containsExactly(expectedShortcutInfo)
         verify(mockCursor).markRestored()
         verify(mockCursor).checkAndAddItem(any(), any(), anyOrNull())
     }
@@ -478,4 +500,346 @@
         assertThat(actualFolderInfo.options).isEqualTo(expectedFolderInfo.options)
         verify(mockCursor).checkAndAddItem(actualFolderInfo, mockBgDataModel, null)
     }
+
+    @Test
+    fun `When valid TYPE_REAL App Widget then add item`() {
+
+        // Given
+        val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
+        val expectedComponentName =
+            ComponentName.unflattenFromString(expectedProvider)!!.flattenToString()
+        val expectedRestoreStatus = FLAG_UI_NOT_READY
+        val expectedAppWidgetId = 0
+        mockCursor.apply {
+            itemType = ITEM_TYPE_APPWIDGET
+            user = mUserHandle
+            restoreFlag = FLAG_UI_NOT_READY
+            container = CONTAINER_DESKTOP
+            whenever(isOnWorkspaceOrHotseat).thenCallRealMethod()
+            whenever(appWidgetProvider).thenReturn(expectedProvider)
+            whenever(appWidgetId).thenReturn(expectedAppWidgetId)
+            whenever(spanX).thenReturn(2)
+            whenever(spanY).thenReturn(1)
+            whenever(options).thenReturn(0)
+            whenever(appWidgetSource).thenReturn(20)
+            whenever(applyCommonProperties(any())).thenCallRealMethod()
+            whenever(
+                    updater()
+                        .put(Favorites.APPWIDGET_PROVIDER, expectedComponentName)
+                        .put(Favorites.APPWIDGET_ID, expectedAppWidgetId)
+                        .put(Favorites.RESTORED, expectedRestoreStatus)
+                        .commit()
+                )
+                .thenReturn(1)
+        }
+        val expectedWidgetInfo =
+            LauncherAppWidgetInfo().apply {
+                appWidgetId = expectedAppWidgetId
+                providerName = ComponentName.unflattenFromString(expectedProvider)
+                restoreStatus = expectedRestoreStatus
+            }
+        val expectedWidgetProviderInfo =
+            mock<LauncherAppWidgetProviderInfo>().apply {
+                provider = ComponentName.unflattenFromString(expectedProvider)
+                whenever(user).thenReturn(mUserHandle)
+            }
+        val inflationResult =
+            WidgetInflater.InflationResult(
+                type = WidgetInflater.TYPE_REAL,
+                widgetInfo = expectedWidgetProviderInfo
+            )
+        mockWidgetInflater =
+            mock<WidgetInflater>().apply {
+                whenever(inflateAppWidget(any())).thenReturn(inflationResult)
+            }
+        val packageUserKey = PackageUserKey("com.google.android.testApp", mUserHandle)
+        mInstallingPkgs[packageUserKey] = PackageInstaller.SessionInfo()
+
+        // When
+        itemProcessorUnderTest =
+            createWorkspaceItemProcessorUnderTest(widgetProvidersMap = mWidgetProvidersMap)
+        itemProcessorUnderTest.processItem()
+
+        // Then
+        val widgetInfoCaptor = ArgumentCaptor.forClass(LauncherAppWidgetInfo::class.java)
+        verify(mockCursor).checkAndAddItem(widgetInfoCaptor.capture(), eq(mockBgDataModel))
+        val actualWidgetInfo = widgetInfoCaptor.value
+        with(actualWidgetInfo) {
+            assertThat(providerName).isEqualTo(expectedWidgetInfo.providerName)
+            assertThat(restoreStatus).isEqualTo(expectedWidgetInfo.restoreStatus)
+            assertThat(targetComponent).isEqualTo(expectedWidgetInfo.targetComponent)
+            assertThat(appWidgetId).isEqualTo(expectedWidgetInfo.appWidgetId)
+        }
+        val expectedComponentKey =
+            ComponentKey(expectedWidgetProviderInfo.provider, expectedWidgetProviderInfo.user)
+        assertThat(mWidgetProvidersMap[expectedComponentKey]).isEqualTo(expectedWidgetProviderInfo)
+    }
+
+    @Test
+    fun `When valid Pending Widget then checkAndAddItem`() {
+
+        // Given
+        mockCursor =
+            mock<LoaderCursor>().apply {
+                itemType = ITEM_TYPE_APPWIDGET
+                id = 1
+                user = UserHandle(1)
+                restoreFlag = FLAG_UI_NOT_READY
+                container = CONTAINER_DESKTOP
+                whenever(isOnWorkspaceOrHotseat).thenCallRealMethod()
+                whenever(appWidgetProvider)
+                    .thenReturn("com.google.android.testApp/com.android.testApp.testAppProvider")
+                whenever(appWidgetId).thenReturn(0)
+                whenever(spanX).thenReturn(2)
+                whenever(spanY).thenReturn(1)
+                whenever(options).thenReturn(0)
+                whenever(appWidgetSource).thenReturn(20)
+                whenever(applyCommonProperties(any())).thenCallRealMethod()
+            }
+        val mockProviderInfo =
+            mock<LauncherAppWidgetProviderInfo>().apply {
+                provider = mock()
+                whenever(user).thenReturn(UserHandle(1))
+            }
+        val inflationResult =
+            WidgetInflater.InflationResult(
+                type = WidgetInflater.TYPE_PENDING,
+                widgetInfo = mockProviderInfo
+            )
+        mockWidgetInflater =
+            mock<WidgetInflater>().apply {
+                whenever(inflateAppWidget(any())).thenReturn(inflationResult)
+            }
+        itemProcessorUnderTest =
+            createWorkspaceItemProcessorUnderTest(widgetProvidersMap = mWidgetProvidersMap)
+
+        // When
+        itemProcessorUnderTest.processItem()
+
+        // Then
+        verify(mockCursor).checkAndAddItem(any(), any())
+    }
+
+    @Test
+    fun `When Unrestored Pending App Widget then mark deleted`() {
+
+        // Given
+        val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
+        mockCursor =
+            mock<LoaderCursor>().apply {
+                itemType = ITEM_TYPE_APPWIDGET
+                id = 1
+                user = UserHandle(1)
+                restoreFlag = FLAG_UI_NOT_READY
+                container = CONTAINER_DESKTOP
+                whenever(isOnWorkspaceOrHotseat).thenCallRealMethod()
+                whenever(appWidgetProvider).thenReturn(expectedProvider)
+                whenever(appWidgetId).thenReturn(0)
+                whenever(spanX).thenReturn(2)
+                whenever(spanY).thenReturn(1)
+                whenever(options).thenReturn(0)
+                whenever(appWidgetSource).thenReturn(20)
+                whenever(applyCommonProperties(any())).thenCallRealMethod()
+            }
+        mInstallingPkgs = hashMapOf()
+        val inflationResult =
+            WidgetInflater.InflationResult(type = WidgetInflater.TYPE_PENDING, widgetInfo = null)
+        mockWidgetInflater =
+            mock<WidgetInflater>().apply {
+                whenever(inflateAppWidget(any())).thenReturn(inflationResult)
+            }
+        val expectedComponentName = ComponentName.unflattenFromString(expectedProvider)
+
+        // When
+        itemProcessorUnderTest =
+            createWorkspaceItemProcessorUnderTest(widgetProvidersMap = mWidgetProvidersMap)
+        itemProcessorUnderTest.processItem()
+
+        // Then
+        verify(mockCursor)
+            .markDeleted(
+                "processWidget: Unrestored Pending widget removed: id=1, appWidgetId=0, component=$expectedComponentName, restoreFlag:=4",
+                LauncherRestoreEventLogger.RestoreError.APP_NOT_INSTALLED
+            )
+    }
+
+    @Test
+    fun `When Pending App Widget has not started restore then update db and add item`() {
+
+        val mockitoSession =
+            ExtendedMockito.mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .mockStatic(WidgetSections::class.java)
+                .startMocking()
+        try {
+            // Given
+            val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
+            val expectedComponentName =
+                ComponentName.unflattenFromString(expectedProvider)!!.flattenToString()
+            val expectedRestoreStatus = FLAG_UI_NOT_READY or FLAG_RESTORE_STARTED
+            val expectedAppWidgetId = 0
+            mockCursor.apply {
+                itemType = ITEM_TYPE_APPWIDGET
+                user = mUserHandle
+                restoreFlag = FLAG_UI_NOT_READY
+                container = CONTAINER_DESKTOP
+                whenever(isOnWorkspaceOrHotseat).thenCallRealMethod()
+                whenever(appWidgetProvider).thenReturn(expectedProvider)
+                whenever(appWidgetId).thenReturn(expectedAppWidgetId)
+                whenever(spanX).thenReturn(2)
+                whenever(spanY).thenReturn(1)
+                whenever(options).thenReturn(0)
+                whenever(appWidgetSource).thenReturn(20)
+                whenever(applyCommonProperties(any())).thenCallRealMethod()
+                whenever(
+                        updater()
+                            .put(Favorites.APPWIDGET_PROVIDER, expectedComponentName)
+                            .put(Favorites.APPWIDGET_ID, expectedAppWidgetId)
+                            .put(Favorites.RESTORED, expectedRestoreStatus)
+                            .commit()
+                    )
+                    .thenReturn(1)
+            }
+            val inflationResult =
+                WidgetInflater.InflationResult(
+                    type = WidgetInflater.TYPE_PENDING,
+                    widgetInfo = null
+                )
+            mockWidgetInflater =
+                mock<WidgetInflater>().apply {
+                    whenever(inflateAppWidget(any())).thenReturn(inflationResult)
+                }
+            val packageUserKey = PackageUserKey("com.google.android.testApp", mUserHandle)
+            mInstallingPkgs[packageUserKey] = PackageInstaller.SessionInfo()
+
+            // When
+            itemProcessorUnderTest =
+                createWorkspaceItemProcessorUnderTest(widgetProvidersMap = mWidgetProvidersMap)
+            itemProcessorUnderTest.processItem()
+
+            // Then
+            val expectedWidgetInfo =
+                LauncherAppWidgetInfo().apply {
+                    appWidgetId = expectedAppWidgetId
+                    providerName = ComponentName.unflattenFromString(expectedProvider)
+                    restoreStatus = expectedRestoreStatus
+                }
+            verify(
+                    mockCursor
+                        .updater()
+                        .put(Favorites.APPWIDGET_PROVIDER, expectedProvider)
+                        .put(Favorites.APPWIDGET_ID, expectedAppWidgetId)
+                        .put(Favorites.RESTORED, expectedRestoreStatus)
+                )
+                .commit()
+            val widgetInfoCaptor = ArgumentCaptor.forClass(LauncherAppWidgetInfo::class.java)
+            verify(mockCursor).checkAndAddItem(widgetInfoCaptor.capture(), eq(mockBgDataModel))
+            val actualWidgetInfo = widgetInfoCaptor.value
+            with(actualWidgetInfo) {
+                assertThat(providerName).isEqualTo(expectedWidgetInfo.providerName)
+                assertThat(restoreStatus).isEqualTo(expectedWidgetInfo.restoreStatus)
+                assertThat(targetComponent).isEqualTo(expectedWidgetInfo.targetComponent)
+                assertThat(appWidgetId).isEqualTo(expectedWidgetInfo.appWidgetId)
+            }
+        } finally {
+            mockitoSession.finishMocking()
+        }
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING)
+    fun `When Archived Pending App Widget then checkAndAddItem`() {
+        val mockitoSession =
+            ExtendedMockito.mockitoSession().mockStatic(Utilities::class.java).startMocking()
+        try {
+            // Given
+            val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
+            val expectedComponentName = ComponentName.unflattenFromString(expectedProvider)
+            val expectedPackage = expectedComponentName!!.packageName
+            mockPmHelper =
+                mock<PackageManagerHelper>().apply {
+                    whenever(isAppArchived(expectedPackage)).thenReturn(true)
+                }
+            mockCursor =
+                mock<LoaderCursor>().apply {
+                    itemType = ITEM_TYPE_APPWIDGET
+                    id = 1
+                    user = UserHandle(1)
+                    restoreFlag = FLAG_UI_NOT_READY
+                    container = CONTAINER_DESKTOP
+                    whenever(isOnWorkspaceOrHotseat).thenCallRealMethod()
+                    whenever(appWidgetProvider).thenReturn(expectedProvider)
+                    whenever(appWidgetId).thenReturn(0)
+                    whenever(spanX).thenReturn(2)
+                    whenever(spanY).thenReturn(1)
+                    whenever(options).thenReturn(0)
+                    whenever(appWidgetSource).thenReturn(20)
+                    whenever(applyCommonProperties(any())).thenCallRealMethod()
+                }
+            mInstallingPkgs = hashMapOf()
+            val inflationResult =
+                WidgetInflater.InflationResult(
+                    type = WidgetInflater.TYPE_PENDING,
+                    widgetInfo = null
+                )
+            mockWidgetInflater =
+                mock<WidgetInflater>().apply {
+                    whenever(inflateAppWidget(any())).thenReturn(inflationResult)
+                }
+            itemProcessorUnderTest =
+                createWorkspaceItemProcessorUnderTest(widgetProvidersMap = mWidgetProvidersMap)
+
+            // When
+            itemProcessorUnderTest.processItem()
+
+            // Then
+            verify(mockCursor).checkAndAddItem(any(), any())
+        } finally {
+            mockitoSession.finishMocking()
+        }
+    }
+
+    @Test
+    fun `When widget inflation result is TYPE_DELETE then mark deleted`() {
+        // Given
+        val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
+        val expectedComponentName = ComponentName.unflattenFromString(expectedProvider)
+        val expectedPackage = expectedComponentName!!.packageName
+        mockPmHelper =
+            mock<PackageManagerHelper>().apply {
+                whenever(isAppArchived(expectedPackage)).thenReturn(true)
+            }
+        mockCursor =
+            mock<LoaderCursor>().apply {
+                itemType = ITEM_TYPE_APPWIDGET
+                id = 1
+                user = UserHandle(1)
+                container = CONTAINER_DESKTOP
+                whenever(spanX).thenReturn(2)
+                whenever(spanY).thenReturn(1)
+                whenever(appWidgetProvider).thenReturn(expectedProvider)
+                whenever(isOnWorkspaceOrHotseat).thenCallRealMethod()
+                whenever(applyCommonProperties(any())).thenCallRealMethod()
+            }
+        mInstallingPkgs = hashMapOf()
+        val inflationResult =
+            WidgetInflater.InflationResult(
+                type = WidgetInflater.TYPE_DELETE,
+                widgetInfo = null,
+                reason = "test_delete_reason",
+                restoreErrorType = MISSING_WIDGET_PROVIDER
+            )
+        mockWidgetInflater =
+            mock<WidgetInflater>().apply {
+                whenever(inflateAppWidget(any())).thenReturn(inflationResult)
+            }
+        itemProcessorUnderTest =
+            createWorkspaceItemProcessorUnderTest(widgetProvidersMap = mWidgetProvidersMap)
+
+        // When
+        itemProcessorUnderTest.processItem()
+
+        // Then
+        verify(mockCursor).markDeleted(inflationResult.reason, inflationResult.restoreErrorType)
+    }
 }
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 9c289d6..99e15ba 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -15,10 +15,7 @@
  */
 package com.android.launcher3.ui;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
-import static android.view.Display.DEFAULT_DISPLAY;
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
@@ -29,7 +26,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
-import android.app.ActivityOptions;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -575,21 +571,6 @@
                 true /* newTask */);
     }
 
-    /** alternative of startAppFast where app is guaranteed to launch in fullscreen mode */
-    public static void startAppFastInFullscreen(String packageName) {
-        ActivityOptions options = ActivityOptions.makeBasic();
-        options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN);
-        options.setLaunchDisplayId(DEFAULT_DISPLAY);
-        options.setLaunchActivityType(ACTIVITY_TYPE_STANDARD);
-
-        startIntent(
-                getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage(
-                        packageName),
-                By.pkg(packageName).depth(0),
-                true /* newTask */,
-                options);
-    }
-
     public static void startTestActivity(String activityName, String activityLabel) {
         final String packageName = getAppPackageName();
         final Intent intent = getInstrumentation().getContext().getPackageManager().
@@ -625,8 +606,7 @@
                 false /* newTask */);
     }
 
-    private static void startIntent(
-            Intent intent, BySelector selector, boolean newTask, ActivityOptions options) {
+    private static void startIntent(Intent intent, BySelector selector, boolean newTask) {
         intent.addCategory(Intent.CATEGORY_LAUNCHER);
         if (newTask) {
             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
@@ -634,12 +614,7 @@
             intent.addFlags(
                     Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
         }
-
-        if (options != null) {
-            getInstrumentation().getTargetContext().startActivity(intent, options.toBundle());
-        } else {
-            getInstrumentation().getTargetContext().startActivity(intent);
-        }
+        getInstrumentation().getTargetContext().startActivity(intent);
         assertTrue("App didn't start: " + selector,
                 TestHelpers.wait(Until.hasObject(selector), DEFAULT_UI_TIMEOUT));
 
@@ -650,10 +625,6 @@
                 DEFAULT_ACTIVITY_TIMEOUT, launcherInstrumentation);
     }
 
-    private static void startIntent(Intent intent, BySelector selector, boolean newTask) {
-        startIntent(intent, selector, newTask, null);
-    }
-
     public static ActivityInfo resolveSystemAppInfo(String category) {
         return getInstrumentation().getContext().getPackageManager().resolveActivity(
                 new Intent(Intent.ACTION_MAIN).addCategory(category),
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
index 7845dec..9dbd866 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
@@ -86,7 +86,6 @@
      * A custom shortcut is a 1x1 widget that launches a specific intent when user tap on it.
      * Custom shortcuts are replaced by deep shortcuts after api 25.
      */
-    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
     @Test
     @PortraitLandscape
     public void testDragCustomShortcut() throws Throwable {
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
index 9afde0d..490cff2 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
@@ -29,6 +29,7 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.TestUtil;
+import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 
 import org.junit.After;
 import org.junit.Before;
@@ -67,6 +68,7 @@
      * move between workspaces. After, make sure we can launch an app from the Workspace.
      * @throws Exception if we can't set the defaults icons that will appear at the beginning.
      */
+    @ScreenRecord // b/331261431
     @Test
     public void testWorkspace() throws Exception {
         // Set workspace  that includes the chrome Activity app icon on the hotseat.