Merge "Clear swipe region cache on nav mode switch" into ub-launcher3-rvc-dev
diff --git a/quickstep/recents_ui_overrides/res/values/dimens.xml b/quickstep/recents_ui_overrides/res/values/dimens.xml
index 363840a..9266b06 100644
--- a/quickstep/recents_ui_overrides/res/values/dimens.xml
+++ b/quickstep/recents_ui_overrides/res/values/dimens.xml
@@ -20,6 +20,7 @@
     <dimen name="chip_hint_start_padding">10dp</dimen>
     <dimen name="chip_hint_end_padding">12dp</dimen>
     <dimen name="chip_hint_horizontal_margin">20dp</dimen>
+    <dimen name="chip_hint_vertical_offset">16dp</dimen>
     <dimen name="chip_hint_elevation">2dp</dimen>
     <dimen name="chip_icon_size">16dp</dimen>
     <dimen name="chip_text_height">26dp</dimen>
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 8cf5da2..79b4002 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -197,14 +197,15 @@
             case INDEX_RECENTS_FADE_ANIM:
                 return ObjectAnimator.ofFloat(mLauncher.getOverviewPanel(),
                         RecentsView.CONTENT_ALPHA, values);
-            case INDEX_RECENTS_TRANSLATE_X_ANIM:
-                // TODO: Do not assume motion across X axis for adjacent page
+            case INDEX_RECENTS_TRANSLATE_X_ANIM: {
+                RecentsView rv = mLauncher.getOverviewPanel();
                 return new SpringAnimationBuilder(mLauncher)
-                        .setMinimumVisibleChange(1f / mLauncher.getOverviewPanel().getWidth())
+                        .setMinimumVisibleChange(1f / rv.getPageOffsetScale())
                         .setDampingRatio(0.8f)
                         .setStiffness(250)
                         .setValues(values)
-                        .build(mLauncher.getOverviewPanel(), ADJACENT_PAGE_OFFSET);
+                        .build(rv, ADJACENT_PAGE_OFFSET);
+            }
             case INDEX_PAUSE_TO_OVERVIEW_ANIM: {
                 StateAnimationConfig config = new StateAnimationConfig();
                 config.duration = ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index 77118d5..2b456ec 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.uioverrides.touchcontrollers;
 
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
@@ -23,6 +22,7 @@
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.touch.AbstractStateChangeTouchController.SUCCESS_TRANSITION_PROGRESS;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.animation.ValueAnimator;
@@ -131,12 +131,8 @@
         final PendingAnimation builder = new PendingAnimation(accuracy);
         if (mStartState == OVERVIEW) {
             RecentsView recentsView = mLauncher.getOverviewPanel();
-            float pullbackDist = mPullbackDistance;
-            if (!recentsView.isRtl()) {
-                pullbackDist = -pullbackDist;
-            }
-
-            builder.setFloat(recentsView, VIEW_TRANSLATE_X, pullbackDist, PULLBACK_INTERPOLATOR);
+            builder.setFloat(recentsView, ADJACENT_PAGE_OFFSET,
+                    -mPullbackDistance / recentsView.getPageOffsetScale(), PULLBACK_INTERPOLATOR);
             if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
                 builder.addOnFrameCallback(
                         () -> recentsView.redrawLiveTile(false /* mightNeedToRefill */));
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
index 71aa2e8..381ecf1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -166,8 +166,8 @@
         float velocityDp = dpiFromPx(velocity);
         boolean isFling = Math.abs(velocityDp) > 1;
         LauncherStateManager stateManager = mLauncher.getStateManager();
-        if (isFling) {
-            // When flinging, go back to home instead of overview.
+        boolean goToHomeInsteadOfOverview = isFling;
+        if (goToHomeInsteadOfOverview) {
             if (velocity > 0) {
                 stateManager.goToState(NORMAL, true,
                         () -> onSwipeInteractionCompleted(NORMAL, Touch.FLING));
@@ -187,20 +187,21 @@
                         () -> onSwipeInteractionCompleted(NORMAL, Touch.SWIPE)));
                 anim.start();
             }
-        } else {
-            if (mReachedOverview) {
-                float distanceDp = dpiFromPx(Math.max(
-                        Math.abs(mRecentsView.getTranslationX()),
-                        Math.abs(mRecentsView.getTranslationY())));
-                long duration = (long) Math.max(TRANSLATION_ANIM_MIN_DURATION_MS,
-                        distanceDp / TRANSLATION_ANIM_VELOCITY_DP_PER_MS);
-                mRecentsView.animate()
-                        .translationX(0)
-                        .translationY(0)
-                        .setInterpolator(ACCEL_DEACCEL)
-                        .setDuration(duration)
-                        .withEndAction(this::maybeSwipeInteractionToOverviewComplete);
-            }
+        }
+        if (mReachedOverview) {
+            float distanceDp = dpiFromPx(Math.max(
+                    Math.abs(mRecentsView.getTranslationX()),
+                    Math.abs(mRecentsView.getTranslationY())));
+            long duration = (long) Math.max(TRANSLATION_ANIM_MIN_DURATION_MS,
+                    distanceDp / TRANSLATION_ANIM_VELOCITY_DP_PER_MS);
+            mRecentsView.animate()
+                    .translationX(0)
+                    .translationY(0)
+                    .setInterpolator(ACCEL_DEACCEL)
+                    .setDuration(duration)
+                    .withEndAction(goToHomeInsteadOfOverview
+                            ? null
+                            : this::maybeSwipeInteractionToOverviewComplete);
         }
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
index bf01780..52b40a9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
@@ -1077,7 +1077,8 @@
     }
 
     private void continueComputingRecentsScrollIfNecessary() {
-        if (!mGestureState.hasState(STATE_RECENTS_SCROLLING_FINISHED)) {
+        if (!mGestureState.hasState(STATE_RECENTS_SCROLLING_FINISHED)
+                && !mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
             computeRecentsScrollIfInvisible();
             mRecentsView.post(this::continueComputingRecentsScrollIfNecessary);
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
index 9309110..5abbd86 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
@@ -208,7 +208,6 @@
             float alpha;
             float cornerRadius = 0f;
             float scale = Math.max(mCurrentRect.width(), mTargetRect.width()) / crop.width();
-            int layer = RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers);
             if (app.mode == params.mTargetSet.targetMode) {
                 alpha = mTaskAlphaCallback.getAlpha(app, params.mTargetAlpha);
                 if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
@@ -245,13 +244,11 @@
                 alpha = mBaseAlphaCallback.getAlpha(app, progress);
                 if (ENABLE_QUICKSTEP_LIVE_TILE.get() && params.mLauncherOnTop) {
                     crop = null;
-                    layer = Integer.MAX_VALUE;
                 }
             }
             builder.withAlpha(alpha)
                     .withMatrix(mTmpMatrix)
                     .withWindowCrop(crop)
-                    .withLayer(layer)
                     // Since radius is in Surface space, but we draw the rounded corners in screen
                     // space, we have to undo the scale
                     .withCornerRadius(cornerRadius / scale);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index 90dc5a4..3b6fd13 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -1670,6 +1670,13 @@
         updateCurveProperties();
     }
 
+    /**
+     * TODO: Do not assume motion across X axis for adjacent page
+     */
+    public float getPageOffsetScale() {
+        return Math.max(getWidth(), 1);
+    }
+
     private void updateDeadZoneRects() {
         // Get the deadzone rect surrounding the clear all button to not dismiss overview to home
         mClearAllButtonDeadZoneRect.setEmpty();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
index 80022b4..9b47520 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
@@ -40,6 +40,7 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.TaskOverlayFactory;
@@ -150,9 +151,26 @@
         return (type & TYPE_TASK_MENU) != 0;
     }
 
-    public void setPosition(float x, float y) {
-        setX(x);
-        setY(y + mThumbnailTopMargin);
+    public void setPosition(float x, float y, PagedOrientationHandler pagedOrientationHandler) {
+        float adjustedY = y + mThumbnailTopMargin;
+        // Changing pivot to make computations easier
+        // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set,
+        // which would render the X and Y position set here incorrect
+        setPivotX(0);
+        setPivotY(0);
+        setRotation(pagedOrientationHandler.getDegreesRotated());
+        setX(pagedOrientationHandler.getTaskMenuX(x, mTaskView.getThumbnail()));
+        setY(pagedOrientationHandler.getTaskMenuY(adjustedY, mTaskView.getThumbnail()));
+    }
+
+    public void onRotationChanged() {
+        if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
+            mOpenCloseAnimator.end();
+        }
+        if (mIsOpen) {
+            mOptionLayout.removeAllViews();
+            populateAndLayoutMenu();
+        }
     }
 
     public static TaskMenuView showForTask(TaskView taskView) {
@@ -168,12 +186,16 @@
         }
         mActivity.getDragLayer().addView(this);
         mTaskView = taskView;
-        addMenuOptions(mTaskView);
-        orientAroundTaskView(mTaskView);
+        populateAndLayoutMenu();
         post(this::animateOpen);
         return true;
     }
 
+    private void populateAndLayoutMenu() {
+        addMenuOptions(mTaskView);
+        orientAroundTaskView(mTaskView);
+    }
+
     private void addMenuOptions(TaskView taskView) {
         Drawable icon = taskView.getTask().icon.getConstantState().newDrawable();
         mTaskIcon.setDrawable(icon);
@@ -200,21 +222,26 @@
                 R.layout.task_view_menu_option, this, false);
         menuOption.setIconAndLabelFor(
                 menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
+        LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams();
+        mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp);
         menuOptionView.setOnClickListener(menuOption);
         mOptionLayout.addView(menuOptionView);
     }
 
     private void orientAroundTaskView(TaskView taskView) {
+        PagedOrientationHandler orientationHandler = taskView.getPagedOrientationHandler();
         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
         mActivity.getDragLayer().getDescendantRectRelativeToSelf(taskView, sTempRect);
         Rect insets = mActivity.getDragLayer().getInsets();
         BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams();
-        params.width = taskView.getMeasuredWidth();
+        params.width = orientationHandler.getTaskMenuWidth(taskView.getThumbnail());
         params.gravity = Gravity.START;
         setLayoutParams(params);
         setScaleX(taskView.getScaleX());
         setScaleY(taskView.getScaleY());
-        setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top);
+        mOptionLayout.setOrientation(orientationHandler.getTaskMenuLayoutOrientation());
+        setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top,
+            taskView.getPagedOrientationHandler());
     }
 
     private void animateOpen() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
index b0758c9..3cd2b32 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -174,11 +174,12 @@
 
     // Order in which the footers appear. Lower order appear below higher order.
     public static final int INDEX_DIGITAL_WELLBEING_TOAST = 0;
-    public static final int INDEX_PROACTIVE_SUGGEST = 1;
     private final FooterWrapper[] mFooters = new FooterWrapper[2];
     private float mFooterVerticalOffset = 0;
     private float mFooterAlpha = 1;
     private int mStackHeight;
+    private View mContextualChipWrapper;
+    private View mContextualChip;
 
     public TaskView(Context context) {
         this(context, null);
@@ -255,8 +256,14 @@
                     footer.animateHide();
                 }
             }
