Snap for 7479185 from a29220f6b6fd7a87f8550e479b216c693642b854 to sc-v2-release

Change-Id: I3ec2119bbf229f5dd9a798801f14054f13abd76c
diff --git a/quickstep/res/drawable/ic_all_set.xml b/quickstep/res/drawable/ic_all_set.xml
index a6852aa..656c596 100644
--- a/quickstep/res/drawable/ic_all_set.xml
+++ b/quickstep/res/drawable/ic_all_set.xml
@@ -15,10 +15,10 @@
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
     android:width="42dp"
-    android:height="40dp"
+    android:height="42dp"
     android:viewportWidth="42"
-    android:viewportHeight="40">
+    android:viewportHeight="42">
     <path
-        android:pathData="M38,14H25.38L27.28,4.86L27.34,4.22C27.34,3.4 27,2.64 26.46,2.1L24.34,0C24.34,0 10.16,13.7 10,14H0V40H32C33.66,40 35.08,39 35.68,37.56L41.72,23.46C41.9,23 42,22.52 42,22V18C42,15.8 40.2,14 38,14ZM10,36H4V18H10V36ZM38,22L32,36H14V16L22.68,7.32L20,18H38V22Z"
+        android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"
         android:fillColor="#FFFFFF"/>
 </vector>
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
index d61a895..dfa17d6 100644
--- a/quickstep/res/layout/taskbar.xml
+++ b/quickstep/res/layout/taskbar.xml
@@ -15,6 +15,7 @@
 -->
 <com.android.launcher3.taskbar.TaskbarDragLayer
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/taskbar_container"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
@@ -39,6 +40,7 @@
             android:id="@+id/start_nav_buttons"
             android:layout_width="wrap_content"
             android:layout_height="match_parent"
+            android:orientation="horizontal"
             android:paddingLeft="@dimen/taskbar_nav_buttons_spacing"
             android:paddingRight="@dimen/taskbar_nav_buttons_spacing"
             android:gravity="center_vertical"
@@ -54,4 +56,14 @@
             android:layout_gravity="end"/>
     </FrameLayout>
 
+    <View
+        android:id="@+id/stashed_handle"
+        tools:comment1="The actual size and shape will be set as a ViewOutlineProvider at runtime"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        tools:comment2="TODO: Tint dynamically"
+        android:background="?android:attr/textColorPrimary"
+        android:clipToOutline="true"
+        android:layout_gravity="bottom"/>
+
 </com.android.launcher3.taskbar.TaskbarDragLayer>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index d8899a6..4f62b34 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -153,4 +153,7 @@
     <dimen name="taskbar_folder_margin">16dp</dimen>
     <dimen name="taskbar_nav_buttons_spacing">16dp</dimen>
     <dimen name="taskbar_nav_buttons_size">48dp</dimen>
+    <dimen name="taskbar_stashed_size">24dp</dimen>
+    <dimen name="taskbar_stashed_handle_width">220dp</dimen>
+    <dimen name="taskbar_stashed_handle_height">6dp</dimen>
 </resources>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 66ccee6..2d1e304 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -497,8 +497,6 @@
                     view -> viewsToAnimate.add(((CellLayout) view).getShortcutsAndWidgets()));
 
             viewsToAnimate.add(mLauncher.getHotseat());
-            // Add QSB
-            viewsToAnimate.add(mLauncher.findViewById(R.id.search_container_all_apps));
 
             viewsToAnimate.forEach(view -> {
                 view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 12ac0f5..06fd660 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -50,21 +50,23 @@
     private final TaskbarHotseatController mHotseatController;
 
     private final TaskbarActivityContext mContext;
-    final TaskbarDragLayer mTaskbarDragLayer;
-    final TaskbarView mTaskbarView;
+    private final TaskbarDragLayer mTaskbarDragLayer;
+    private final TaskbarView mTaskbarView;
 
     private final AnimatedFloat mIconAlignmentForResumedState =
             new AnimatedFloat(this::onIconAlignmentRatioChanged);
     private final AnimatedFloat mIconAlignmentForGestureState =
             new AnimatedFloat(this::onIconAlignmentRatioChanged);
 
+    // Initialized in init.
+    private TaskbarControllers mControllers;
     private AnimatedFloat mTaskbarBackgroundAlpha;
     private AlphaProperty mIconAlphaForHome;
-    private boolean mIsAnimatingToLauncher;
+    private boolean mIsAnimatingToLauncherViaResume;
+    private boolean mIsAnimatingToLauncherViaGesture;
     private TaskbarKeyguardController mKeyguardController;
 
     private LauncherState mTargetStateOverride = null;
-    private TaskbarControllers mControllers;
 
     public LauncherTaskbarUIController(
             BaseQuickstepLauncher launcher, TaskbarActivityContext context) {
@@ -80,13 +82,14 @@
 
     @Override
     protected void init(TaskbarControllers taskbarControllers) {
-        mTaskbarBackgroundAlpha = taskbarControllers.taskbarDragLayerController
-                .getTaskbarBackgroundAlpha();
-        MultiValueAlpha taskbarIconAlpha = taskbarControllers.taskbarViewController
-                .getTaskbarIconAlpha();
-        mIconAlphaForHome = taskbarIconAlpha.getProperty(ALPHA_INDEX_HOME);
         mControllers = taskbarControllers;
 
+        mTaskbarBackgroundAlpha = mControllers.taskbarDragLayerController
+                .getTaskbarBackgroundAlpha();
+
+        MultiValueAlpha taskbarIconAlpha = mControllers.taskbarViewController.getTaskbarIconAlpha();
+        mIconAlphaForHome = taskbarIconAlpha.getProperty(ALPHA_INDEX_HOME);
+
         mHotseatController.init();
         mLauncher.setTaskbarUIController(this);
         mKeyguardController = taskbarControllers.taskbarKeyguardController;
@@ -109,19 +112,17 @@
 
     @Override
     protected boolean isTaskbarTouchable() {
-        return !mIsAnimatingToLauncher && mTargetStateOverride == null;
+        return !isAnimatingToLauncher() && !mControllers.taskbarStashController.isStashed();
+    }
+
+    private boolean isAnimatingToLauncher() {
+        return mIsAnimatingToLauncherViaResume || mIsAnimatingToLauncherViaGesture;
     }
 
     @Override
     protected void updateContentInsets(Rect outContentInsets) {
-        // TaskbarDragLayer provides insets to other apps based on contentInsets. These
-        // insets should stay consistent even if we expand TaskbarDragLayer's bounds, e.g.
-        // to show a floating view like Folder. Thus, we set the contentInsets to be where
-        // mTaskbarView is, since its position never changes and insets rather than overlays.
-        outContentInsets.left = mTaskbarView.getLeft();
-        outContentInsets.top = mTaskbarView.getTop();
-        outContentInsets.right = mTaskbarDragLayer.getWidth() - mTaskbarView.getRight();
-        outContentInsets.bottom = mTaskbarDragLayer.getHeight() - mTaskbarView.getBottom();
+        int contentHeight = mControllers.taskbarStashController.getContentHeight();
+        outContentInsets.top = mTaskbarDragLayer.getHeight() - contentHeight;
     }
 
     /**
@@ -137,13 +138,20 @@
             }
         }
 
+        long duration = QuickstepTransitionManager.CONTENT_ALPHA_DURATION;
         ObjectAnimator anim = mIconAlignmentForResumedState.animateToValue(
                 getCurrentIconAlignmentRatio(), isResumed ? 1 : 0)
-                .setDuration(QuickstepTransitionManager.CONTENT_ALPHA_DURATION);
+                .setDuration(duration);
 
-        anim.addListener(AnimatorListeners.forEndCallback(() -> mIsAnimatingToLauncher = false));
+        anim.addListener(AnimatorListeners.forEndCallback(
+                () -> mIsAnimatingToLauncherViaResume = false));
         anim.start();
-        mIsAnimatingToLauncher = isResumed;
+        mIsAnimatingToLauncherViaResume = isResumed;
+
+        if (!isResumed) {
+            TaskbarStashController stashController = mControllers.taskbarStashController;
+            stashController.animateToIsStashed(stashController.isStashedInApp(), duration);
+        }
     }
 
     /**
@@ -155,36 +163,48 @@
     public Animator createAnimToLauncher(@NonNull LauncherState toState,
             @NonNull RecentsAnimationCallbacks callbacks,
             long duration) {
+        TaskbarStashController stashController = mControllers.taskbarStashController;
         ObjectAnimator animator = mIconAlignmentForGestureState
-                .animateToValue(mIconAlignmentForGestureState.value, 1)
+                .animateToValue(1)
                 .setDuration(duration);
         animator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
                 mTargetStateOverride = null;
+                animator.removeListener(this);
             }
 
             @Override
             public void onAnimationStart(Animator animation) {
                 mTargetStateOverride = toState;
+                mIsAnimatingToLauncherViaGesture = true;
+                // TODO: base this on launcher state
+                stashController.animateToIsStashed(false, duration);
             }
         });
         callbacks.addListener(new RecentsAnimationListener() {
             @Override
             public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
-                endGestureStateOverride();
+                endGestureStateOverride(true);
             }
 
             @Override
             public void onRecentsAnimationFinished(RecentsAnimationController controller) {
-                endGestureStateOverride();
+                endGestureStateOverride(!controller.getFinishTargetIsLauncher());
             }
 
-            private void endGestureStateOverride() {
+            private void endGestureStateOverride(boolean finishedToApp) {
                 callbacks.removeListener(this);
+                mIsAnimatingToLauncherViaGesture = false;
+
                 mIconAlignmentForGestureState
-                        .animateToValue(mIconAlignmentForGestureState.value, 0)
+                        .animateToValue(0)
                         .start();
+
+                if (finishedToApp) {
+                    // We only need this for the exiting live tile case.
+                    stashController.animateToIsStashed(stashController.isStashedInApp());
+                }
             }
         });
         return animator;
@@ -215,6 +235,11 @@
         }
     }
 
+    @Override
+    public boolean onLongPressToUnstashTaskbar() {
+        return mControllers.taskbarStashController.onLongPressToUnstashTaskbar();
+    }
+
     /**
      * Should be called when one or more items in the Hotseat have changed.
      */
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
new file mode 100644
index 0000000..8c14ff6
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import android.animation.Animator;
+import android.content.res.Resources;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.launcher3.anim.RevealOutlineAnimation;
+import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.quickstep.AnimatedFloat;
+
+/**
+ * Handles properties/data collection, then passes the results to our stashed handle View to render.
+ */
+public class StashedHandleViewController {
+
+    private final TaskbarActivityContext mActivity;
+    private final View mStashedHandleView;
+    private final int mStashedHandleWidth;
+    private final int mStashedHandleHeight;
+    private final AnimatedFloat mTaskbarStashedHandleAlpha = new AnimatedFloat(
+            this::updateStashedHandleAlpha);
+
+    // Initialized in init.
+    private TaskbarControllers mControllers;
+
+    // The bounds we want to clip to in the settled state when showing the stashed handle.
+    private final Rect mStashedHandleBounds = new Rect();
+    private float mStashedHandleRadius;
+
+    private boolean mIsAtStashedRevealBounds = true;
+
+    public StashedHandleViewController(TaskbarActivityContext activity, View stashedHandleView) {
+        mActivity = activity;
+        mStashedHandleView = stashedHandleView;
+        final Resources resources = mActivity.getResources();
+        mStashedHandleWidth = resources.getDimensionPixelSize(R.dimen.taskbar_stashed_handle_width);
+        mStashedHandleHeight = resources.getDimensionPixelSize(
+                R.dimen.taskbar_stashed_handle_height);
+    }
+
+    public void init(TaskbarControllers controllers) {
+        mControllers = controllers;
+        mStashedHandleView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarSize;
+
+        updateStashedHandleAlpha();
+
+        final int stashedTaskbarHeight = mControllers.taskbarStashController.getStashedHeight();
+        mStashedHandleView.setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                final int stashedCenterX = view.getWidth() / 2;
+                final int stashedCenterY = view.getHeight() - stashedTaskbarHeight / 2;
+                mStashedHandleBounds.set(
+                        stashedCenterX - mStashedHandleWidth / 2,
+                        stashedCenterY - mStashedHandleHeight / 2,
+                        stashedCenterX + mStashedHandleWidth / 2,
+                        stashedCenterY + mStashedHandleHeight / 2);
+                mStashedHandleRadius = view.getHeight() / 2f;
+                outline.setRoundRect(mStashedHandleBounds, mStashedHandleRadius);
+            }
+        });
+    }
+
+    public AnimatedFloat getStashedHandleAlpha() {
+        return mTaskbarStashedHandleAlpha;
+    }
+
+    /**
+     * Creates and returns a {@link RevealOutlineAnimation} Animator that updates the stashed handle
+     * shape and size. When stashed, the shape is a thin rounded pill. When unstashed, the shape
+     * morphs into the size of where the taskbar icons will be.
+     */
+    public @Nullable Animator createRevealAnimToIsStashed(boolean isStashed) {
+        if (mIsAtStashedRevealBounds == isStashed) {
+            return null;
+        }
+        mIsAtStashedRevealBounds = isStashed;
+        final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider(
+                mStashedHandleRadius, mStashedHandleRadius,
+                mControllers.taskbarViewController.getIconLayoutBounds(), mStashedHandleBounds);
+        return handleRevealProvider.createRevealAnimator(mStashedHandleView, !isStashed);
+    }
+
+    protected void updateStashedHandleAlpha() {
+        mStashedHandleView.setAlpha(mTaskbarStashedHandleAlpha.value);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 4142610..f4703d3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -115,6 +115,7 @@
                 R.layout.taskbar, null, false);
         TaskbarView taskbarView = mDragLayer.findViewById(R.id.taskbar_view);
         FrameLayout navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view);
+        View stashedHandleView = mDragLayer.findViewById(R.id.stashed_handle);
 
         // Construct controllers.
         mControllers = new TaskbarControllers(this,
@@ -125,7 +126,9 @@
                         R.color.popup_color_primary_light),
                 new TaskbarDragLayerController(this, mDragLayer),
                 new TaskbarViewController(this, taskbarView),
-                new TaskbarKeyguardController(this));
+                new TaskbarKeyguardController(this),
+                new StashedHandleViewController(this, stashedHandleView),
+                new TaskbarStashController(this));
 
         Display display = windowContext.getDisplay();
         Context c = display.getDisplayId() == Display.DEFAULT_DISPLAY
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index c48c28b..8279a47 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -32,6 +32,8 @@
     public final TaskbarDragLayerController taskbarDragLayerController;
     public final TaskbarViewController taskbarViewController;
     public final TaskbarKeyguardController taskbarKeyguardController;