+            if (mContextualChip != null) {
+                mContextualChip.animate().scaleX(0f).scaleY(0f).setDuration(300);
+            }
             mIconView.animate().alpha(0.0f);
         } else {
+            if (mContextualChip != null) {
+                mContextualChip.animate().scaleX(1f).scaleY(1f).setDuration(300);
+            }
             mIconView.animate().alpha(1.0f);
         }
 
@@ -466,6 +473,7 @@
         int iconRotation = orientationState.getTouchRotation();
         PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler();
         boolean isRtl = orientationHandler.getRecentsRtlSetting(getResources());
+        LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams();
         int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
         LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
         int rotation = orientationState.getTouchRotationDegrees();
@@ -473,7 +481,8 @@
             case Surface.ROTATION_90:
                 iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
                 iconParams.rightMargin = -thumbnailPadding;
-                iconParams.leftMargin = iconParams.topMargin = iconParams.bottomMargin = 0;
+                iconParams.leftMargin = 0;
+                iconParams.topMargin = snapshotParams.topMargin / 2;
                 break;
             case Surface.ROTATION_180:
                 iconParams.gravity = BOTTOM | CENTER_HORIZONTAL;
@@ -483,17 +492,21 @@
             case Surface.ROTATION_270:
                 iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
                 iconParams.leftMargin = -thumbnailPadding;
-                iconParams.rightMargin = iconParams.topMargin = iconParams.bottomMargin = 0;
+                iconParams.rightMargin = 0;
+                iconParams.topMargin = snapshotParams.topMargin / 2;
                 break;
             case Surface.ROTATION_0:
             default:
                 iconParams.gravity = TOP | CENTER_HORIZONTAL;
-                iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin =
-                    iconParams.bottomMargin = 0;
+                iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
                 break;
         }
         mIconView.setLayoutParams(iconParams);
         mIconView.setRotation(rotation);
+
+        if (mMenuView != null) {
+            mMenuView.onRotationChanged();
+        }
     }
 
     private void setIconAndDimTransitionProgress(float progress, boolean invert) {
@@ -600,7 +613,10 @@
         }
 
         if (mMenuView != null) {
-            mMenuView.setPosition(getX() - getRecentsView().getScrollX(), getY());
+            PagedOrientationHandler pagedOrientationHandler = getPagedOrientationHandler();
+            RecentsView recentsView = getRecentsView();
+            mMenuView.setPosition(getX() - recentsView.getScrollX(),
+                    getY() - recentsView.getScrollY(), pagedOrientationHandler);
             mMenuView.setScaleX(getScaleX());
             mMenuView.setScaleY(getScaleY());
         }
@@ -657,6 +673,51 @@
         return oldFooter;
     }
 
+    /**
+     * Sets the contextual chip.
+     *
+     * @param view Wrapper view containing contextual chip.
+     */
+    public void setContextualChip(View view) {
+        if (mContextualChipWrapper != null) {
+            removeView(mContextualChipWrapper);
+        }
+        if (view != null) {
+            mContextualChipWrapper = view;
+            LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
+                    LayoutParams.WRAP_CONTENT);
+            layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL;
+            layoutParams.bottomMargin = (int)
+                    (((MarginLayoutParams) mSnapshotView.getLayoutParams()).bottomMargin
+                            - getExpectedViewHeight(view) + getResources().getDimension(
+                            R.dimen.chip_hint_vertical_offset));
+            mContextualChip = ((FrameLayout) mContextualChipWrapper).getChildAt(0);
+            mContextualChip.setScaleX(0f);
+            mContextualChip.setScaleY(0f);
+            addView(view, getChildCount(), layoutParams);
+            view.setAlpha(mFooterAlpha);
+            if (mContextualChip != null) {
+                mContextualChip.animate().scaleX(1f).scaleY(1f).setDuration(50);
+            }
+        }
+
+    }
+
+    /**
+     * Clears the contextual chip from TaskView.
+     *
+     * @return The contextual chip wrapper view to be recycled.
+     */
+    public View clearContextualChip() {
+        if (mContextualChipWrapper != null) {
+            removeView(mContextualChipWrapper);
+        }
+        View oldContextualChipWrapper = mContextualChipWrapper;
+        mContextualChipWrapper = null;
+        mContextualChip = null;
+        return oldContextualChipWrapper;
+    }
+
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
@@ -750,14 +811,7 @@
             mDelegate = mOldOutlineProvider == null
                     ? ViewOutlineProvider.BACKGROUND : mOldOutlineProvider;
 
-            int h = view.getLayoutParams().height;
-            if (h > 0) {
-                mExpectedHeight = h;
-            } else {
-                int m = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY - 1, MeasureSpec.AT_MOST);
-                view.measure(m, m);
-                mExpectedHeight = view.getMeasuredHeight();
-            }
+            mExpectedHeight = getExpectedViewHeight(view);
             mOldPaddingBottom = view.getPaddingBottom();
 
             if (mOldOutlineProvider != null) {
@@ -819,6 +873,19 @@
         }
     }
 
+    private int getExpectedViewHeight(View view) {
+        int expectedHeight;
+        int h = view.getLayoutParams().height;
+        if (h > 0) {
+            expectedHeight = h;
+        } else {
+            int m = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY - 1, MeasureSpec.AT_MOST);
+            view.measure(m, m);
+            expectedHeight = view.getMeasuredHeight();
+        }
+        return expectedHeight;
+    }
+
     @Override
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(info);
@@ -874,6 +941,10 @@
         return (RecentsView) getParent();
     }
 
+    PagedOrientationHandler getPagedOrientationHandler() {
+        return getRecentsView().mOrientationState.getOrientationHandler();
+    }
+
     public void notifyTaskLaunchFailed(String tag) {
         String msg = "Failed to launch task";
         if (mTask != null) {
diff --git a/quickstep/res/layout/gesture_tutorial_fragment.xml b/quickstep/res/layout/gesture_tutorial_fragment.xml
index 69481ad..190290e 100644
--- a/quickstep/res/layout/gesture_tutorial_fragment.xml
+++ b/quickstep/res/layout/gesture_tutorial_fragment.xml
@@ -18,6 +18,12 @@
     android:layout_height="match_parent"
     android:background="@color/gesture_tutorial_background_color">
 
+    <View
+        android:id="@+id/gesture_tutorial_ripple_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@drawable/gesture_tutorial_ripple"/>
+
     <ImageView
         android:id="@+id/gesture_tutorial_fragment_hand_coaching"
         android:layout_width="match_parent"
@@ -66,6 +72,17 @@
             style="@style/TextAppearance.GestureTutorial.Subtitle"/>
     </LinearLayout>
 
+    <TextView
+        android:id="@+id/gesture_tutorial_fragment_feedback_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="10dp"
+        android:layout_centerHorizontal="true"
+        android:layout_above="@id/gesture_tutorial_fragment_action_button"
+        android:layout_marginStart="@dimen/gesture_tutorial_feedback_margin_start_end"
+        android:layout_marginEnd="@dimen/gesture_tutorial_feedback_margin_start_end"
+        style="@style/TextAppearance.GestureTutorial.Feedback"/>
+
     <!-- android:stateListAnimator="@null" removes shadow and normal on click behavior (increase
          of elevation and shadow) which is replaced by ripple effect in android:foreground -->
     <Button
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 18dc19c..b06dc6b 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -83,5 +83,6 @@
     <!-- Tips Gesture Tutorial -->
     <dimen name="gesture_tutorial_title_margin_start_end">40dp</dimen>
     <dimen name="gesture_tutorial_subtitle_margin_start_end">16dp</dimen>
+    <dimen name="gesture_tutorial_feedback_margin_start_end">24dp</dimen>
     <dimen name="gesture_tutorial_button_margin_start_end">18dp</dimen>
 </resources>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 61690ae..b474a32 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -98,12 +98,22 @@
     <string name="back_gesture_tutorial_playground_title_swipe_inward_right_edge" translatable="false">Try the back gesture</string>
     <!-- Subtitle shown during interactive parts of Back gesture tutorial for right edge. [CHAR LIMIT=60] -->
     <string name="back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge" translatable="false">Start at the right edge and swipe toward the middle</string>
+    <!-- Feedback shown during interactive parts of Back gesture tutorial for right edge when the gesture is too far from the edge. [CHAR LIMIT=100] -->
+    <string name="back_gesture_feedback_swipe_too_far_from_right_edge" translatable="false">Make sure you swipe from the far right edge</string>
+    <!-- Feedback shown during interactive parts of Back gesture tutorial for right edge when the gesture is cancelled. [CHAR LIMIT=100] -->
+    <string name="back_gesture_feedback_cancelled_right_edge" translatable="false">Make sure you swipe straight to the left and let go</string>
 
     <!-- Title shown during interactive part of Back gesture tutorial for left edge. [CHAR LIMIT=30] -->
     <string name="back_gesture_tutorial_playground_title_swipe_inward_left_edge" translatable="false">Try the other side</string>
     <!-- Subtitle shown during interactive parts of Back gesture tutorial for left edge. [CHAR LIMIT=60] -->
     <string name="back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge" translatable="false">That\'s it! Now try swiping from the left edge.</string>
+    <!-- Feedback shown during interactive parts of Back gesture tutorial for left edge when the gesture is too far from the edge. [CHAR LIMIT=100] -->
+    <string name="back_gesture_feedback_swipe_too_far_from_left_edge" translatable="false">Make sure you swipe from the far left edge</string>
+    <!-- Feedback shown during interactive parts of Back gesture tutorial for left edge when the gesture is cancelled. [CHAR LIMIT=100] -->
+    <string name="back_gesture_feedback_cancelled_left_edge" translatable="false">Make sure you swipe straight to the right and let go</string>
 
+    <!-- Feedback shown during interactive parts of Back gesture tutorial when the gesture is within the nav bar region. [CHAR LIMIT=100] -->
+    <string name="back_gesture_feedback_swipe_in_nav_bar" translatable="false">Make sure you don\'t swipe too close to the bottom of the screen</string>
     <!-- Subtitle shown on the confirmation screen after successful gesture. [CHAR LIMIT=60] -->
     <string name="back_gesture_tutorial_confirm_subtitle" translatable="false">To change the sensitivity of the back gesture, go to Settings</string>
 
@@ -112,6 +122,12 @@
     <string name="home_gesture_tutorial_playground_title" translatable="false">Tutorial: Go Home</string>
     <!-- Subtitle shown during interactive parts of Home gesture tutorial. [CHAR LIMIT=60] -->
     <string name="home_gesture_tutorial_playground_subtitle" translatable="false">Try swiping upward from the bottom edge of the screen</string>
+    <!-- Feedback shown during interactive parts of Home gesture tutorial when the gesture is started too far from the edge. [CHAR LIMIT=100] -->
+    <string name="home_gesture_feedback_swipe_too_far_from_edge" translatable="false">Make sure you swipe from the bottom edge of the screen</string>
+    <!-- Feedback shown during interactive parts of Home gesture tutorial when the Overview gesture is detected. [CHAR LIMIT=100] -->
+    <string name="home_gesture_feedback_overview_detected" translatable="false">Make sure you don\'t pause before letting go</string>
+    <!-- Feedback shown during interactive parts of Home gesture tutorial when the gesture is horizontal instead of vertical. [CHAR LIMIT=100] -->
+    <string name="home_gesture_feedback_wrong_swipe_direction" translatable="false">Make sure you swipe straight up</string>
 
   <!-- Title shown on the confirmation screen after successful gesture. [CHAR LIMIT=30] -->
   <string name="gesture_tutorial_confirm_title" translatable="false">All set</string>
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index 4915f5f..3926988 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -47,6 +47,14 @@
         <item name="android:textSize">21sp</item>
     </style>
 
+    <style name="TextAppearance.GestureTutorial.Feedback"
+        parent="TextAppearance.GestureTutorial">
+        <item name="android:gravity">center</item>
+        <item name="android:textColor">@color/gesture_tutorial_feedback_color</item>
+        <item name="android:letterSpacing">0.03</item>
+        <item name="android:textSize">21sp</item>
+    </style>
+
     <style name="TextAppearance.GestureTutorial.ButtonLabel"
         parent="TextAppearance.GestureTutorial.CallToAction">
         <item name="android:gravity">center</item>
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 65763d4..af63a25 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -161,6 +161,8 @@
     @Override
     protected void setupViews() {
         super.setupViews();
+
+        SysUINavigationMode.INSTANCE.get(this).updateMode();
         mActionsView = findViewById(R.id.overview_actions_view);
         ((RecentsView) getOverviewPanel()).init(mActionsView);
 
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index 70cbd82..1cb0aa4 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -611,7 +611,6 @@
                                 .withWindowCrop(target.screenSpaceBounds)
                                 .withAlpha(1f);
                     }
-                    builder.withLayer(RemoteAnimationProvider.getLayer(target, MODE_OPENING));
                     params[i] = builder.build();
                 }
                 surfaceApplier.scheduleApply(params);
@@ -718,7 +717,6 @@
                     params[i] = new SurfaceParams.Builder(target.leash)
                             .withAlpha(1f)
                             .withWindowCrop(target.screenSpaceBounds)
-                            .withLayer(RemoteAnimationProvider.getLayer(target, MODE_OPENING))
                             .withCornerRadius(cornerRadius)
                             .build();
                 }
@@ -775,7 +773,6 @@
                     }
                     params[i] = builder
                             .withWindowCrop(target.screenSpaceBounds)
-                            .withLayer(RemoteAnimationProvider.getLayer(target, MODE_CLOSING))
                             .build();
                 }
                 surfaceApplier.scheduleApply(params);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index cc3fd97..e5c9fc9 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -22,10 +22,12 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.TimeInterpolator;
@@ -132,7 +134,12 @@
             return TouchInteractionService.isConnected() ?
                     mLauncher.getStateManager().getLastState() : NORMAL;
         } else if (fromState == OVERVIEW) {
-            return isDragTowardPositive ? ALL_APPS : NORMAL;
+            LauncherState positiveDragTarget = ALL_APPS;
+            if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(mLauncher)) {
+                // Don't allow swiping up to all apps.
+                positiveDragTarget = OVERVIEW;
+            }
+            return isDragTowardPositive ? positiveDragTarget : NORMAL;
         } else if (fromState == NORMAL && isDragTowardPositive) {
             int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
             return mAllowDragToOverview && TouchInteractionService.isConnected()
diff --git a/quickstep/src/com/android/quickstep/SysUINavigationMode.java b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
index 6a10b37..c715c93 100644
--- a/quickstep/src/com/android/quickstep/SysUINavigationMode.java
+++ b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
@@ -74,15 +74,20 @@
         mContext.registerReceiver(new BroadcastReceiver() {
             @Override
             public void onReceive(Context context, Intent intent) {
-                Mode oldMode = mMode;
-                initializeMode();
-                if (mMode != oldMode) {
-                    dispatchModeChange();
-                }
+                updateMode();
             }
         }, getPackageFilter("android", ACTION_OVERLAY_CHANGED));
     }
 