+    public final StashedHandleViewController stashedHandleViewController;
+    public final TaskbarStashController taskbarStashController;
 
     /** Do not store this controller, as it may change at runtime. */
     @NonNull public TaskbarUIController uiController = TaskbarUIController.DEFAULT;
@@ -43,7 +45,9 @@
             RotationButtonController rotationButtonController,
             TaskbarDragLayerController taskbarDragLayerController,
             TaskbarViewController taskbarViewController,
-            TaskbarKeyguardController taskbarKeyguardController) {
+            TaskbarKeyguardController taskbarKeyguardController,
+            StashedHandleViewController stashedHandleViewController,
+            TaskbarStashController taskbarStashController) {
         this.taskbarActivityContext = taskbarActivityContext;
         this.taskbarDragController = taskbarDragController;
         this.navButtonController = navButtonController;
@@ -52,6 +56,8 @@
         this.taskbarDragLayerController = taskbarDragLayerController;
         this.taskbarViewController = taskbarViewController;
         this.taskbarKeyguardController = taskbarKeyguardController;
+        this.stashedHandleViewController = stashedHandleViewController;
+        this.taskbarStashController = taskbarStashController;
     }
 
     /**
@@ -67,6 +73,8 @@
         taskbarDragLayerController.init(this);
         taskbarViewController.init(this);
         taskbarKeyguardController.init(navbarButtonsViewController);
+        stashedHandleViewController.init(this);
+        taskbarStashController.init(this);
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index ac121ab..cd1baf7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -40,10 +40,11 @@
 public class TaskbarDragLayer extends BaseDragLayer<TaskbarActivityContext> {
 
     private final Paint mTaskbarBackgroundPaint;
+    private final OnComputeInsetsListener mTaskbarInsetsComputer = this::onComputeTaskbarInsets;
 
     private TaskbarDragLayerController.TaskbarDragLayerCallbacks mControllerCallbacks;
 
-    private final OnComputeInsetsListener mTaskbarInsetsComputer = this::onComputeTaskbarInsets;
+    private float mTaskbarBackgroundOffset;
 
     public TaskbarDragLayer(@NonNull Context context) {
         this(context, null);
@@ -118,8 +119,10 @@
 
     @Override
     protected void dispatchDraw(Canvas canvas) {
-        canvas.drawRect(0, canvas.getHeight() - mControllerCallbacks.getTaskbarBackgroundHeight(),
-                canvas.getWidth(), canvas.getHeight(), mTaskbarBackgroundPaint);
+        float backgroundHeight = mControllerCallbacks.getTaskbarBackgroundHeight()
+                * (1f - mTaskbarBackgroundOffset);
+        canvas.drawRect(0, canvas.getHeight() - backgroundHeight, canvas.getWidth(),
+                canvas.getHeight(), mTaskbarBackgroundPaint);
         super.dispatchDraw(canvas);
     }
 
@@ -132,6 +135,15 @@
         invalidate();
     }
 
+    /**
+     * Sets the translation of the background color behind all the Taskbar contents.
+     * @param offset 0 is fully onscreen, 1 is fully offscreen.
+     */
+    protected void setTaskbarBackgroundOffset(float offset) {
+        mTaskbarBackgroundOffset = offset;
+        invalidate();
+    }
+
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index db5c387..e15e9ff 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -39,6 +39,8 @@
     // Alpha properties for taskbar background.
     private final AnimatedFloat mBgTaskbar = new AnimatedFloat(this::updateBackgroundAlpha);
     private final AnimatedFloat mBgNavbar = new AnimatedFloat(this::updateBackgroundAlpha);
+    // Translation property for taskbar background.
+    private final AnimatedFloat mBgOffset = new AnimatedFloat(this::updateBackgroundOffset);
 
     // Initialized in init.
     private TaskbarControllers mControllers;
@@ -78,10 +80,18 @@
         return mBgNavbar;
     }
 
+    public AnimatedFloat getTaskbarBackgroundOffset() {
+        return mBgOffset;
+    }
+
     private void updateBackgroundAlpha() {
         mTaskbarDragLayer.setTaskbarBackgroundAlpha(Math.max(mBgNavbar.value, mBgTaskbar.value));
     }
 