+    /** Updates navigation mode when needed. */
+    public void updateMode() {
+        Mode oldMode = mMode;
+        initializeMode();
+        if (mMode != oldMode) {
+            dispatchModeChange();
+        }
+    }
+
     private void initializeMode() {
         int modeInt = getSystemIntegerRes(mContext, NAV_BAR_INTERACTION_MODE_RES_NAME);
         for(Mode m : Mode.values()) {
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
index d58ab5d..fe95e83 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -22,6 +22,7 @@
 
 import com.android.launcher3.R;
 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
+import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
 
 /** A {@link TutorialController} for the Back tutorial. */
 final class BackGestureTutorialController extends TutorialController {
@@ -34,7 +35,7 @@
     void transitToController() {
         super.transitToController();
         if (mTutorialType != BACK_NAVIGATION_COMPLETE) {
-            mHandCoachingAnimation.startLoopedAnimation(mTutorialType);
+            showHandCoachingAnimation();
         }
     }
 
@@ -95,16 +96,10 @@
     public void onBackGestureAttempted(BackGestureResult result) {
         switch (mTutorialType) {
             case RIGHT_EDGE_BACK_NAVIGATION:
-                if (result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
-                    hideHandCoachingAnimation();
-                    mTutorialFragment.changeController(LEFT_EDGE_BACK_NAVIGATION);
-                }
+                handleAttemptFromRight(result);
                 break;
             case LEFT_EDGE_BACK_NAVIGATION:
-                if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT) {
-                    hideHandCoachingAnimation();
-                    mTutorialFragment.changeController(BACK_NAVIGATION_COMPLETE);
-                }
+                handleAttemptFromLeft(result);
                 break;
             case BACK_NAVIGATION_COMPLETE:
                 if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
@@ -114,4 +109,57 @@
                 break;
         }
     }
+
+    private void handleAttemptFromRight(BackGestureResult result) {
+        switch (result) {
+            case BACK_COMPLETED_FROM_RIGHT:
+                hideFeedback();
+                hideHandCoachingAnimation();
+                showRippleEffect(
+                        () -> mTutorialFragment.changeController(LEFT_EDGE_BACK_NAVIGATION));
+                break;
+            case BACK_CANCELLED_FROM_RIGHT:
+                showFeedback(R.string.back_gesture_feedback_cancelled_right_edge);
+                break;
+            case BACK_COMPLETED_FROM_LEFT:
+            case BACK_CANCELLED_FROM_LEFT:
+            case BACK_NOT_STARTED_TOO_FAR_FROM_EDGE:
+                showFeedback(R.string.back_gesture_feedback_swipe_too_far_from_right_edge);
+                break;
+            case BACK_NOT_STARTED_IN_NAV_BAR_REGION:
+                showFeedback(R.string.back_gesture_feedback_swipe_in_nav_bar);
+                break;
+        }
+    }
+
+    private void handleAttemptFromLeft(BackGestureResult result) {
+        switch (result) {
+            case BACK_COMPLETED_FROM_LEFT:
+                hideFeedback();
+                hideHandCoachingAnimation();
+                showRippleEffect(
+                        () -> mTutorialFragment.changeController(BACK_NAVIGATION_COMPLETE));
+                break;
+            case BACK_CANCELLED_FROM_LEFT:
+                showFeedback(R.string.back_gesture_feedback_cancelled_left_edge);
+                break;
+            case BACK_COMPLETED_FROM_RIGHT:
+            case BACK_CANCELLED_FROM_RIGHT:
+            case BACK_NOT_STARTED_TOO_FAR_FROM_EDGE:
+                showFeedback(R.string.back_gesture_feedback_swipe_too_far_from_left_edge);
+                break;
+            case BACK_NOT_STARTED_IN_NAV_BAR_REGION:
+                showFeedback(R.string.back_gesture_feedback_swipe_in_nav_bar);
+                break;
+        }
+    }
+
+    @Override
+    public void onNavBarGestureAttempted(NavBarGestureResult result) {
+        if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
+            if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) {
+                mTutorialFragment.closeTutorial();
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
index 59067c1..bef50ea 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
@@ -15,6 +15,9 @@
  */
 package com.android.quickstep.interaction;
 
+import android.view.MotionEvent;
+import android.view.View;
+
 import com.android.launcher3.R;
 import com.android.quickstep.interaction.TutorialController.TutorialType;
 
@@ -29,4 +32,17 @@
     TutorialController createController(TutorialType type) {
         return new BackGestureTutorialController(this, type);
     }
+
+    @Override
+    Class<? extends TutorialController> getControllerClass() {
+        return BackGestureTutorialController.class;
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent motionEvent) {
+        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN && mTutorialController != null) {
+            mTutorialController.setRippleHotspot(motionEvent.getX(), motionEvent.getY());
+        }
+        return super.onTouch(view, motionEvent);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
index 89c57a0..e4b348e 100644
--- a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
@@ -58,6 +58,7 @@
     private final PointF mDownPoint = new PointF();
     private boolean mThresholdCrossed = false;
     private boolean mAllowGesture = false;
+    private BackGestureResult mDisallowedGestureReason;
     private boolean mIsEnabled;
     private int mLeftInset;
     private int mRightInset;
@@ -145,11 +146,13 @@
     private boolean isWithinTouchRegion(int x, int y) {
         // Disallow if too far from the edge
         if (x > mEdgeWidth + mLeftInset && x < (mDisplaySize.x - mEdgeWidth - mRightInset)) {
+            mDisallowedGestureReason = BackGestureResult.BACK_NOT_STARTED_TOO_FAR_FROM_EDGE;
             return false;
         }
 
         // Disallow if we are in the bottom gesture area
         if (y >= (mDisplaySize.y - mBottomGestureHeight)) {
+            mDisallowedGestureReason = BackGestureResult.BACK_NOT_STARTED_IN_NAV_BAR_REGION;
             return false;
         }
 
@@ -169,12 +172,12 @@
         int action = ev.getActionMasked();
         if (action == MotionEvent.ACTION_DOWN) {
             boolean isOnLeftEdge = ev.getX() <= mEdgeWidth + mLeftInset;
+            mDisallowedGestureReason = BackGestureResult.UNKNOWN;
             mAllowGesture = isWithinTouchRegion((int) ev.getX(), (int) ev.getY());
+            mDownPoint.set(ev.getX(), ev.getY());
             if (mAllowGesture) {
                 mEdgeBackPanel.setIsLeftPanel(isOnLeftEdge);
                 mEdgeBackPanel.onMotionEvent(ev);
-
-                mDownPoint.set(ev.getX(), ev.getY());
                 mThresholdCrossed = false;
             }
         } else if (mAllowGesture) {
@@ -193,7 +196,6 @@
                     if (dy > dx && dy > mTouchSlop) {
                         cancelGesture(ev);
                         return;
-
                     } else if (dx > dy && dx > mTouchSlop) {
                         mThresholdCrossed = true;
                     }
@@ -206,8 +208,10 @@
         }
 
         if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
-            if (!mAllowGesture && mGestureCallback != null) {
-                mGestureCallback.onBackGestureAttempted(BackGestureResult.BACK_NOT_STARTED);
+            float dx = Math.abs(ev.getX() - mDownPoint.x);
+            float dy = Math.abs(ev.getY() - mDownPoint.y);
+            if (dx > dy && dx > mTouchSlop && !mAllowGesture && mGestureCallback != null) {
+                mGestureCallback.onBackGestureAttempted(mDisallowedGestureReason);
             }
         }
     }
@@ -223,7 +227,8 @@
         BACK_COMPLETED_FROM_RIGHT,
         BACK_CANCELLED_FROM_LEFT,
         BACK_CANCELLED_FROM_RIGHT,
-        BACK_NOT_STARTED,
+        BACK_NOT_STARTED_TOO_FAR_FROM_EDGE,
+        BACK_NOT_STARTED_IN_NAV_BAR_REGION,
     }
 
     /** Callback to let the UI react to attempted back gestures. */
diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
index 0bf996d..0e45376 100644
--- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java
@@ -21,6 +21,7 @@
 
 import com.android.launcher3.R;
 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
+import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
 
 /** A {@link TutorialController} for the Home tutorial. */
 final class HomeGestureTutorialController extends TutorialController {
@@ -33,7 +34,7 @@
     void transitToController() {
         super.transitToController();
         if (mTutorialType != HOME_NAVIGATION_COMPLETE) {
-            mHandCoachingAnimation.startLoopedAnimation(mTutorialType);
+            showHandCoachingAnimation();
         }
     }
 
@@ -82,4 +83,33 @@
                 break;
         }
     }
+
+    @Override
+    public void onNavBarGestureAttempted(NavBarGestureResult result) {
+        switch (mTutorialType) {
+            case HOME_NAVIGATION:
+                switch (result) {
+                    case HOME_GESTURE_COMPLETED:
+                        hideHandCoachingAnimation();
+                        mTutorialFragment.changeController(HOME_NAVIGATION_COMPLETE);
+                        break;
+                    case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE:
+                    case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE:
+                        showFeedback(R.string.home_gesture_feedback_swipe_too_far_from_edge);
+                        break;
+                    case OVERVIEW_GESTURE_COMPLETED:
+                        showFeedback(R.string.home_gesture_feedback_overview_detected);
+                        break;
+                    case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
+                        showFeedback(R.string.home_gesture_feedback_wrong_swipe_direction);
+                        break;
+                }
+                break;
+            case HOME_NAVIGATION_COMPLETE:
+                if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) {
+                    mTutorialFragment.closeTutorial();
+                }
+                break;
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
index 613f188..e2a9d12 100644
--- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialFragment.java
@@ -29,4 +29,9 @@
     TutorialController createController(TutorialType type) {
         return new HomeGestureTutorialController(this, type);
     }
+
+    @Override
+    Class<? extends TutorialController> getControllerClass() {
+        return HomeGestureTutorialController.class;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
new file mode 100644
index 0000000..6d8caa2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2020 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.interaction;
+
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_GESTURE_COMPLETED;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_NOT_STARTED_TOO_FAR_FROM_EDGE;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_GESTURE_COMPLETED;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+import com.android.launcher3.ResourceUtils;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.util.NavBarPosition;
+import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
+
+/** Utility class to handle home gestures. */
+public class NavBarGestureHandler implements OnTouchListener {
+
+    private static final String LOG_TAG = "NavBarGestureHandler";
+
+    private final Point mDisplaySize = new Point();
+    private final TriggerSwipeUpTouchTracker mSwipeUpTouchTracker;
+    private int mBottomGestureHeight;
+    private boolean mTouchCameFromNavBar;
+    private NavBarGestureAttemptCallback mGestureCallback;
+
+    NavBarGestureHandler(Context context) {
+        final Display display = context.getDisplay();
+        final int displayRotation;
+        if (display == null) {
+            displayRotation = Surface.ROTATION_0;
+        } else {
+            displayRotation = display.getRotation();
+            display.getRealSize(mDisplaySize);
+        }
+        mSwipeUpTouchTracker =
+                new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/,
+                        new NavBarPosition(Mode.NO_BUTTON, displayRotation),
+                        null /*onInterceptTouch*/, this::onSwipeUp);
+
+        final Resources resources = context.getResources();
+        mBottomGestureHeight =
+                ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, resources);
+    }
+
+    void registerNavBarGestureAttemptCallback(NavBarGestureAttemptCallback callback) {
+        mGestureCallback = callback;
+    }
+
+    void unregisterNavBarGestureAttemptCallback() {
+        mGestureCallback = null;
+    }
+
+    private void onSwipeUp(boolean wasFling) {
+        if (mGestureCallback == null) {
+            return;
+        }
+        if (mTouchCameFromNavBar) {
+            mGestureCallback.onNavBarGestureAttempted(wasFling
+                    ? HOME_GESTURE_COMPLETED : OVERVIEW_GESTURE_COMPLETED);
+        } else {
+            mGestureCallback.onNavBarGestureAttempted(wasFling
+                    ? HOME_NOT_STARTED_TOO_FAR_FROM_EDGE : OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE);
+        }
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent motionEvent) {
+        int action = motionEvent.getAction();
+        boolean intercepted = mSwipeUpTouchTracker.interceptedTouch();
+        if (action == MotionEvent.ACTION_DOWN) {
+            mTouchCameFromNavBar = motionEvent.getRawY() >= mDisplaySize.y - mBottomGestureHeight;
+            mSwipeUpTouchTracker.init();
+        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+            if (mGestureCallback != null && !intercepted && mTouchCameFromNavBar) {
+                mGestureCallback.onNavBarGestureAttempted(
+                        HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION);
+                intercepted = true;
+            }
+        }
+        mSwipeUpTouchTracker.onMotionEvent(motionEvent);
+        return intercepted;
+    }
+
+    enum NavBarGestureResult {
+        UNKNOWN,
+        HOME_GESTURE_COMPLETED,
+        OVERVIEW_GESTURE_COMPLETED,
+        HOME_NOT_STARTED_TOO_FAR_FROM_EDGE,
+        OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE,
+        HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION  // Side swipe on nav bar.
+    }
+
+    /** Callback to let the UI react to attempted nav bar gestures. */
+    interface NavBarGestureAttemptCallback {
+        /** Called whenever any touch is completed. */
+        void onNavBarGestureAttempted(NavBarGestureResult result);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index f0cb567..1e29f44 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.interaction;
 
+import android.graphics.drawable.RippleDrawable;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.Button;
@@ -26,20 +27,30 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.R;
-import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
+import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback;
 
-abstract class TutorialController {
+abstract class TutorialController implements BackGestureAttemptCallback,
+        NavBarGestureAttemptCallback {
+
+    private static final int FEEDBACK_VISIBLE_MS = 3000;
+    private static final int FEEDBACK_ANIMATION_MS = 500;
+    private static final int RIPPLE_VISIBLE_MS = 300;
 
     final TutorialFragment mTutorialFragment;
-    final TutorialType mTutorialType;
+    TutorialType mTutorialType;
 
     final ImageButton mCloseButton;
     final TextView mTitleTextView;
     final TextView mSubtitleTextView;
+    final TextView mFeedbackView;
+    final View mRippleView;
+    final RippleDrawable mRippleDrawable;
     final TutorialHandAnimation mHandCoachingAnimation;
     final ImageView mHandCoachingView;
     final Button mActionTextButton;
     final Button mActionButton;
+    private final Runnable mHideFeedbackRunnable;
 
     TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
         mTutorialFragment = tutorialFragment;
@@ -50,15 +61,24 @@
         mCloseButton.setOnClickListener(button -> mTutorialFragment.closeTutorial());
         mTitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_title_view);
         mSubtitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_subtitle_view);
+        mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view);
+        mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view);
+        mRippleDrawable = (RippleDrawable) mRippleView.getBackground();
         mHandCoachingAnimation = tutorialFragment.getHandAnimation();
         mHandCoachingView = rootView.findViewById(R.id.gesture_tutorial_fragment_hand_coaching);
         mHandCoachingView.bringToFront();
         mActionTextButton =
                 rootView.findViewById(R.id.gesture_tutorial_fragment_action_text_button);
         mActionButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button);
+
+        mHideFeedbackRunnable =
+                () -> mFeedbackView.animate().alpha(0).setDuration(FEEDBACK_ANIMATION_MS)
+                        .withEndAction(this::showHandCoachingAnimation).start();
     }
 
-    abstract void onBackGestureAttempted(BackGestureResult result);
+    void setTutorialType(TutorialType tutorialType) {
+        mTutorialType = tutorialType;
+    }
 
     @Nullable
     Integer getTitleStringId() {
@@ -80,16 +100,52 @@
         return null;
     }
 
+    void showFeedback(int resId) {
+        hideHandCoachingAnimation();
+        mFeedbackView.setText(resId);
+        mFeedbackView.animate().alpha(1).setDuration(FEEDBACK_ANIMATION_MS).start();
+        mFeedbackView.removeCallbacks(mHideFeedbackRunnable);
+        mFeedbackView.postDelayed(mHideFeedbackRunnable, FEEDBACK_VISIBLE_MS);
+    }
+
+    void hideFeedback() {
+        mFeedbackView.setText(null);
+        mFeedbackView.removeCallbacks(mHideFeedbackRunnable);
+        mFeedbackView.clearAnimation();
+        mFeedbackView.setAlpha(0);
+    }
+
+    void setRippleHotspot(float x, float y) {
+        mRippleDrawable.setHotspot(x, y);
+    }
+
+    void showRippleEffect(@Nullable Runnable onCompleteRunnable) {
+        mRippleDrawable.setState(
+                new int[] {android.R.attr.state_pressed, android.R.attr.state_enabled});
+        mRippleView.postDelayed(() -> {
+            mRippleDrawable.setState(new int[] {});
+            if (onCompleteRunnable != null) {
+                onCompleteRunnable.run();
+            }
+        }, RIPPLE_VISIBLE_MS);
+    }
+
     void onActionButtonClicked(View button) {}
 
     void onActionTextButtonClicked(View button) {}
 
+    void showHandCoachingAnimation() {
+        mHandCoachingAnimation.startLoopedAnimation(mTutorialType);
+    }
+
     void hideHandCoachingAnimation() {
         mHandCoachingAnimation.stop();
+        mHandCoachingView.setVisibility(View.INVISIBLE);
     }
 
     @CallSuper
     void transitToController() {
+        hideFeedback();
         updateTitles();
         updateActionButtons();
     }
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
index 6346a9b..3a56b0e 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
@@ -21,7 +21,9 @@
 import android.os.Bundle;
 import android.util.Log;
 import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
+import android.view.View.OnTouchListener;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
 
@@ -31,13 +33,11 @@
 import androidx.fragment.app.FragmentActivity;
 
 import com.android.launcher3.R;
-import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
-import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
 import com.android.quickstep.interaction.TutorialController.TutorialType;
 
 import java.net.URISyntaxException;
 
-abstract class TutorialFragment extends Fragment implements BackGestureAttemptCallback {
+abstract class TutorialFragment extends Fragment implements OnTouchListener {
 
     private static final String LOG_TAG = "TutorialFragment";
     private static final String SYSTEM_NAVIGATION_SETTING_INTENT =
@@ -52,6 +52,7 @@
     View mRootView;
     TutorialHandAnimation mHandCoachingAnimation;
     EdgeBackGestureHandler mEdgeBackGestureHandler;
+    NavBarGestureHandler mNavBarGestureHandler;
 
     public static TutorialFragment newInstance(TutorialType tutorialType) {
         TutorialFragment fragment = getFragmentForTutorialType(tutorialType);
@@ -85,19 +86,22 @@
 
     abstract TutorialController createController(TutorialType type);
 
+    abstract Class<? extends TutorialController> getControllerClass();
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
         mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
         mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext());
-        mEdgeBackGestureHandler.registerBackGestureAttemptCallback(this);
+        mNavBarGestureHandler = new NavBarGestureHandler(getContext());
     }
 
     @Override
     public void onDestroy() {
         super.onDestroy();
         mEdgeBackGestureHandler.unregisterBackGestureAttemptCallback();
+        mNavBarGestureHandler.unregisterNavBarGestureAttemptCallback();
     }
 
     @Override
@@ -111,7 +115,7 @@
             mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right);
             return insets;
         });
-        mRootView.setOnTouchListener(mEdgeBackGestureHandler);
+        mRootView.setOnTouchListener(this);
         mHandCoachingAnimation = new TutorialHandAnimation(getContext(), mRootView,
                 getHandAnimationResId());
         return mRootView;
@@ -129,6 +133,13 @@
         mHandCoachingAnimation.stop();
     }
 
+    @Override
+    public boolean onTouch(View view, MotionEvent motionEvent) {
+        // Note: Using logical or to ensure both functions get called.
+        return mEdgeBackGestureHandler.onTouch(view, motionEvent)
+                | mNavBarGestureHandler.onTouch(view, motionEvent);
+    }
+
     void onAttachedToWindow() {
         mEdgeBackGestureHandler.setViewGroupParent((ViewGroup) getRootView());
     }
@@ -138,8 +149,14 @@
     }
 
     void changeController(TutorialType tutorialType) {
-        mTutorialController = createController(tutorialType);
+        if (getControllerClass().isInstance(mTutorialController)) {
+            mTutorialController.setTutorialType(tutorialType);
+        } else {
+            mTutorialController = createController(tutorialType);
+        }
         mTutorialController.transitToController();
+        mEdgeBackGestureHandler.registerBackGestureAttemptCallback(mTutorialController);
+        mNavBarGestureHandler.registerNavBarGestureAttemptCallback(mTutorialController);
         mTutorialType = tutorialType;
     }
 
@@ -157,13 +174,6 @@
         return mHandCoachingAnimation;
     }
 
-    @Override
-    public void onBackGestureAttempted(BackGestureResult result) {
-        if (mTutorialController != null) {
-            mTutorialController.onBackGestureAttempted(result);
-        }
-    }
-
     void closeTutorial() {
         FragmentActivity activity = getActivity();
         if (activity != null) {
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java b/quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java
index 5362aaf..c810e43 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java
@@ -45,6 +45,7 @@
 
     /** [Re]starts animation for the given tutorial. */
     void startLoopedAnimation(TutorialType tutorialType) {
+        mHandCoachingView.setVisibility(View.VISIBLE);
         if (mGestureAnimation.isRunning()) {
             stop();
         }
diff --git a/quickstep/src/com/android/quickstep/util/NavBarPosition.java b/quickstep/src/com/android/quickstep/util/NavBarPosition.java
index 74e6b29..0a98e1b 100644
--- a/quickstep/src/com/android/quickstep/util/NavBarPosition.java
+++ b/quickstep/src/com/android/quickstep/util/NavBarPosition.java
@@ -35,6 +35,11 @@
         mDisplayRotation = info.rotation;
     }
 
+    public NavBarPosition(SysUINavigationMode.Mode mode, int displayRotation) {
+        mMode = mode;
+        mDisplayRotation = displayRotation;
+    }
+
     public boolean isRightEdge() {
         return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_90;
     }
diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
index 21b97ec..4cd0206 100644
--- a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
+++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
@@ -62,15 +62,12 @@
     static void prepareTargetsForFirstFrame(RemoteAnimationTargetCompat[] targets,
             TransactionCompat t, int boostModeTargets) {
         for (RemoteAnimationTargetCompat target : targets) {
-            t.setLayer(target.leash, getLayer(target, boostModeTargets));
             t.show(target.leash);
         }
     }
 
     public static int getLayer(RemoteAnimationTargetCompat target, int boostModeTarget) {
-        return target.mode == boostModeTarget
-                ? Z_BOOST_BASE + target.prefixOrderIndex
-                : target.prefixOrderIndex;
+        return target.prefixOrderIndex;
     }
 
     /**
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index 0afe4a8..40265c4 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -203,7 +203,7 @@
                         + launcher.getNavigationModeMismatchError(),
                 () -> launcher.getNavigationModeMismatchError() == null,
                 60000 /* b/148422894 */, launcher);
-        AbstractLauncherUiTest.checkDetectedLeaks();
+        AbstractLauncherUiTest.checkDetectedLeaks(launcher);
         return true;
     }
 
diff --git a/res/drawable/gesture_tutorial_ripple.xml b/res/drawable/gesture_tutorial_ripple.xml
new file mode 100644
index 0000000..ca45662
--- /dev/null
+++ b/res/drawable/gesture_tutorial_ripple.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple android:color="@color/gesture_tutorial_ripple_color"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@android:id/mask"
+        android:drawable="@color/gesture_tutorial_background_color" />
+</ripple>
\ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
index adcff9a..c9c893e 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -39,8 +39,10 @@
     <color name="all_apps_bg_hand_fill_dark">#9AA0A6</color>
 
     <color name="gesture_tutorial_background_color">#FFFFFFFF</color>
-    <color name="gesture_tutorial_subtitle_color">#99000000</color> <!-- 60% black -->
     <color name="gesture_tutorial_title_color">#FF000000</color>
+    <color name="gesture_tutorial_subtitle_color">#99000000</color> <!-- 60% black -->
+    <color name="gesture_tutorial_feedback_color">#FF000000</color>
+    <color name="gesture_tutorial_ripple_color">#A0C2F9</color> <!-- Light Blue -->
     <color name="gesture_tutorial_action_button_label_color">#FFFFFFFF</color>
     <color name="gesture_tutorial_primary_color">#1A73E8</color> <!-- Blue -->
 
diff --git a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java b/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
index b7340cf..bbbe21e 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
@@ -130,7 +130,7 @@
         }
         helper.close();
 