+    private void updateBackgroundOffset() {
+        mTaskbarDragLayer.setTaskbarBackgroundOffset(mBgOffset.value);
+    }
+
     /**
      * Callbacks for {@link TaskbarDragLayer} to interact with its controller.
      */
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
new file mode 100644
index 0000000..57600d7
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static android.view.HapticFeedbackConstants.LONG_PRESS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.annotation.Nullable;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.quickstep.AnimatedFloat;
+
+/**
+ * Coordinates between controllers such as TaskbarViewController and StashedHandleViewController to
+ * create a cohesive animation between stashed/unstashed states.
+ */
+public class TaskbarStashController {
+
+    /**
+     * How long to stash/unstash when manually invoked via long press.
+     */
+    private static final long TASKBAR_STASH_DURATION = 300;
+
+    /**
+     * The scale TaskbarView animates to when being stashed.
+     */
+    private static final float STASHED_TASKBAR_SCALE = 0.5f;
+
+    /**
+     * The SharedPreferences key for whether user has manually stashed the taskbar.
+     */
+    private static final String SHARED_PREFS_STASHED_KEY = "taskbar_is_stashed";
+
+    /**
+     * Whether taskbar should be stashed out of the box.
+     */
+    private static final boolean DEFAULT_STASHED_PREF = false;
+
+    private final TaskbarActivityContext mActivity;
+    private final SharedPreferences mPrefs;
+    private final int mStashedHeight;
+    private final int mUnstashedHeight;
+
+    // Initialized in init.
+    private TaskbarControllers mControllers;
+    // Taskbar background properties.
+    private AnimatedFloat mTaskbarBackgroundOffset;
+    // TaskbarView icon properties.
+    private AlphaProperty mIconAlphaForStash;
+    private AnimatedFloat mIconScaleForStash;
+    private AnimatedFloat mIconTranslationYForStash;
+    // Stashed handle properties.
+    private AnimatedFloat mTaskbarStashedHandleAlpha;
+
+    /** Whether the user has manually invoked taskbar stashing, which we persist. */
+    private boolean mIsStashedInApp;
+    /** Whether we are currently visually stashed (might change based on launcher state). */
+    private boolean mIsStashed = false;
+
+    private @Nullable AnimatorSet mAnimator;
+
+    public TaskbarStashController(TaskbarActivityContext activity) {
+        mActivity = activity;
+        mPrefs = Utilities.getPrefs(mActivity);
+        final Resources resources = mActivity.getResources();
+        mStashedHeight = resources.getDimensionPixelSize(R.dimen.taskbar_stashed_size);
+        mUnstashedHeight = mActivity.getDeviceProfile().taskbarSize;
+    }
+
+    public void init(TaskbarControllers controllers) {
+        mControllers = controllers;
+
+        TaskbarDragLayerController dragLayerController = controllers.taskbarDragLayerController;
+        mTaskbarBackgroundOffset = dragLayerController.getTaskbarBackgroundOffset();
+
+        TaskbarViewController taskbarViewController = controllers.taskbarViewController;
+        mIconAlphaForStash = taskbarViewController.getTaskbarIconAlpha().getProperty(
+                TaskbarViewController.ALPHA_INDEX_STASH);
+        mIconScaleForStash = taskbarViewController.getTaskbarIconScaleForStash();
+        mIconTranslationYForStash = taskbarViewController.getTaskbarIconTranslationYForStash();
+
+        StashedHandleViewController stashedHandleController =
+                controllers.stashedHandleViewController;
+        mTaskbarStashedHandleAlpha = stashedHandleController.getStashedHandleAlpha();
+
+        mIsStashedInApp = supportsStashing()
+                && mPrefs.getBoolean(SHARED_PREFS_STASHED_KEY, DEFAULT_STASHED_PREF);
+    }
+
+    /**
+     * Returns whether the user can manually stash the taskbar based on the current device state.
+     */
+    private boolean supportsStashing() {
+        return !mActivity.isThreeButtonNav();
+    }
+
+    /**
+     * Returns whether the taskbar is currently visually stashed.
+     */
+    public boolean isStashed() {
+        return mIsStashed;
+    }
+
+    /**
+     * Returns whether the user has manually stashed the taskbar in apps.
+     */
+    public boolean isStashedInApp() {
+        return mIsStashedInApp;
+    }
+
+    public int getContentHeight() {
+        return isStashed() ? mStashedHeight : mUnstashedHeight;
+    }
+
+    public int getStashedHeight() {
+        return mStashedHeight;
+    }
+
+    /**
+     * Should be called when long pressing the nav region when taskbar is present.
+     * @return Whether taskbar was stashed and now is unstashed.
+     */
+    public boolean onLongPressToUnstashTaskbar() {
+        if (!isStashed()) {
+            // We only listen for long press on the nav region to unstash the taskbar. To stash the
+            // taskbar, we use an OnLongClickListener on TaskbarView instead.
+            return false;
+        }
+        if (updateAndAnimateIsStashedInApp(false)) {
+            mControllers.taskbarActivityContext.getDragLayer().performHapticFeedback(LONG_PRESS);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Updates whether we should stash the taskbar when in apps, and animates to the changed state.
+     * @return Whether we started an animation to either be newly stashed or unstashed.
+     */
+    public boolean updateAndAnimateIsStashedInApp(boolean isStashedInApp) {
+        if (!supportsStashing()) {
+            return false;
+        }
+        if (mIsStashedInApp != isStashedInApp) {
+            boolean wasStashed = mIsStashedInApp;
+            mIsStashedInApp = isStashedInApp;
+            mPrefs.edit().putBoolean(SHARED_PREFS_STASHED_KEY, mIsStashedInApp).apply();
+            boolean isStashed = mIsStashedInApp;
+            if (wasStashed != isStashed) {
+                createAnimToIsStashed(isStashed, TASKBAR_STASH_DURATION).start();
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Starts an animation to the new stashed state with a default duration.
+     */
+    public void animateToIsStashed(boolean isStashed) {
+        animateToIsStashed(isStashed, TASKBAR_STASH_DURATION);
+    }
+
+    /**
+     * Starts an animation to the new stashed state with the specified duration.
+     */
+    public void animateToIsStashed(boolean isStashed, long duration) {
+        createAnimToIsStashed(isStashed, duration).start();
+    }
+
+    private Animator createAnimToIsStashed(boolean isStashed, long duration) {
+        AnimatorSet fullLengthAnimatorSet = new AnimatorSet();
+        // Not exactly half and may overlap. See [first|second]HalfDurationScale below.
+        AnimatorSet firstHalfAnimatorSet = new AnimatorSet();
+        AnimatorSet secondHalfAnimatorSet = new AnimatorSet();
+
+        final float firstHalfDurationScale;
+        final float secondHalfDurationScale;
+
+        if (isStashed) {
+            firstHalfDurationScale = 0.75f;
+            secondHalfDurationScale = 0.5f;
+            final float stashTranslation = (mUnstashedHeight - mStashedHeight) / 2f;
+
+            fullLengthAnimatorSet.playTogether(
+                    mTaskbarBackgroundOffset.animateToValue(1),
+                    mIconTranslationYForStash.animateToValue(stashTranslation)
+            );
+            firstHalfAnimatorSet.playTogether(
+                    mIconAlphaForStash.animateToValue(0),
+                    mIconScaleForStash.animateToValue(STASHED_TASKBAR_SCALE)
+            );
+            secondHalfAnimatorSet.playTogether(
+                    mTaskbarStashedHandleAlpha.animateToValue(1)
+            );
+        } else  {
+            firstHalfDurationScale = 0.5f;
+            secondHalfDurationScale = 0.75f;
+
+            fullLengthAnimatorSet.playTogether(
+                    mTaskbarBackgroundOffset.animateToValue(0),
+                    mIconScaleForStash.animateToValue(1),
+                    mIconTranslationYForStash.animateToValue(0)
+            );
+            firstHalfAnimatorSet.playTogether(
+                    mTaskbarStashedHandleAlpha.animateToValue(0)
+            );
+            secondHalfAnimatorSet.playTogether(
+                    mIconAlphaForStash.animateToValue(1)
+            );
+        }
+
+        Animator stashedHandleRevealAnim = mControllers.stashedHandleViewController
+                .createRevealAnimToIsStashed(isStashed);
+        if (stashedHandleRevealAnim != null) {
+            fullLengthAnimatorSet.play(stashedHandleRevealAnim);
+        }
+
+        fullLengthAnimatorSet.setDuration(duration);
+        firstHalfAnimatorSet.setDuration((long) (duration * firstHalfDurationScale));
+        secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale));
+        secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale)));
+
+        if (mAnimator != null) {
+            mAnimator.cancel();
+        }
+        mAnimator = new AnimatorSet();
+        mAnimator.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet,
+                secondHalfAnimatorSet);
+        mAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mIsStashed = isStashed;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAnimator = null;
+            }
+        });
+        return mAnimator;
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 260cedc..6d0e3c6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -33,4 +33,8 @@
     }
 
     protected void updateContentInsets(Rect outContentInsets) { }