-        helper = new DatabaseHelper(mContext, DB_FILE) {
+        helper = new DatabaseHelper(mContext, DB_FILE, false) {
             @Override
             public void onOpen(SQLiteDatabase db) { }
         };
@@ -161,7 +161,7 @@
 
         DbDowngradeHelper.updateSchemaFile(mSchemaFile, LauncherProvider.SCHEMA_VERSION, mContext);
 
-        DatabaseHelper dbHelper = new DatabaseHelper(mContext, DB_FILE) {
+        DatabaseHelper dbHelper = new DatabaseHelper(mContext, DB_FILE, false) {
             @Override
             public void onOpen(SQLiteDatabase db) { }
         };
diff --git a/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
index 58174c7..ee73b82 100644
--- a/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -95,7 +95,7 @@
         private final long mProfileId;
 
         MyDatabaseHelper(long profileId) {
-            super(RuntimeEnvironment.application, null);
+            super(RuntimeEnvironment.application, null, false);
             mProfileId = profileId;
         }
 
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index e8e88c4..d7d4a27 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -79,8 +79,10 @@
 
     private static final int[] STATE_PRESSED = new int[] {android.R.attr.state_pressed};
 
-    private final PointF mTranslationForReorder = new PointF(0, 0);
-    private float mScaleForReorder = 1f;
+    private final PointF mTranslationForReorderBounce = new PointF(0, 0);
+    private final PointF mTranslationForReorderPreview = new PointF(0, 0);
+
+    private float mScaleForReorderBounce = 1f;
 
     private static final Property<BubbleTextView, Float> DOT_SCALE_PROPERTY
             = new Property<BubbleTextView, Float>(Float.TYPE, "dotScale") {
@@ -675,24 +677,39 @@
         return mIconSize;
     }
 
-    public void setReorderOffset(float x, float y) {
-        mTranslationForReorder.set(x, y);
-        super.setTranslationX(x);
-        super.setTranslationY(y);
+    private void updateTranslation() {
+        super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x);
+        super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y);
     }
 
-    public void getReorderOffset(PointF offset) {
-        offset.set(mTranslationForReorder);
+    public void setReorderBounceOffset(float x, float y) {
+        mTranslationForReorderBounce.set(x, y);
+        updateTranslation();
     }
 
-    public void setReorderScale(float scale) {
-        mScaleForReorder = scale;
+    public void getReorderBounceOffset(PointF offset) {
+        offset.set(mTranslationForReorderBounce);
+    }
+
+    @Override
+    public void setReorderPreviewOffset(float x, float y) {
+        mTranslationForReorderPreview.set(x, y);
+        updateTranslation();
+    }
+
+    @Override
+    public void getReorderPreviewOffset(PointF offset) {
+        offset.set(mTranslationForReorderPreview);
+    }
+
+    public void setReorderBounceScale(float scale) {
+        mScaleForReorderBounce = scale;
         super.setScaleX(scale);
         super.setScaleY(scale);
     }
 
-    public float getReorderScale() {
-        return mScaleForReorder;
+    public float getReorderBounceScale() {
+        return mScaleForReorderBounce;
     }
 
     public View getView() {
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 71a787f..ed71ddc 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -852,9 +852,10 @@
             int delay, boolean permanent, boolean adjustOccupied) {
         ShortcutAndWidgetContainer clc = getShortcutsAndWidgets();
 
-        if (clc.indexOfChild(child) != -1) {
+        if (clc.indexOfChild(child) != -1 && (child instanceof Reorderable)) {
             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
             final ItemInfo info = (ItemInfo) child.getTag();
+            final Reorderable item = (Reorderable) child;
 
             // We cancel any existing animations
             if (mReorderAnimators.containsKey(lp)) {
@@ -862,13 +863,18 @@
                 mReorderAnimators.remove(lp);
             }
 
-            final int oldX = lp.x;
-            final int oldY = lp.y;
+
             if (adjustOccupied) {
                 GridOccupancy occupied = permanent ? mOccupied : mTmpOccupied;
                 occupied.markCells(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, false);
                 occupied.markCells(cellX, cellY, lp.cellHSpan, lp.cellVSpan, true);
             }
+
+            // Compute the new x and y position based on the new cellX and cellY
+            // We leverage the actual layout logic in the layout params and hence need to modify
+            // state and revert that state.
+            final int oldX = lp.x;
+            final int oldY = lp.y;
             lp.isLockedToGrid = true;
             if (permanent) {
                 lp.cellX = info.cellX = cellX;
@@ -878,15 +884,23 @@
                 lp.tmpCellY = cellY;
             }
             clc.setupLp(child);
-            lp.isLockedToGrid = false;
             final int newX = lp.x;
             final int newY = lp.y;
-
             lp.x = oldX;
             lp.y = oldY;
+            lp.isLockedToGrid = false;
+            // End compute new x and y
+
+            item.getReorderPreviewOffset(mTmpPointF);
+            final float initPreviewOffsetX = mTmpPointF.x;
+            final float initPreviewOffsetY = mTmpPointF.y;
+            final float finalPreviewOffsetX = newX - oldX;
+            final float finalPreviewOffsetY = newY - oldY;
+
 
             // Exit early if we're not actually moving the view
-            if (oldX == newX && oldY == newY) {
+            if (finalPreviewOffsetX == 0 && finalPreviewOffsetY == 0
+                    && initPreviewOffsetX == 0 && initPreviewOffsetY == 0) {
                 lp.isLockedToGrid = true;
                 return true;
             }
@@ -899,9 +913,9 @@
                 @Override
                 public void onAnimationUpdate(ValueAnimator animation) {
                     float r = (Float) animation.getAnimatedValue();
-                    lp.x = (int) ((1 - r) * oldX + r * newX);
-                    lp.y = (int) ((1 - r) * oldY + r * newY);
-                    child.requestLayout();
+                    float x = (1 - r) * initPreviewOffsetX + r * finalPreviewOffsetX;
+                    float y = (1 - r) * initPreviewOffsetY + r * finalPreviewOffsetY;
+                    item.setReorderPreviewOffset(x, y);
                 }
             });
             va.addListener(new AnimatorListenerAdapter() {
@@ -912,6 +926,7 @@
                     // place just yet.
                     if (!cancelled) {
                         lp.isLockedToGrid = true;
+                        item.setReorderPreviewOffset(0, 0);
                         child.requestLayout();
                     }
                     if (mReorderAnimators.containsKey(lp)) {
@@ -1930,10 +1945,10 @@
             finalDeltaX = 0;
             finalDeltaY = 0;
 
-            child.getReorderOffset(mTmpPointF);
+            child.getReorderBounceOffset(mTmpPointF);
             initDeltaX = mTmpPointF.x;
             initDeltaY = mTmpPointF.y;
-            initScale = child.getReorderScale();
+            initScale = child.getReorderBounceScale();
             finalScale = mChildScale - (CHILD_DIVIDEND / child.getView().getWidth()) * initScale;
 
             int dir = mode == MODE_HINT ? -1 : 1;
@@ -2010,9 +2025,9 @@
             float r1 = (mode == MODE_HINT && repeating) ? 1.0f : animationProgress;
             float x = r1 * finalDeltaX + (1 - r1) * initDeltaX;
             float y = r1 * finalDeltaY + (1 - r1) * initDeltaY;
-            child.setReorderOffset(x, y);
+            child.setReorderBounceOffset(x, y);
             float s = animationProgress * finalScale + (1 - animationProgress) * initScale;
-            child.setReorderScale(s);
+            child.setReorderBounceScale(s);
         }
 
         private void cancel() {
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 0a1fad1..63b90ae 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -17,6 +17,7 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.Utilities.getDevicePrefs;
+import static com.android.launcher3.Utilities.getPointString;
 import static com.android.launcher3.config.FeatureFlags.APPLY_CONFIG_AT_RUNTIME;
 import static com.android.launcher3.settings.SettingsActivity.GRID_OPTIONS_PREFERENCE_KEY;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -69,6 +70,9 @@
     public static final MainThreadInitializedObject<InvariantDeviceProfile> INSTANCE =
             new MainThreadInitializedObject<>(InvariantDeviceProfile::new);
 
+    public static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size";
+    public static final String KEY_MIGRATION_SRC_HOTSEAT_COUNT = "migration_src_hotseat_count";
+
     private static final String KEY_IDP_GRID_NAME = "idp_grid_name";
 
     private static final float ICON_SIZE_DEFINED_IN_APP_DP = 48;
@@ -165,6 +169,10 @@
         if (!newGridName.equals(gridName)) {
             Utilities.getPrefs(context).edit().putString(KEY_IDP_GRID_NAME, newGridName).apply();
         }
+        Utilities.getPrefs(context).edit()
+                .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, numHotseatIcons)
+                .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(numColumns, numRows))
+                .apply();
 
         mConfigMonitor = new ConfigMonitor(context,
                 APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess);
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 8d20bd6..21cd04e 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -146,7 +146,8 @@
      */
     protected synchronized void createDbIfNotExists() {
         if (mOpenHelper == null) {
-            mOpenHelper = DatabaseHelper.createDatabaseHelper(getContext());
+            mOpenHelper = DatabaseHelper.createDatabaseHelper(
+                    getContext(), false /* forMigration */);
 
             if (RestoreDbTask.isPending(getContext())) {
                 if (!RestoreDbTask.performRestore(getContext(), mOpenHelper,
@@ -406,12 +407,8 @@
                 return result;
             }
             case LauncherSettings.Settings.METHOD_REFRESH_BACKUP_TABLE: {
-                // TODO(pinyaoting): Update the behavior here.
-                if (!MULTI_DB_GRID_MIRATION_ALGO.get()) {
-                    mOpenHelper.mBackupTableExists =
-                            tableExists(mOpenHelper.getReadableDatabase(),
-                                    Favorites.BACKUP_TABLE_NAME);
-                }
+                mOpenHelper.mBackupTableExists = tableExists(mOpenHelper.getReadableDatabase(),
+                        Favorites.BACKUP_TABLE_NAME);
                 return null;
             }
             case LauncherSettings.Settings.METHOD_RESTORE_BACKUP_TABLE: {
@@ -430,7 +427,8 @@
                                     InvariantDeviceProfile.INSTANCE.get(getContext()).dbFile,
                                     Favorites.TMP_TABLE,
                                     () -> mOpenHelper,
-                                    () -> DatabaseHelper.createDatabaseHelper(getContext())));
+                                    () -> DatabaseHelper.createDatabaseHelper(
+                                            getContext(), true /* forMigration */)));
                     return result;
                 }
             }
@@ -441,7 +439,8 @@
                             prepForMigration(
                                     arg /* dbFile */,
                                     Favorites.PREVIEW_TABLE_NAME,
-                                    () -> DatabaseHelper.createDatabaseHelper(getContext(), arg),
+                                    () -> DatabaseHelper.createDatabaseHelper(
+                                            getContext(), arg, true /* forMigration */),
                                     () -> mOpenHelper));
                     return result;
                 }
@@ -451,11 +450,7 @@
     }
 
     private void onAddOrDeleteOp(SQLiteDatabase db) {
-        if (MULTI_DB_GRID_MIRATION_ALGO.get()) {
-            // TODO(pingyaoting): Implement the behavior here.
-        } else {
-            mOpenHelper.onAddOrDeleteOp(db);
-        }
+        mOpenHelper.onAddOrDeleteOp(db);
     }
 
     /**
@@ -609,20 +604,22 @@
     public static class DatabaseHelper extends NoLocaleSQLiteHelper implements
             LayoutParserCallback {
         private final Context mContext;
+        private final boolean mForMigration;
         private int mMaxItemId = -1;
         private int mMaxScreenId = -1;
         private boolean mBackupTableExists;
 
-        static DatabaseHelper createDatabaseHelper(Context context) {
-            return createDatabaseHelper(context, null);
+        static DatabaseHelper createDatabaseHelper(Context context, boolean forMigration) {
+            return createDatabaseHelper(context, null, forMigration);
         }
 
-        static DatabaseHelper createDatabaseHelper(Context context, String dbName) {
+        static DatabaseHelper createDatabaseHelper(Context context, String dbName,
+                boolean forMigration) {
             if (dbName == null) {
                 dbName = MULTI_DB_GRID_MIRATION_ALGO.get() ? InvariantDeviceProfile.INSTANCE.get(
                         context).dbFile : LauncherFiles.LAUNCHER_DB;
             }
-            DatabaseHelper databaseHelper = new DatabaseHelper(context, dbName);
+            DatabaseHelper databaseHelper = new DatabaseHelper(context, dbName, forMigration);
             // Table creation sometimes fails silently, which leads to a crash loop.
             // This way, we will try to create a table every time after crash, so the device
             // would eventually be able to recover.
@@ -643,9 +640,10 @@
         /**
          * Constructor used in tests and for restore.
          */
-        public DatabaseHelper(Context context, String dbName) {
+        public DatabaseHelper(Context context, String dbName, boolean forMigration) {
             super(context, dbName, SCHEMA_VERSION);
             mContext = context;
+            mForMigration = forMigration;
         }
 
         protected void initIds() {
@@ -670,11 +668,13 @@
 
             // Fresh and clean launcher DB.
             mMaxItemId = initializeMaxItemId(db);
-            onEmptyDbCreated();
+            if (!mForMigration) {
+                onEmptyDbCreated();
+            }
         }
 
         protected void onAddOrDeleteOp(SQLiteDatabase db) {
-            if (!MULTI_DB_GRID_MIRATION_ALGO.get() && mBackupTableExists) {
+            if (mBackupTableExists) {
                 dropTable(db, Favorites.BACKUP_TABLE_NAME);
                 mBackupTableExists = false;
             }
diff --git a/src/com/android/launcher3/Reorderable.java b/src/com/android/launcher3/Reorderable.java
index 5112eaf..047fb01 100644
--- a/src/com/android/launcher3/Reorderable.java
+++ b/src/com/android/launcher3/Reorderable.java
@@ -22,17 +22,24 @@
 public interface Reorderable {
 
     /**
-     * Set the offset related to reorder hint and "bounce" animations
+     * Set the offset related to reorder hint and bounce animations
      */
-    void setReorderOffset(float x, float y);
+    void setReorderBounceOffset(float x, float y);
 
-    void getReorderOffset(PointF offset);
+    void getReorderBounceOffset(PointF offset);
+
+    /**
+     * Set the offset related to previewing the new reordered position
+     */
+    void setReorderPreviewOffset(float x, float y);
+
+    void getReorderPreviewOffset(PointF offset);
 
     /**
      * Set the scale related to reorder hint and "bounce" animations
      */
-    void setReorderScale(float scale);
-    float getReorderScale();
+    void setReorderBounceScale(float scale);
+    float getReorderBounceScale();
 
     /**
      * Get the com.android.view related to this object
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 65d3cd2..df30f7b 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -127,7 +127,7 @@
             "ENABLE_DEEP_SHORTCUT_ICON_CACHE", true, "R/W deep shortcut in IconCache");
 
     public static final BooleanFlag MULTI_DB_GRID_MIRATION_ALGO = getDebugFlag(
-            "MULTI_DB_GRID_MIRATION_ALGO", false, "Use the multi-db grid migration algorithm");
+            "MULTI_DB_GRID_MIRATION_ALGO", true, "Use the multi-db grid migration algorithm");
 
     public static final BooleanFlag ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER = getDebugFlag(
             "ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER", true, "Show launcher preview in grid picker");
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 12d88df..c287190 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -332,7 +332,7 @@
                         .map(info -> info.suggestedFolderNames)
                         .map(folderNames -> (FolderNameInfo[]) folderNames
                                 .getParcelableArrayExtra(FolderInfo.EXTRA_FOLDER_SUGGESTIONS))
-                        .ifPresent(nameInfos -> showLabelSuggestions(nameInfos));
+                        .ifPresent(this::showLabelSuggestions);
             }
             mFolderName.setHint("");
             mIsEditingName = true;
@@ -1450,7 +1450,7 @@
             if (hasFocus) {
                 startEditingFolderName();
             } else {
-                logEditFolderLabel();
+                logCurrentFolderLabelState();
                 mFolderName.dispatchBackKey();
             }
         }
@@ -1649,7 +1649,7 @@
         return mContent;
     }
 
-    private void logEditFolderLabel() {
+    protected void logCurrentFolderLabelState() {
         LauncherEvent launcherEvent = LauncherEvent.newBuilder()
                 .setAction(Action.newBuilder().setType(Action.Type.SOFT_KEYBOARD))
                 .addSrcTarget(newEditTextTargetBuilder()
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 96bdc2a..b875a0b 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -121,8 +121,9 @@
     private float mDotScale;
     private Animator mDotScaleAnim;
 
-    private final PointF mTranslationForReorder = new PointF(0, 0);
-    private float mScaleForReorder = 1f;
+    private final PointF mTranslationForReorderBounce = new PointF(0, 0);
+    private final PointF mTranslationForReorderPreview = new PointF(0, 0);
+    private float mScaleForReorderBounce = 1f;
 
     private static final Property<FolderIcon, Float> DOT_SCALE_PROPERTY
             = new Property<FolderIcon, Float>(Float.TYPE, "dotScale") {
@@ -416,6 +417,7 @@
             mPreviewItemManager.hidePreviewItem(finalIndex, false);
             mFolder.showItem(item);
             setLabelSuggestion(nameInfos);
+            mFolder.logCurrentFolderLabelState();
             invalidate();
         }, DROP_IN_ANIMATION_DURATION);
     }
@@ -438,7 +440,6 @@
         onTitleChanged(mInfo.title);
         mFolder.mFolderName.setText(mInfo.title);
         mFolder.mLauncher.getModelWriter().updateItemInDatabase(mInfo);
-        // TODO: Add logging while folder creation.
     }
 
 
@@ -712,25 +713,39 @@
         mPreviewItemManager.onFolderClose(currentPage);
     }
 
-
-    public void setReorderOffset(float x, float y) {
-        mTranslationForReorder.set(x, y);
-        super.setTranslationX(x);
-        super.setTranslationY(y);
+    private void updateTranslation() {
+        super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x);
+        super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y);
     }
 
-    public void getReorderOffset(PointF offset) {
-        offset.set(mTranslationForReorder);
+    public void setReorderBounceOffset(float x, float y) {
+        mTranslationForReorderBounce.set(x, y);
+        updateTranslation();
     }
 
-    public void setReorderScale(float scale) {
-        mScaleForReorder = scale;
+    public void getReorderBounceOffset(PointF offset) {
+        offset.set(mTranslationForReorderBounce);
+    }
+
+    @Override
+    public void setReorderPreviewOffset(float x, float y) {
+        mTranslationForReorderPreview.set(x, y);
+        updateTranslation();
+    }
+
+    @Override
+    public void getReorderPreviewOffset(PointF offset) {
+        offset.set(mTranslationForReorderPreview);
+    }
+
+    public void setReorderBounceScale(float scale) {
+        mScaleForReorderBounce = scale;
         super.setScaleX(scale);
         super.setScaleY(scale);
     }
 
-    public float getReorderScale() {
-        return mScaleForReorder;
+    public float getReorderBounceScale() {
+        return mScaleForReorderBounce;
     }
 
     public View getView() {
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java
index b27e4ea..e8a52bd 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTask.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java
@@ -1,5 +1,7 @@
 package com.android.launcher3.model;
 
+import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_HOTSEAT_COUNT;
+import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_WORKSPACE_SIZE;
 import static com.android.launcher3.LauncherSettings.Settings.EXTRA_VALUE;
 import static com.android.launcher3.Utilities.getPointString;
 import static com.android.launcher3.Utilities.parsePoint;
@@ -53,9 +55,6 @@
     private static final String TAG = "GridSizeMigrationTask";
     private static final boolean DEBUG = true;
 
-    private static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size";
-    private static final String KEY_MIGRATION_SRC_HOTSEAT_COUNT = "migration_src_hotseat_count";
-
     // These are carefully selected weights for various item types (Math.random?), to allow for
     // the least absurd migration experience.
     private static final float WT_SHORTCUT = 1;
@@ -894,8 +893,7 @@
         String gridSizeString = getPointString(idp.numColumns, idp.numRows);
 
         return !gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, ""))
-                || idp.numHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
-                idp.numHotseatIcons);
+                || idp.numHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, -1);
     }
 
     /** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
index 1c44fc3..4a28218 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_HOTSEAT_COUNT;
+import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_WORKSPACE_SIZE;
 import static com.android.launcher3.Utilities.getPointString;
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
 
@@ -63,9 +65,6 @@
  */
 public class GridSizeMigrationTaskV2 {
 
-    public static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size";
-    public static final String KEY_MIGRATION_SRC_HOTSEAT_COUNT = "migration_src_hotseat_count";
-
     private static final String TAG = "GridSizeMigrationTaskV2";
     private static final boolean DEBUG = true;
 
@@ -110,8 +109,7 @@
         String gridSizeString = getPointString(idp.numColumns, idp.numRows);
 
         return !gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, ""))
-                || idp.numHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
-                idp.numHotseatIcons);
+                || idp.numHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, -1);
     }
 
     /** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */
@@ -148,14 +146,6 @@
 
         SharedPreferences prefs = Utilities.getPrefs(context);
         String gridSizeString = getPointString(idp.numColumns, idp.numRows);
-
-        if (gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, ""))
-                && idp.numHotseatIcons == prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
-                idp.numHotseatIcons)) {
-            // Skip if workspace and hotseat sizes have not changed.
-            return true;
-        }
-
         HashSet<String> validPackages = getValidPackages(context);
         int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons);
 
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 0d97596..2fa6051 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -42,7 +42,6 @@
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.ShortcutRequest;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInfoMatcher;
@@ -86,10 +85,6 @@
 
     @Override
     public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList appsList) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.APP_NOT_DISABLED, "PackageUpdatedTask: " + mOp + ", " +
-                    Arrays.toString(mPackages));
-        }
         final Context context = app.getContext();
         final IconCache iconCache = app.getIconCache();
 
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index bd843e7..14f9a3e 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -37,6 +37,8 @@
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.util.ContentWriter;
 
+import java.util.Optional;
+
 /**
  * Represents an item in the launcher.
  */
@@ -248,24 +250,29 @@
 
         LauncherAtom.ItemInfo.Builder itemBuilder = LauncherAtom.ItemInfo.newBuilder();
         itemBuilder.setIsWork(user != Process.myUserHandle());
-        ComponentName cn = getTargetComponent();
+        Optional<ComponentName> nullableComponent = Optional.ofNullable(getTargetComponent());
         switch (itemType) {
             case ITEM_TYPE_APPLICATION:
-                itemBuilder.setApplication(LauncherAtom.Application.newBuilder()
-                        .setComponentName(cn.flattenToShortString())
-                        .setPackageName(cn.getPackageName()));
+                itemBuilder
+                        .setApplication(nullableComponent
+                                .map(component -> LauncherAtom.Application.newBuilder()
+                                        .setComponentName(component.flattenToShortString())
+                                        .setPackageName(component.getPackageName()))
+                                .orElse(LauncherAtom.Application.newBuilder()));
                 break;
             case ITEM_TYPE_DEEP_SHORTCUT:
             case ITEM_TYPE_SHORTCUT:
-                itemBuilder.setShortcut(LauncherAtom.Shortcut.newBuilder()
-                        .setShortcutName(cn.flattenToShortString()));
+                itemBuilder
+                        .setShortcut(nullableComponent
+                                .map(component -> LauncherAtom.Shortcut.newBuilder()
+                                        .setShortcutName(component.flattenToShortString()))
+                                .orElse(LauncherAtom.Shortcut.newBuilder()));
                 break;
             case ITEM_TYPE_APPWIDGET:
                 setItemBuilder(itemBuilder);
                 break;
             default:
                 break;
-
         }
         if (fInfo != null) {
             LauncherAtom.FolderContainer.Builder folderBuilder =
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 82f2eb4..a5a06b4 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -96,6 +96,4 @@
     public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation";
 
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
-
-    public static final String APP_NOT_DISABLED = "b/139891609";
 }
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index e290685..dc50053 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.touch;
 
+import static android.widget.ListPopupWindow.WRAP_CONTENT;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
@@ -30,6 +31,7 @@
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
+import android.widget.LinearLayout;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.PagedView;
@@ -224,6 +226,33 @@
     }
 
     @Override
+    public float getTaskMenuX(float x, View thumbnailView) {
+        return thumbnailView.getMeasuredWidth() + x;
+    }
+
+    @Override
+    public float getTaskMenuY(float y, View thumbnailView) {
+        return y;
+    }
+
+    @Override
+    public int getTaskMenuWidth(View view) {
+        return view.getMeasuredHeight();
+    }
+
+    @Override
+    public int getTaskMenuLayoutOrientation() {
+        return LinearLayout.HORIZONTAL;
+    }
+
+    @Override
+    public void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp) {
+        lp.width = 0;
+        lp.height = WRAP_CONTENT;
+        lp.weight = 1;
+    }
+
+    @Override
     public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
         boolean layoutChild) {
         final int childHeight = child.getMeasuredHeight();
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index b8396e1..cc15f99 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -27,6 +27,7 @@
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
+import android.widget.LinearLayout;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.PagedView;
@@ -90,6 +91,11 @@
     void getCurveProperties(PagedView view, Rect insets, CurveProperties out);
     boolean isGoingUp(float displacement);
     boolean isLayoutNaturalToLauncher();
+    float getTaskMenuX(float x, View thumbnailView);
+    float getTaskMenuY(float y, View thumbnailView);
+    int getTaskMenuWidth(View view);
+    int getTaskMenuLayoutOrientation();
+    void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp);
 
     /**
      * Maps the velocity from the coordinate plane of the foreground app to that
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index dad00a4..7c30e29 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -30,6 +30,7 @@
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
+import android.widget.LinearLayout;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.PagedView;
@@ -222,6 +223,31 @@
     }
 
     @Override
+    public float getTaskMenuX(float x, View thumbnailView) {
+        return x;
+    }
+
+    @Override
+    public float getTaskMenuY(float y, View thumbnailView) {
+        return y;
+    }
+
+    @Override
+    public int getTaskMenuWidth(View view) {
+        return view.getMeasuredWidth();
+    }
+
+    @Override
+    public int getTaskMenuLayoutOrientation() {
+        return LinearLayout.VERTICAL;
+    }
+
+    @Override
+    public void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp) {
+        // no-op, defaults are fine
+    }
+
+    @Override
     public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
         boolean layoutChild) {
         final int childWidth = child.getMeasuredWidth();
diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
index 5ce8a57..7beb7f7 100644
--- a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
@@ -20,6 +20,7 @@
 import android.graphics.PointF;
 import android.graphics.RectF;
 import android.view.Surface;
+import android.view.View;
 
 import com.android.launcher3.Utilities;
 
@@ -64,4 +65,14 @@
         float oldY = velocity.y;
         velocity.set(oldY, -oldX);
     }
+
+    @Override
+    public float getTaskMenuX(float x, View thumbnailView) {
+        return x;
+    }
+
+    @Override
+    public float getTaskMenuY(float y, View thumbnailView) {
+        return y + thumbnailView.getMeasuredHeight();
+    }
 }
diff --git a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
index 5ea01d9..a4e7daa 100644
--- a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
@@ -46,8 +46,9 @@
      */
     private final PointF mTranslationForCentering = new PointF(0, 0);
 