+
+    protected boolean onLongPressToUnstashTaskbar() {
+        return false;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 7753f96..820d40a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -94,8 +94,10 @@
 
     protected void init(TaskbarViewController.TaskbarViewCallbacks callbacks) {
         mControllerCallbacks = callbacks;
-        mIconClickListener = mControllerCallbacks.getOnClickListener();
-        mIconLongClickListener = mControllerCallbacks.getOnLongClickListener();
+        mIconClickListener = mControllerCallbacks.getIconOnClickListener();
+        mIconLongClickListener = mControllerCallbacks.getIconOnLongClickListener();
+
+        setOnLongClickListener(mControllerCallbacks.getBackgroundOnLongClickListener());
     }
 
     private void removeAndRecycle(View view) {
@@ -235,6 +237,10 @@
         return isShown() && mIconLayoutBounds.contains(xInOurCoordinates, yInOurCoorindates);
     }
 
+    public Rect getIconLayoutBounds() {
+        return mIconLayoutBounds;
+    }
+
     // FolderIconParent implemented methods.
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index c7ac4a4..50c26b3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -17,8 +17,8 @@
 
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.quickstep.AnimatedFloat.VALUE;
 
 import android.graphics.Rect;
 import android.view.View;
@@ -28,6 +28,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.MultiValueAlpha;
+import com.android.quickstep.AnimatedFloat;
 
 /**
  * Handles properties/data collection, then passes the results to TaskbarView to render.
@@ -38,10 +39,16 @@
     public static final int ALPHA_INDEX_HOME = 0;
     public static final int ALPHA_INDEX_IME = 1;
     public static final int ALPHA_INDEX_KEYGUARD = 2;
+    public static final int ALPHA_INDEX_STASH = 3;
 
     private final TaskbarActivityContext mActivity;
     private final TaskbarView mTaskbarView;
     private final MultiValueAlpha mTaskbarIconAlpha;
+    private final AnimatedFloat mTaskbarIconScaleForStash = new AnimatedFloat(this::updateScale);
+    private final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat(
+            this::updateTranslationY);
+    private final AnimatedFloat mTaskbarIconTranslationYForStash = new AnimatedFloat(
+            this::updateTranslationY);
 
     // Initialized in init.
     private TaskbarControllers mControllers;
@@ -54,7 +61,7 @@
     public TaskbarViewController(TaskbarActivityContext activity, TaskbarView taskbarView) {
         mActivity = activity;
         mTaskbarView = taskbarView;
-        mTaskbarIconAlpha = new MultiValueAlpha(mTaskbarView, 3);
+        mTaskbarIconAlpha = new MultiValueAlpha(mTaskbarView, 4);
         mTaskbarIconAlpha.setUpdateVisibility(true);
     }
 
@@ -62,6 +69,8 @@
         mControllers = controllers;
         mTaskbarView.init(new TaskbarViewCallbacks());
         mTaskbarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarSize;
+
+        mTaskbarIconScaleForStash.updateValue(1f);
     }
 
     public boolean areIconsVisible() {
@@ -86,6 +95,32 @@
         mTaskbarView.setClickAndLongClickListenersForIcon(icon);
     }
 
+    public Rect getIconLayoutBounds() {
+        return mTaskbarView.getIconLayoutBounds();
+    }
+
+    public AnimatedFloat getTaskbarIconScaleForStash() {
+        return mTaskbarIconScaleForStash;
+    }
+
+    public AnimatedFloat getTaskbarIconTranslationYForStash() {
+        return mTaskbarIconTranslationYForStash;
+    }
+
+    /**
+     * Applies scale properties for the entire TaskbarView (rather than individual icons).
+     */
+    private void updateScale() {
+        float scale = mTaskbarIconScaleForStash.value;
+        mTaskbarView.setScaleX(scale);
+        mTaskbarView.setScaleY(scale);
+    }
+
+    private void updateTranslationY() {
+        mTaskbarView.setTranslationY(mTaskbarIconTranslationYForHome.value
+                + mTaskbarIconTranslationYForStash.value);
+    }
+
     /**
      * Sets the taskbar icon alignment relative to Launcher hotseat icons
      * @param alignmentRatio [0, 1]
@@ -116,7 +151,7 @@
                         / launcherDp.numShownHotseatIcons;
 
         int offsetY = launcherDp.getTaskbarOffsetY();
-        setter.setFloat(mTaskbarView, VIEW_TRANSLATE_Y, -offsetY, LINEAR);
+        setter.setFloat(mTaskbarIconTranslationYForHome, VALUE, -offsetY, LINEAR);
 
         int collapsedHeight = mActivity.getDeviceProfile().taskbarSize;
         int expandedHeight = collapsedHeight + offsetY;
@@ -144,12 +179,16 @@
      * Callbacks for {@link TaskbarView} to interact with its controller.
      */
     public class TaskbarViewCallbacks {
-        public View.OnClickListener getOnClickListener() {
+        public View.OnClickListener getIconOnClickListener() {
             return mActivity::onTaskbarIconClicked;
         }
 
-        public View.OnLongClickListener getOnLongClickListener() {
+        public View.OnLongClickListener getIconOnLongClickListener() {
             return mControllers.taskbarDragController::startDragOnLongClick;
         }
+
+        public View.OnLongClickListener getBackgroundOnLongClickListener() {
+            return view -> mControllers.taskbarStashController.updateAndAnimateIsStashedInApp(true);
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/AnimatedFloat.java b/quickstep/src/com/android/quickstep/AnimatedFloat.java
index f7e8781..95c8710 100644
--- a/quickstep/src/com/android/quickstep/AnimatedFloat.java
+++ b/quickstep/src/com/android/quickstep/AnimatedFloat.java
@@ -53,6 +53,16 @@
         mUpdateCallback = updateCallback;
     }
 
+    /**
+     * Returns an animation from the current value to the given value.
+     */
+    public ObjectAnimator animateToValue(float end) {
+        return animateToValue(value, end);
+    }
+
+    /**
+     * Returns an animation from the given start value to the given end value.
+     */
     public ObjectAnimator animateToValue(float start, float end) {
         cancelAnimation();
         mValueAnimator = ObjectAnimator.ofFloat(this, VALUE, start, end);
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 2699b07..1412b1a 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -364,6 +364,14 @@
     }
 
     /**
+     * Called when we detect a long press in the nav region before passing the gesture slop.
+     * @return Whether taskbar handled the long press, and thus should cancel the gesture.
+     */
+    public boolean onLongPressToUnstashTaskbar() {
+        return false;
+    }
+
+    /**
      * Returns the color of the scrim behind overview when at rest in this state.
      * Return {@link Color#TRANSPARENT} for no scrim.
      */
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index 0b2a057..3580ee5 100644
--- a/quickstep/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -39,6 +39,7 @@
     int TYPE_OVERSCROLL = 1 << 9;
     int TYPE_SYSUI_OVERLAY = 1 << 10;
     int TYPE_ONE_HANDED = 1 << 11;
+    int TYPE_TASKBAR_STASH = 1 << 12;
 
     String[] NAMES = new String[] {
            "TYPE_NO_OP",                    // 0
@@ -53,6 +54,7 @@
             "TYPE_OVERSCROLL",              // 9
             "TYPE_SYSUI_OVERLAY",           // 10
             "TYPE_ONE_HANDED",              // 11
+            "TYPE_TASKBAR_STASH",           // 12
     };
 
     InputConsumer NO_OP = () -> TYPE_NO_OP;
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 799a4c2..09474a1 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -303,6 +303,15 @@
     }
 
     @Override
+    public boolean onLongPressToUnstashTaskbar() {
+        LauncherTaskbarUIController taskbarController = getTaskbarController();
+        if (taskbarController == null) {
+            return super.onLongPressToUnstashTaskbar();
+        }
+        return taskbarController.onLongPressToUnstashTaskbar();
+    }
+
+    @Override
     protected int getOverviewScrimColorForState(BaseQuickstepLauncher launcher,
             LauncherState state) {
         return state.getWorkspaceScrimColor(launcher);
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 0ebe13b..9e69ef9 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -46,6 +46,8 @@
     private boolean mUseLauncherSysBarFlags = false;
     private boolean mSplitScreenMinimized = false;
     private boolean mFinishRequested = false;
+    // Only valid when mFinishRequested == true.
+    private boolean mFinishTargetIsLauncher;
     private RunnableList mPendingFinishCallbacks = new RunnableList();
 
     public RecentsAnimationController(RecentsAnimationControllerCompat controller,
@@ -145,6 +147,7 @@
 
         // Finish not yet requested
         mFinishRequested = true;
+        mFinishTargetIsLauncher = toRecents;
         mOnFinishedListener.accept(this);
         mPendingFinishCallbacks.add(callback);
         UI_HELPER_EXECUTOR.execute(() -> {
@@ -201,4 +204,12 @@
     public RecentsAnimationControllerCompat getController() {
         return mController;
     }
+
+    /**
+     * RecentsAnimationListeners can check this in onRecentsAnimationFinished() to determine whether
+     * the animation was finished to launcher vs an app.
+     */
+    public boolean getFinishTargetIsLauncher() {
+        return mFinishTargetIsLauncher;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 47ca3d2..37cb979 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -95,6 +95,7 @@
 import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
 import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
 import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer;
+import com.android.quickstep.inputconsumers.TaskbarStashInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.AssistantUtilities;
 import com.android.quickstep.util.ProtoTracer;
@@ -673,6 +674,14 @@
                         mDeviceState, event);
             }
 
+            // If Taskbar is present, we listen for long press to unstash it.
+            BaseActivityInterface activityInterface = newGestureState.getActivityInterface();
+            StatefulActivity activity = activityInterface.getCreatedActivity();
+            if (activity != null && activity.getDeviceProfile().isTaskbarPresent) {
+                base = new TaskbarStashInputConsumer(this, base, mInputMonitorCompat,
+                        activityInterface);
+            }
+
             if (FeatureFlags.ENABLE_QUICK_CAPTURE_GESTURE.get()) {
                 OverscrollPlugin plugin = null;
                 if (FeatureFlags.FORCE_LOCAL_OVERSCROLL_PLUGIN.get()) {
@@ -832,7 +841,13 @@
     }
 
     private void reset() {
-        mConsumer = mUncheckedConsumer = mResetGestureInputConsumer;
+        if (mResetGestureInputConsumer != null) {
+            mConsumer = mUncheckedConsumer = mResetGestureInputConsumer;
+        } else {
+            // mResetGestureInputConsumer isn't initialized until onUserUnlocked(), so reset to
+            // NO_OP until then (we never want these to be null).
+            mConsumer = mUncheckedConsumer = InputConsumer.NO_OP;
+        }
         mGestureState = DEFAULT_STATE;
         // By default, use batching of the input events, but check receiver before using in the rare
         // case that the monitor was disposed before the swipe settled
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
new file mode 100644
index 0000000..83f689f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.inputconsumers;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+
+import com.android.quickstep.BaseActivityInterface;
+import com.android.quickstep.InputConsumer;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+/**
+ * Listens for a long press, and cancels the current gesture if that causes Taskbar to be unstashed.
+ */
+public class TaskbarStashInputConsumer extends DelegateInputConsumer {
+
+    private final BaseActivityInterface mActivityInterface;
+    private final GestureDetector mLongPressDetector;
+
+    public TaskbarStashInputConsumer(Context context, InputConsumer delegate,
+            InputMonitorCompat inputMonitor, BaseActivityInterface activityInterface) {
+        super(delegate, inputMonitor);
+        mActivityInterface = activityInterface;
+
+        mLongPressDetector = new GestureDetector(context, new SimpleOnGestureListener() {
+            @Override
+            public void onLongPress(MotionEvent motionEvent) {
+                onLongPressDetected(motionEvent);
+            }
+        });
+    }
+
+    @Override
+    public int getType() {
+        return TYPE_TASKBAR_STASH | mDelegate.getType();
+    }
+
+    @Override
+    public void onMotionEvent(MotionEvent ev) {
+        mLongPressDetector.onTouchEvent(ev);
+        if (mState != STATE_ACTIVE) {
+            mDelegate.onMotionEvent(ev);
+        }
+    }
+
+    private void onLongPressDetected(MotionEvent motionEvent) {
+        if (mActivityInterface.onLongPressToUnstashTaskbar()) {
+            setActive(motionEvent);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
index 50da93b..df94d0b 100644
--- a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
+++ b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.view.View;
@@ -98,6 +99,14 @@
         alpha.setDuration(DURATION_MS);
         alpha.setInterpolator(Interpolators.DECELERATED_EASE);
         mAnimators.play(alpha);
+
+        mAnimators.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                SCALE_PROPERTY.set(v, 1f);
+                v.setAlpha(1f);
+            }
+        });
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 45bcc87..4332c76 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -2236,7 +2236,7 @@
     public PendingAnimation createTaskDismissAnimation(TaskView dismissedTaskView,
             boolean animateTaskView, boolean shouldRemoveTask, long duration) {
         if (mPendingAnimation != null) {
-            mPendingAnimation.createPlaybackController().dispatchOnCancel();
+            mPendingAnimation.createPlaybackController().dispatchOnCancel().dispatchOnEnd();
         }
         PendingAnimation anim = new PendingAnimation(duration);
 
diff --git a/res/layout/widgets_list_row_header.xml b/res/layout/widgets_list_row_header.xml
index 0bfa2b2..3eaa7ea 100644
--- a/res/layout/widgets_list_row_header.xml
+++ b/res/layout/widgets_list_row_header.xml
@@ -20,7 +20,6 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_marginHorizontal="16dp"
-    android:layout_marginBottom="@dimen/widget_list_entry_bottom_margin"
     android:paddingVertical="@dimen/widget_list_header_view_vertical_padding"
     android:orientation="horizontal"
     launcher:appIconSize="48dp">
diff --git a/res/layout/widgets_table_container.xml b/res/layout/widgets_table_container.xml
index 4f70a05..fac21fd 100644
--- a/res/layout/widgets_table_container.xml
+++ b/res/layout/widgets_table_container.xml
@@ -18,5 +18,4 @@
     android:id="@+id/widgets_table"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_marginHorizontal="16dp"
-    android:layout_marginBottom="@dimen/widget_list_entry_bottom_margin"/>
+    android:layout_marginHorizontal="16dp"/>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index f4a0a3d..f5c08c5 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -147,7 +147,7 @@
     <dimen name="widget_list_content_corner_radius">4dp</dimen>
 
     <dimen name="widget_list_header_view_vertical_padding">20dp</dimen>
-    <dimen name="widget_list_entry_bottom_margin">2dp</dimen>
+    <dimen name="widget_list_entry_spacing">2dp</dimen>
 
     <dimen name="widget_preview_shadow_blur">0.5dp</dimen>
     <dimen name="widget_preview_key_shadow_distance">1dp</dimen>
diff --git a/res/values/id.xml b/res/values/id.xml
index d941716..0e2dff0 100644
--- a/res/values/id.xml
+++ b/res/values/id.xml
@@ -16,6 +16,7 @@
 -->
 <resources>
     <item type="id" name="apps_list_view_work" />
+    <item type="id" name="tag_widget_entry" />
     <item type="id" name="view_type_widgets_list" />
     <item type="id" name="view_type_widgets_header" />
     <item type="id" name="view_type_widgets_search_header" />
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 7d8b82a..85ca280 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -278,12 +278,19 @@
         }
     }
 
-    public void dispatchOnStart() {
+    public AnimatorPlaybackController dispatchOnStart() {
         callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationStart);
+        return this;
     }
 
-    public void dispatchOnCancel() {
+    public AnimatorPlaybackController dispatchOnCancel() {
         callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationCancel);
+        return this;
+    }
+
+    public AnimatorPlaybackController dispatchOnEnd() {
+        callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationEnd);
+        return this;
     }
 
     public void dispatchSetInterpolator(TimeInterpolator interpolator) {
@@ -328,7 +335,7 @@
         public void onAnimationSuccess(Animator animator) {
             // We wait for the spring (if any) to finish running before completing the end callback.
             if (!mDispatched) {
-                callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationEnd);
+                dispatchOnEnd();
                 if (mEndAction != null) {
                     mEndAction.run();
                 }
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index 3abcc2b..7091d2b 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -168,6 +168,8 @@
         cellY = info.cellY;
         spanX = info.spanX;
         spanY = info.spanY;
+        minSpanX = info.minSpanX;
+        minSpanY = info.minSpanY;
         rank = info.rank;
         screenId = info.screenId;
         itemType = info.itemType;
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index dbd9c28..915e140 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -113,12 +113,6 @@
         Utilities.getPrefs(getApplicationContext()).registerOnSharedPreferenceChangeListener(this);
     }
 
-    @Override
-    protected void onStop() {
-        super.onStop();
-        finish();
-    }
-
     /**
      * Obtains the preference fragment to instantiate in this activity.
      *
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index 13d6568..b34af97 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -209,7 +209,7 @@
 
         // Cancel the current animation. This will reset mState to mCurrentStableState, so store it.
         STATE_TYPE fromState = mState;
-        mConfig.reset();
+        cancelAnimation();
 
         if (!animated) {
             mAtomicAnimationFactory.cancelAllStateElementAnimation();
@@ -303,7 +303,7 @@
     public AnimatorPlaybackController createAnimationToNewWorkspace(STATE_TYPE state,
             StateAnimationConfig config) {
         config.userControlled = true;
-        mConfig.reset();
+        cancelAnimation();
         config.copyTo(mConfig);
         mConfig.playbackController = createAnimationToNewWorkspaceInternal(state)
                 .createPlaybackController();
@@ -393,6 +393,11 @@
      */
     public void cancelAnimation() {
         mConfig.reset();
+        // It could happen that a new animation is set as a result of an endListener on the
+        // existing animation.
+        while (mConfig.currentAnimation != null || mConfig.playbackController != null) {
+            mConfig.reset();
+        }
     }
 
     public void setCurrentUserControlledAnimation(AnimatorPlaybackController controller) {
@@ -508,14 +513,19 @@
          * Cancels the current animation and resets config variables.
          */
         public void reset() {
+            AnimatorSet anim = currentAnimation;
+            AnimatorPlaybackController pc = playbackController;
+
             DEFAULT.copyTo(this);
             targetState = null;
+            currentAnimation = null;
+            playbackController = null;
+            changeId++;
 
-            if (playbackController != null) {
-                playbackController.getAnimationPlayer().cancel();
-                playbackController.dispatchOnCancel();
-            } else if (currentAnimation != null) {
-                AnimatorSet anim = currentAnimation;
+            if (pc != null) {
+                pc.getAnimationPlayer().cancel();
+                pc.dispatchOnCancel().dispatchOnEnd();
+            } else if (anim != null) {
                 anim.setDuration(0);
                 if (!anim.isStarted()) {
                     // If the animation is not started the listeners do not get notified,
@@ -525,10 +535,6 @@
                 }
                 anim.cancel();
             }
-
-            currentAnimation = null;
-            playbackController = null;
-            changeId++;
         }
 
         @Override
diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java
index 5be9529..8591872 100644
--- a/src/com/android/launcher3/util/MultiValueAlpha.java
+++ b/src/com/android/launcher3/util/MultiValueAlpha.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.util;
 
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
 import android.util.FloatProperty;
 import android.view.View;
 
@@ -121,5 +123,12 @@
         public String toString() {
             return Float.toString(mValue);
         }
+
+        /**
+         * Creates and returns an Animator from the current value to the given value.
+         */
+        public Animator animateToValue(float value) {
+            return ObjectAnimator.ofFloat(this, VALUE, value);
+        }
     }
 }
diff --git a/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
index c0f89bc..055e4ec 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
@@ -60,7 +60,7 @@
         WidgetsListSearchHeaderEntry otherEntry = (WidgetsListSearchHeaderEntry) obj;
         return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
                 && mTitleSectionName.equals(otherEntry.mTitleSectionName)
-                && mIsWidgetListShown;
+                && mIsWidgetListShown == otherEntry.mIsWidgetListShown;
     }
 
     /** Returns a copy of this {@link WidgetsListSearchHeaderEntry} with the widget list shown. */
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
index 5d9adf0..b668c90 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_APP_EXPANDED;
 
 import android.content.Context;
+import android.graphics.Rect;
 import android.os.Process;
 import android.util.Log;
 import android.util.Size;
@@ -34,6 +35,7 @@
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.Adapter;
+import androidx.recyclerview.widget.RecyclerView.LayoutParams;
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
 import com.android.launcher3.BaseActivity;
@@ -107,7 +109,8 @@
     @Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
     @Nullable private RecyclerView mRecyclerView;
     @Nullable private PackageUserKey mPendingClickHeader;
-    private int mShortcutPreviewPadding;
+    private final int mShortcutPreviewPadding;
+    private final int mSpacingBetweenEntries;
 
     private final WidgetPreviewLoadedCallback mPreviewLoadedCallback =
             ignored -> updateVisibleEntries();
@@ -141,11 +144,28 @@
         mShortcutPreviewPadding =
                 2 * context.getResources()
                         .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
+        mSpacingBetweenEntries =
+                context.getResources().getDimensionPixelSize(R.dimen.widget_list_entry_spacing);
     }
 
     @Override
     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
         mRecyclerView = recyclerView;
+
+        mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
+            @Override
+            public void getItemOffsets(
+                    @NonNull Rect outRect,
+                    @NonNull View view,
+                    @NonNull RecyclerView parent,
+                    @NonNull RecyclerView.State state) {
+                super.getItemOffsets(outRect, view, parent, state);
+                int position = ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
+                boolean isHeader =
+                        view.getTag(R.id.tag_widget_entry) instanceof WidgetsListBaseEntry.Header;
+                outRect.top += position > 0 && isHeader ? mSpacingBetweenEntries : 0;
+            }
+        });
     }
 
     @Override
@@ -329,7 +349,9 @@
     @Override
     public void onBindViewHolder(ViewHolder holder, int pos) {
         ViewHolderBinder viewHolderBinder = mViewHolderBinders.get(getItemViewType(pos));
+        WidgetsListBaseEntry entry = mVisibleEntries.get(pos);
         viewHolderBinder.bindViewHolder(holder, mVisibleEntries.get(pos), pos);
+        holder.itemView.setTag(R.id.tag_widget_entry, entry);
     }
 
     @Override
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
index cdab964..ef2adbb 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -30,7 +30,6 @@
 
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
-import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.LauncherAppState;
@@ -59,7 +58,6 @@
     @Nullable private HandlerRunnable mIconLoadRequest;
     @Nullable private Drawable mIconDrawable;
     private final int mIconSize;
-    private final int mBottomMarginSize;
 
     private ImageView mAppIcon;
     private TextView mTitle;
@@ -86,8 +84,6 @@
                 R.styleable.WidgetsListRowHeader, defStyleAttr, /* defStyleRes= */ 0);
         mIconSize = a.getDimensionPixelSize(R.styleable.WidgetsListRowHeader_appIconSize,
                 grid.iconSizePx);
-        mBottomMarginSize =
-                getResources().getDimensionPixelSize(R.dimen.widget_list_entry_bottom_margin);
     }
 
     @Override