-    private final PointF mTranslationForReorder = new PointF(0, 0);
-    private float mScaleForReorder = 1f;
+    private final PointF mTranslationForReorderBounce = new PointF(0, 0);
+    private final PointF mTranslationForReorderPreview = new PointF(0, 0);
+    private float mScaleForReorderBounce = 1f;
 
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mChildrenFocused;
@@ -152,45 +153,63 @@
         setSelected(childIsFocused);
     }
 
-    public void setScaleToFit(float scale) {
-        mScaleToFit = scale;
-        super.setScaleX(scale * mScaleForReorder);
-        super.setScaleY(scale * mScaleForReorder);
-    }
-
-    public float getScaleToFit() {
-        return mScaleToFit;
-    }
-
     public View getView() {
         return this;
     }
 
+    private void updateTranslation() {
+        super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x
+                + mTranslationForCentering.x);
+        super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y
+                + mTranslationForCentering.y);
+    }
 
     public void setTranslationForCentering(float x, float y) {
         mTranslationForCentering.set(x, y);
-        super.setTranslationX(x + mTranslationForReorder.x);
-        super.setTranslationY(y + mTranslationForReorder.y);
+        updateTranslation();
     }
 
-    public void setReorderOffset(float x, float y) {
-        mTranslationForReorder.set(x, y);
-        super.setTranslationX(mTranslationForCentering.x + x);
-        super.setTranslationY(mTranslationForCentering.y + y);
+    public void setReorderBounceOffset(float x, float y) {
+        mTranslationForReorderBounce.set(x, y);
+        updateTranslation();
     }
 
-    public void getReorderOffset(PointF offset) {
-        offset.set(mTranslationForReorder);
+    public void getReorderBounceOffset(PointF offset) {
+        offset.set(mTranslationForReorderBounce);
     }
 
-    public void setReorderScale(float scale) {
-        mScaleForReorder = scale;
-        super.setScaleX(mScaleToFit * scale);
-        super.setScaleY(mScaleToFit * scale);
+    @Override
+    public void setReorderPreviewOffset(float x, float y) {
+        mTranslationForReorderPreview.set(x, y);
+        updateTranslation();
     }
 
-    public float getReorderScale() {
-        return mScaleForReorder;
+    @Override
+    public void getReorderPreviewOffset(PointF offset) {
+        offset.set(mTranslationForReorderPreview);
+    }
+
+    private void updateScale() {
+        super.setScaleX(mScaleToFit * mScaleForReorderBounce);
+        super.setScaleY(mScaleToFit * mScaleForReorderBounce);
+    }
+
+    public void setReorderBounceScale(float scale) {
+        mScaleForReorderBounce = scale;
+        updateScale();
+    }
+
+    public float getReorderBounceScale() {
+        return mScaleForReorderBounce;
+    }
+
+    public void setScaleToFit(float scale) {
+        mScaleToFit = scale;
+        updateScale();
+    }
+
+    public float getScaleToFit() {
+        return mScaleToFit;
     }
 
     @Override
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 0ba5a36..9c8e278 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -98,9 +98,10 @@
     public static final long DEFAULT_UI_TIMEOUT = 10000;
     private static final String TAG = "AbstractLauncherUiTest";
 
-    private static String sDetectedActivityLeak;
+    private static String sStrictmodeDetectedActivityLeak;
     private static boolean sActivityLeakReported;
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+    private static final ActivityLeakTracker ACTIVITY_LEAK_TRACKER = new ActivityLeakTracker();
 
     protected LooperExecutor mMainThreadExecutor = MAIN_EXECUTOR;
     protected final UiDevice mDevice = UiDevice.getInstance(getInstrumentation());
@@ -113,30 +114,51 @@
         if (TestHelpers.isInLauncherProcess()) {
             StrictMode.VmPolicy.Builder builder =
                     new StrictMode.VmPolicy.Builder()
-                            .detectActivityLeaks()
+// b/154772063
+//                            .detectActivityLeaks()
                             .penaltyLog()
                             .penaltyListener(Runnable::run, violation -> {
-                                // Runs in the main thread. We can't dumpheap in the main thread,
-                                // so let's just mark the fact that the leak has happened.
-                                if (sDetectedActivityLeak == null) {
-                                    sDetectedActivityLeak = violation.toString();
-                                    try {
-                                        Debug.dumpHprofData(
-                                                getInstrumentation().getTargetContext()
-                                                        .getFilesDir().getPath()
-                                                        + "/ActivityLeakHeapDump.hprof");
-                                    } catch (Throwable e) {
-                                        Log.e(TAG, "dumpHprofData failed", e);
-                                    }
+                                if (sStrictmodeDetectedActivityLeak == null) {
+                                    sStrictmodeDetectedActivityLeak = violation.toString() + ", "
+                                            + dumpHprofData() + ".";
                                 }
                             });
             StrictMode.setVmPolicy(builder.build());
         }
     }
 
-    public static void checkDetectedLeaks() {
-        if (sDetectedActivityLeak != null && !sActivityLeakReported) {
+    public static void checkDetectedLeaks(LauncherInstrumentation launcher) {
+        if (sActivityLeakReported) return;
+
+        if (sStrictmodeDetectedActivityLeak != null) {
+            // Report from the test thread strictmode violations detected in the main thread.
             sActivityLeakReported = true;
+            Assert.fail(sStrictmodeDetectedActivityLeak);
+        }
+
+        // Check whether activity leak detector has found leaked activities.
+        Wait.atMost(AbstractLauncherUiTest::getActivityLeakErrorMessage,
+                () -> {
+                    launcher.getTotalPssKb();  // Triggers GC
+                    return MAIN_EXECUTOR.submit(
+                            () -> ACTIVITY_LEAK_TRACKER.noLeakedActivities()).get();
+                }, DEFAULT_UI_TIMEOUT, launcher);
+    }
+
+    private static String getActivityLeakErrorMessage() {
+        sActivityLeakReported = true;
+        return "Activity leak detector has found leaked activities, " + dumpHprofData() + ".";
+    }
+
+    private static String dumpHprofData() {
+        try {
+            final String fileName = getInstrumentation().getTargetContext().getFilesDir().getPath()
+                    + "/ActivityLeakHeapDump.hprof";
+            Debug.dumpHprofData(fileName);
+            return "memory dump filename: " + fileName;
+        } catch (Throwable e) {
+            Log.e(TAG, "dumpHprofData failed", e);
+            return "failed to save memory dump";
         }
     }
 
@@ -263,7 +285,7 @@
         if (mLauncherPid != 0) {
             assertEquals("Launcher crashed, pid mismatch:", mLauncherPid, mLauncher.getPid());
         }
-        checkDetectedLeaks();
+        checkDetectedLeaks(mLauncher);
     }
 
     protected void clearLauncherData() throws IOException, InterruptedException {
diff --git a/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java b/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
new file mode 100644
index 0000000..e9258e9
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2020 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.ui;
+
+import android.app.Activity;
+import android.app.Application;
+import android.os.Bundle;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.launcher3.tapl.TestHelpers;
+
+import java.util.WeakHashMap;
+
+class ActivityLeakTracker implements Application.ActivityLifecycleCallbacks {
+    private final WeakHashMap<Activity, Boolean> mActivities = new WeakHashMap<>();
+
+    ActivityLeakTracker() {
+        if (!TestHelpers.isInLauncherProcess()) return;
+        final Application app =
+                (Application) InstrumentationRegistry.getTargetContext().getApplicationContext();
+        app.registerActivityLifecycleCallbacks(this);
+    }
+
+    @Override
+    public void onActivityCreated(Activity activity, Bundle bundle) {
+        mActivities.put(activity, true);
+    }
+
+    @Override
+    public void onActivityStarted(Activity activity) {
+    }
+
+    @Override
+    public void onActivityResumed(Activity activity) {
+    }
+
+    @Override
+    public void onActivityPaused(Activity activity) {
+    }
+
+    @Override
+    public void onActivityStopped(Activity activity) {
+    }
+
+    @Override
+    public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
+    }
+
+    @Override
+    public void onActivityDestroyed(Activity activity) {
+    }
+
+    public boolean noLeakedActivities() {
+        int liveActivities = 0;
+        int destroyedActivities = 0;
+
+        for (Activity activity : mActivities.keySet()) {
+            if (activity.isDestroyed()) {
+                ++destroyedActivities;
+            } else {
+                ++liveActivities;
+            }
+        }
+
+        if (liveActivities > 2)  return false;
+
+        // It's OK to have 1 leaked activity if no active activities exist.
+        return liveActivities == 0 ? destroyedActivities <= 1 : destroyedActivities == 0;
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
index 38f50c1..266f0ae 100644
--- a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
+++ b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
@@ -56,7 +56,7 @@
             private void evaluateInPortrait() throws Throwable {
                 mTest.mDevice.setOrientationNatural();
                 mTest.mLauncher.setExpectedRotation(Surface.ROTATION_0);
-                AbstractLauncherUiTest.checkDetectedLeaks();
+                AbstractLauncherUiTest.checkDetectedLeaks(mTest.mLauncher);
                 base.evaluate();
                 mTest.getDevice().pressHome();
             }
@@ -64,7 +64,7 @@
             private void evaluateInLandscape() throws Throwable {
                 mTest.mDevice.setOrientationLeft();
                 mTest.mLauncher.setExpectedRotation(Surface.ROTATION_90);
-                AbstractLauncherUiTest.checkDetectedLeaks();
+                AbstractLauncherUiTest.checkDetectedLeaks(mTest.mLauncher);
                 base.evaluate();
                 mTest.getDevice().pressHome();
             }
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 57000a0..34e425d 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -64,7 +64,7 @@
         test.waitForResumed("Launcher internal state is still Background");
         // Check that we switched to home.
         test.mLauncher.getWorkspace();
-        AbstractLauncherUiTest.checkDetectedLeaks();
+        AbstractLauncherUiTest.checkDetectedLeaks(test.mLauncher);
     }
 
     // Please don't add negative test cases for methods that fail only after a long wait.