@@ -146,13 +142,6 @@
     public void setExpanded(boolean isExpanded) {
         this.mIsExpanded = isExpanded;
         mExpandToggle.setChecked(isExpanded);
-        if (getLayoutParams() instanceof RecyclerView.LayoutParams) {
-            int bottomMargin = isExpanded ? 0 : mBottomMarginSize;
-            RecyclerView.LayoutParams layoutParams =
-                    ((RecyclerView.LayoutParams) getLayoutParams());
-            layoutParams.bottomMargin = bottomMargin;
-            setLayoutParams(layoutParams);
-        }
     }
 
     /** Sets the {@link WidgetsListDrawableState} and refreshes the background drawable. */
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
index 3ee8b33..7671841 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
@@ -52,7 +52,7 @@
     private HeaderViewDimensionsProvider mHeaderViewDimensionsProvider;
     private int mLastVisibleWidgetContentTableHeight = 0;
     private int mWidgetHeaderHeight = 0;
-    private final int mCollapsedHeaderBottomMarginSize;
+    private final int mSpacingBetweenEntries;
     @Nullable private OnContentChangeListener mOnContentChangeListener;
 
     public WidgetsRecyclerView(Context context) {
@@ -72,9 +72,9 @@
         ActivityContext activity = ActivityContext.lookupContext(getContext());
         DeviceProfile grid = activity.getDeviceProfile();
 
-        // The bottom margin used when the header is not expanded.
-        mCollapsedHeaderBottomMarginSize =
-                getResources().getDimensionPixelSize(R.dimen.widget_list_entry_bottom_margin);
+        // The spacing used between entries.
+        mSpacingBetweenEntries =
+                getResources().getDimensionPixelSize(R.dimen.widget_list_entry_spacing);
     }
 
     @Override
@@ -270,16 +270,16 @@
         if (untilIndex > mAdapter.getItems().size()) {
             untilIndex = mAdapter.getItems().size();
         }
-        int expandedHeaderPosition = mAdapter.getSelectedHeaderPosition().orElse(-1);
         int totalItemsHeight = 0;
         for (int i = 0; i < untilIndex; i++) {
             WidgetsListBaseEntry entry = mAdapter.getItems().get(i);
             if (entry instanceof WidgetsListHeaderEntry
                     || entry instanceof WidgetsListSearchHeaderEntry) {
                 totalItemsHeight += mWidgetHeaderHeight;
-                if (expandedHeaderPosition != i) {
-                    // If the header is collapsed, include the bottom margin it will use.
-                    totalItemsHeight += mCollapsedHeaderBottomMarginSize;
+                if (i > 0) {
+                    // Each header contains the spacing between entries as top decoration, except
+                    // the first one.
+                    totalItemsHeight += mSpacingBetweenEntries;
                 }
             } else if (entry instanceof WidgetsListContentEntry) {
                 totalItemsHeight += mLastVisibleWidgetContentTableHeight;