Merge "Detects nav bar gestures to progress through Home tutorial." into ub-launcher3-rvc-dev
diff --git a/quickstep/recents_ui_overrides/res/layout/overview_panel.xml b/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
index eac0bfa..fe57e9b 100644
--- a/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
+++ b/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
@@ -13,12 +13,20 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.quickstep.views.LauncherRecentsView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:accessibilityPaneTitle="@string/accessibility_recent_apps"
-    android:clipChildren="false"
-    android:clipToPadding="false"
-    android:theme="@style/HomeScreenElementTheme"
-    android:visibility="invisible" />
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <com.android.quickstep.views.LauncherRecentsView
+        android:id="@+id/overview_panel"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:accessibilityPaneTitle="@string/accessibility_recent_apps"
+        android:clipChildren="false"
+        android:clipToPadding="false"
+        android:theme="@style/HomeScreenElementTheme"
+        android:visibility="invisible" />
+
+    <include
+        android:id="@+id/overview_actions_view"
+        layout="@layout/overview_actions_container" />
+
+</merge>
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 0019ecb..79b4002 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -30,6 +30,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
 import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
 import static com.android.quickstep.TaskViewUtils.getRecentsWindowAnimator;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -37,7 +38,6 @@
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.content.Context;
-import android.util.FloatProperty;
 import android.view.View;
 
 import androidx.annotation.NonNull;
@@ -49,7 +49,6 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.SpringAnimationBuilder;
 import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.quickstep.util.AppWindowAnimationHelper;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -198,16 +197,15 @@
             case INDEX_RECENTS_FADE_ANIM:
                 return ObjectAnimator.ofFloat(mLauncher.getOverviewPanel(),
                         RecentsView.CONTENT_ALPHA, values);
-            case INDEX_RECENTS_TRANSLATE_X_ANIM:
-                PagedOrientationHandler orientationHandler =
-                    ((RecentsView)mLauncher.getOverviewPanel()).getPagedViewOrientedState()
-                        .getOrientationHandler();
-                FloatProperty<View> translate = orientationHandler.getPrimaryViewTranslate();
-                return new SpringAnimationBuilder<>(mLauncher.getOverviewPanel(), translate)
+            case INDEX_RECENTS_TRANSLATE_X_ANIM: {
+                RecentsView rv = mLauncher.getOverviewPanel();
+                return new SpringAnimationBuilder(mLauncher)
+                        .setMinimumVisibleChange(1f / rv.getPageOffsetScale())
                         .setDampingRatio(0.8f)
                         .setStiffness(250)
                         .setValues(values)
-                        .build(mLauncher);
+                        .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/PredictedAppIcon.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 8af26c6..7520688 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -19,11 +19,13 @@
 import static com.android.launcher3.graphics.IconShape.getShape;
 
 import android.content.Context;
+import android.graphics.BlurMaskFilter;
 import android.graphics.Canvas;
 import android.graphics.Color;
-import android.graphics.DashPathEffect;
 import android.graphics.Paint;
+import android.graphics.Path;
 import android.graphics.Rect;
+import android.os.Process;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
@@ -39,6 +41,7 @@
 import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.hybridhotseat.HotseatPredictionController;
 import com.android.launcher3.icons.IconNormalizer;
+import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.touch.ItemClickHandler;
@@ -51,13 +54,18 @@
 public class PredictedAppIcon extends DoubleShadowBubbleTextView implements
         LauncherAccessibilityDelegate.AccessibilityActionHandler {
 
+    private static final int RING_SHADOW_COLOR = 0x99000000;
     private static final float RING_EFFECT_RATIO = 0.11f;
 
     boolean mIsDrawingDot = false;
     private final DeviceProfile mDeviceProfile;
     private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private final Path mRingPath = new Path();
     private boolean mIsPinned = false;
-    private int mNormalizedIconRadius;
+    private final int mNormalizedIconRadius;
+    private final BlurMaskFilter mShadowFilter;
+    private int mPlateColor;
+
 
     public PredictedAppIcon(Context context) {
         this(context, null, 0);
@@ -73,13 +81,18 @@
         mNormalizedIconRadius = IconNormalizer.getNormalizedCircleSize(getIconSize()) / 2;
         setOnClickListener(ItemClickHandler.INSTANCE);
         setOnFocusChangeListener(Launcher.getLauncher(context).getFocusHandler());
+        int shadowSize = context.getResources().getDimensionPixelSize(
+                R.dimen.blur_size_thin_outline);
+        mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER);
     }
 
     @Override
     public void onDraw(Canvas canvas) {
         int count = canvas.save();
         if (!mIsPinned) {
-            drawEffect(canvas);
+            boolean isBadged = getTag() instanceof WorkspaceItemInfo
+                    && !Process.myUserHandle().equals(((ItemInfo) getTag()).user);
+            drawEffect(canvas, isBadged);
             canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO);
             canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO);
         }
@@ -102,7 +115,7 @@
     public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
         super.applyFromWorkspaceItem(info);
         int color = IconPalette.getMutedColor(info.bitmap.color, 0.54f);
-        mIconRingPaint.setColor(ColorUtils.setAlphaComponent(color, 200));
+        mPlateColor = ColorUtils.setAlphaComponent(color, 200);
         if (mIsPinned) {
             setContentDescription(info.contentDescription);
         } else {
@@ -174,9 +187,25 @@
         return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
     }
 
-    private void drawEffect(Canvas canvas) {
-        getShape().drawShape(canvas, getOutlineOffsetX(), getOutlineOffsetY(),
-                mNormalizedIconRadius, mIconRingPaint);
+    private void drawEffect(Canvas canvas, boolean isBadged) {
+        mRingPath.reset();
+        getShape().addToPath(mRingPath, getOutlineOffsetX(), getOutlineOffsetY(),
+                mNormalizedIconRadius);
+        if (isBadged) {
+            float outlineSize = mNormalizedIconRadius * RING_EFFECT_RATIO * 2;
+            float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO);
+            float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize;
+            float badgeInset = mNormalizedIconRadius * 2 - badgeSize;
+            getShape().addToPath(mRingPath, getOutlineOffsetX() + badgeInset,
+                    getOutlineOffsetY() + badgeInset, badgeSize / 2);
+
+        }
+        mIconRingPaint.setColor(RING_SHADOW_COLOR);
+        mIconRingPaint.setMaskFilter(mShadowFilter);
+        canvas.drawPath(mRingPath, mIconRingPaint);
+        mIconRingPaint.setColor(mPlateColor);
+        mIconRingPaint.setMaskFilter(null);
+        canvas.drawPath(mRingPath, mIconRingPaint);
     }
 
     /**
@@ -205,10 +234,8 @@
             mOffsetX = icon.getOutlineOffsetX();
             mOffsetY = icon.getOutlineOffsetY();
             mIconRadius = icon.mNormalizedIconRadius;
-            mOutlinePaint.setStyle(Paint.Style.STROKE);
-            mOutlinePaint.setStrokeWidth(5);
-            mOutlinePaint.setPathEffect(new DashPathEffect(new float[]{15, 15}, 0));
-            mOutlinePaint.setColor(Color.argb(100, 245, 245, 245));
+            mOutlinePaint.setStyle(Paint.Style.FILL);
+            mOutlinePaint.setColor(Color.argb(24, 245, 245, 245));
         }
 
         /**
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 3d6e519..2f55fda 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -15,19 +15,14 @@
  */
 package com.android.launcher3.uioverrides;
 
-import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
 import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
-import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
 import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 
 import android.annotation.TargetApi;
 import android.os.Build;
 import android.util.FloatProperty;
-import android.view.View;
-import android.view.animation.Interpolator;
 
 import androidx.annotation.NonNull;
 
@@ -37,6 +32,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.MultiValueAlpha;
 import com.android.quickstep.views.ClearAllButton;
 import com.android.quickstep.views.LauncherRecentsView;
 import com.android.quickstep.views.RecentsView;
@@ -60,7 +56,7 @@
             mRecentsView.updateEmptyMessage();
             mRecentsView.resetTaskVisuals();
         }
-        setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, state, LINEAR);
+        setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, state);
         mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress());
     }
 
@@ -78,22 +74,17 @@
                     AnimationSuccessListener.forRunnable(mRecentsView::resetTaskVisuals));
         }
 
-        setAlphas(builder, toState,
-                config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
+        setAlphas(builder, toState);
         builder.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
                 toState.getOverviewFullscreenProgress(), LINEAR);
     }
 
-    private void setAlphas(PropertySetter propertySetter, LauncherState state,
-            Interpolator actionInterpolator) {
+    private void setAlphas(PropertySetter propertySetter, LauncherState state) {
         float buttonAlpha = (state.getVisibleElements(mLauncher) & OVERVIEW_BUTTONS) != 0 ? 1 : 0;
         propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
                 buttonAlpha, LINEAR);
-
-        View actionsView = mLauncher.getActionsView();
-        if (actionsView != null) {
-            propertySetter.setFloat(actionsView, VIEW_ALPHA, buttonAlpha, actionInterpolator);
-        }
+        propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
+                MultiValueAlpha.VALUE, buttonAlpha, LINEAR);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index a87d6d1..e57e841 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -61,13 +61,16 @@
     }
 
     @Override
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return new float[] {getOverviewScale(launcher), NO_OFFSET};
+    }
+
+    private float getOverviewScale(Launcher launcher) {
         // Initialize the recents view scale to what it would be when starting swipe up
         RecentsView recentsView = launcher.getOverviewPanel();
         int taskCount = recentsView.getTaskViewCount();
-        if (taskCount == 0) {
-            return super.getOverviewScaleAndTranslation(launcher);
-        }
+        if (taskCount == 0) return 1;
+
         TaskView dummyTask;
         if (recentsView.getCurrentPage() >= recentsView.getTaskViewStartIndex()) {
             if (recentsView.getCurrentPage() <= taskCount - 1) {
@@ -78,8 +81,8 @@
         } else {
             dummyTask = recentsView.getTaskViewAt(0);
         }
-        return recentsView.getTempAppWindowAnimationHelper().updateForFullscreenOverview(dummyTask)
-                .getScaleAndTranslation();
+        return recentsView.getTempAppWindowAnimationHelper()
+                .updateForFullscreenOverview(dummyTask).getSrcToTargetScale();
     }
 
     @Override
@@ -106,7 +109,7 @@
     }
 
     @Override
-    public float getDepth(Context context) {
+    protected float getDepthUnchecked(Context context) {
         return 1f;
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
index 1288e7b..b27f16a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
@@ -24,24 +24,18 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.states.StateAnimationConfig;
 
 public class OverviewPeekState extends OverviewState {
+    private static final float OVERVIEW_OFFSET = 0.7f;
+
     public OverviewPeekState(int id) {
         super(id);
     }
 
     @Override
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        ScaleAndTranslation result = super.getOverviewScaleAndTranslation(launcher);
-        result.translationX = NORMAL.getOverviewScaleAndTranslation(launcher).translationX
-                - launcher.getResources().getDimension(R.dimen.overview_peek_distance);
-        if (Utilities.isRtl(launcher.getResources())) {
-            result.translationX = -result.translationX;
-        }
-        return result;
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return new float[] {NO_SCALE, OVERVIEW_OFFSET};
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
index bcfb11c..e44f59f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -24,7 +24,6 @@
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
-import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
@@ -123,8 +122,8 @@
     }
 
     @Override
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        return new ScaleAndTranslation(1f, 0f, 0f);
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return new float[] {NO_SCALE, NO_OFFSET};
     }
 
     @Override
@@ -144,7 +143,6 @@
 
     @Override
     public void onStateTransitionEnd(Launcher launcher) {
-        launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE);
         DiscoveryBounce.showForOverviewIfNeeded(launcher);
         RecentsView recentsView = launcher.getOverviewPanel();
         AccessibilityManagerCompat.sendCustomAccessibilityEvent(
@@ -205,7 +203,7 @@
     }
 
     @Override
-    public float getDepth(Context context) {
+    protected float getDepthUnchecked(Context context) {
         return 1f;
     }
 
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/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index c92a872..f4f8bc9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -40,17 +40,15 @@
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.CANCEL;
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE;
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.graphics.PointF;
 import android.view.MotionEvent;
-import android.view.View;
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.BaseQuickstepLauncher;
@@ -59,6 +57,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.OverviewScrim;
 import com.android.launcher3.states.StateAnimationConfig;
@@ -237,58 +236,32 @@
     private void setupOverviewAnimators() {
         final LauncherState fromState = QUICK_SWITCH;
         final LauncherState toState = OVERVIEW;
-        LauncherState.ScaleAndTranslation fromScaleAndTranslation = fromState
-                .getOverviewScaleAndTranslation(mLauncher);
-        LauncherState.ScaleAndTranslation toScaleAndTranslation = toState
-                .getOverviewScaleAndTranslation(mLauncher);
-        // Update RecentView's translationX to have it start offscreen.
-        float startScale = Utilities.mapRange(
-                SCALE_DOWN_INTERPOLATOR.getInterpolation(Y_ANIM_MIN_PROGRESS),
-                fromScaleAndTranslation.scale,
-                toScaleAndTranslation.scale);
-        fromScaleAndTranslation.translationX = mRecentsView.getOffscreenTranslationX(startScale);
 
         // Set RecentView's initial properties.
-        mRecentsView.setScaleX(fromScaleAndTranslation.scale);
-        mRecentsView.setScaleY(fromScaleAndTranslation.scale);
-        mRecentsView.setTranslationX(fromScaleAndTranslation.translationX);
-        mRecentsView.setTranslationY(fromScaleAndTranslation.translationY);
+        SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
+        ADJACENT_PAGE_OFFSET.set(mRecentsView, 1f);
         mRecentsView.setContentAlpha(1);
         mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
 
+        float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
         // As we drag right, animate the following properties:
         //   - RecentsView translationX
         //   - OverviewScrim
-        AnimatorSet xOverviewAnim = new AnimatorSet();
-        xOverviewAnim.play(ObjectAnimator.ofFloat(mRecentsView, View.TRANSLATION_X,
-                toScaleAndTranslation.translationX));
-        xOverviewAnim.play(ObjectAnimator.ofFloat(
-                mLauncher.getDragLayer().getOverviewScrim(), OverviewScrim.SCRIM_PROGRESS,
-                toState.getOverviewScrimAlpha(mLauncher)));
-        long xAccuracy = (long) (mXRange * 2);
-        xOverviewAnim.setDuration(xAccuracy);
-        mXOverviewAnim = AnimatorPlaybackController.wrap(xOverviewAnim, xAccuracy);
+        PendingAnimation xAnim = new PendingAnimation((long) (mXRange * 2));
+        xAnim.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1], LINEAR);
+        xAnim.setFloat(mLauncher.getDragLayer().getOverviewScrim(), OverviewScrim.SCRIM_PROGRESS,
+                toState.getOverviewScrimAlpha(mLauncher), LINEAR);
+        mXOverviewAnim = xAnim.createPlaybackController();
         mXOverviewAnim.dispatchOnStart();
 
         // As we drag up, animate the following properties:
-        //   - RecentsView translationY
         //   - RecentsView scale
         //   - RecentsView fullscreenProgress
-        AnimatorSet yAnimation = new AnimatorSet();
-        Animator translateYAnim = ObjectAnimator.ofFloat(mRecentsView, View.TRANSLATION_Y,
-                toScaleAndTranslation.translationY);
-        Animator scaleAnim = ObjectAnimator.ofFloat(mRecentsView, SCALE_PROPERTY,
-                toScaleAndTranslation.scale);
-        Animator fullscreenProgressAnim = ObjectAnimator.ofFloat(mRecentsView, FULLSCREEN_PROGRESS,
-                fromState.getOverviewFullscreenProgress(), toState.getOverviewFullscreenProgress());
-        scaleAnim.setInterpolator(SCALE_DOWN_INTERPOLATOR);
-        fullscreenProgressAnim.setInterpolator(SCALE_DOWN_INTERPOLATOR);
-        yAnimation.play(translateYAnim);
-        yAnimation.play(scaleAnim);
-        yAnimation.play(fullscreenProgressAnim);
-        long yAccuracy = (long) (mYRange * 2);
-        yAnimation.setDuration(yAccuracy);
-        mYOverviewAnim = AnimatorPlaybackController.wrap(yAnimation, yAccuracy);
+        PendingAnimation yAnim = new PendingAnimation((long) (mYRange * 2));
+        yAnim.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0], SCALE_DOWN_INTERPOLATOR);
+        yAnim.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
+                toState.getOverviewFullscreenProgress(), SCALE_DOWN_INTERPOLATOR);
+        mYOverviewAnim = yAnim.createPlaybackController();
         mYOverviewAnim.dispatchOnStart();
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 1b3610a..f6f892b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -24,6 +24,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.view.MotionEvent;
+import android.view.View;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
@@ -209,9 +210,11 @@
             mPendingAnimation = mRecentsView.createTaskLaunchAnimation(
                     mTaskBeingDragged, maxDuration, Interpolators.ZOOM_IN);
 
-            mTempCords[1] = mTaskBeingDragged.getHeight();
-            dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords);
-            mEndDisplacement = dl.getHeight() - mTempCords[1];
+            // Since the thumbnail is what is filling the screen, based the end displacement on it.
+            View thumbnailView = mTaskBeingDragged.getThumbnail();
+            mTempCords[1] = orientationHandler.getSecondaryDimension(thumbnailView);
+            dl.getDescendantCoordRelativeToSelf(thumbnailView, mTempCords);
+            mEndDisplacement = secondaryLayerDimension - mTempCords[1];
         }
         mEndDisplacement *= verticalFactor;
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
index fadde37..d22e5af 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -31,7 +31,6 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.Build;
-import android.util.Log;
 import android.util.Pair;
 import android.view.MotionEvent;
 import android.view.View;
@@ -46,7 +45,6 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.views.FloatingIconView;
@@ -133,13 +131,19 @@
         mDeviceState = deviceState;
         mGestureState = gestureState;
         mActivityInterface = gestureState.getActivityInterface();
-        mActivityInitListener =
-                mActivityInterface.createActivityInitListener(this::onActivityInit);
+        mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit);
         mInputConsumer = inputConsumer;
         mAppWindowAnimationHelper = new AppWindowAnimationHelper(context);
         mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
+    }
+
+    /**
+     * To be called at the end of constructor of subclasses. This calls various methods which can
+     * depend on proper class initialization.
+     */
+    protected void initAfterSubclassConstructor() {
         initTransitionEndpoints(InvariantDeviceProfile.INSTANCE.get(mContext)
-            .getDeviceProfile(mContext));
+                .getDeviceProfile(mContext));
     }
 
     protected void performHapticFeedback() {
@@ -244,6 +248,10 @@
         return mRecentsAnimationTargets != null && mRecentsAnimationTargets.hasTargets();
     }
 
+    protected void updateSource(Rect stackBounds, RemoteAnimationTargetCompat runningTarget) {
+        mAppWindowAnimationHelper.updateSource(stackBounds, runningTarget);
+    }
+
     @Override
     public void onRecentsAnimationStart(RecentsAnimationController recentsAnimationController,
             RecentsAnimationTargets targets) {
@@ -267,7 +275,7 @@
         dp.updateInsets(targets.homeContentInsets);
         dp.updateIsSeascape(mContext);
         if (runningTaskTarget != null) {
-            mAppWindowAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget);
+            updateSource(overviewStackBounds, runningTaskTarget);
         }
 
         mAppWindowAnimationHelper.prepareAnimation(dp, false /* isOpening */);
@@ -317,6 +325,7 @@
 
         mTransitionDragLength = mActivityInterface.getSwipeUpDestinationAndLength(
                 dp, mContext, TEMP_RECT);
+
         if (!dp.isMultiWindowMode) {
             // When updating the target rect, also update the home bounds since the location on
             // screen of the launcher window may be stale (position is not updated until first
@@ -325,13 +334,14 @@
             mAppWindowAnimationHelper.updateHomeBounds(getStackBounds(dp));
         }
         int displayRotation = 0;
-        if (mOrientedState != null) {
+        if (mOrientedState != null && mOrientedState.isMultipleOrientationSupportedByDevice()) {
             // TODO(b/150300347): The first recents animation after launcher is started with the
             //  foreground app not in landscape will look funky until that bug is fixed
             displayRotation = mOrientedState.getDisplayRotation();
 
             RectF tempRectF = new RectF(TEMP_RECT);
-            mOrientedState.mapRectFromNormalOrientation(tempRectF, dp.widthPx, dp.heightPx);
+            mOrientedState.mapRectFromRotation(displayRotation,
+                    tempRectF, dp.widthPx, dp.heightPx);
             tempRectF.roundOut(TEMP_RECT);
         }
         mAppWindowAnimationHelper.updateTargetRect(TEMP_RECT);
@@ -395,11 +405,15 @@
 
     public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { }
 
-    public void initWhenReady() {
+    /**
+     * Registers a callback to run when the activity is ready.
+     * @param intent The intent that will be used to start the activity if it doesn't exist already.
+     */
+    public void initWhenReady(Intent intent) {
         // Preload the plan
         RecentsModel.INSTANCE.get(mContext).getTasks(null);
 
-        mActivityInitListener.register();
+        mActivityInitListener.register(intent);
     }
 
     /**
@@ -407,11 +421,12 @@
      */
     protected void applyTransformUnchecked() {
         float shift = mCurrentShift.value;
-        float offset = mRecentsView == null ? 0 : mRecentsView.getScrollOffset();
+        float offset = mRecentsView == null ? 0 : mRecentsView.getScrollOffsetScaled();
         float taskSize = getOrientationHandler()
             .getPrimarySize(mAppWindowAnimationHelper.getTargetRect());
         float offsetScale = getTaskCurveScaleForOffset(offset, taskSize);
-        mTransformParams.setProgress(shift)
+        mTransformParams
+                .setProgress(shift)
                 .setOffset(offset)
                 .setOffsetScale(offsetScale)
                 .setTargetSet(mRecentsAnimationTargets)
@@ -457,6 +472,7 @@
             FloatingIconView fiv = (FloatingIconView) floatingView;
             anim.addAnimatorListener(fiv);
             fiv.setOnTargetChangeListener(anim::onTargetPositionChanged);
+            fiv.setFastFinishRunnable(anim::end);
         }
 
         AnimatorPlaybackController homeAnim = homeAnimationFactory.createActivityAnimationToHome();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
index 46b026e..1b2979b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -129,6 +129,7 @@
         mEndTargetAnimationParams.put(LAST_TASK, new EndTargetAnimationParams(0, 150, 1));
         mEndTargetAnimationParams.put(NEW_TASK, new EndTargetAnimationParams(0, 150, 1));
 
+        initAfterSubclassConstructor();
         initStateCallbacks();
     }
 
@@ -188,10 +189,10 @@
     }
 
     @Override
-    public void initWhenReady() {
+    public void initWhenReady(Intent intent) {
         if (mInQuickSwitchMode) {
             // Only init if we are in quickswitch mode
-            super.initWhenReady();
+            super.initWhenReady(intent);
         }
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java
index 33fe5a9..fd17551 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/ImageActionsApi.java
@@ -42,9 +42,10 @@
 public class ImageActionsApi {
 
     private static final String TAG = BuildConfig.APPLICATION_ID + "ImageActionsApi";
-    private final Context mContext;
-    private final Supplier<Bitmap> mBitmapSupplier;
-    private final SystemUiProxy mSystemUiProxy;
+
+    protected final Context mContext;
+    protected final Supplier<Bitmap> mBitmapSupplier;
+    protected final SystemUiProxy mSystemUiProxy;
 
     public ImageActionsApi(Context context, Supplier<Bitmap> bitmapSupplier) {
         mContext = context;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
index 455ae76..b4764dc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -15,8 +15,6 @@
  */
 package com.android.quickstep;
 
-import static android.view.View.TRANSLATION_Y;
-
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_RECENTS_FADE_ANIM;
 import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_RECENTS_TRANSLATE_X_ANIM;
@@ -25,15 +23,14 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.INSTANT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.quickstep.LauncherSwipeHandler.RECENTS_ATTACH_DURATION;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
 import android.content.Context;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -57,7 +54,6 @@
 import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statehandlers.DepthController.ClampedDepthProperty;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.quickstep.SysUINavigationMode.Mode;
@@ -81,7 +77,6 @@
  */
 public final class LauncherActivityInterface implements BaseActivityInterface<Launcher> {
 
-    private Runnable mAdjustInterpolatorsRunnable;
     private Pair<Float, Float> mSwipeUpPullbackStartAndMaxProgress =
             BaseActivityInterface.super.getSwipeUpPullbackStartAndMaxProgress();
 
@@ -243,14 +238,6 @@
             }
 
             @Override
-            public void adjustActivityControllerInterpolators() {
-                if (mAdjustInterpolatorsRunnable != null) {
-                    mAdjustInterpolatorsRunnable.run();
-                    mAdjustInterpolatorsRunnable = null;
-                }
-            }
-
-            @Override
             public void onTransitionCancelled() {
                 launcher.getStateManager().goToState(startState, false /* animate */);
             }
@@ -272,42 +259,24 @@
                         .createStateElementAnimation(
                         INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
 
-                int runningTaskIndex = recentsView.getRunningTaskIndex();
-                if (runningTaskIndex == recentsView.getTaskViewStartIndex()) {
-                    // If we are on the first task (we haven't quick switched), translate recents in
-                    // from the side. Calculate the start translation based on current scale/scroll.
-                    float currScale = recentsView.getScaleX();
-                    float scrollOffsetX = recentsView.getScrollOffset();
-                    float offscreenX = recentsView.getOffscreenTranslationX(currScale);
-
-                    float fromTranslation = attached ? offscreenX - scrollOffsetX : 0;
-                    float toTranslation = attached ? 0 : offscreenX - scrollOffsetX;
-                    launcher.getStateManager()
-                            .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
-
-                    PagedOrientationHandler pagedOrientationHandler =
-                        recentsView.getPagedViewOrientedState().getOrientationHandler();
-                    if (!recentsView.isShown() && animate) {
-                        pagedOrientationHandler
-                            .getPrimaryViewTranslate().set(recentsView, fromTranslation);
-                    } else {
-                        fromTranslation =
-                            pagedOrientationHandler.getPrimaryViewTranslate().get(recentsView);
-                    }
-
-                    if (!animate) {
-                        pagedOrientationHandler
-                            .getPrimaryViewTranslate().set(recentsView, toTranslation);
-                    } else {
-                        launcher.getStateManager().createStateElementAnimation(
-                                INDEX_RECENTS_TRANSLATE_X_ANIM,
-                                fromTranslation, toTranslation).start();
-                    }
-
-                    fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
+                float fromTranslation = attached ? 1 : 0;
+                float toTranslation = attached ? 0 : 1;
+                launcher.getStateManager()
+                        .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
+                if (!recentsView.isShown() && animate) {
+                    ADJACENT_PAGE_OFFSET.set(recentsView, fromTranslation);
                 } else {
-                    fadeAnim.setInterpolator(ACCEL_DEACCEL);
+                    fromTranslation = ADJACENT_PAGE_OFFSET.get(recentsView);
                 }
+                if (!animate) {
+                    ADJACENT_PAGE_OFFSET.set(recentsView, toTranslation);
+                } else {
+                    launcher.getStateManager().createStateElementAnimation(
+                            INDEX_RECENTS_TRANSLATE_X_ANIM,
+                            fromTranslation, toTranslation).start();
+                }
+
+                fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
                 fadeAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0).start();
             }
         };
@@ -365,51 +334,20 @@
             return;
         }
 
-        LauncherState.ScaleAndTranslation fromScaleAndTranslation
-                = fromState.getOverviewScaleAndTranslation(launcher);
-        LauncherState.ScaleAndTranslation endScaleAndTranslation
-                = endState.getOverviewScaleAndTranslation(launcher);
-        float fromTranslationY = fromScaleAndTranslation.translationY;
-        float endTranslationY = endScaleAndTranslation.translationY;
         float fromFullscreenProgress = fromState.getOverviewFullscreenProgress();
         float endFullscreenProgress = endState.getOverviewFullscreenProgress();
 
-        Animator scale = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY,
-                fromScaleAndTranslation.scale, endScaleAndTranslation.scale);
-        Animator translateY = ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y,
-                fromTranslationY, endTranslationY);
+        float fromScale = fromState.getOverviewScaleAndOffset(launcher)[0];
+        float endScale = endState.getOverviewScaleAndOffset(launcher)[0];
+
+        Animator scale = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, fromScale, endScale);
         Animator applyFullscreenProgress = ObjectAnimator.ofFloat(recentsView,
                 RecentsView.FULLSCREEN_PROGRESS, fromFullscreenProgress, endFullscreenProgress);
-        anim.playTogether(scale, translateY, applyFullscreenProgress);
-
-        mAdjustInterpolatorsRunnable = () -> {
-            // Adjust the translateY interpolator to account for the running task's top inset.
-            // When progress <= 1, this is handled by each task view as they set their fullscreen
-            // progress. However, once we go to progress > 1, fullscreen progress stays at 0, so
-            // recents as a whole needs to translate further to keep up with the app window.
-            TaskView runningTaskView = recentsView.getRunningTaskView();
-            if (runningTaskView == null) {
-                runningTaskView = recentsView.getCurrentPageTaskView();
-                if (runningTaskView == null) {
-                    // There are no task views in LockTask mode when Overview is enabled.
-                    return;
-                }
-            }
-            TimeInterpolator oldInterpolator = translateY.getInterpolator();
-            Rect fallbackInsets = launcher.getDeviceProfile().getInsets();
-            float extraTranslationY = runningTaskView.getThumbnail().getInsets(fallbackInsets).top;
-            float normalizedTranslationY = extraTranslationY / (fromTranslationY - endTranslationY);
-            translateY.setInterpolator(t -> {
-                float newT = oldInterpolator.getInterpolation(t);
-                return newT <= 1f ? newT : newT + normalizedTranslationY * (newT - 1);
-            });
-        };
+        anim.playTogether(scale, applyFullscreenProgress);
 
         // Start pulling back when RecentsView scale is 0.75f, and let it go down to 0.5f.
-        float pullbackStartProgress = (0.75f - fromScaleAndTranslation.scale)
-                / (endScaleAndTranslation.scale - fromScaleAndTranslation.scale);
-        float pullbackMaxProgress = (0.5f - fromScaleAndTranslation.scale)
-                / (endScaleAndTranslation.scale - fromScaleAndTranslation.scale);
+        float pullbackStartProgress = (0.75f - fromScale) / (endScale - fromScale);
+        float pullbackMaxProgress = (0.5f - fromScale) / (endScale - fromScale);
         mSwipeUpPullbackStartAndMaxProgress = new Pair<>(
                 pullbackStartProgress, pullbackMaxProgress);
     }
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 31a2814..52b40a9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
@@ -17,6 +17,8 @@
 
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
@@ -39,16 +41,17 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.PointF;
+import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.Build;
 import android.os.SystemClock;
-import android.util.Log;
 import android.view.View;
 import android.view.View.OnApplyWindowInsetsListener;
 import android.view.ViewTreeObserver.OnDrawListener;
@@ -67,7 +70,6 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -78,9 +80,11 @@
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.AppWindowAnimationHelper.TargetAlphaProvider;
+import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
+import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.views.LiveTileOverlay;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -177,6 +181,9 @@
     private AnimatorPlaybackController mLauncherTransitionController;
     private boolean mHasLauncherTransitionControllerStarted;
 
+    private final TaskViewSimulator mTaskViewSimulator;
+    private AnimatorPlaybackController mWindowTransitionController;
+
     private AnimationFactory mAnimationFactory = (t) -> { };
 
     private boolean mWasLauncherAlreadyVisible;
@@ -201,6 +208,10 @@
         mTaskAnimationManager = taskAnimationManager;
         mTouchTimeMs = touchTimeMs;
         mContinuingLastGesture = continuingLastGesture;
+        mTaskViewSimulator = new TaskViewSimulator(
+                context, LayoutUtils::calculateLauncherTaskSize, true);
+
+        initAfterSubclassConstructor();
         initStateCallbacks();
     }
 
@@ -473,23 +484,11 @@
         } else if (mContinuingLastGesture
                 && mRecentsView.getRunningTaskIndex() != mRecentsView.getNextPage()) {
             recentsAttachedToAppWindow = true;
-            animate = false;
         } else if (runningTaskTarget != null && isNotInRecents(runningTaskTarget)) {
             // The window is going away so make sure recents is always visible in this case.
             recentsAttachedToAppWindow = true;
-            animate = false;
         } else {
             recentsAttachedToAppWindow = mIsShelfPeeking || mIsLikelyToStartNewTask;
-            if (animate) {
-                // Only animate if an adjacent task view is visible on screen.
-                TaskView adjacentTask1 = mRecentsView.getNextTaskView();
-                TaskView adjacentTask2 = mRecentsView.getPreviousTaskView();
-                float prevTranslationX = mRecentsView.getTranslationX();
-                mRecentsView.setTranslationX(0);
-                animate = (adjacentTask1 != null && adjacentTask1.getGlobalVisibleRect(TEMP_RECT))
-                        || (adjacentTask2 != null && adjacentTask2.getGlobalVisibleRect(TEMP_RECT));
-                mRecentsView.setTranslationX(prevTranslationX);
-            }
         }
         mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
     }
@@ -523,6 +522,34 @@
         mAnimationFactory.createActivityInterface(mTransitionDragLength);
     }
 
+    @Override
+    protected void updateSource(Rect stackBounds, RemoteAnimationTargetCompat runningTarget) {
+        super.updateSource(stackBounds, runningTarget);
+        mTaskViewSimulator.setPreview(runningTarget, mRecentsAnimationTargets);
+    }
+
+    @Override
+    protected void initTransitionEndpoints(DeviceProfile dp) {
+        super.initTransitionEndpoints(dp);
+        mTaskViewSimulator.setDp(dp, false /* isOpening */);
+        mTaskViewSimulator.setLayoutRotation(
+                mDeviceState.getCurrentActiveRotation(),
+                mDeviceState.getDisplayRotation());
+
+        AnimatorSet anim = new AnimatorSet();
+        anim.setDuration(mTransitionDragLength * 2);
+        anim.setInterpolator(t -> t * mDragLengthFactor);
+        anim.play(ObjectAnimator.ofFloat(mTaskViewSimulator.recentsViewScale,
+                AnimatedFloat.VALUE,
+                mTaskViewSimulator.getFullScreenScale(), 1));
+        anim.play(ObjectAnimator.ofFloat(mTaskViewSimulator.fullScreenProgress,
+                AnimatedFloat.VALUE,
+                BACKGROUND_APP.getOverviewFullscreenProgress(),
+                OVERVIEW.getOverviewFullscreenProgress()));
+        mWindowTransitionController =
+                AnimatorPlaybackController.wrap(anim, mTransitionDragLength * 2);
+    }
+
     /**
      * We don't want to change mLauncherTransitionController if mGestureState.getEndTarget() == HOME
      * (it has its own animation) or if we're already animating the current controller.
@@ -542,7 +569,6 @@
     private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) {
         mLauncherTransitionController = anim;
         mLauncherTransitionController.dispatchSetInterpolator(t -> t * mDragLengthFactor);
-        mAnimationFactory.adjustActivityControllerInterpolators();
         mLauncherTransitionController.dispatchOnStart();
         updateLauncherTransitionProgress();
     }
@@ -555,7 +581,9 @@
     @Override
     public void updateFinalShift() {
         if (mRecentsAnimationTargets != null) {
-            applyTransformUnchecked();
+            // Base class expects applyTransformUnchecked to be called here.
+            // TODO: Remove this dependency for swipe-up animation.
+            // applyTransformUnchecked();
             updateSysUiFlags(mCurrentShift.value);
         }
 
@@ -575,6 +603,16 @@
             }
         }
 
+        if (mWindowTransitionController != null) {
+            float progress = mCurrentShift.value / mDragLengthFactor;
+            mWindowTransitionController.setPlayFraction(progress);
+            mTransformParams
+                    .setTargetSet(mRecentsAnimationTargets)
+                    .setLauncherOnTop(true);
+
+            mTaskViewSimulator.setScroll(mRecentsView == null ? 0 : mRecentsView.getScrollOffset());
+            mTaskViewSimulator.apply(mTransformParams);
+        }
         updateLauncherTransitionProgress();
     }
 
@@ -1020,7 +1058,6 @@
             mLauncherTransitionController.dispatchSetInterpolator(t -> end);
         } else {
             mLauncherTransitionController.dispatchSetInterpolator(adjustedInterpolator);
-            mAnimationFactory.adjustActivityControllerInterpolators();
         }
         mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration));
 
@@ -1040,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);
         }
@@ -1296,6 +1334,7 @@
 
     private void setTargetAlphaProvider(TargetAlphaProvider provider) {
         mAppWindowAnimationHelper.setTaskAlphaCallback(provider);
+        mTaskViewSimulator.setTaskAlphaCallback(provider);
         updateFinalShift();
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
index 42d944f..52a2558 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
@@ -71,6 +71,7 @@
         mRecentsRootView = findViewById(R.id.drag_layer);
         mFallbackRecentsView = findViewById(R.id.overview_panel);
         mRecentsRootView.recreateControllers();
+        mFallbackRecentsView.init(findViewById(R.id.overview_actions_view));
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
index fbf29af..147f933 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
@@ -16,7 +16,6 @@
 
 package com.android.quickstep;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
 
 import android.content.Context;
@@ -84,47 +83,47 @@
     /**
      * Overlay on each task handling Overview Action Buttons.
      */
-    public static class TaskOverlay {
+    public static class TaskOverlay<T extends OverviewActionsView> {
 
         private final Context mApplicationContext;
-        private OverviewActionsView mActionsView;
-        private final TaskThumbnailView mThumbnailView;
+        protected final TaskThumbnailView mThumbnailView;
 
+        private T mActionsView;
 
         protected TaskOverlay(TaskThumbnailView taskThumbnailView) {
             mApplicationContext = taskThumbnailView.getContext().getApplicationContext();
             mThumbnailView = taskThumbnailView;
         }
 
+        protected T getActionsView() {
+            if (mActionsView == null) {
+                mActionsView = BaseActivity.fromContext(mThumbnailView.getContext()).findViewById(
+                        R.id.overview_actions_view);
+            }
+            return mActionsView;
+        }
+
         /**
          * Called when the current task is interactive for the user
          */
         public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix) {
             ImageActionsApi imageApi = new ImageActionsApi(
                     mApplicationContext, mThumbnailView::getThumbnail);
+            getActionsView().setCallbacks(new OverlayUICallbacks() {
+                @Override
+                public void onShare() {
+                    imageApi.startShareActivity();
+                }
 
-            if (mActionsView == null && ENABLE_OVERVIEW_ACTIONS.get()
-                    && SysUINavigationMode.removeShelfFromOverview(mApplicationContext)) {
-                mActionsView = BaseActivity.fromContext(mThumbnailView.getContext()).findViewById(
-                        R.id.overview_actions_view);
-            }
-            if (mActionsView != null) {
-                mActionsView.setListener(new OverviewActionsView.Listener() {
-                    @Override
-                    public void onShare() {
-                        imageApi.startShareActivity();
-                    }
-
-                    @Override
-                    public void onScreenshot() {
-                        imageApi.saveScreenshot(mThumbnailView.getThumbnail(),
-                                getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key.id);
-                    }
-                });
-            }
-
+                @Override
+                public void onScreenshot() {
+                    imageApi.saveScreenshot(mThumbnailView.getThumbnail(),
+                            getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key.id);
+                }
+            });
         }
 
+
         /**
          * Called when the overlay is no longer used.
          */
@@ -161,4 +160,16 @@
             return Insets.of(0, 0, 0, 0);
         }
     }
+
+    /**
+     * Callbacks the Ui can generate. This is the only way for a Ui to call methods on the
+     * controller.
+     */
+    public interface OverlayUICallbacks {
+        /** User has indicated they want to share the current task. */
+        void onShare();
+
+        /** User has indicated they want to screenshot the current task. */
+        void onScreenshot();
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index 0fb51f3..28c2b97 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -23,6 +23,7 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.quickstep.GestureState.DEFAULT_STATE;
+import static com.android.quickstep.util.RecentsOrientedState.isFixedRotationTransformEnabled;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
@@ -90,7 +91,6 @@
 import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.InputMonitorCompat;
-import com.android.systemui.shared.system.RecentsAnimationListener;
 import com.android.systemui.shared.tracing.ProtoTraceable;
 
 import java.io.FileDescriptor;
@@ -499,7 +499,10 @@
             }
         }
 
-        ActiveGestureLog.INSTANCE.addLog("onMotionEvent", event.getActionMasked());
+        if (mUncheckedConsumer != InputConsumer.NO_OP) {
+            ActiveGestureLog.INSTANCE.addLog("onMotionEvent", event.getActionMasked());
+        }
+
         boolean cleanUpConsumer = (action == ACTION_UP || action == ACTION_CANCEL)
                 && mConsumer != null
                 && !mConsumer.getActiveConsumerInHierarchy().isConsumerDetachedFromGesture();
@@ -595,7 +598,7 @@
     }
 
     private void handleOrientationSetup(InputConsumer baseInputConsumer) {
-        if (!FeatureFlags.ENABLE_FIXED_ROTATION_TRANSFORM.get()) {
+        if (!isFixedRotationTransformEnabled(this)) {
             return;
         }
         mDeviceState.enableMultipleRegions(baseInputConsumer instanceof OtherActivityInputConsumer);
@@ -739,12 +742,14 @@
 
         final BaseActivityInterface<BaseDraggingActivity> activityInterface =
                 mOverviewComponentObserver.getActivityInterface();
+        final Intent overviewIntent = new Intent(
+                mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState());
         if (activityInterface.getCreatedActivity() == null) {
             // Make sure that UI states will be initialized.
             activityInterface.createActivityInitListener((wasVisible) -> {
                 AppLaunchTracker.INSTANCE.get(TouchInteractionService.this);
                 return false;
-            }).register();
+            }).register(overviewIntent);
         } else if (fromInit) {
             // The activity has been created before the initialization of overview service. It is
             // usually happens when booting or launcher is the top activity, so we should already
@@ -752,8 +757,7 @@
             return;
         }
 
-        mTaskAnimationManager.preloadRecentsAnimation(
-                mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState());
+        mTaskAnimationManager.preloadRecentsAnimation(overviewIntent);
     }
 
     @Override
@@ -802,6 +806,7 @@
             if (mGestureState != null) {
                 mGestureState.dump(pw);
             }
+            SysUINavigationMode.INSTANCE.get(this).dump(pw);
             pw.println("TouchState:");
             BaseDraggingActivity createdOverviewActivity = mOverviewComponentObserver == null ? null
                     : mOverviewComponentObserver.getActivityInterface().getCreatedActivity();
@@ -832,7 +837,7 @@
 
     private BaseSwipeUpHandler createLauncherSwipeHandler(GestureState gestureState,
             long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
-        return  new LauncherSwipeHandler(this, mDeviceState, mTaskAnimationManager,
+        return new LauncherSwipeHandler(this, mDeviceState, mTaskAnimationManager,
                 gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
     }
 
@@ -858,11 +863,6 @@
         }
     }
 
-    public static void startRecentsActivityAsync(Intent intent, RecentsAnimationListener listener) {
-        UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
-                .startRecentsActivity(intent, null, listener, null, null));
-    }
-
     @Override
     public void onPluginConnected(OverscrollPlugin overscrollPlugin, Context context) {
         mOverscrollPlugin = overscrollPlugin;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
index dc0c194..3cf9b2c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -26,10 +26,10 @@
 import android.view.View;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.Utilities;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
@@ -57,7 +57,6 @@
     private boolean mInOverviewState = true;
 
     private float mZoomScale = 1f;
-    private float mZoomTranslationY = 0f;
 
     private RunningTaskInfo mRunningTaskInfo;
 
@@ -66,7 +65,12 @@
     }
 
     public FallbackRecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
+        super(context, attrs, defStyleAttr, false);
+    }
+
+    @Override
+    public void init(OverviewActionsView actionsView) {
+        super.init(actionsView);
         setOverviewStateEnabled(true);
         setOverlayEnabled(true);
     }
@@ -139,14 +143,11 @@
 
         if (getTaskViewCount() == 0) {
             mZoomScale = 1f;
-            mZoomTranslationY = 0f;
         } else {
             TaskView dummyTask = getTaskViewAt(0);
-            ScaleAndTranslation sat = getTempAppWindowAnimationHelper()
+            mZoomScale = getTempAppWindowAnimationHelper()
                     .updateForFullscreenOverview(dummyTask)
-                    .getScaleAndTranslation();
-            mZoomScale = sat.scale;
-            mZoomTranslationY = sat.translationY;
+                    .getSrcToTargetScale();
         }
 
         setZoomProgress(mZoomInProgress);
@@ -155,7 +156,6 @@
     public void setZoomProgress(float progress) {
         mZoomInProgress = progress;
         SCALE_PROPERTY.set(this, Utilities.mapRange(mZoomInProgress, 1, mZoomScale));
-        TRANSLATION_Y.set(this, Utilities.mapRange(mZoomInProgress, 0, mZoomTranslationY));
         FULLSCREEN_PROGRESS.set(this, mZoomInProgress);
     }
 
@@ -195,9 +195,4 @@
         }
         super.applyLoadPlan(tasks);
     }
-
-    @Override
-    protected boolean supportsVerticalLandscape() {
-        return false;
-    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index fe9ef2b..1f6c506 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -333,7 +333,8 @@
                 mTaskAnimationManager.isRecentsAnimationRunning(), isLikelyToStartNewTask);
         mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished);
         mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler::onMotionPauseChanged);
-        mInteractionHandler.initWhenReady();
+        Intent intent = new Intent(mInteractionHandler.getLaunchIntent());
+        mInteractionHandler.initWhenReady(intent);
 
         if (mTaskAnimationManager.isRecentsAnimationRunning()) {
             mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(mGestureState);
@@ -341,7 +342,6 @@
             mTaskAnimationManager.notifyRecentsAnimationState(mInteractionHandler);
             notifyGestureStarted();
         } else {
-            Intent intent = mInteractionHandler.getLaunchIntent();
             intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
             mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(mGestureState, intent,
                     mInteractionHandler);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
index 0a21413..c49b8f2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
@@ -36,17 +36,14 @@
 import com.android.launcher3.R;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
-import com.android.quickstep.views.LauncherRecentsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.plugins.OverscrollPlugin;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 /**
  * Input consumer for handling events to pass to an {@code OverscrollPlugin}.
- *
- * @param <T> Draggable activity subclass used by RecentsView
  */
-public class OverscrollInputConsumer<T extends BaseDraggingActivity> extends DelegateInputConsumer {
+public class OverscrollInputConsumer extends DelegateInputConsumer {
 
     private static final String TAG = "OverscrollInputConsumer";
 
@@ -61,12 +58,12 @@
 
     private final float mSquaredSlop;
 
-    private final Context mContext;
     private final GestureState mGestureState;
     @Nullable
     private final OverscrollPlugin mPlugin;
     private final GestureDetector mGestureDetector;
 
+    @Nullable
     private RecentsView mRecentsView;
 
     public OverscrollInputConsumer(Context context, GestureState gestureState,
@@ -77,7 +74,6 @@
                 .getInteger(R.integer.assistant_gesture_corner_deg_threshold);
         mFlingThresholdPx = context.getResources()
             .getDimension(R.dimen.gestures_overscroll_fling_threshold);
-        mContext = context;
         mGestureState = gestureState;
         mPlugin = plugin;
 
@@ -85,9 +81,6 @@
 
         mSquaredSlop = slop * slop;
         mGestureDetector = new GestureDetector(context, new FlingGestureListener());
-
-        gestureState.getActivityInterface().createActivityInitListener(this::onActivityInit)
-                .register();
     }
 
     @Override
@@ -95,12 +88,6 @@
         return TYPE_OVERSCROLL | mDelegate.getType();
     }
 
-    private boolean onActivityInit(Boolean alreadyOnHome) {
-        mRecentsView = mGestureState.getActivityInterface().getCreatedActivity().getOverviewPanel();
-
-        return true;
-    }
-
     @Override
     public void onMotionEvent(MotionEvent ev) {
         switch (ev.getActionMasked()) {
@@ -191,10 +178,17 @@
     }
 
     private boolean isOverscrolled() {
+        if (mRecentsView == null) {
+            BaseDraggingActivity activity = mGestureState.getActivityInterface()
+                    .getCreatedActivity();
+            if (activity != null) {
+                mRecentsView = activity.getOverviewPanel();
+            }
+        }
+
         // Make sure there isn't an app to quick switch to on our right
         int maxIndex = 0;
-        if ((mRecentsView instanceof LauncherRecentsView)
-                && ((LauncherRecentsView) mRecentsView).hasRecentsExtraCard()) {
+        if (mRecentsView != null && mRecentsView.hasRecentsExtraCard()) {
             maxIndex = 1;
         }
 
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 75a5976..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
@@ -30,13 +30,11 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.Build;
-import android.view.Surface;
 
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.Interpolators;
@@ -60,7 +58,7 @@
 public class AppWindowAnimationHelper {
 
     // The bounds of the source app in device coordinates
-    private final Rect mSourceStackBounds = new Rect();
+    private final RectF mSourceStackBounds = new RectF();
     // The insets of the source app
     private final Rect mSourceInsets = new Rect();
     // The source app bounds with the source insets applied, in the device coordinates
@@ -159,14 +157,10 @@
         mSourceRect.set(scaledTargetRect);
     }
 
-    private float getSrcToTargetScale() {
-        if (mOrientedState == null ||
-            (mOrientedState.getDisplayRotation() == Surface.ROTATION_0
-                || mOrientedState.getDisplayRotation() == Surface.ROTATION_180)) {
-            return mSourceRect.width() / mTargetRect.width();
-        } else {
-            return mSourceRect.height() / mTargetRect.height();
-        }
+    public float getSrcToTargetScale() {
+        return LayoutUtils.getTaskScale(mOrientedState,
+                mSourceRect.width(), mSourceRect.height(),
+                mTargetRect.width(), mTargetRect.height());
     }
 
     public void prepareAnimation(DeviceProfile dp, boolean isOpening) {
@@ -214,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) {
@@ -251,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);
@@ -273,12 +264,14 @@
             mTmpRectF.set(mTargetRect);
             Utilities.scaleRectFAboutCenter(mTmpRectF, params.mOffsetScale);
             mCurrentRect.set(mRectFEvaluator.evaluate(params.mProgress, mSourceRect, mTmpRectF));
-            if (mOrientedState == null || mOrientedState.areMultipleLayoutOrientationsDisabled()) {
+            if (mOrientedState == null
+                    || !mOrientedState.isMultipleOrientationSupportedByDevice()) {
                 mCurrentRect.offset(params.mOffset, 0);
             } else {
                 int displayRotation = mOrientedState.getDisplayRotation();
+                int launcherRotation = mOrientedState.getLauncherRotation();
                 mOrientedState.getOrientationHandler().offsetTaskRect(mCurrentRect,
-                    params.mOffset, displayRotation);
+                    params.mOffset, displayRotation, launcherRotation);
             }
         }
 
@@ -377,15 +370,6 @@
         return this;
     }
 
-    /**
-     * @return The source rect's scale and translation relative to the target rect.
-     */
-    public LauncherState.ScaleAndTranslation getScaleAndTranslation() {
-        float scale = getSrcToTargetScale();
-        float translationY = mSourceRect.centerY() - mSourceRect.top - mTargetRect.centerY();
-        return new LauncherState.ScaleAndTranslation(scale, 0, translationY);
-    }
-
     private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) {
         SystemUiProxy proxy = SystemUiProxy.INSTANCE.get(activity);
         if (proxy.isActive()) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
index dde7605..8a6c4a1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
@@ -207,9 +207,23 @@
                 mRectScaleAnim.skipToEnd();
             }
         }
+        mRectXAnimEnded = true;
+        mRectYAnimEnded = true;
+        mRectScaleAnimEnded = true;
+        maybeOnEnd();
+    }
+
+    private boolean isEnded() {
+        return mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded;
     }
 
     private void onUpdate() {
+        if (isEnded()) {
+            // Prevent further updates from being called. This can happen between callbacks for
+            // ending the x/y/scale animations.
+            return;
+        }
+
         if (!mOnUpdateListeners.isEmpty()) {
             float currentWidth = Utilities.mapRange(mCurrentScaleProgress, mStartRect.width(),
                     mTargetRect.width());
@@ -229,7 +243,7 @@
     }
 
     private void maybeOnEnd() {
-        if (mAnimsStarted && mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded) {
+        if (mAnimsStarted && isEnded()) {
             mAnimsStarted = false;
             for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
                 animatorListener.onAnimationEnd(null);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
index bde6f9a..13c20f1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
@@ -28,6 +28,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -185,13 +186,13 @@
         ResourceProvider rp = DynamicResource.provider(v.getContext());
         float stiffness = rp.getFloat(R.dimen.staggered_stiffness);
         float damping = rp.getFloat(R.dimen.staggered_damping_ratio);
-        ObjectAnimator springTransY = new SpringAnimationBuilder<>(v, VIEW_TRANSLATE_Y)
+        ValueAnimator springTransY = new SpringAnimationBuilder(v.getContext())
                 .setStiffness(stiffness)
                 .setDampingRatio(damping)
                 .setMinimumVisibleChange(1f)
                 .setEndValue(0)
                 .setStartVelocity(mVelocity)
-                .build(v.getContext());
+                .build(v, VIEW_TRANSLATE_Y);
         springTransY.setStartDelay(startDelay);
         mAnimators.play(springTransY);
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
new file mode 100644
index 0000000..0bc021b
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -0,0 +1,298 @@
+/*
+ * 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.util;
+
+import static android.view.Surface.ROTATION_0;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.states.RotationHelper.deltaRotation;
+import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE;
+import static com.android.quickstep.util.AppWindowAnimationHelper.applySurfaceParams;
+import static com.android.quickstep.util.RecentsOrientedState.isFixedRotationTransformEnabled;
+import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
+import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.RecentsAnimationTargets;
+import com.android.quickstep.util.AppWindowAnimationHelper.TargetAlphaProvider;
+import com.android.quickstep.util.AppWindowAnimationHelper.TransformParams;
+import com.android.quickstep.views.RecentsView.ScrollState;
+import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper;
+import com.android.quickstep.views.TaskView;
+import com.android.quickstep.views.TaskView.FullscreenDrawParams;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+
+/**
+ * A utility class which emulates the layout behavior of TaskView and RecentsView
+ */
+public class TaskViewSimulator {
+
+    private final Rect mTmpCropRect = new Rect();
+    private final RectF mTempRectF = new RectF();
+
+    private final RecentsOrientedState mOrientationState;
+    private final Context mContext;
+    private final TaskSizeProvider mSizeProvider;
+
+    private final Rect mTaskRect = new Rect();
+    private final PointF mPivot = new PointF();
+    private DeviceProfile mDp;
+
+    private final Matrix mMatrix = new Matrix();
+    private RemoteAnimationTargetCompat mRunningTarget;
+    private RecentsAnimationTargets mAllTargets;
+
+    // Whether to boost the opening animation target layers, or the closing
+    private int mBoostModeTargetLayers = -1;
+    private TargetAlphaProvider mTaskAlphaCallback = (t, a) -> a;
+
+    // Thumbnail view properties
+    private final Rect mThumbnailPosition = new Rect();
+    private final ThumbnailData mThumbnailData = new ThumbnailData();
+    private final PreviewPositionHelper mPositionHelper;
+    private final Matrix mInversePositionMatrix = new Matrix();
+
+    // TaskView properties
+    private final FullscreenDrawParams mCurrentFullscreenParams;
+    private float mCurveScale = 1;
+
+    // RecentsView properties
+    public final AnimatedFloat recentsViewScale = new AnimatedFloat(() -> { });
+    public final AnimatedFloat fullScreenProgress = new AnimatedFloat(() -> { });
+    private final ScrollState mScrollState = new ScrollState();
+    private final int mPageSpacing;
+
+    // Cached calculations
+    private boolean mLayoutValid = false;
+    private boolean mScrollValid = false;
+
+    public TaskViewSimulator(Context context, TaskSizeProvider sizeProvider,
+            boolean rotationSupportedByActivity) {
+        mContext = context;
+        mSizeProvider = sizeProvider;
+        mPositionHelper = new PreviewPositionHelper(context);
+
+        mOrientationState = new RecentsOrientedState(context, rotationSupportedByActivity,
+                i -> { });
+        // We do not need to attach listeners as the simulator is created just for the gesture
+        // duration, and any settings are unlikely to change during this
+        mOrientationState.initWithoutListeners();
+
+        mCurrentFullscreenParams = new FullscreenDrawParams(context);
+        mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
+    }
+
+    /**
+     * Sets the device profile for the current state
+     */
+    public void setDp(DeviceProfile dp, boolean isOpening) {
+        mDp = dp;
+        mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING;
+        mLayoutValid = false;
+    }
+
+    /**
+     * @see com.android.quickstep.views.RecentsView#setLayoutRotation(int, int)
+     */
+    public void setLayoutRotation(int touchRotation, int displayRotation) {
+        int launcherRotation;
+        if (!mOrientationState.isMultipleOrientationSupportedByDevice()
+                || mOrientationState.isHomeRotationAllowed()) {
+            launcherRotation = displayRotation;
+        } else {
+            launcherRotation = ROTATION_0;
+        }
+
+        mOrientationState.update(touchRotation, displayRotation, launcherRotation);
+        mLayoutValid = false;
+    }
+
+    /**
+     * @see com.android.quickstep.views.RecentsView#FULLSCREEN_PROGRESS
+     */
+    public float getFullScreenScale() {
+        if (mDp == null) {
+            return 1;
+        }
+        mSizeProvider.calculateTaskSize(mContext, mDp, mTaskRect);
+        return mOrientationState.getFullScreenScaleAndPivot(mTaskRect, mDp, mPivot);
+    }
+
+    /**
+     * Sets the targets which the simulator will control
+     */
+    public void setPreview(
+            RemoteAnimationTargetCompat runningTarget, RecentsAnimationTargets allTargets) {
+        mRunningTarget = runningTarget;
+        mAllTargets = allTargets;
+
+        mThumbnailData.insets.set(mRunningTarget.contentInsets);
+        // TODO: What is this?
+        mThumbnailData.windowingMode = WINDOWING_MODE_FULLSCREEN;
+
+        mThumbnailPosition.set(runningTarget.screenSpaceBounds);
+        // TODO: Should sourceContainerBounds already have this offset?
+        mThumbnailPosition.offsetTo(mRunningTarget.position.x, mRunningTarget.position.y);
+
+        mLayoutValid = false;
+    }
+
+    /**
+     * Updates the scroll for RecentsView
+     */
+    public void setScroll(int scroll) {
+        if (mScrollState.scroll != scroll) {
+            mScrollState.scroll = scroll;
+            mScrollValid = false;
+        }
+    }
+
+    /**
+     * Sets an alternate function which can be used to control the alpha
+     */
+    public void setTaskAlphaCallback(TargetAlphaProvider callback) {
+        mTaskAlphaCallback = callback;
+    }
+
+    /**
+     * Applies the target to the previously set parameters
+     */
+    public void apply(TransformParams params) {
+        if (mDp == null || mRunningTarget == null) {
+            return;
+        }
+        if (!mLayoutValid) {
+            mLayoutValid = true;
+
+            getFullScreenScale();
+            mThumbnailData.rotation = isFixedRotationTransformEnabled(mContext)
+                    ? mOrientationState.getDisplayRotation() : mPositionHelper.getCurrentRotation();
+
+            mPositionHelper.updateThumbnailMatrix(mThumbnailPosition, mThumbnailData,
+                    mDp.isMultiWindowMode, mTaskRect.width(), mTaskRect.height());
+
+            mPositionHelper.getMatrix().invert(mInversePositionMatrix);
+
+            PagedOrientationHandler poh = mOrientationState.getOrientationHandler();
+            mScrollState.halfPageSize =
+                    poh.getPrimaryValue(mTaskRect.width(), mTaskRect.height()) / 2;
+            mScrollState.halfScreenSize = poh.getPrimaryValue(mDp.widthPx, mDp.heightPx) / 2;
+            mScrollValid = false;
+        }
+
+        if (!mScrollValid) {
+            mScrollValid = true;
+            int start = mOrientationState.getOrientationHandler()
+                    .getPrimaryValue(mTaskRect.left, mTaskRect.top);
+            mScrollState.screenCenter = start + mScrollState.scroll + mScrollState.halfPageSize;
+            mScrollState.updateInterpolation(start, mPageSpacing);
+            mCurveScale = TaskView.getCurveScaleForInterpolation(mScrollState.linearInterpolation);
+        }
+
+        float progress = Utilities.boundToRange(fullScreenProgress.value, 0, 1);
+        mCurrentFullscreenParams.setProgress(
+                progress, recentsViewScale.value, mTaskRect.width(), mDp, mPositionHelper);
+
+        // Apply thumbnail matrix
+        RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets;
+        float scale = mCurrentFullscreenParams.mScale;
+        float taskWidth = mTaskRect.width();
+        float taskHeight = mTaskRect.height();
+
+        mMatrix.set(mPositionHelper.getMatrix());
+        mMatrix.postScale(scale, scale);
+        mMatrix.postTranslate(insets.left, insets.top);
+
+        // Apply TaskView matrix: scale, translate, scroll
+        mMatrix.postScale(mCurveScale, mCurveScale, taskWidth / 2, taskHeight / 2);
+        mMatrix.postTranslate(mTaskRect.left, mTaskRect.top);
+        mOrientationState.getOrientationHandler().set(
+                mMatrix, MATRIX_POST_TRANSLATE, mScrollState.scroll);
+
+        // Apply recensView matrix
+        mMatrix.postScale(recentsViewScale.value, recentsViewScale.value, mPivot.x, mPivot.y);
+        postDisplayRotation(deltaRotation(
+                mOrientationState.getLauncherRotation(), mOrientationState.getDisplayRotation()),
+                mDp.widthPx, mDp.heightPx, mMatrix);
+
+        // Crop rect is the inverse of thumbnail matrix
+        mTempRectF.set(-insets.left, -insets.top,
+                taskWidth + insets.right, taskHeight + insets.bottom);
+        mInversePositionMatrix.mapRect(mTempRectF);
+        mTempRectF.roundOut(mTmpCropRect);
+
+        SurfaceParams[] surfaceParams = new SurfaceParams[mAllTargets.unfilteredApps.length];
+        for (int i = 0; i < mAllTargets.unfilteredApps.length; i++) {
+            RemoteAnimationTargetCompat app = mAllTargets.unfilteredApps[i];
+            SurfaceParams.Builder builder = new SurfaceParams.Builder(app.leash)
+                    .withLayer(RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers));
+
+            if (app.mode == mAllTargets.targetMode) {
+                float alpha = mTaskAlphaCallback.getAlpha(app, params.getTargetAlpha());
+                if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
+                    // Fade out Assistant overlay.
+                    if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT
+                            && app.isNotInRecents) {
+                        alpha = Interpolators.ACCEL_2.getInterpolation(fullScreenProgress.value);
+                    }
+
+                    builder.withAlpha(alpha)
+                            .withMatrix(mMatrix)
+                            .withWindowCrop(mTmpCropRect)
+                            .withCornerRadius(mCurrentFullscreenParams.mCurrentDrawnCornerRadius);
+                } else if (params.getTargetSet().hasRecents) {
+                    // If home has a different target then recents, reverse anim the home target.
+                    builder.withAlpha(fullScreenProgress.value * params.getTargetAlpha());
+                }
+            } else {
+                builder.withAlpha(1);
+                if (ENABLE_QUICKSTEP_LIVE_TILE.get() && params.isLauncherOnTop()) {
+                    builder.withLayer(Integer.MAX_VALUE);
+                }
+            }
+            surfaceParams[i] = builder.build();
+        }
+
+        applySurfaceParams(params.getSyncTransactionApplier(), surfaceParams);
+    }
+
+    /**
+     * Interface for calculating taskSize
+     */
+    public interface TaskSizeProvider {
+
+        /**
+         * Sets the outRect to the expected taskSize
+         */
+        void calculateTaskSize(Context context, DeviceProfile dp, Rect outRect);
+    }
+
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index 98eb29a..0b6d340 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -45,9 +45,7 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statehandlers.DepthController;
-import com.android.launcher3.states.RotationHelper;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.views.ScrimView;
@@ -88,10 +86,6 @@
         }
     };
 
-    private RotationHelper.ForcedRotationChangedListener mForcedRotationChangedListener =
-            isForcedRotation -> LauncherRecentsView.this
-                    .disableMultipleLayoutRotations(!isForcedRotation);
-
     public LauncherRecentsView(Context context) {
         this(context, null);
     }
@@ -101,12 +95,17 @@
     }
 
     public LauncherRecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        setContentAlpha(0);
+        super(context, attrs, defStyleAttr, true);
         mActivity.getStateManager().addStateListener(this);
     }
 
     @Override
+    public void init(OverviewActionsView actionsView) {
+        super.init(actionsView);
+        setContentAlpha(0);
+    }
+
+    @Override
     public void startHome() {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             switchToScreenshot(null,
@@ -183,27 +182,6 @@
         LayoutUtils.calculateLauncherTaskSize(getContext(), dp, outRect);
     }
 
-    /**
-     * @return The translationX to apply to this view so that the first task is just offscreen.
-     */
-    public float getOffscreenTranslationX(float recentsScale) {
-        LauncherState.ScaleAndTranslation overviewScaleAndTranslation =
-            NORMAL.getOverviewScaleAndTranslation(mActivity);
-        float offscreen = mOrientationHandler.getTranslationValue(overviewScaleAndTranslation);
-        // Offset since scale pushes tasks outwards.
-        getTaskSize(sTempRect);
-        int taskSize = mOrientationHandler.getPrimarySize(sTempRect);
-        offscreen += taskSize * (recentsScale - 1) / 2;
-        if (mRunningTaskTileHidden) {
-            // The first task is hidden, so offset by its width.
-            offscreen -= (taskSize + getPageSpacing()) * recentsScale;
-        }
-        if (isRtl()) {
-            offscreen = -offscreen;
-        }
-        return offscreen;
-    }
-
     @Override
     protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
@@ -286,12 +264,6 @@
     }
 
     @Override
-    protected boolean supportsVerticalLandscape() {
-        return FeatureFlags.ENABLE_FIXED_ROTATION_TRANSFORM.get()
-                && !mOrientationState.areMultipleLayoutOrientationsDisabled();
-    }
-
-    @Override
     public void reset() {
         super.reset();
 
@@ -344,7 +316,6 @@
         super.onAttachedToWindow();
         PluginManagerWrapper.INSTANCE.get(getContext()).addPluginListener(
                 mRecentsExtraCardPluginListener, RecentsExtraCard.class);
-        mActivity.getRotationHelper().addForcedRotationCallback(mForcedRotationChangedListener);
     }
 
     @Override
@@ -352,7 +323,6 @@
         super.onDetachedFromWindow();
         PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(
                 mRecentsExtraCardPluginListener);
-        mActivity.getRotationHelper().removeForcedRotationCallback(mForcedRotationChangedListener);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
index 6a37e2b..d160686 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
@@ -16,34 +16,61 @@
 
 package com.android.quickstep.views;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
+
 import android.content.Context;
 import android.util.AttributeSet;
-import android.view.LayoutInflater;
 import android.view.View;
+import android.view.View.OnClickListener;
 import android.widget.FrameLayout;
 
+import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.R;
+import com.android.launcher3.util.MultiValueAlpha;
+import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.quickstep.TaskOverlayFactory.OverlayUICallbacks;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /**
  * View for showing action buttons in Overview
  */
-public class OverviewActionsView extends FrameLayout {
+public class OverviewActionsView<T extends OverlayUICallbacks> extends FrameLayout
+        implements OnClickListener {
 
-    private final View mScreenshotButton;
-    private final View mShareButton;
+    @IntDef(flag = true, value = {
+            HIDDEN_UNSUPPORTED_NAVIGATION,
+            HIDDEN_DISABLED_FEATURE,
+            HIDDEN_NON_ZERO_ROTATION,
+            HIDDEN_NO_TASKS,
+            HIDDEN_GESTURE_RUNNING,
+            HIDDEN_NO_RECENTS,
+            HIDDEN_FULLESCREEN_PROGRESS})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ActionsHiddenFlags { }
 
-    /**
-     * Listener for taps on the various actions.
-     */
-    public interface Listener {
-        /** User has initiated the share actions. */
-        void onShare();
+    public static final int HIDDEN_UNSUPPORTED_NAVIGATION = 1 << 0;
+    public static final int HIDDEN_DISABLED_FEATURE = 1 << 1;
+    public static final int HIDDEN_NON_ZERO_ROTATION = 1 << 2;
+    public static final int HIDDEN_NO_TASKS = 1 << 3;
+    public static final int HIDDEN_GESTURE_RUNNING = 1 << 4;
+    public static final int HIDDEN_NO_RECENTS = 1 << 5;
+    public static final int HIDDEN_FULLESCREEN_PROGRESS = 1 << 6;
 
-        /** User has initiated the screenshot action. */
-        void onScreenshot();
-    }
+    private static final int INDEX_CONTENT_ALPHA = 0;
+    private static final int INDEX_VISIBILITY_ALPHA = 1;
+    private static final int INDEX_HIDDEN_FLAGS_ALPHA = 2;
+
+    private final MultiValueAlpha mMultiValueAlpha;
+
+    @ActionsHiddenFlags
+    private int mHiddenFlags;
+
+    protected T mCallbacks;
 
     public OverviewActionsView(Context context) {
         this(context, null);
@@ -54,26 +81,62 @@
     }
 
     public OverviewActionsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
+        super(context, attrs, defStyleAttr, 0);
+        mMultiValueAlpha = new MultiValueAlpha(this, 3);
     }
 
-    public OverviewActionsView(Context context, AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-        LayoutInflater.from(context).inflate(R.layout.overview_actions, this, true);
-        mShareButton = findViewById(R.id.action_share);
-        mScreenshotButton = findViewById(R.id.action_screenshot);
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        findViewById(R.id.action_share).setOnClickListener(this);
+        findViewById(R.id.action_screenshot).setOnClickListener(this);
     }
 
     /**
      * Set listener for callbacks on action button taps.
      *
-     * @param listener for callbacks, or {@code null} to clear the listener.
+     * @param callbacks for callbacks, or {@code null} to clear the listener.
      */
-    public void setListener(@Nullable OverviewActionsView.Listener listener) {
-        mShareButton.setOnClickListener(
-                listener == null ? null : view -> listener.onShare());
-        mScreenshotButton.setOnClickListener(
-                listener == null ? null : view -> listener.onScreenshot());
+    public void setCallbacks(T callbacks) {
+        mCallbacks = callbacks;
+    }
+
+    @Override
+    public void onClick(View view) {
+        if (mCallbacks == null) {
+            return;
+        }
+        int id = view.getId();
+        if (id == R.id.action_share) {
+            mCallbacks.onShare();
+        } else if (id == R.id.action_screenshot) {
+            mCallbacks.onScreenshot();
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        updateHiddenFlags(HIDDEN_DISABLED_FEATURE, !ENABLE_OVERVIEW_ACTIONS.get());
+        updateHiddenFlags(HIDDEN_UNSUPPORTED_NAVIGATION, !removeShelfFromOverview(getContext()));
+    }
+
+    public void updateHiddenFlags(@ActionsHiddenFlags int visibilityFlags, boolean enable) {
+        if (enable) {
+            mHiddenFlags |= visibilityFlags;
+        } else {
+            mHiddenFlags &= ~visibilityFlags;
+        }
+        boolean isHidden = mHiddenFlags != 0;
+        mMultiValueAlpha.getProperty(INDEX_HIDDEN_FLAGS_ALPHA).setValue(isHidden ? 0 : 1);
+        setVisibility(isHidden ? INVISIBLE : VISIBLE);
+    }
+
+    public AlphaProperty getContentAlpha() {
+        return mMultiValueAlpha.getProperty(INDEX_CONTENT_ALPHA);
+    }
+
+    public AlphaProperty getVisibilityAlpha() {
+        return mMultiValueAlpha.getProperty(INDEX_VISIBILITY_ALPHA);
     }
 }
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 a18f7ba..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
@@ -28,7 +28,6 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 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.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.TASK_DISMISS_SWIPE_UP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.TASK_LAUNCH_SWIPE_DOWN;
@@ -39,6 +38,11 @@
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
+import static com.android.quickstep.views.OverviewActionsView.HIDDEN_FULLESCREEN_PROGRESS;
+import static com.android.quickstep.views.OverviewActionsView.HIDDEN_GESTURE_RUNNING;
+import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION;
+import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS;
+import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS;
 
 import android.animation.AnimatorSet;
 import android.animation.LayoutTransition;
@@ -50,8 +54,10 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Configuration;
 import android.graphics.Canvas;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.Typeface;
@@ -66,12 +72,10 @@
 import android.util.FloatProperty;
 import android.util.Property;
 import android.util.SparseBooleanArray;
-import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
-import android.view.OrientationEventListener;
 import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
@@ -85,9 +89,7 @@
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
-import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
@@ -100,7 +102,6 @@
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statehandlers.DepthController;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.touch.PagedOrientationHandler.CurveProperties;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -115,19 +116,18 @@
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.RecentsModel.TaskVisualsChangeListener;
-import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.ViewUtils;
 import com.android.quickstep.util.AppWindowAnimationHelper;
-import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.systemui.plugins.ResourceProvider;
 import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.ConfigurationCompat;
 import com.android.systemui.shared.system.LauncherEventUtil;
 import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
@@ -172,10 +172,23 @@
                 }
             };
 
-    protected final RecentsOrientedState mOrientationState = new RecentsOrientedState();
+    public static final FloatProperty<RecentsView> ADJACENT_PAGE_OFFSET =
+            new FloatProperty<RecentsView>("adjacentPageOffset") {
+                @Override
+                public void setValue(RecentsView recentsView, float v) {
+                    if (recentsView.mAdjacentPageOffset != v) {
+                        recentsView.mAdjacentPageOffset = v;
+                        recentsView.updateAdjacentPageOffset();
+                    }
+                }
 
-    private OrientationEventListener mOrientationListener;
-    private int mPreviousRotation;
+                @Override
+                public Float get(RecentsView recentsView) {
+                    return recentsView.mAdjacentPageOffset;
+                }
+            };
+
+    protected final RecentsOrientedState mOrientationState;
     protected RecentsAnimationController mRecentsAnimationController;
     protected RecentsAnimationTargets mRecentsAnimationTargets;
     protected AppWindowAnimationHelper mAppWindowAnimationHelper;
@@ -185,6 +198,7 @@
     protected boolean mEnableDrawingLiveTile = false;
     protected final Rect mTempRect = new Rect();
     protected final RectF mTempRectF = new RectF();
+    private final PointF mTempPointF = new PointF();
 
     private static final int DISMISS_TASK_DURATION = 300;
     private static final int ADDITION_TASK_DURATION = 200;
@@ -195,7 +209,6 @@
     private final float mFastFlingVelocity;
     private final RecentsModel mModel;
     private final int mTaskTopMargin;
-    private final int mTaskBottomMargin;
     private final ClearAllButton mClearAllButton;
     private final Rect mClearAllButtonDeadZoneRect = new Rect();
     private final Rect mTaskViewDeadZoneRect = new Rect();
@@ -214,6 +227,8 @@
     private boolean mOverlayEnabled;
     protected boolean mFreezeViewVisibility;
 
+    private float mAdjacentPageOffset = 0;
+
     /**
      * TODO: Call reloadIdNeeded in onTaskStackChanged.
      */
@@ -328,8 +343,7 @@
 
     // Keeps track of the index where the first TaskView should be
     private int mTaskViewStartIndex = 0;
-    private View mActionsView;
-    private boolean mGestureRunning = false;
+    private OverviewActionsView mActionsView;
 
     private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener =
             (inMultiWindowMode) -> {
@@ -339,14 +353,17 @@
         }
     };
 
-    public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
+    public RecentsView(Context context, AttributeSet attrs, int defStyleAttr,
+            boolean rotationSupportedByActivity) {
         super(context, attrs, defStyleAttr);
         setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing));
         setEnableFreeScroll(true);
+        mOrientationState = new RecentsOrientedState(
+                context, rotationSupportedByActivity, this::animateRecentsRotationInPlace);
 
         mFastFlingVelocity = getResources()
                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
-        mActivity = (T) BaseActivity.fromContext(context);
+        mActivity = BaseActivity.fromContext(context);
         mModel = RecentsModel.INSTANCE.get(context);
         mIdp = InvariantDeviceProfile.INSTANCE.get(context);
         mTempAppWindowAnimationHelper =
@@ -362,7 +379,6 @@
         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
         mTaskTopMargin = getResources()
                 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
-        mTaskBottomMargin = LayoutUtils.thumbnailBottomMargin(context);
         mSquaredTouchSlop = squaredTouchSlop(context);
 
         mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents);
@@ -378,26 +394,10 @@
                 .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
         setWillNotDraw(false);
         updateEmptyMessage();
-        disableMultipleLayoutRotations(!supportsVerticalLandscape());
+        mOrientationHandler = mOrientationState.getOrientationHandler();
 
         // Initialize quickstep specific cache params here, as this is constructed only once
         mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
-
-        mOrientationListener = new OrientationEventListener(getContext()) {
-            @Override
-            public void onOrientationChanged(int i) {
-                int rotation = RecentsOrientedState.getRotationForUserDegreesRotated(i);
-                if (mPreviousRotation != rotation) {
-                    animateRecentsRotationInPlace(rotation);
-                    if (rotation == 0) {
-                        showActionsView();
-                    } else {
-                        hideActionsView();
-                    }
-                    mPreviousRotation = rotation;
-                }
-            }
-        };
     }
 
     public OverScroller getScroller() {
@@ -468,6 +468,10 @@
         reset();
     }
 
+    public void init(OverviewActionsView actionsView) {
+        mActionsView = actionsView;
+    }
+
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
@@ -481,7 +485,7 @@
         mIPinnedStackAnimationListener.setActivity(mActivity);
         SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(
                 mIPinnedStackAnimationListener);
-        setActionsView();
+        mOrientationState.init();
     }
 
     @Override
@@ -496,6 +500,7 @@
         mIdp.removeOnChangeListener(this);
         SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(null);
         mIPinnedStackAnimationListener.setActivity(null);
+        mOrientationState.destroy();
     }
 
     @Override
@@ -507,6 +512,7 @@
             TaskView taskView = (TaskView) child;
             mHasVisibleTaskData.delete(taskView.getTask().key.id);
             mTaskViewPool.recycle(taskView);
+            mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
         }
         updateTaskStartIndex(child);
     }
@@ -519,6 +525,7 @@
         // child direction back to match system settings.
         child.setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_LTR : View.LAYOUT_DIRECTION_RTL);
         updateTaskStartIndex(child);
+        mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, false);
     }
 
     private void updateTaskStartIndex(View affectingView) {
@@ -549,17 +556,9 @@
     }
 
     public void setOverviewStateEnabled(boolean enabled) {
-        if (supportsVerticalLandscape()
-                && !TestProtocol.sDisableSensorRotation // Ignore hardware dependency for tests
-                && mOrientationListener.canDetectOrientation()) {
-            if (enabled) {
-                mOrientationListener.enable();
-            } else {
-                mOrientationListener.disable();
-            }
-        }
         mOverviewStateEnabled = enabled;
         updateTaskStackListenerState();
+        mOrientationState.setRotationWatcherEnabled(enabled);
         if (!enabled) {
             // Reset the running task when leaving overview since it can still have a reference to
             // its thumbnail
@@ -585,6 +584,15 @@
     }
 
     @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        int windowConfigurationRotation = ConfigurationCompat
+                .getWindowConfigurationRotation(getResources().getConfiguration());
+        setLayoutInternal(mOrientationState.getTouchRotation(),
+                mOrientationState.getDisplayRotation(), windowConfigurationRotation);
+    }
+
+    @Override
     public boolean onTouchEvent(MotionEvent ev) {
         super.onTouchEvent(ev);
         final int x = (int) ev.getX();
@@ -668,7 +676,6 @@
         if (getTaskViewCount() != requiredTaskCount) {
             if (indexOfChild(mClearAllButton) != -1) {
                 removeView(mClearAllButton);
-                hideActionsView();
             }
             for (int i = getTaskViewCount(); i < requiredTaskCount; i++) {
                 addView(mTaskViewPool.getView());
@@ -678,7 +685,6 @@
             }
             if (requiredTaskCount > 0) {
                 addView(mClearAllButton);
-                showActionsView();
             }
         }
 
@@ -718,7 +724,6 @@
         if (indexOfChild(mClearAllButton) != -1) {
             removeView(mClearAllButton);
         }
-        hideActionsView();
     }
 
     public int getTaskViewCount() {
@@ -778,9 +783,7 @@
         for (int i = 0; i < taskCount; i++) {
             getTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
         }
-        if (mActionsView != null) {
-            mActionsView.setVisibility(fullscreenProgress == 0 ? VISIBLE : INVISIBLE);
-        }
+        mActionsView.updateHiddenFlags(HIDDEN_FULLESCREEN_PROGRESS, fullscreenProgress > 0);
     }
 
     private void updateTaskStackListenerState() {
@@ -803,7 +806,6 @@
         mTaskHeight = mTempRect.height();
 
         mTempRect.top -= mTaskTopMargin;
-        mTempRect.bottom += mTaskBottomMargin;
         setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
                 dp.widthPx - mInsets.right - mTempRect.right,
                 dp.heightPx - mInsets.bottom - mTempRect.bottom);
@@ -980,7 +982,7 @@
         setEnableDrawingLiveTile(false);
         setRunningTaskHidden(true);
         setRunningTaskIconScaledDown(true);
-        mGestureRunning = true;
+        mActionsView.updateHiddenFlags(HIDDEN_GESTURE_RUNNING, true);
     }
 
     /**
@@ -998,10 +1000,6 @@
     }
 
     private void animateRecentsRotationInPlace(int newRotation) {
-        if (!supportsVerticalLandscape()) {
-            return;
-        }
-
         AnimatorSet pa = setRecentsChangedOrientation(true);
         pa.addListener(AnimationSuccessListener.forRunnable(() -> {
             setLayoutRotation(newRotation, mOrientationState.getDisplayRotation());
@@ -1026,7 +1024,6 @@
         return as;
     }
 
-    abstract protected boolean supportsVerticalLandscape();
 
     private void rotateAllChildTasks() {
         for (int i = 0; i < getTaskViewCount(); i++) {
@@ -1046,7 +1043,7 @@
         }
         setRunningTaskHidden(false);
         animateUpRunningTaskIconScale();
-        mGestureRunning = false;
+        mActionsView.updateHiddenFlags(HIDDEN_GESTURE_RUNNING, false);
     }
 
     /**
@@ -1057,13 +1054,12 @@
      */
     public void showCurrentTask(int runningTaskId) {
         if (getTaskView(runningTaskId) == null) {
-            boolean wasEmpty = getTaskViewCount() == 0;
+            boolean wasEmpty = getChildCount() == 0;
             // Add an empty view for now until the task plan is loaded and applied
             final TaskView taskView = mTaskViewPool.getView();
             addView(taskView, mTaskViewStartIndex);
             if (wasEmpty) {
                 addView(mClearAllButton);
-                showActionsView();
             }
             // The temporary running task is only used for the duration between the start of the
             // gesture and the task list is loaded and applied
@@ -1387,7 +1383,6 @@
 
                     if (getTaskViewCount() == 0) {
                         removeViewInLayout(mClearAllButton);
-                        hideActionsView();
                         startHome();
                     } else {
                         snapToPageImmediately(pageToSnapTo);
@@ -1530,14 +1525,12 @@
         int alphaInt = Math.round(alpha * 255);
         mEmptyMessagePaint.setAlpha(alphaInt);
         mEmptyIcon.setAlpha(alphaInt);
+        mActionsView.getContentAlpha().setValue(mContentAlpha);
+
         if (alpha > 0) {
             setVisibility(VISIBLE);
-            if (!mGestureRunning) {
-                showActionsView();
-            }
         } else if (!mFreezeViewVisibility) {
             setVisibility(GONE);
-            hideActionsView();
         }
     }
 
@@ -1548,34 +1541,38 @@
     public void setFreezeViewVisibility(boolean freezeViewVisibility) {
         if (mFreezeViewVisibility != freezeViewVisibility) {
             mFreezeViewVisibility = freezeViewVisibility;
-
             if (!mFreezeViewVisibility) {
                 setVisibility(mContentAlpha > 0 ? VISIBLE : GONE);
-                if (mContentAlpha > 0) {
-                    showActionsView();
-                } else {
-                    hideActionsView();
-                }
             }
         }
     }
 
+    @Override
+    public void setVisibility(int visibility) {
+        super.setVisibility(visibility);
+        if (mActionsView != null) {
+            mActionsView.updateHiddenFlags(HIDDEN_NO_RECENTS, visibility != VISIBLE);
+        }
+    }
+
     public void setLayoutRotation(int touchRotation, int displayRotation) {
-        if (mOrientationState.update(touchRotation, displayRotation)) {
+        int launcherRotation = mOrientationState.getLauncherRotation();
+        setLayoutInternal(touchRotation, displayRotation, launcherRotation);
+    }
+
+    private void setLayoutInternal(int touchRotation, int displayRotation, int launcherRotation) {
+        if (mOrientationState.update(touchRotation, displayRotation, launcherRotation)) {
             mOrientationHandler = mOrientationState.getOrientationHandler();
             mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
             setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
             mClearAllButton.setRotation(mOrientationHandler.getDegreesRotated());
+            mActivity.getDragLayer().recreateControllers();
+            mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION,
+                    touchRotation != 0 || launcherRotation != 0);
             requestLayout();
         }
     }
 
-    public void disableMultipleLayoutRotations(boolean disable) {
-        mOrientationState.disableMultipleOrientations(disable);
-        mOrientationHandler = mOrientationState.getOrientationHandler();
-        requestLayout();
-    }
-
     public RecentsOrientedState getPagedViewOrientedState() {
         return mOrientationState;
     }
@@ -1590,11 +1587,6 @@
     }
 
     @Nullable
-    public TaskView getPreviousTaskView() {
-        return getTaskViewAtByAbsoluteIndex(getRunningTaskIndex() - 1);
-    }
-
-    @Nullable
     public TaskView getCurrentPageTaskView() {
         return getTaskViewAtByAbsoluteIndex(getCurrentPage());
     }
@@ -1653,11 +1645,36 @@
 
         updateEmptyStateUi(changed);
 
-        // Set the pivot points to match the task preview center
-        setPivotY(((mInsets.top + getPaddingTop() + mTaskTopMargin)
-                + (getHeight() - mInsets.bottom - getPaddingBottom() - mTaskBottomMargin)) / 2);
-        setPivotX(((mInsets.left + getPaddingLeft())
-                + (getWidth() - mInsets.right - getPaddingRight())) / 2);
+        // Update the pivots such that when the task is scaled, it fills the full page
+        getTaskSize(mTempRect);
+        getPagedViewOrientedState().getFullScreenScaleAndPivot(
+                mTempRect, mActivity.getDeviceProfile(), mTempPointF);
+        setPivotX(mTempPointF.x);
+        setPivotY(mTempPointF.y);
+        updateAdjacentPageOffset();
+    }
+
+    private void updateAdjacentPageOffset() {
+        float offset = mAdjacentPageOffset * getWidth();
+        if (mIsRtl) {
+            offset = -offset;
+        }
+        int count = getChildCount();
+
+        TaskView runningTask = mRunningTaskId == -1 ? null : getTaskView(mRunningTaskId);
+        int midPoint = runningTask == null ? -1 : indexOfChild(runningTask);
+
+        for (int i = 0; i < count; i++) {
+            getChildAt(i).setTranslationX(i == midPoint ? 0 : (i < midPoint ? -offset : offset));
+        }
+        updateCurveProperties();
+    }
+
+    /**
+     * TODO: Do not assume motion across X axis for adjacent page
+     */
+    public float getPageOffsetScale() {
+        return Math.max(getWidth(), 1);
     }
 
     private void updateDeadZoneRects() {
@@ -1739,14 +1756,10 @@
         int centerTaskIndex = getCurrentPage();
         boolean launchingCenterTask = taskIndex == centerTaskIndex;
 
-        LauncherState.ScaleAndTranslation toScaleAndTranslation = appWindowAnimationHelper
-                .getScaleAndTranslation();
-        float toScale = toScaleAndTranslation.scale;
-        float toTranslationY = toScaleAndTranslation.translationY;
+        float toScale = appWindowAnimationHelper.getSrcToTargetScale();
         if (launchingCenterTask) {
             RecentsView recentsView = tv.getRecentsView();
             anim.play(ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, toScale));
-            anim.play(ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y, toTranslationY));
             anim.play(ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, 1));
         } else {
             // We are launching an adjacent task, so parallax the center and other adjacent task.
@@ -2006,23 +2019,29 @@
         return mClearAllButton;
     }
 
+
     /**
      * @return How many pixels the running task is offset on the x-axis due to the current scrollX.
      */
-    public float getScrollOffset() {
+    public int getScrollOffset() {
         if (getRunningTaskIndex() == -1) {
             return 0;
         }
-        int startScroll = getScrollForPage(getRunningTaskIndex());
-        int offsetX = startScroll - mOrientationHandler.getPrimaryScroll(this);
-        offsetX *= mOrientationHandler.getPrimaryScale(this);
-        return offsetX;
+        return getScrollForPage(getRunningTaskIndex()) - mOrientationHandler.getPrimaryScroll(this);
+    }
+
+    /**
+     * @return How many pixels the running task is offset on the x-axis due to the current scrollX
+     * and parent scale.
+     */
+    public float getScrollOffsetScaled() {
+        return getScrollOffset() * mOrientationHandler.getPrimaryScale(this);
     }
 
     public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
         float degreesRotated;
         if (navbarRotation == 0) {
-            degreesRotated = mOrientationState.getTouchRotationDegrees();
+            degreesRotated = mOrientationHandler.getDegreesRotated();
         } else {
             degreesRotated = -navbarRotation;
         }
@@ -2035,7 +2054,8 @@
         // PagedOrientationHandler
         return e -> {
             if (navbarRotation != 0
-                    && !mOrientationState.areMultipleLayoutOrientationsDisabled()) {
+                    && mOrientationState.isMultipleOrientationSupportedByDevice()
+                    && !mOrientationState.getOrientationHandler().isLayoutNaturalToLauncher()) {
                 mOrientationState.flipVertical(e);
                 super.onTouchEvent(e);
                 mOrientationState.flipVertical(e);
@@ -2122,36 +2142,4 @@
             mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
         }
     }
-
-    private void showActionsView() {
-        if (mActionsView != null && getTaskViewCount() > 0) {
-            mActionsView.setVisibility(VISIBLE);
-        }
-    }
-
-    private void hideActionsView() {
-        if (mActionsView != null) {
-            mActionsView.setVisibility(GONE);
-        }
-    }
-
-    private void setActionsView() {
-        if (mActionsView == null && ENABLE_OVERVIEW_ACTIONS.get()
-                && SysUINavigationMode.removeShelfFromOverview(mActivity)) {
-            mActionsView = ((ViewGroup) getParent()).findViewById(R.id.overview_actions_view);
-            if (mActionsView != null) {
-                InsettableFrameLayout.LayoutParams layoutParams =
-                        new InsettableFrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
-                                getResources().getDimensionPixelSize(
-                                        R.dimen.overview_actions_height));
-                layoutParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
-                int margin = getResources().getDimensionPixelSize(
-                        R.dimen.overview_actions_horizontal_margin);
-                layoutParams.setMarginStart(margin);
-                layoutParams.setMarginEnd(margin);
-                mActionsView.setLayoutParams(layoutParams);
-                showActionsView();
-            }
-        }
-    }
 }
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/TaskThumbnailView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
index 178ff32..a05e0fa 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -36,11 +36,9 @@
 import android.graphics.Shader;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
-import android.util.Log;
 import android.util.Property;
 import android.view.Surface;
 import android.view.View;
-import android.view.ViewGroup;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.R;
@@ -50,7 +48,7 @@
 import com.android.launcher3.util.Themes;
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
-import com.android.quickstep.util.TaskCornerRadius;
+import com.android.quickstep.views.TaskView.FullscreenDrawParams;
 import com.android.systemui.plugins.OverviewScreenshotActions;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.shared.recents.model.Task;
@@ -66,6 +64,8 @@
     private static final ColorMatrix SATURATION_COLOR_MATRIX = new ColorMatrix();
     private static final RectF EMPTY_RECT_F = new RectF();
 
+    private static final FullscreenDrawParams TEMP_PARAMS = new FullscreenDrawParams();
+
     public static final Property<TaskThumbnailView, Float> DIM_ALPHA =
             new FloatProperty<TaskThumbnailView>("dimAlpha") {
                 @Override
@@ -87,12 +87,11 @@
     private final Paint mClearPaint = new Paint();
     private final Paint mDimmingPaintAfterClearing = new Paint();
 
-    private final Matrix mMatrix = new Matrix();
-
-    private float mClipBottom = -1;
     // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
-    private RectF mClippedInsets = new RectF();
-    private TaskView.FullscreenDrawParams mFullscreenParams;
+    private final Rect mPreviewRect = new Rect();
+    private final PreviewPositionHelper mPreviewPositionHelper;
+    // Initialize with dummy value. It is overridden later by TaskView
+    private TaskView.FullscreenDrawParams mFullscreenParams = TEMP_PARAMS;
 
     private Task mTask;
     private ThumbnailData mThumbnailData;
@@ -103,7 +102,6 @@
     private float mSaturation = 1f;
 
     private boolean mOverlayEnabled;
-    private boolean mIsOrientationChanged;
     private OverviewScreenshotActions mOverviewScreenshotActionsPlugin;
 
     public TaskThumbnailView(Context context) {
@@ -123,7 +121,7 @@
         mDimmingPaintAfterClearing.setColor(Color.BLACK);
         mActivity = BaseActivity.fromContext(context);
         mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText);
-        mFullscreenParams = new TaskView.FullscreenDrawParams(TaskCornerRadius.get(context));
+        mPreviewPositionHelper = new PreviewPositionHelper(context);
     }
 
     public void bind(Task task) {
@@ -172,8 +170,7 @@
             mOverlay.reset();
         }
         if (mOverviewScreenshotActionsPlugin != null) {
-            mOverviewScreenshotActionsPlugin
-                .setupActions((ViewGroup) getTaskView(), getThumbnail(), mActivity);
+            mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
         }
         updateThumbnailPaintFilter();
     }
@@ -270,9 +267,8 @@
         PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this);
     }
 
-    public RectF getInsetsToDrawInFullscreen(boolean isMultiWindowMode) {
-        // Don't show insets in multi window mode.
-        return isMultiWindowMode ? EMPTY_RECT_F : mClippedInsets;
+    public PreviewPositionHelper getPreviewPositionHelper() {
+        return mPreviewPositionHelper;
     }
 
     public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) {
@@ -294,16 +290,17 @@
         // Draw the background in all cases, except when the thumbnail data is opaque
         final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null
                 || mThumbnailData == null;
-        if (drawBackgroundOnly || mClipBottom > 0 || mThumbnailData.isTranslucent) {
+        if (drawBackgroundOnly || mPreviewPositionHelper.mClipBottom > 0
+                || mThumbnailData.isTranslucent) {
             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint);
             if (drawBackgroundOnly) {
                 return;
             }
         }
 
-        if (mClipBottom > 0) {
+        if (mPreviewPositionHelper.mClipBottom > 0) {
             canvas.save();
-            canvas.clipRect(x, y, width, mClipBottom);
+            canvas.clipRect(x, y, width, mPreviewPositionHelper.mClipBottom);
             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
             canvas.restore();
         } else {
@@ -324,8 +321,9 @@
 
     private void updateOverlay() {
         // The overlay doesn't really work when the screenshot is rotated, so don't add it.
-        if (mOverlayEnabled && !mIsOrientationChanged && mBitmapShader != null && mThumbnailData != null) {
-            mOverlay.initOverlay(mTask, mThumbnailData, mMatrix);
+        if (mOverlayEnabled && !mPreviewPositionHelper.mIsOrientationChanged
+                && mBitmapShader != null && mThumbnailData != null) {
+            mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix);
         } else {
             mOverlay.reset();
         }
@@ -346,76 +344,17 @@
     }
 
     private void updateThumbnailMatrix() {
-        boolean isRotated = false;
-        boolean isOrientationDifferent = false;
-        mClipBottom = -1;
+        mPreviewPositionHelper.mClipBottom = -1;
+        mPreviewPositionHelper.mIsOrientationChanged = false;
         if (mBitmapShader != null && mThumbnailData != null) {
-            float scale = mThumbnailData.scale;
-            Rect thumbnailInsets = mThumbnailData.insets;
-            final float thumbnailWidth = mThumbnailData.thumbnail.getWidth() -
-                    (thumbnailInsets.left + thumbnailInsets.right) * scale;
-            final float thumbnailHeight = mThumbnailData.thumbnail.getHeight() -
-                    (thumbnailInsets.top + thumbnailInsets.bottom) * scale;
+            mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
+                    mThumbnailData.thumbnail.getHeight());
+            mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
+                    mActivity.isInMultiWindowMode(), getMeasuredWidth(), getMeasuredHeight());
 
-            final float thumbnailScale;
-            int thumbnailRotation = mThumbnailData.rotation;
-            int currentRotation = ConfigurationCompat.getWindowConfigurationRotation(
-                    getResources().getConfiguration());
-            int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
-            // Landscape vs portrait change
-            boolean windowingModeSupportsRotation = !mActivity.isInMultiWindowMode()
-                    && mThumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
-            isOrientationDifferent = isOrientationChange(deltaRotate)
-                && windowingModeSupportsRotation;
-            if (getMeasuredWidth() == 0) {
-                // If we haven't measured , skip the thumbnail drawing and only draw the background
-                // color
-                thumbnailScale = 0f;
-            } else {
-                // Rotate the screenshot if not in multi-window mode
-                isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
-                // Scale the screenshot to always fit the width of the card.
-
-                thumbnailScale = isOrientationDifferent
-                        ? getMeasuredWidth() / thumbnailHeight
-                        : getMeasuredWidth() / thumbnailWidth;
-            }
-
-            if (!isRotated) {
-                // No Rotation
-                mClippedInsets.offsetTo(thumbnailInsets.left * scale,
-                    thumbnailInsets.top * scale);
-                mMatrix.setTranslate(-mClippedInsets.left, -mClippedInsets.top);
-            } else {
-                setThumbnailRotation(deltaRotate, thumbnailInsets, scale);
-            }
-
-            final float widthWithInsets;
-            final float heightWithInsets;
-            if (isOrientationDifferent) {
-                widthWithInsets = mThumbnailData.thumbnail.getHeight() * thumbnailScale;
-                heightWithInsets = mThumbnailData.thumbnail.getWidth() * thumbnailScale;
-            } else {
-                widthWithInsets = mThumbnailData.thumbnail.getWidth() * thumbnailScale;
-                heightWithInsets = mThumbnailData.thumbnail.getHeight() * thumbnailScale;
-            }
-            mClippedInsets.left *= thumbnailScale;
-            mClippedInsets.top *= thumbnailScale;
-            mClippedInsets.right = widthWithInsets - mClippedInsets.left - getMeasuredWidth();
-            mClippedInsets.bottom = heightWithInsets - mClippedInsets.top - getMeasuredHeight();
-
-            mMatrix.postScale(thumbnailScale, thumbnailScale);
-            mBitmapShader.setLocalMatrix(mMatrix);
-
-            float bitmapHeight = Math.max((isOrientationDifferent ? thumbnailWidth : thumbnailHeight)
-                    * thumbnailScale, 0);
-            if (Math.round(bitmapHeight) < getMeasuredHeight()) {
-                mClipBottom = bitmapHeight;
-            }
+            mBitmapShader.setLocalMatrix(mPreviewPositionHelper.mMatrix);
             mPaint.setShader(mBitmapShader);
         }
-
-        mIsOrientationChanged = isOrientationDifferent;
         invalidate();
 
         // Update can be called from {@link #onSizeChanged} during layout, post handling of overlay
@@ -423,51 +362,6 @@
         post(this::updateOverlay);
     }
 
-    private int getRotationDelta(int oldRotation, int newRotation) {
-        int delta = newRotation - oldRotation;
-        if (delta < 0) delta += 4;
-        return delta;
-    }
-
-    /**
-     * @param deltaRotation the number of 90 degree turns from the current orientation
-     * @return {@code true} if the change in rotation results in a shift from landscape to portrait
-     * or vice versa, {@code false} otherwise
-     */
-    private boolean isOrientationChange(int deltaRotation) {
-        return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
-    }
-
-    private void setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale) {
-        int newLeftInset = 0;
-        int newTopInset = 0;
-        int translateX = 0;
-        int translateY = 0;
-
-        mMatrix.setRotate(90 * deltaRotate);
-        switch (deltaRotate) { /* Counter-clockwise */
-            case Surface.ROTATION_90:
-                newLeftInset = thumbnailInsets.bottom;
-                newTopInset = thumbnailInsets.left;
-                translateX = mThumbnailData.thumbnail.getHeight();
-                break;
-            case Surface.ROTATION_270:
-                newLeftInset = thumbnailInsets.top;
-                newTopInset = thumbnailInsets.right;
-                translateY = mThumbnailData.thumbnail.getWidth();
-                break;
-            case Surface.ROTATION_180:
-                newLeftInset = -thumbnailInsets.top;
-                newTopInset = -thumbnailInsets.left;
-                translateX = mThumbnailData.thumbnail.getWidth();
-                translateY = mThumbnailData.thumbnail.getHeight();
-                break;
-        }
-        mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale);
-        mMatrix.postTranslate(translateX - mClippedInsets.left,
-                translateY - mClippedInsets.top);
-    }
-
     @Override
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
         super.onSizeChanged(w, h, oldw, oldh);
@@ -511,4 +405,158 @@
         }
         return mThumbnailData.thumbnail;
     }
+
+    /**
+     * Utility class to position the thumbnail in the TaskView
+     */
+    public static class PreviewPositionHelper {
+
+        // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
+        private final RectF mClippedInsets = new RectF();
+        private final Matrix mMatrix = new Matrix();
+        private float mClipBottom = -1;
+        private boolean mIsOrientationChanged;
+
+        private final Context mContext;
+
+        public PreviewPositionHelper(Context context) {
+            mContext = context;
+        }
+
+        public int getCurrentRotation() {
+            return ConfigurationCompat.getWindowConfigurationRotation(
+                    mContext.getResources().getConfiguration());
+        }
+
+        public Matrix getMatrix() {
+            return mMatrix;
+        }
+
+        /**
+         * Updates the matrix based on the provided parameters
+         */
+        public void updateThumbnailMatrix(Rect thumbnailPosition, ThumbnailData thumbnailData,
+                boolean isInMultiWindowMode, int canvasWidth, int canvasHeight) {
+            boolean isRotated = false;
+            boolean isOrientationDifferent;
+            mClipBottom = -1;
+
+            float scale = thumbnailData.scale;
+            Rect thumbnailInsets = thumbnailData.insets;
+            final float thumbnailWidth = thumbnailPosition.width()
+                    - (thumbnailInsets.left + thumbnailInsets.right) * scale;
+            final float thumbnailHeight = thumbnailPosition.height()
+                    - (thumbnailInsets.top + thumbnailInsets.bottom) * scale;
+
+            final float thumbnailScale;
+            int thumbnailRotation = thumbnailData.rotation;
+            int currentRotation = getCurrentRotation();
+            int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
+
+            // Landscape vs portrait change
+            boolean windowingModeSupportsRotation = !isInMultiWindowMode
+                    && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
+            isOrientationDifferent = isOrientationChange(deltaRotate)
+                    && windowingModeSupportsRotation;
+            if (canvasWidth == 0) {
+                // If we haven't measured , skip the thumbnail drawing and only draw the background
+                // color
+                thumbnailScale = 0f;
+            } else {
+                // Rotate the screenshot if not in multi-window mode
+                isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
+                // Scale the screenshot to always fit the width of the card.
+                thumbnailScale = isOrientationDifferent
+                        ? canvasWidth / thumbnailHeight
+                        : canvasWidth / thumbnailWidth;
+            }
+
+            if (!isRotated) {
+                // No Rotation
+                mClippedInsets.offsetTo(thumbnailInsets.left * scale,
+                        thumbnailInsets.top * scale);
+                mMatrix.setTranslate(-mClippedInsets.left, -mClippedInsets.top);
+            } else {
+                setThumbnailRotation(deltaRotate, thumbnailInsets, scale, thumbnailPosition);
+            }
+            mMatrix.postTranslate(-thumbnailPosition.left, -thumbnailPosition.top);
+
+            final float widthWithInsets;
+            final float heightWithInsets;
+            if (isOrientationDifferent) {
+                widthWithInsets = thumbnailPosition.height() * thumbnailScale;
+                heightWithInsets = thumbnailPosition.width() * thumbnailScale;
+            } else {
+                widthWithInsets = thumbnailPosition.width() * thumbnailScale;
+                heightWithInsets = thumbnailPosition.height() * thumbnailScale;
+            }
+            mClippedInsets.left *= thumbnailScale;
+            mClippedInsets.top *= thumbnailScale;
+            mClippedInsets.right = widthWithInsets - mClippedInsets.left - canvasWidth;
+            mClippedInsets.bottom = heightWithInsets - mClippedInsets.top - canvasHeight;
+
+            mMatrix.postScale(thumbnailScale, thumbnailScale);
+
+            float bitmapHeight = Math.max(0,
+                    (isOrientationDifferent ? thumbnailWidth : thumbnailHeight) * thumbnailScale);
+            if (Math.round(bitmapHeight) < canvasHeight) {
+                mClipBottom = bitmapHeight;
+            }
+            mIsOrientationChanged = isOrientationDifferent;
+        }
+
+        private int getRotationDelta(int oldRotation, int newRotation) {
+            int delta = newRotation - oldRotation;
+            if (delta < 0) delta += 4;
+            return delta;
+        }
+
+        /**
+         * @param deltaRotation the number of 90 degree turns from the current orientation
+         * @return {@code true} if the change in rotation results in a shift from landscape to
+         * portrait or vice versa, {@code false} otherwise
+         */
+        private boolean isOrientationChange(int deltaRotation) {
+            return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
+        }
+
+        private void setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale,
+                Rect thumbnailPosition) {
+            int newLeftInset = 0;
+            int newTopInset = 0;
+            int translateX = 0;
+            int translateY = 0;
+
+            mMatrix.setRotate(90 * deltaRotate);
+            switch (deltaRotate) { /* Counter-clockwise */
+                case Surface.ROTATION_90:
+                    newLeftInset = thumbnailInsets.bottom;
+                    newTopInset = thumbnailInsets.left;
+                    translateX = thumbnailPosition.height();
+                    break;
+                case Surface.ROTATION_270:
+                    newLeftInset = thumbnailInsets.top;
+                    newTopInset = thumbnailInsets.right;
+                    translateY = thumbnailPosition.width();
+                    break;
+                case Surface.ROTATION_180:
+                    newLeftInset = -thumbnailInsets.top;
+                    newTopInset = -thumbnailInsets.left;
+                    translateX = thumbnailPosition.width();
+                    translateY = thumbnailPosition.height();
+                    break;
+            }
+            mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale);
+            mMatrix.postTranslate(translateX - mClippedInsets.left,
+                    translateY - mClippedInsets.top);
+        }
+
+        /**
+         * Insets to used for clipping the thumbnail (in case it is drawing outside its own space)
+         */
+        public RectF getInsetsToDrawInFullscreen(boolean isMultiWindowMode) {
+            // Don't show insets in multi window mode.
+            return isMultiWindowMode ? EMPTY_RECT_F : mClippedInsets;
+        }
+    }
 }
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 470b720..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
@@ -56,6 +56,7 @@
 import android.widget.Toast;
 
 import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -77,11 +78,11 @@
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
-import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.TaskCornerRadius;
 import com.android.quickstep.views.RecentsView.PageCallbacks;
 import com.android.quickstep.views.RecentsView.ScrollState;
+import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
@@ -157,8 +158,6 @@
     private float mCurveScale;
     private float mFullscreenProgress;
     private final FullscreenDrawParams mCurrentFullscreenParams;
-    private final float mCornerRadius;
-    private final float mWindowCornerRadius;
     private final BaseDraggingActivity mActivity;
 
     private ObjectAnimator mIconAndDimAnimator;
@@ -175,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);
@@ -211,9 +211,8 @@
                     TaskUtils.getLaunchComponentKeyForTask(getTask().key));
             mActivity.getStatsLogManager().log(TASK_LAUNCH_TAP, buildProto());
         });
-        mCornerRadius = TaskCornerRadius.get(context);
-        mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context.getResources());
-        mCurrentFullscreenParams = new FullscreenDrawParams(mCornerRadius);
+
+        mCurrentFullscreenParams = new FullscreenDrawParams(context);
         mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this);
 
         mOutlineProvider = new TaskOutlineProvider(getContext(), mCurrentFullscreenParams);
@@ -236,11 +235,6 @@
         super.onFinishInflate();
         mSnapshotView = findViewById(R.id.snapshot);
         mIconView = findViewById(R.id.icon);
-        final Context context = getContext();
-
-        TaskView.LayoutParams thumbnailParams = (LayoutParams) mSnapshotView.getLayoutParams();
-        thumbnailParams.bottomMargin = LayoutUtils.thumbnailBottomMargin(context);
-        mSnapshotView.setLayoutParams(thumbnailParams);
     }
 
     public boolean isTaskOverlayModal() {
@@ -262,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);
         }
 
@@ -474,7 +474,6 @@
         PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler();
         boolean isRtl = orientationHandler.getRecentsRtlSetting(getResources());
         LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams();
-        snapshotParams.bottomMargin = LayoutUtils.thumbnailBottomMargin(getContext());
         int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
         LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
         int rotation = orientationState.getTouchRotationDegrees();
@@ -482,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;
@@ -492,18 +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;
         }
-        mSnapshotView.setLayoutParams(snapshotParams);
         mIconView.setLayoutParams(iconParams);
         mIconView.setRotation(rotation);
+
+        if (mMenuView != null) {
+            mMenuView.onRotationChanged();
+        }
     }
 
     private void setIconAndDimTransitionProgress(float progress, boolean invert) {
@@ -610,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());
         }
@@ -667,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);
@@ -699,21 +750,16 @@
         return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR;
     }
 
-    public void setCurveScale(float curveScale) {
+    private void setCurveScale(float curveScale) {
         mCurveScale = curveScale;
-        onScaleChanged();
+        setScaleX(mCurveScale);
+        setScaleY(mCurveScale);
     }
 
     public float getCurveScale() {
         return mCurveScale;
     }
 
-    private void onScaleChanged() {
-        float scale = mCurveScale;
-        setScaleX(scale);
-        setScaleY(scale);
-    }
-
     @Override
     public boolean hasOverlappingRendering() {
         // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
@@ -723,13 +769,11 @@
     private static final class TaskOutlineProvider extends ViewOutlineProvider {
 
         private final int mMarginTop;
-        private final int mMarginBottom;
         private FullscreenDrawParams mFullscreenParams;
 
         TaskOutlineProvider(Context context, FullscreenDrawParams fullscreenParams) {
             mMarginTop = context.getResources().getDimensionPixelSize(
                     R.dimen.task_thumbnail_top_margin);
-            mMarginBottom = LayoutUtils.thumbnailBottomMargin(context);
             mFullscreenParams = fullscreenParams;
         }
 
@@ -744,7 +788,7 @@
             outline.setRoundRect(0,
                     (int) (mMarginTop * scale),
                     (int) ((insets.left + view.getWidth() + insets.right) * scale),
-                    (int) ((insets.top + view.getHeight() + insets.bottom - mMarginBottom) * scale),
+                    (int) ((insets.top + view.getHeight() + insets.bottom) * scale),
                     mFullscreenParams.mCurrentDrawnCornerRadius);
         }
     }
@@ -767,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) {
@@ -836,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);
@@ -891,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) {
@@ -917,23 +971,11 @@
         setClipToPadding(!isFullscreen);
 
         TaskThumbnailView thumbnail = getThumbnail();
-        boolean isMultiWindowMode = mActivity.getDeviceProfile().isMultiWindowMode;
-        RectF insets = thumbnail.getInsetsToDrawInFullscreen(isMultiWindowMode);
-        float currentInsetsLeft = insets.left * mFullscreenProgress;
-        float currentInsetsRight = insets.right * mFullscreenProgress;
-        mCurrentFullscreenParams.setInsets(currentInsetsLeft,
-                insets.top * mFullscreenProgress,
-                currentInsetsRight,
-                insets.bottom * mFullscreenProgress);
-        float fullscreenCornerRadius = isMultiWindowMode ? 0 : mWindowCornerRadius;
-        mCurrentFullscreenParams.setCornerRadius(Utilities.mapRange(mFullscreenProgress,
-                mCornerRadius, fullscreenCornerRadius) / getRecentsView().getScaleX());
-        // We scaled the thumbnail to fit the content (excluding insets) within task view width.
-        // Now that we are drawing left/right insets again, we need to scale down to fit them.
-        if (getWidth() > 0) {
-            mCurrentFullscreenParams.setScale(getWidth()
-                    / (getWidth() + currentInsetsLeft + currentInsetsRight));
-        }
+        mCurrentFullscreenParams.setProgress(
+                mFullscreenProgress,
+                getRecentsView().getScaleX(),
+                getWidth(), mActivity.getDeviceProfile(),
+                thumbnail.getPreviewPositionHelper());
 
         if (!getRecentsView().isTaskIconScaledDown(this)) {
             // Some of the items in here are dependent on the current fullscreen params, but don't
@@ -971,26 +1013,51 @@
     /**
      * We update and subsequently draw these in {@link #setFullscreenProgress(float)}.
      */
-    static class FullscreenDrawParams {
-        RectF mCurrentDrawnInsets = new RectF();
-        float mCurrentDrawnCornerRadius;
+    public static class FullscreenDrawParams {
+
+        private final float mCornerRadius;
+        private final float mWindowCornerRadius;
+
+        public RectF mCurrentDrawnInsets = new RectF();
+        public float mCurrentDrawnCornerRadius;
         /** The current scale we apply to the thumbnail to adjust for new left/right insets. */
-        float mScale = 1;
+        public float mScale = 1;
 
-        public FullscreenDrawParams(float cornerRadius) {
-            setCornerRadius(cornerRadius);
+        public FullscreenDrawParams(Context context) {
+            mCornerRadius = TaskCornerRadius.get(context);
+            mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context.getResources());
+
+            mCurrentDrawnCornerRadius = mCornerRadius;
         }
 
-        public void setInsets(float left, float top, float right, float bottom) {
-            mCurrentDrawnInsets.set(left, top, right, bottom);
+        public FullscreenDrawParams() {
+            mCurrentDrawnCornerRadius = mWindowCornerRadius =  mCornerRadius = 0;
         }
 
-        public void setCornerRadius(float cornerRadius) {
-            mCurrentDrawnCornerRadius = cornerRadius;
+        /**
+         * Sets the progress in range [0, 1]
+         */
+        public void setProgress(float fullscreenProgress, float parentScale, int previewWidth,
+                DeviceProfile dp, PreviewPositionHelper pph) {
+            boolean isMultiWindowMode = dp.isMultiWindowMode;
+            RectF insets = pph.getInsetsToDrawInFullscreen(isMultiWindowMode);
+
+            float currentInsetsLeft = insets.left * fullscreenProgress;
+            float currentInsetsRight = insets.right * fullscreenProgress;
+            mCurrentDrawnInsets.set(currentInsetsLeft, insets.top * fullscreenProgress,
+                    currentInsetsRight, insets.bottom * fullscreenProgress);
+            float fullscreenCornerRadius = isMultiWindowMode ? 0 : mWindowCornerRadius;
+
+            mCurrentDrawnCornerRadius =
+                    Utilities.mapRange(fullscreenProgress, mCornerRadius, fullscreenCornerRadius)
+                            / parentScale;
+
+            // We scaled the thumbnail to fit the content (excluding insets) within task view width.
+            // Now that we are drawing left/right insets again, we need to scale down to fit them.
+            if (previewWidth > 0) {
+                mScale = previewWidth / (previewWidth + currentInsetsLeft + currentInsetsRight);
+            }
         }
 
-        public void setScale(float scale) {
-            mScale = scale;
-        }
     }
 }
diff --git a/quickstep/res/layout/overview_actions.xml b/quickstep/res/layout/overview_actions.xml
deleted file mode 100644
index ad5efb6..0000000
--- a/quickstep/res/layout/overview_actions.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<merge xmlns:android="http://schemas.android.com/apk/res/android">
-    <LinearLayout
-        android:id="@+id/action_buttons"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center"
-        android:orientation="horizontal">
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="1dp"
-            android:layout_weight="1" >
-        </Space>
-        <Button
-            android:id="@+id/action_screenshot"
-            style="@style/OverviewActionButton"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:drawableTop="@drawable/ic_screenshot"
-            android:text="@string/action_screenshot" />
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="1dp"
-            android:layout_weight="1" >
-        </Space>
-
-        <Button
-            android:id="@+id/action_share"
-            style="@style/OverviewActionButton"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:drawableTop="@drawable/ic_share"
-            android:text="@string/action_share" />
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="1dp"
-            android:layout_weight="1" >
-        </Space>
-    </LinearLayout>
-
-</merge>
diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml
index 328c20b..e163991 100644
--- a/quickstep/res/layout/overview_actions_container.xml
+++ b/quickstep/res/layout/overview_actions_container.xml
@@ -16,8 +16,48 @@
 -->
 <com.android.quickstep.views.OverviewActionsView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:visibility="gone">
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/overview_actions_height"
+    android:layout_gravity="center_horizontal|bottom"
+    android:layout_marginLeft="@dimen/overview_actions_horizontal_margin"
+    android:layout_marginRight="@dimen/overview_actions_horizontal_margin" >
+
+    <LinearLayout
+        android:id="@+id/action_buttons"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:orientation="horizontal">
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="1" >
+        </Space>
+        <Button
+            android:id="@+id/action_screenshot"
+            style="@style/OverviewActionButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:drawableTop="@drawable/ic_screenshot"
+            android:text="@string/action_screenshot" />
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="1" >
+        </Space>
+
+        <Button
+            android:id="@+id/action_share"
+            style="@style/OverviewActionButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:drawableTop="@drawable/ic_share"
+            android:text="@string/action_share" />
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="1" >
+        </Space>
+    </LinearLayout>
 
 </com.android.quickstep.views.OverviewActionsView>
\ No newline at end of file
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index a688f9a..2b11ca0 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -13,13 +13,11 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<resources xmlns:tools="http://schemas.android.com/tools">
+<resources>
     <string name="task_overlay_factory_class" translatable="false"/>
 
     <!-- Activities which block home gesture -->
-    <string-array name="gesture_blocking_activities" tools:ignore="InconsistentArrays">
-        <item>com.android.launcher3/com.android.quickstep.interaction.GestureSandboxActivity</item>
-    </string-array>
+    <string-array name="gesture_blocking_activities" translatable="false"/>
 
     <string name="stats_log_manager_class" translatable="false">com.android.quickstep.logging.StatsLogCompatManager</string>
 
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 48e25bd..af63a25 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -28,9 +28,7 @@
 import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.os.CancellationSignal;
-import android.view.View;
 
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.WellbeingModel;
@@ -39,7 +37,6 @@
 import com.android.launcher3.proxy.StartActivityParams;
 import com.android.launcher3.statehandlers.BackButtonAlphaHandler;
 import com.android.launcher3.statehandlers.DepthController;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.uioverrides.RecentsViewStateController;
 import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.UiThreadHelper;
@@ -52,6 +49,7 @@
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.RemoteFadeOutAnimationListener;
 import com.android.quickstep.util.ShelfPeekAnim;
+import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -75,7 +73,7 @@
 
     private final ShelfPeekAnim mShelfPeekAnim = new ShelfPeekAnim(this);
 
-    private View mActionsView;
+    private OverviewActionsView mActionsView;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -163,20 +161,20 @@
     @Override
     protected void setupViews() {
         super.setupViews();
-        mActionsView = findViewById(R.id.overview_actions_view);
 
+        SysUINavigationMode.INSTANCE.get(this).updateMode();
+        mActionsView = findViewById(R.id.overview_actions_view);
+        ((RecentsView) getOverviewPanel()).init(mActionsView);
 
         if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(this)) {
             // Overview is above all other launcher elements, including qsb, so move it to the top.
             getOverviewPanel().bringToFront();
-            if (mActionsView != null) {
-                mActionsView.bringToFront();
-            }
+            mActionsView.bringToFront();
         }
     }
 
-    public View getActionsView() {
-        return mActionsView;
+    public <T extends OverviewActionsView> T getActionsView() {
+        return (T) mActionsView;
     }
 
     @Override
@@ -207,17 +205,6 @@
     }
 
     @Override
-    protected ScaleAndTranslation getOverviewScaleAndTranslationForNormalState() {
-        if (SysUINavigationMode.getMode(this) == Mode.NO_BUTTON) {
-            PagedOrientationHandler layoutVertical =
-                ((RecentsView)getOverviewPanel()).getPagedViewOrientedState().getOrientationHandler();
-            return layoutVertical.getScaleAndTranslation(getDeviceProfile(),
-                getOverviewPanel());
-        }
-        return super.getOverviewScaleAndTranslationForNormalState();
-    }
-
-    @Override
     public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) {
         QuickstepAppTransitionManagerImpl appTransitionManager =
                 (QuickstepAppTransitionManagerImpl) getAppTransitionManager();
@@ -240,6 +227,12 @@
     }
 
     @Override
+    public float[] getNormalOverviewScaleAndOffset() {
+        return SysUINavigationMode.getMode(this) == Mode.NO_BUTTON
+                ? new float[] {1, 1} : new float[] {1.1f, 0};
+    }
+
+    @Override
     public void onDragLayerHierarchyChanged() {
         onLauncherStateOrFocusChanged();
     }
diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java
index fbd7a8a..7fb0d43 100644
--- a/quickstep/src/com/android/launcher3/LauncherInitListener.java
+++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java
@@ -23,7 +23,6 @@
 import android.os.CancellationSignal;
 import android.os.Handler;
 
-import com.android.launcher3.util.ActivityTracker;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -45,7 +44,7 @@
     }
 
     @Override
-    public boolean init(Launcher launcher, boolean alreadyOnHome) {
+    public boolean handleInit(Launcher launcher, boolean alreadyOnHome) {
         if (mRemoteAnimationProvider != null) {
             QuickstepAppTransitionManagerImpl appTransitionManager =
                     (QuickstepAppTransitionManagerImpl) launcher.getAppTransitionManager();
@@ -71,7 +70,7 @@
             }, cancellationSignal);
         }
         launcher.deferOverlayCallbacksUntilNextResumeOrStop();
-        return super.init(launcher, alreadyOnHome);
+        return super.handleInit(launcher, alreadyOnHome);
     }
 
     @Override
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/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
index f83737e..5f5d6dc 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
@@ -177,7 +177,14 @@
     }
 
     private void setDepth(float depth) {
-        mDepth = depth;
+        // Round out the depth to dedupe frequent, non-perceptable updates
+        int depthI = (int) (depth * 256);
+        float depthF = depthI / 256f;
+        if (Float.compare(mDepth, depthF) == 0) {
+            return;
+        }
+
+        mDepth = depthF;
         if (mSurface == null || !mSurface.isValid()) {
             return;
         }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index 0f45196..33011ac 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -17,8 +17,6 @@
 package com.android.launcher3.uioverrides;
 
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
@@ -26,23 +24,22 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCRIM_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
 import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_PEEK;
 import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 
 import android.util.FloatProperty;
-import android.view.View;
 
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.graphics.OverviewScrim;
 import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.views.RecentsView;
 
 /**
  * State handler for recents view. Manages UI changes and animations for recents view based off the
@@ -50,7 +47,7 @@
  *
  * @param <T> the recents view
  */
-public abstract class BaseRecentsViewStateController<T extends View>
+public abstract class BaseRecentsViewStateController<T extends RecentsView>
         implements StateHandler {
     protected final T mRecentsView;
     protected final BaseQuickstepLauncher mLauncher;
@@ -62,14 +59,9 @@
 
     @Override
     public void setState(@NonNull LauncherState state) {
-        ScaleAndTranslation scaleAndTranslation = state.getOverviewScaleAndTranslation(mLauncher);
-        float translationX = scaleAndTranslation.translationX;
-        if (mRecentsView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
-            translationX = -translationX;
-        }
-        SCALE_PROPERTY.set(mRecentsView, scaleAndTranslation.scale);
-        mRecentsView.setTranslationX(translationX);
-        mRecentsView.setTranslationY(scaleAndTranslation.translationY);
+        float[] scaleAndOffset = state.getOverviewScaleAndOffset(mLauncher);
+        SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
+        ADJACENT_PAGE_OFFSET.set(mRecentsView, scaleAndOffset[1]);
 
         getContentAlphaProperty().set(mRecentsView, state.overviewUi ? 1f : 0);
         OverviewScrim scrim = mLauncher.getDragLayer().getOverviewScrim();
@@ -98,17 +90,11 @@
      */
     void setStateWithAnimationInternal(@NonNull final LauncherState toState,
             @NonNull StateAnimationConfig config, @NonNull PendingAnimation setter) {
-        ScaleAndTranslation scaleAndTranslation = toState.getOverviewScaleAndTranslation(mLauncher);
-        float translationX = scaleAndTranslation.translationX;
-        if (mRecentsView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
-            translationX = -translationX;
-        }
-        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndTranslation.scale,
+        float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
+        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0],
                 config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
-        setter.setFloat(mRecentsView, VIEW_TRANSLATE_X, translationX,
+        setter.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1],
                 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
-        setter.setFloat(mRecentsView, VIEW_TRANSLATE_Y, scaleAndTranslation.translationY,
-                config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
 
         setter.setFloat(mRecentsView, getContentAlphaProperty(), toState.overviewUi ? 1 : 0,
                 config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index 93e02a1..ea71d97 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -87,7 +87,7 @@
     }
 
     @Override
-    public float getDepth(Context context) {
+    protected float getDepthUnchecked(Context context) {
         return 1f;
     }
 
@@ -102,9 +102,8 @@
     }
 
     @Override
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        float slightParallax = -launcher.getDeviceProfile().allAppsCellHeightPx * 0.3f;
-        return new ScaleAndTranslation(0.9f, 0f, slightParallax);
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return new float[] {0.9f, 0};
     }
 
     @Override
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/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 94ef15a..e4bb9aa 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -135,8 +135,6 @@
 
         void createActivityInterface(long transitionLength);
 
-        default void adjustActivityControllerInterpolators() { }
-
         default void onTransitionCancelled() { }
 
         default void setShelfState(ShelfPeekAnim.ShelfAnimState animState,
diff --git a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
index 495c092..2e99500 100644
--- a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
@@ -22,6 +22,7 @@
 import static android.view.MotionEvent.ACTION_POINTER_DOWN;
 import static android.view.MotionEvent.ACTION_UP;
 
+import static com.android.launcher3.states.RotationHelper.deltaRotation;
 import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
 
 import android.content.res.Resources;
@@ -138,7 +139,8 @@
      * @param info The current displayInfo
      */
     void enableMultipleRegions(boolean enableMultipleRegions, DefaultDisplay.Info info) {
-        mEnableMultipleRegions = enableMultipleRegions;
+        mEnableMultipleRegions = enableMultipleRegions &&
+                mMode != SysUINavigationMode.Mode.TWO_BUTTONS;
         if (!enableMultipleRegions) {
             mQuickStepStartingRotation = -1;
             resetSwipeRegions(info);
@@ -364,16 +366,4 @@
             return false;
         }
     }
-
-    /**
-     * @return how many factors {@param newRotation} is rotated 90 degrees clockwise.
-     * E.g. 1->Rotated by 90 degrees clockwise, 2->Rotated 180 clockwise...
-     * A value of 0 means no rotation has been applied
-     */
-    @SurfaceRotation
-    private static int deltaRotation(int oldRotation, int newRotation) {
-        int delta = newRotation - oldRotation;
-        if (delta < 0) delta += 4;
-        return delta;
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 0a00a61..a6ce2b5 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -21,6 +21,7 @@
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS;
 import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
+import static com.android.quickstep.util.RecentsOrientedState.isFixedRotationTransformEnabled;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
@@ -35,7 +36,6 @@
 import android.app.ActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -51,8 +51,6 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.DefaultDisplay;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
 import com.android.quickstep.util.NavBarPosition;
@@ -176,7 +174,7 @@
     }
 
     private void setupOrientationSwipeHandler() {
-        if (!FeatureFlags.ENABLE_FIXED_ROTATION_TRANSFORM.get()) {
+        if (!isFixedRotationTransformEnabled(mContext)) {
             return;
         }
 
diff --git a/quickstep/src/com/android/quickstep/SysUINavigationMode.java b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
index 375e589..c715c93 100644
--- a/quickstep/src/com/android/quickstep/SysUINavigationMode.java
+++ b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
@@ -26,6 +26,7 @@
 
 import com.android.launcher3.util.MainThreadInitializedObject;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -73,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()) {
@@ -128,6 +134,11 @@
         return getMode(context) != Mode.TWO_BUTTONS;
     }
 
+    public void dump(PrintWriter pw) {
+        pw.println("SysUINavigationMode:");
+        pw.println("  mode=" + mMode.name());
+    }
+
     public interface NavigationModeChangeListener {
 
         void onNavigationModeChanged(Mode newMode);
diff --git a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java b/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
index b1c72ce..dfb8c1d 100644
--- a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
+++ b/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
@@ -28,9 +28,11 @@
 
 public class ActivityInitListener<T extends BaseActivity> implements SchedulerCallback<T> {
 
-    private final BiPredicate<T, Boolean> mOnInitListener;
+    private BiPredicate<T, Boolean> mOnInitListener;
     private final ActivityTracker<T> mActivityTracker;
 
+    private boolean mIsRegistered = false;
+
     /**
      * @param onInitListener a callback made when the activity is initialized. The callback should
      *                       return true to continue receiving callbacks (ie. for if the activity is
@@ -43,27 +45,46 @@
     }
 
     @Override
-    public boolean init(T activity, boolean alreadyOnHome) {
+    public final boolean init(T activity, boolean alreadyOnHome) {
+        if (!mIsRegistered) {
+            return false;
+        }
+        return handleInit(activity, alreadyOnHome);
+    }
+
+    protected boolean handleInit(T activity, boolean alreadyOnHome) {
         return mOnInitListener.test(activity, alreadyOnHome);
     }
 
     /**
      * Registers the activity-created listener. If the activity is already created, then the
      * callback provided in the constructor will be called synchronously.
+     * @param intent The intent that will be used to initialize the activity, if the activity
+     *               doesn't already exist. We add the callback as an extra on this intent.
      */
-    public void register() {
-        mActivityTracker.schedule(this);
+    public void register(Intent intent) {
+        mIsRegistered = true;
+        mActivityTracker.runCallbackWhenActivityExists(this, intent);
     }
 
+    /**
+     * After calling this, we won't {@link #init} even when the activity is ready.
+     */
     public void unregister() {
-        mActivityTracker.clearReference(this);
+        mIsRegistered = false;
+        mOnInitListener = null;
     }
 
+    /**
+     * Starts the given intent with the provided animation. Unlike {@link #register(Intent)}, this
+     * method will not call {@link #init} if the activity already exists, it will only call it when
+     * we get handleIntent() for the provided intent that we're starting.
+     */
     public void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider,
             Context context, Handler handler, long duration) {
-        register();
+        mIsRegistered = true;
 
         Bundle options = animProvider.toActivityOptions(handler, duration, context).toBundle();
-        context.startActivity(addToIntent(new Intent((intent))), options);
+        context.startActivity(addToIntent(new Intent(intent)), options);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index 1f1a999..4edf2fb 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -63,7 +63,7 @@
             if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context)) {
                 //TODO: this needs to account for the swipe gesture height and accessibility
                 // UI when shown.
-                extraSpace = 0;
+                extraSpace = res.getDimensionPixelSize(R.dimen.overview_actions_height);
             } else {
                 extraSpace = getDefaultSwipeHeight(context, dp) + dp.workspacePageIndicatorHeight
                         + res.getDimensionPixelSize(
@@ -75,7 +75,14 @@
     }
 
     public static void calculateFallbackTaskSize(Context context, DeviceProfile dp, Rect outRect) {
-        calculateTaskSize(context, dp, 0, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE, outRect);
+        float extraSpace;
+        if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context)) {
+            extraSpace = context.getResources()
+                    .getDimensionPixelSize(R.dimen.overview_actions_height);
+        } else {
+            extraSpace = 0;
+        }
+        calculateTaskSize(context, dp, extraSpace, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE, outRect);
     }
 
     @AnyThread
@@ -123,8 +130,6 @@
         }
 
         float topIconMargin = res.getDimension(R.dimen.task_thumbnail_top_margin);
-        float bottomMargin = thumbnailBottomMargin(context);
-
         float paddingVert = overviewActionsEnabled && removeShelfFromOverview(context)
                 ? 0 : res.getDimension(R.dimen.task_card_vert_space);
 
@@ -134,7 +139,7 @@
         int launcherVisibleHeight = dp.heightPx - insets.top - insets.bottom;
 
         float availableHeight = launcherVisibleHeight
-                - topIconMargin - extraVerticalSpace - paddingVert - bottomMargin;
+                - topIconMargin - extraVerticalSpace - paddingVert;
         float availableWidth = launcherVisibleWidth - paddingHorz;
 
         float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight);
@@ -144,7 +149,7 @@
         // Center in the visible space
         float x = insets.left + (launcherVisibleWidth - outWidth) / 2;
         float y = insets.top + Math.max(topIconMargin,
-                (launcherVisibleHeight - extraVerticalSpace - outHeight - bottomMargin) / 2);
+                (launcherVisibleHeight - extraVerticalSpace - outHeight) / 2);
         outRect.set(Math.round(x), Math.round(y),
                 Math.round(x) + Math.round(outWidth), Math.round(y) + Math.round(outHeight));
     }
@@ -163,14 +168,16 @@
     }
 
     /**
-     * Get the margin that the task thumbnail view should use.
-     * @return the margin in pixels.
+     * Gets the scale that should be applied to the TaskView so that it matches the target
      */
-    public static int thumbnailBottomMargin(Context context) {
-        if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context)) {
-            return context.getResources().getDimensionPixelSize(R.dimen.overview_actions_height);
+    public static float getTaskScale(RecentsOrientedState orientedState,
+            float srcWidth, float srcHeight, float targetWidth, float targetHeight) {
+        if (orientedState == null
+                || orientedState.isHomeRotationAllowed()
+                || orientedState.isDisplayPhoneNatural()) {
+            return srcWidth / targetWidth;
         } else {
-            return 0;
+            return srcHeight / targetHeight;
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
index ab2484d..a678cb5 100644
--- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
+++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
@@ -18,6 +18,8 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
 
 import android.content.SharedPreferences;
 
@@ -58,7 +60,14 @@
             });
         }
 
-        if (!getBoolean(SHELF_BOUNCE_SEEN)) {
+        boolean shelfBounceSeen = getBoolean(SHELF_BOUNCE_SEEN);
+        if (!shelfBounceSeen && ENABLE_OVERVIEW_ACTIONS.get()
+                && removeShelfFromOverview(launcher)) {
+            // There's no shelf in overview, so don't bounce it (can't get to all apps anyway).
+            shelfBounceSeen = true;
+            mSharedPrefs.edit().putBoolean(SHELF_BOUNCE_SEEN, shelfBounceSeen).apply();
+        }
+        if (!shelfBounceSeen) {
             mStateManager.addStateListener(new StateListener() {
                 @Override
                 public void onStateTransitionStart(LauncherState toState) { }
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index f72e458..74daeca 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -16,25 +16,42 @@
 
 package com.android.quickstep.util;
 
-import static android.hardware.camera2.params.OutputConfiguration.ROTATION_180;
+import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
 import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
 
+import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.database.ContentObserver;
 import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
 import android.graphics.RectF;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.Log;
 import android.view.MotionEvent;
+import android.view.OrientationEventListener;
 import android.view.Surface;
 
 import androidx.annotation.IntDef;
 
-import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.touch.PortraitPagedViewHandler;
 
 import java.lang.annotation.Retention;
+import java.util.function.IntConsumer;
 
 /**
  * Container to hold orientation/rotation related information for Launcher.
@@ -44,8 +61,19 @@
  * This class has initial default state assuming the device and foreground app have
  * no ({@link Surface#ROTATION_0} rotation.
  */
-public final class RecentsOrientedState {
+public final class RecentsOrientedState implements SharedPreferences.OnSharedPreferenceChangeListener {
 
+    private static final String TAG = "RecentsOrientedState";
+    private static final boolean DEBUG = false;
+
+    private static final String FIXED_ROTATION_TRANSFORM_SETTING_NAME = "fixed_rotation_transform";
+
+    private ContentObserver mSystemAutoRotateObserver = new ContentObserver(new Handler()) {
+        @Override
+        public void onChange(boolean selfChange) {
+            updateAutoRotateSetting();
+        }
+    };
     @Retention(SOURCE)
     @IntDef({ROTATION_0, ROTATION_90, ROTATION_180, ROTATION_270})
     public @interface SurfaceRotation {}
@@ -54,15 +82,83 @@
 
     private @SurfaceRotation int mTouchRotation = ROTATION_0;
     private @SurfaceRotation int mDisplayRotation = ROTATION_0;
-    /**
-     * If {@code true} we default to {@link PortraitPagedViewHandler} and don't support any fake
-     * launcher orientations.
-     */
-    private boolean mDisableMultipleOrientations;
+    private @SurfaceRotation int mLauncherRotation = Surface.ROTATION_0;
+
+    // Launcher activity supports multiple orientation, but fallback activity does not
+    private static final int FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY = 1 << 0;
+    // Multiple orientation is only supported if density is < 600
+    private static final int FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY = 1 << 1;
+    // Feature flag controlling the multi-orientation feature
+    private static final int FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_FLAG = 1 << 2;
+    // Shared prefs for rotation, only if activity supports it
+    private static final int FLAG_HOME_ROTATION_ALLOWED_IN_PREFS = 1 << 3;
+    // If the user has enabled system rotation
+    private static final int FLAG_SYSTEM_ROTATION_ALLOWED = 1 << 4;
+    // Whether to rotation sensor is supported on the device
+    private static final int FLAG_ROTATION_WATCHER_SUPPORTED = 1 << 5;
+    // Whether to enable rotation watcher when multi-rotation is supported
+    private static final int FLAG_ROTATION_WATCHER_ENABLED = 1 << 6;
+
+    private static final int MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE =
+            FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY
+            | FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY
+            | FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_FLAG;
+
+    private static final int MASK_ACTIVITY_ROTATING =
+            FLAG_HOME_ROTATION_ALLOWED_IN_PREFS | FLAG_SYSTEM_ROTATION_ALLOWED;
+
+    // State for which rotation watcher will be enabled.
+    // We skip it when home rotation is enabled as in that case, activity itself rotates
+    private static final int VALUE_ROTATION_WATCHER_ENABLED =
+            MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE | FLAG_SYSTEM_ROTATION_ALLOWED
+                    | FLAG_ROTATION_WATCHER_SUPPORTED | FLAG_ROTATION_WATCHER_ENABLED;
+
+    private final ContentResolver mContentResolver;
+    private final SharedPreferences mSharedPrefs;
+    private final OrientationEventListener mOrientationListener;
 
     private final Matrix mTmpMatrix = new Matrix();
     private final Matrix mTmpInverseMatrix = new Matrix();
 
+    private int mFlags;
+    private int mPreviousRotation = ROTATION_0;
+
+    /**
+     * @param rotationChangeListener Callback for receiving rotation events when rotation watcher
+     *                              is enabled
+     * @see #setRotationWatcherEnabled(boolean)
+     */
+    public RecentsOrientedState(Context context, boolean rotationSupportedByActivity,
+            IntConsumer rotationChangeListener) {
+        mContentResolver = context.getContentResolver();
+        mSharedPrefs = Utilities.getPrefs(context);
+        mOrientationListener = new OrientationEventListener(context) {
+            @Override
+            public void onOrientationChanged(int degrees) {
+                int newRotation = getRotationForUserDegreesRotated(degrees);
+                if (newRotation != mPreviousRotation) {
+                    mPreviousRotation = newRotation;
+                    rotationChangeListener.accept(newRotation);
+                }
+            }
+        };
+
+        mFlags = rotationSupportedByActivity ? FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY : 0;
+
+        Resources res = context.getResources();
+        int originalSmallestWidth = res.getConfiguration().smallestScreenWidthDp
+                * res.getDisplayMetrics().densityDpi / DENSITY_DEVICE_STABLE;
+        if (originalSmallestWidth < 600) {
+            mFlags |= FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY;
+        }
+        if (isFixedRotationTransformEnabled(context)) {
+            mFlags |= FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_FLAG;
+        }
+        if (mOrientationListener.canDetectOrientation()) {
+            mFlags |= FLAG_ROTATION_WATCHER_SUPPORTED;
+        }
+    }
+
     /**
      * Sets the appropriate {@link PagedOrientationHandler} for {@link #mOrientationHandler}
      * @param touchRotation The rotation the nav bar region that is touched is in
@@ -72,19 +168,29 @@
      *         false otherwise
      */
     public boolean update(
-            @SurfaceRotation int touchRotation, @SurfaceRotation int displayRotation) {
-        if (!FeatureFlags.ENABLE_FIXED_ROTATION_TRANSFORM.get()) {
+            @SurfaceRotation int touchRotation, @SurfaceRotation int displayRotation,
+            @SurfaceRotation int launcherRotation) {
+        if (!isMultipleOrientationSupportedByDevice()) {
             return false;
         }
-        if (mDisableMultipleOrientations) {
-            return false;
-        }
-        if (mDisplayRotation == displayRotation && mTouchRotation == touchRotation) {
+        if (mDisplayRotation == displayRotation && mTouchRotation == touchRotation
+                && launcherRotation == mLauncherRotation) {
             return false;
         }
 
+        mLauncherRotation = launcherRotation;
         mDisplayRotation = displayRotation;
         mTouchRotation = touchRotation;
+
+        if (canLauncherRotate() || mLauncherRotation == mTouchRotation) {
+            // TODO(b/153476489) Need to determine when launcher is rotated
+            mOrientationHandler = PagedOrientationHandler.HOME_ROTATED;
+            if (DEBUG) {
+                Log.d(TAG, "Set Orientation Handler: " + mOrientationHandler);
+            }
+            return true;
+        }
+
         if (mTouchRotation == ROTATION_90) {
             mOrientationHandler = PagedOrientationHandler.LANDSCAPE;
         } else if (mTouchRotation == ROTATION_270) {
@@ -92,22 +198,81 @@
         } else {
             mOrientationHandler = PagedOrientationHandler.PORTRAIT;
         }
+        if (DEBUG) {
+            Log.d(TAG, "Set Orientation Handler: " + mOrientationHandler);
+        }
         return true;
     }
 
-    public boolean areMultipleLayoutOrientationsDisabled() {
-        return mDisableMultipleOrientations;
+    private void setFlag(int mask, boolean enabled) {
+        boolean wasRotationEnabled = !TestProtocol.sDisableSensorRotation
+                && mFlags == VALUE_ROTATION_WATCHER_ENABLED;
+        if (enabled) {
+            mFlags |= mask;
+        } else {
+            mFlags &= ~mask;
+        }
+
+        boolean isRotationEnabled = !TestProtocol.sDisableSensorRotation
+                && mFlags == VALUE_ROTATION_WATCHER_ENABLED;
+        if (wasRotationEnabled != isRotationEnabled) {
+            UI_HELPER_EXECUTOR.execute(() -> {
+                if (isRotationEnabled) {
+                    mOrientationListener.enable();
+                } else {
+                    mOrientationListener.disable();
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
+        updateHomeRotationSetting();
+    }
+
+    private void updateAutoRotateSetting() {
+        setFlag(FLAG_SYSTEM_ROTATION_ALLOWED, Settings.System.getInt(mContentResolver,
+                Settings.System.ACCELEROMETER_ROTATION, 1) == 1);
+    }
+
+    private void updateHomeRotationSetting() {
+        setFlag(FLAG_HOME_ROTATION_ALLOWED_IN_PREFS,
+                mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, false));
     }
 
     /**
-     * Setting this preference will render future calls to {@link #update(int, int)} as a no-op.
+     * Initializes aany system values and registers corresponding change listeners. It must be
+     * paired with {@link #destroy()} call
      */
-    public void disableMultipleOrientations(boolean disable) {
-        mDisableMultipleOrientations = disable;
-        if (disable) {
-            mDisplayRotation = mTouchRotation = ROTATION_0;
-            mOrientationHandler = PagedOrientationHandler.PORTRAIT;
+    public void init() {
+        if (isMultipleOrientationSupportedByDevice()) {
+            mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
+            mContentResolver.registerContentObserver(
+                    Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
+                    false, mSystemAutoRotateObserver);
         }
+        initWithoutListeners();
+    }
+
+    /**
+     * Unregisters any previously registered listeners.
+     */
+    public void destroy() {
+        if (isMultipleOrientationSupportedByDevice()) {
+            mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
+            mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver);
+        }
+        setRotationWatcherEnabled(false);
+    }
+
+    /**
+     * Initializes the OrientationState without attaching any listeners. This can be used when
+     * the object is short lived.
+     */
+    public void initWithoutListeners() {
+        updateAutoRotateSetting();
+        updateHomeRotationSetting();
     }
 
     @SurfaceRotation
@@ -120,6 +285,31 @@
         return mTouchRotation;
     }
 
+    @SurfaceRotation
+    public int getLauncherRotation() {
+        return mLauncherRotation;
+    }
+
+    public boolean isMultipleOrientationSupportedByDevice() {
+        return (mFlags & MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE)
+                == MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE;
+    }
+
+    public boolean isHomeRotationAllowed() {
+        return (mFlags & FLAG_HOME_ROTATION_ALLOWED_IN_PREFS) != 0;
+    }
+
+    public boolean canLauncherRotate() {
+        return (mFlags & MASK_ACTIVITY_ROTATING) == MASK_ACTIVITY_ROTATING;
+    }
+
+    /**
+     * Enables or disables the rotation watcher for listening to rotation callbacks
+     */
+    public void setRotationWatcherEnabled(boolean isEnabled) {
+        setFlag(FLAG_ROTATION_WATCHER_ENABLED, isEnabled);
+    }
+
     public int getTouchRotationDegrees() {
         switch (mTouchRotation) {
             case ROTATION_90:
@@ -134,6 +324,25 @@
         }
     }
 
+    /**
+     * Returns the scale and pivot so that the provided taskRect can fit the provided full size
+     */
+    public float getFullScreenScaleAndPivot(Rect taskView, DeviceProfile dp, PointF outPivot) {
+        Rect insets = dp.getInsets();
+        float fullWidth = dp.widthPx - insets.left - insets.right;
+        float fullHeight = dp.heightPx - insets.top - insets.bottom;
+        final float scale = LayoutUtils.getTaskScale(this,
+                fullWidth, fullHeight, taskView.width(), taskView.height());
+
+        if (scale == 1) {
+            outPivot.set(fullWidth / 2, fullHeight / 2);
+        } else {
+            float factor = scale / (scale - 1);
+            outPivot.set(taskView.left * factor, taskView.top * factor);
+        }
+        return scale;
+    }
+
     public PagedOrientationHandler getOrientationHandler() {
         return mOrientationHandler;
     }
@@ -166,8 +375,12 @@
     }
 
     public void mapRectFromNormalOrientation(RectF src, int screenWidth, int screenHeight) {
+        mapRectFromRotation(mDisplayRotation, src, screenWidth, screenHeight);
+    }
+
+    public void mapRectFromRotation(int rotation, RectF src, int screenWidth, int screenHeight) {
         mTmpMatrix.reset();
-        postDisplayRotation(mDisplayRotation, screenWidth, screenHeight, mTmpMatrix);
+        postDisplayRotation(rotation, screenWidth, screenHeight, mTmpMatrix);
         mTmpMatrix.mapRect(src);
     }
 
@@ -192,6 +405,10 @@
         }
     }
 
+    public boolean isDisplayPhoneNatural() {
+        return mDisplayRotation == Surface.ROTATION_0 || mDisplayRotation == Surface.ROTATION_180;
+    }
+
     /**
      * Posts the transformation on the matrix representing the provided display rotation
      */
@@ -214,4 +431,13 @@
                 break;
         }
     }
+
+    /**
+     * Returns true if system can keep Launcher fixed to portrait layout even if the
+     * foreground app is rotated
+     */
+    public static boolean isFixedRotationTransformEnabled(Context context) {
+        return Settings.Global.getInt(
+                context.getContentResolver(), FIXED_ROTATION_TRANSFORM_SETTING_NAME, 1) == 1;
+    }
 }
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/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
index 8ecd88a..115294a 100644
--- a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
+++ b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
@@ -17,10 +17,11 @@
 
 import static androidx.test.InstrumentationRegistry.getContext;
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getTargetContext;
 
+import static com.android.launcher3.common.WidgetUtils.createWidgetInfo;
 import static com.android.launcher3.testcomponent.TestCommandReceiver.EXTRA_VALUE;
 import static com.android.launcher3.testcomponent.TestCommandReceiver.SET_LIST_VIEW_SERVICE_BINDER;
-import static com.android.launcher3.ui.widget.BindWidgetTest.createWidgetInfo;
 import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON;
 
 import static org.junit.Assert.assertEquals;
@@ -187,7 +188,7 @@
             LauncherSettings.Settings.call(mResolver,
                     LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
             LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
-            LauncherAppWidgetInfo item = createWidgetInfo(info, true);
+            LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);
 
             addItemToScreen(item);
             assertTrue("Widget is not present",
diff --git a/res/layout/launcher.xml b/res/layout/launcher.xml
index de13277..a137908 100644
--- a/res/layout/launcher.xml
+++ b/res/layout/launcher.xml
@@ -45,12 +45,8 @@
 
         <include
             android:id="@+id/overview_panel"
-            layout="@layout/overview_panel"
-            android:visibility="gone" />
+            layout="@layout/overview_panel" />
 
-        <include
-            android:id="@+id/overview_actions_view"
-            layout="@layout/overview_actions_container" />
 
         <!-- Keep these behind the workspace so that they are not visible when
          we go into AllApps -->
diff --git a/res/layout/overview_panel.xml b/res/layout/overview_panel.xml
index 2637f03..f513688 100644
--- a/res/layout/overview_panel.xml
+++ b/res/layout/overview_panel.xml
@@ -17,4 +17,5 @@
 <Space
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="0dp"
-    android:layout_height="0dp" />
\ No newline at end of file
+    android:layout_height="0dp"
+    android:visibility="gone" />
\ No newline at end of file
diff --git a/res/layout/secondary_launcher.xml b/res/layout/secondary_launcher.xml
index 98cfc34..fdf4446 100644
--- a/res/layout/secondary_launcher.xml
+++ b/res/layout/secondary_launcher.xml
@@ -17,7 +17,8 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:id="@+id/drag_layer" >
+    android:id="@+id/drag_layer"
+    android:padding="@dimen/dynamic_grid_edge_margin">
 
     <GridView
         android:layout_width="match_parent"
@@ -119,8 +120,7 @@
             android:singleLine="true"
             android:textColor="?android:attr/textColorSecondary"
             android:textColorHint="@drawable/all_apps_search_hint"
-            android:textSize="16sp"
-            android:translationY="24dp" />
+            android:textSize="16sp" />
 
         <include layout="@layout/all_apps_fast_scroller" />
     </com.android.launcher3.allapps.AllAppsContainerView>
diff --git a/res/layout/system_shortcut_icon_only.xml b/res/layout/system_shortcut_icon_only.xml
index b8b5b8c..5a81f70 100644
--- a/res/layout/system_shortcut_icon_only.xml
+++ b/res/layout/system_shortcut_icon_only.xml
@@ -19,7 +19,7 @@
     android:layout_width="@dimen/system_shortcut_header_icon_touch_size"
     android:layout_height="@dimen/system_shortcut_header_icon_touch_size"
     android:background="?android:attr/selectableItemBackgroundBorderless"
-    android:tint="?android:attr/textColorHint"
+    android:tint="?attr/iconOnlyShortcutColor"
     android:tintMode="src_in"
     android:padding="@dimen/system_shortcut_header_icon_padding"
     android:theme="@style/PopupItem" />
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index e946835..d4cb6d7 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -34,6 +34,7 @@
     <attr name="workspaceStatusBarScrim" format="reference" />
     <attr name="widgetsTheme" format="reference" />
     <attr name="loadingIconColor" format="color" />
+    <attr name="iconOnlyShortcutColor" format="color"/>
 
     <attr name="folderDotColor" format="color" />
     <attr name="folderFillColor" format="color" />
diff --git a/res/values/config.xml b/res/values/config.xml
index 0657b86..41bb909 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -125,11 +125,11 @@
     <item name="all_apps_spring_damping_ratio" type="dimen" format="float">0.75</item>
     <item name="all_apps_spring_stiffness" type="dimen" format="float">600</item>
 
-    <item name="dismiss_task_trans_y_damping_ratio" type="dimen" format="float">0.5</item>
-    <item name="dismiss_task_trans_y_stiffness" type="dimen" format="float">1500</item>
+    <item name="dismiss_task_trans_y_damping_ratio" type="dimen" format="float">0.73</item>
+    <item name="dismiss_task_trans_y_stiffness" type="dimen" format="float">800</item>
 
-    <item name="dismiss_task_trans_x_damping_ratio" type="dimen" format="float">0.5</item>
-    <item name="dismiss_task_trans_x_stiffness" type="dimen" format="float">1500</item>
+    <item name="dismiss_task_trans_x_damping_ratio" type="dimen" format="float">0.73</item>
+    <item name="dismiss_task_trans_x_stiffness" type="dimen" format="float">800</item>
 
     <item name="horizontal_spring_damping_ratio" type="dimen" format="float">0.8</item>
     <item name="horizontal_spring_stiffness" type="dimen" format="float">400</item>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 0b589a2..31b12ad 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -200,7 +200,7 @@
     <dimen name="system_shortcut_icon_size">24dp</dimen>
     <!-- popup_arrow_center_start - system_shortcut_icon_size / 2 -->
     <dimen name="system_shortcut_margin_start">16dp</dimen>
-    <dimen name="system_shortcut_header_height">40dp</dimen>
+    <dimen name="system_shortcut_header_height">48dp</dimen>
     <dimen name="system_shortcut_header_icon_touch_size">48dp</dimen>
     <!-- (touch_size - icon_size) / 2 -->
     <dimen name="system_shortcut_header_icon_padding">12dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 65e0816..5f4bd8e 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -182,7 +182,7 @@
     <string name="wallpaper_button_text">Wallpapers</string>
     <!-- Text for wallpaper change button -->
     <string name="styles_wallpaper_button_text">Styles &amp; wallpapers</string>
-    <!-- Text for settings button -->
+    <!-- Text for settings button [CHAR LIMIT=20]-->
     <string name="settings_button_text">Home settings</string>
     <!-- Message shown when a feature is disabled by the administrator -->
     <string name="msg_disabled_by_admin">Disabled by your admin</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index fcc651b..a2c0f23 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -30,11 +30,12 @@
     </style>
 
     <style name="LauncherTheme" parent="@style/BaseLauncherTheme">
-        <item name="allAppsScrimColor">#EAFFFFFF</item>
+        <item name="android:textColorSecondary">#DE000000</item>
+        <item name="allAppsScrimColor">#FFFFFFFF</item>
         <item name="allAppsInterimScrimAlpha">46</item>
         <item name="allAppsNavBarScrimColor">#66FFFFFF</item>
         <item name="popupColorPrimary">#FFF</item>
-        <item name="popupColorSecondary">#F5F5F5</item> <!-- Gray 100 -->
+        <item name="popupColorSecondary">#F1F3F4</item>
         <item name="popupColorTertiary">#E0E0E0</item> <!-- Gray 300 -->
         <item name="isMainColorDark">false</item>
         <item name="isWorkspaceDarkText">false</item>
@@ -50,6 +51,7 @@
         <item name="folderTextColor">#FF212121</item>
         <item name="folderHintColor">#FF616161</item>
         <item name="loadingIconColor">#CCFFFFFF</item>
+        <item name="iconOnlyShortcutColor">?android:attr/textColorSecondary</item>
         <item name="workProfileOverlayTextColor">#FF212121</item>
 
         <item name="android:windowTranslucentStatus">false</item>
@@ -88,11 +90,11 @@
         <item name="android:textColorHint">#A0FFFFFF</item>
         <item name="android:colorControlHighlight">#A0FFFFFF</item>
         <item name="android:colorPrimary">#FF212121</item>
-        <item name="allAppsScrimColor">#EA212121</item>
+        <item name="allAppsScrimColor">#FF212121</item>
         <item name="allAppsInterimScrimAlpha">102</item>
         <item name="allAppsNavBarScrimColor">#80000000</item>
         <item name="popupColorPrimary">#3C4043</item> <!-- Gray 800 -->
-        <item name="popupColorSecondary">#5F6368</item> <!-- Gray 700 -->
+        <item name="popupColorSecondary">#202124</item>
         <item name="popupColorTertiary">#757575</item> <!-- Gray 600 -->
         <item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
         <item name="folderDotColor">#FF464646</item>
@@ -102,6 +104,7 @@
         <item name="folderHintColor">#FFCCCCCC</item>
         <item name="isMainColorDark">true</item>
         <item name="loadingIconColor">#99FFFFFF</item>
+        <item name="iconOnlyShortcutColor">#B3FFFFFF</item>
         <item name="workProfileOverlayTextColor">@android:color/white</item>
     </style>
 
diff --git a/robolectric_tests/Android.mk b/robolectric_tests/Android.mk
index bbc62e9..3fa9b0a 100644
--- a/robolectric_tests/Android.mk
+++ b/robolectric_tests/Android.mk
@@ -22,7 +22,10 @@
 LOCAL_MODULE_CLASS := JAVA_LIBRARIES
 
 LOCAL_SDK_VERSION := system_current
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SRC_FILES := \
+	$(call all-java-files-under, src) \
+	$(call all-java-files-under, ../tests/src_common)
+
 LOCAL_STATIC_JAVA_LIBRARIES := \
     androidx.test.runner \
     androidx.test.rules \
diff --git a/robolectric_tests/config/robolectric.properties b/robolectric_tests/config/robolectric.properties
index 0579400..c162255 100644
--- a/robolectric_tests/config/robolectric.properties
+++ b/robolectric_tests/config/robolectric.properties
@@ -1,5 +1,6 @@
 sdk=29
 shadows= \
+    com.android.launcher3.shadows.LShadowApplicationPackageManager \
     com.android.launcher3.shadows.LShadowAppPredictionManager \
     com.android.launcher3.shadows.LShadowAppWidgetManager \
     com.android.launcher3.shadows.LShadowBackupManager \
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/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java b/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java
new file mode 100644
index 0000000..0ca5ce6
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.secondarydisplay;
+
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+import static com.android.launcher3.util.LauncherUIHelper.doLayout;
+import static com.android.launcher3.util.Preconditions.assertNotNull;
+
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+import android.os.UserManager;
+import android.provider.Settings;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.allapps.AllAppsPagedView;
+import com.android.launcher3.allapps.AllAppsRecyclerView;
+import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.shadows.ShadowOverrides;
+import com.android.launcher3.util.LauncherLayoutBuilder;
+import com.android.launcher3.util.LauncherModelHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowUserManager;
+
+/**
+ * Tests for {@link SecondaryDisplayLauncher} with work profile
+ */
+@RunWith(RobolectricTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class SDWorkModeTest {
+
+    private static final int SYSTEM_USER = 0;
+    private static final int FLAG_SYSTEM = 0x00000800;
+    private static final int WORK_PROFILE_ID = 10;
+    private static final int FLAG_PROFILE = 0x00001000;
+
+    private Context mTargetContext;
+    private InvariantDeviceProfile mIdp;
+    private LauncherModelHelper mModelHelper;
+
+    private LauncherLayoutBuilder mLayoutBuilder;
+
+    @Before
+    public void setup() throws Exception {
+        mModelHelper = new LauncherModelHelper();
+        mTargetContext = RuntimeEnvironment.application;
+        mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
+        ShadowOverrides.setProvider(UserEventDispatcher.class,
+                c -> mock(UserEventDispatcher.class));
+        Settings.Global.putFloat(mTargetContext.getContentResolver(),
+                Settings.Global.WINDOW_ANIMATION_SCALE, 0);
+
+        mModelHelper.installApp(TEST_PACKAGE);
+        mLayoutBuilder = new LauncherLayoutBuilder();
+    }
+
+    @Test
+    public void testAllAppsList_noWorkProfile() throws Exception {
+        SecondaryDisplayLauncher launcher = loadLauncher();
+        launcher.showAppDrawer(true);
+        doLayout(launcher);
+
+        verifyRecyclerViewCount(launcher.getAppsView().getActiveRecyclerView());
+    }
+
+    @Test
+    public void testAllAppsList_workProfile() throws Exception {
+        ShadowUserManager sum = Shadow.extract(mTargetContext.getSystemService(UserManager.class));
+        sum.addUser(SYSTEM_USER, "me", FLAG_SYSTEM);
+        sum.addUser(WORK_PROFILE_ID, "work", FLAG_PROFILE);
+
+        SecondaryDisplayLauncher launcher = loadLauncher();
+        launcher.showAppDrawer(true);
+        doLayout(launcher);
+
+        AllAppsRecyclerView rv1 = launcher.getAppsView().getActiveRecyclerView();
+        verifyRecyclerViewCount(rv1);
+
+        assertNotNull(launcher.getAppsView().getWorkModeSwitch());
+        assertTrue(launcher.getAppsView().getRecyclerViewContainer() instanceof AllAppsPagedView);
+
+        AllAppsPagedView pagedView =
+                (AllAppsPagedView) launcher.getAppsView().getRecyclerViewContainer();
+        pagedView.snapToPageImmediately(1);
+        doLayout(launcher);
+
+        AllAppsRecyclerView rv2 = launcher.getAppsView().getActiveRecyclerView();
+        verifyRecyclerViewCount(rv2);
+        assertNotSame(rv1, rv2);
+    }
+
+    private SecondaryDisplayLauncher loadLauncher() throws Exception {
+        // Install 100 apps
+        for (int i = 0; i < 100; i++) {
+            mModelHelper.installApp(TEST_PACKAGE + i);
+        }
+        mModelHelper.setupDefaultLayoutProvider(new LauncherLayoutBuilder()).loadModelSync();
+        SecondaryDisplayLauncher launcher =
+                Robolectric.buildActivity(SecondaryDisplayLauncher.class).setup().get();
+        doLayout(launcher);
+        return launcher;
+    }
+
+    private void verifyRecyclerViewCount(AllAppsRecyclerView rv) {
+        int childCount = rv.getChildCount();
+        assertTrue(childCount > 0);
+        assertTrue(childCount < 100);
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowApplicationPackageManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowApplicationPackageManager.java
new file mode 100644
index 0000000..da8b919
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowApplicationPackageManager.java
@@ -0,0 +1,36 @@
+/*
+ * 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.shadows;
+
+import android.os.Process;
+import android.os.UserHandle;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowApplicationPackageManager;
+
+/**
+ * Shadow for {@link ShadowApplicationPackageManager} which create mock predictors
+ */
+@Implements(className = "android.app.ApplicationPackageManager")
+public class LShadowApplicationPackageManager extends ShadowApplicationPackageManager {
+
+    @Implementation
+    public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) {
+        return Process.myUserHandle().equals(user) ? label : "Work " + label;
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
index 76cb747..6a6f0fb 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
@@ -30,7 +30,6 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ShortcutInfo;
-import android.os.Process;
 import android.os.UserHandle;
 import android.util.ArraySet;
 
@@ -80,14 +79,15 @@
     protected LauncherActivityInfo resolveActivity(Intent intent, UserHandle user) {
         ResolveInfo ri = RuntimeEnvironment.application.getPackageManager()
                 .resolveActivity(intent, 0);
-        return ri == null ? null : getLauncherActivityInfo(ri.activityInfo);
+        return ri == null ? null : getLauncherActivityInfo(ri.activityInfo, user);
     }
 
-    public LauncherActivityInfo getLauncherActivityInfo(ActivityInfo activityInfo) {
+    public LauncherActivityInfo getLauncherActivityInfo(
+            ActivityInfo activityInfo, UserHandle user) {
         return callConstructor(LauncherActivityInfo.class,
                 ClassParameter.from(Context.class, RuntimeEnvironment.application),
                 ClassParameter.from(ActivityInfo.class, activityInfo),
-                ClassParameter.from(UserHandle.class, Process.myUserHandle()));
+                ClassParameter.from(UserHandle.class, user));
     }
 
     @Implementation
@@ -104,7 +104,7 @@
                 .setPackage(packageName);
         return RuntimeEnvironment.application.getPackageManager().queryIntentActivities(intent, 0)
                 .stream()
-                .map(ri -> getLauncherActivityInfo(ri.activityInfo))
+                .map(ri -> getLauncherActivityInfo(ri.activityInfo, user))
                 .collect(Collectors.toList());
     }
 
@@ -130,7 +130,7 @@
         Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT).setPackage(packageName);
         return RuntimeEnvironment.application.getPackageManager().queryIntentActivities(intent, 0)
                 .stream()
-                .map(ri -> getLauncherActivityInfo(ri.activityInfo))
+                .map(ri -> getLauncherActivityInfo(ri.activityInfo, user))
                 .collect(Collectors.toList());
     }
 }
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index f69c8ed..e4f201c 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -181,7 +181,9 @@
                         sourceContainer);
             }
             getUserEventDispatcher().logAppLaunch(v, intent, user);
-            getStatsLogManager().log(APP_LAUNCH_TAP, item.buildProto(null, null));
+
+            getStatsLogManager().log(APP_LAUNCH_TAP, item == null ? null
+                    : item.buildProto(null, null));
             return true;
         } catch (NullPointerException|ActivityNotFoundException|SecurityException e) {
             Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index 41eeb78..8eceec0 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -183,10 +183,6 @@
     public void onScrollStateChanged(int state) {
         super.onScrollStateChanged(state);
 
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onScrollStateChanged: " + state);
-        }
-
         if (state == SCROLL_STATE_IDLE) {
             AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext());
         }
@@ -196,10 +192,6 @@
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(info);
         if (isLayoutSuppressed()) info.setScrollable(false);
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS,
-                    "onInitializeAccessibilityNodeInfo, scrollable: " + info.isScrollable());
-        }
     }
 
     @Override
@@ -207,12 +199,8 @@
         final boolean changing = frozen != isLayoutSuppressed();
         super.setLayoutFrozen(frozen);
         if (changing) {
-            if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-                Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "setLayoutFrozen " + frozen
-                        + " @ " + Log.getStackTraceString(new Throwable()));
-                ActivityContext.lookupContext(getContext()).getDragLayer()
-                        .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED);
-            }
+            ActivityContext.lookupContext(getContext()).getDragLayer()
+                    .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED);
         }
     }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 93247ab..d7d4a27 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -29,6 +29,7 @@
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
@@ -70,7 +71,7 @@
  * too aggressive.
  */
 public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, OnResumeCallback,
-        IconLabelDotView, DraggableView {
+        IconLabelDotView, DraggableView, Reorderable {
 
     private static final int DISPLAY_WORKSPACE = 0;
     private static final int DISPLAY_ALL_APPS = 1;
@@ -78,6 +79,10 @@
 
     private static final int[] STATE_PRESSED = new int[] {android.R.attr.state_pressed};
 
+    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") {
@@ -672,6 +677,45 @@
         return mIconSize;
     }
 
+    private void updateTranslation() {
+        super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x);
+        super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y);
+    }
+
+    public void setReorderBounceOffset(float x, float y) {
+        mTranslationForReorderBounce.set(x, y);
+        updateTranslation();
+    }
+
+    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 getReorderBounceScale() {
+        return mScaleForReorderBounce;
+    }
+
+    public View getView() {
+        return this;
+    }
+
     @Override
     public int getViewType() {
         return DRAGGABLE_ICON;
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 99416c4..ed71ddc 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -33,6 +33,7 @@
 import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
@@ -54,7 +55,6 @@
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.folder.PreviewBackground;
@@ -66,7 +66,6 @@
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.views.ActivityContext;
-import com.android.launcher3.widget.LauncherAppWidgetHostView;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -99,6 +98,7 @@
     // return an (x, y) value from helper functions. Do NOT use them to maintain other state.
     @Thunk final int[] mTmpPoint = new int[2];
     @Thunk final int[] mTempLocation = new int[2];
+    final PointF mTmpPointF = new PointF();
 
     // Used to visualize / debug the Grid of the CellLayout
     private static final boolean VISUALIZE_GRID = false;
@@ -136,7 +136,7 @@
     private final Paint mDragOutlinePaint = new Paint();
 
     @Thunk final ArrayMap<LayoutParams, Animator> mReorderAnimators = new ArrayMap<>();
-    @Thunk final ArrayMap<View, ReorderPreviewAnimation> mShakeAnimators = new ArrayMap<>();
+    @Thunk final ArrayMap<Reorderable, ReorderPreviewAnimation> mShakeAnimators = new ArrayMap<>();
 
     private boolean mItemPlacementDirty = false;
 
@@ -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)) {
@@ -1868,10 +1883,11 @@
             boolean skip = mode == ReorderPreviewAnimation.MODE_HINT && solution.intersectingViews
                     != null && !solution.intersectingViews.contains(child);
 
+
             LayoutParams lp = (LayoutParams) child.getLayoutParams();
-            if (c != null && !skip) {
-                ReorderPreviewAnimation rha = new ReorderPreviewAnimation(child, mode, lp.cellX,
-                        lp.cellY, c.cellX, c.cellY, c.spanX, c.spanY);
+            if (c != null && !skip && (child instanceof Reorderable)) {
+                ReorderPreviewAnimation rha = new ReorderPreviewAnimation((Reorderable) child,
+                        mode, lp.cellX, lp.cellY, c.cellX, c.cellY, c.spanX, c.spanY);
                 rha.animate();
             }
         }
@@ -1893,7 +1909,7 @@
     // Class which represents the reorder preview animations. These animations show that an item is
     // in a temporary state, and hint at where the item will return to.
     class ReorderPreviewAnimation {
-        final View child;
+        final Reorderable child;
         float finalDeltaX;
         float finalDeltaY;
         float initDeltaX;
@@ -1913,8 +1929,8 @@
         float animationProgress = 0;
         ValueAnimator a;
 
-        public ReorderPreviewAnimation(View child, int mode, int cellX0, int cellY0, int cellX1,
-                int cellY1, int spanX, int spanY) {
+        public ReorderPreviewAnimation(Reorderable child, int mode, int cellX0, int cellY0,
+                int cellX1, int cellY1, int spanX, int spanY) {
             regionToCenterPoint(cellX0, cellY0, spanX, spanY, mTmpPoint);
             final int x0 = mTmpPoint[0];
             final int y0 = mTmpPoint[1];
@@ -1926,63 +1942,60 @@
 
             this.child = child;
             this.mode = mode;
+            finalDeltaX = 0;
+            finalDeltaY = 0;
 
-            // TODO issue!
-            setInitialAnimationValues(false);
-            finalScale = (mChildScale - (CHILD_DIVIDEND / child.getWidth())) * initScale;
-            finalDeltaX = initDeltaX;
-            finalDeltaY = initDeltaY;
+            child.getReorderBounceOffset(mTmpPointF);
+            initDeltaX = mTmpPointF.x;
+            initDeltaY = mTmpPointF.y;
+            initScale = child.getReorderBounceScale();
+            finalScale = mChildScale - (CHILD_DIVIDEND / child.getView().getWidth()) * initScale;
+
             int dir = mode == MODE_HINT ? -1 : 1;
             if (dX == dY && dX == 0) {
             } else {
                 if (dY == 0) {
-                    finalDeltaX += - dir * Math.signum(dX) * mReorderPreviewAnimationMagnitude;
+                    finalDeltaX = -dir * Math.signum(dX) * mReorderPreviewAnimationMagnitude;
                 } else if (dX == 0) {
-                    finalDeltaY += - dir * Math.signum(dY) * mReorderPreviewAnimationMagnitude;
+                    finalDeltaY = -dir * Math.signum(dY) * mReorderPreviewAnimationMagnitude;
                 } else {
                     double angle = Math.atan( (float) (dY) / dX);
-                    finalDeltaX += (int) (- dir * Math.signum(dX) *
-                            Math.abs(Math.cos(angle) * mReorderPreviewAnimationMagnitude));
-                    finalDeltaY += (int) (- dir * Math.signum(dY) *
-                            Math.abs(Math.sin(angle) * mReorderPreviewAnimationMagnitude));
+                    finalDeltaX = (int) (-dir * Math.signum(dX)
+                            * Math.abs(Math.cos(angle) * mReorderPreviewAnimationMagnitude));
+                    finalDeltaY = (int) (-dir * Math.signum(dY)
+                            * Math.abs(Math.sin(angle) * mReorderPreviewAnimationMagnitude));
                 }
             }
         }
 
-        void setInitialAnimationValues(boolean restoreOriginalValues) {
-            if (restoreOriginalValues) {
-                if (child instanceof LauncherAppWidgetHostView) {
-                    LauncherAppWidgetHostView lahv = (LauncherAppWidgetHostView) child;
-                    initScale = lahv.getScaleToFit();
-                    initDeltaX = lahv.getTranslationForCentering().x;
-                    initDeltaY = lahv.getTranslationForCentering().y;
-                } else {
-                    initScale = mChildScale;
-                    initDeltaX = 0;
-                    initDeltaY = 0;
-                }
-            } else {
-                initScale = child.getScaleX();
-                initDeltaX = child.getTranslationX();
-                initDeltaY = child.getTranslationY();
-            }
+        void setInitialAnimationValuesToBaseline() {
+            initScale = mChildScale;
+            initDeltaX = 0;
+            initDeltaY = 0;
         }
 
         void animate() {
-            boolean noMovement = (finalDeltaX == initDeltaX) && (finalDeltaY == initDeltaY);
+            boolean noMovement = (finalDeltaX == 0) && (finalDeltaY == 0);
 
             if (mShakeAnimators.containsKey(child)) {
                 ReorderPreviewAnimation oldAnimation = mShakeAnimators.get(child);
-                oldAnimation.cancel();
                 mShakeAnimators.remove(child);
+
                 if (noMovement) {
-                    completeAnimationImmediately();
+                    // A previous animation for this item exists, and no new animation will exist.
+                    // Finish the old animation smoothly.
+                    oldAnimation.finishAnimation();
                     return;
+                } else {
+                    // A previous animation for this item exists, and a new one will exist. Stop
+                    // the old animation in its tracks, and proceed with the new one.
+                    oldAnimation.cancel();
                 }
             }
             if (noMovement) {
                 return;
             }
+
             ValueAnimator va = ObjectAnimator.ofFloat(this, ANIMATION_PROGRESS, 0, 1);
             a = va;
 
@@ -1999,7 +2012,7 @@
             va.addListener(new AnimatorListenerAdapter() {
                 public void onAnimationRepeat(Animator animation) {
                     // We make sure to end only after a full period
-                    setInitialAnimationValues(true);
+                    setInitialAnimationValuesToBaseline();
                     repeating = true;
                 }
             });
@@ -2012,11 +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.setTranslationX(x);
-            child.setTranslationY(y);
+            child.setReorderBounceOffset(x, y);
             float s = animationProgress * finalScale + (1 - animationProgress) * initScale;
-            child.setScaleX(s);
-            child.setScaleY(s);
+            child.setReorderBounceScale(s);
         }
 
         private void cancel() {
@@ -2025,27 +2036,27 @@
             }
         }
 
-        @Thunk void completeAnimationImmediately() {
+        /**
+         * Smoothly returns the item to its baseline position / scale
+         */
+        @Thunk void finishAnimation() {
             if (a != null) {
                 a.cancel();
             }
 
-            setInitialAnimationValues(true);
-            a = new PropertyListBuilder()
-                    .scale(initScale)
-                    .translationX(initDeltaX)
-                    .translationY(initDeltaY)
-                    .build(child)
-                    .setDuration(REORDER_ANIMATION_DURATION);
-            Launcher.cast(mActivity).getDragController().addFirstFrameAnimationHelper(a);
+            setInitialAnimationValuesToBaseline();
+            ValueAnimator va = ObjectAnimator.ofFloat(this, ANIMATION_PROGRESS,
+                    animationProgress, 0);
+            a = va;
             a.setInterpolator(DEACCEL_1_5);
+            a.setDuration(REORDER_ANIMATION_DURATION);
             a.start();
         }
     }
 
     private void completeAndClearReorderPreviewAnimations() {
         for (ReorderPreviewAnimation a: mShakeAnimators.values()) {
-            a.completeAnimationImmediately();
+            a.finishAnimation();
         }
         mShakeAnimators.clear();
     }
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index fc3c9fd..a5f98c0 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -22,7 +22,6 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.util.DisplayMetrics;
 import android.view.Surface;
 
 import com.android.launcher3.CellLayout.ContainerType;
@@ -34,6 +33,7 @@
 public class DeviceProfile {
 
     public final InvariantDeviceProfile inv;
+    private final DefaultDisplay.Info mInfo;
 
     // Device properties
     public final boolean isTablet;
@@ -133,9 +133,9 @@
     public DotRenderer mDotRendererWorkSpace;
     public DotRenderer mDotRendererAllApps;
 
-    public DeviceProfile(Context context, InvariantDeviceProfile inv,
-            Point minSize, Point maxSize,
-            int width, int height, boolean isLandscape, boolean isMultiWindowMode) {
+    public DeviceProfile(Context context, InvariantDeviceProfile inv, DefaultDisplay.Info info,
+            Point minSize, Point maxSize, int width, int height, boolean isLandscape,
+            boolean isMultiWindowMode, boolean transposeLayoutWithOrientation) {
 
         this.inv = inv;
         this.isLandscape = isLandscape;
@@ -152,8 +152,8 @@
             availableHeightPx = maxSize.y;
         }
 
+        mInfo = info;
         Resources res = context.getResources();
-        DisplayMetrics dm = res.getDisplayMetrics();
 
         // Constants from resources
         isTablet = res.getBoolean(R.bool.is_tablet);
@@ -163,8 +163,7 @@
         boolean isTallDevice = Float.compare(aspectRatio, TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0;
 
         // Some more constants
-        transposeLayoutWithOrientation =
-                res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation);
+        this.transposeLayoutWithOrientation = transposeLayoutWithOrientation;
 
         context = getContext(context, isVerticalBarLayout()
                 ? Configuration.ORIENTATION_LANDSCAPE
@@ -207,13 +206,14 @@
                 res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_side_padding);
         // Add a bit of space between nav bar and hotseat in vertical bar layout.
         hotseatBarSidePaddingStartPx = isVerticalBarLayout() ? workspacePageIndicatorHeight : 0;
-        hotseatBarSizePx = ResourceUtils.pxFromDp(inv.iconSize, dm) + (isVerticalBarLayout()
+        hotseatBarSizePx = ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics)
+                + (isVerticalBarLayout()
                 ? (hotseatBarSidePaddingStartPx + hotseatBarSidePaddingEndPx)
                 : (res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size)
                         + hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx));
 
         // Calculate all of the remaining variables.
-        updateAvailableDimensions(dm, res);
+        updateAvailableDimensions(res);
 
         // Now that we have all of the variables calculated, we can tune certain sizes.
         if (!isVerticalBarLayout() && isPhone && isTallDevice) {
@@ -227,7 +227,7 @@
             hotseatBarBottomPaddingPx += extraSpace;
 
             // Recalculate the available dimensions using the new hotseat size.
-            updateAvailableDimensions(dm, res);
+            updateAvailableDimensions(res);
         }
         updateWorkspacePadding();
 
@@ -239,12 +239,21 @@
                         IconShape.DEFAULT_PATH_SIZE);
     }
 
-    public DeviceProfile copy(Context context) {
+    public Builder toBuilder(Context context) {
         Point size = new Point(availableWidthPx, availableHeightPx);
-        return new DeviceProfile(context, inv, size, size, widthPx, heightPx, isLandscape,
-                isMultiWindowMode);
+        return new Builder(context, inv, mInfo)
+                .setSizeRange(size, size)
+                .setSize(widthPx, heightPx)
+                .setMultiWindowMode(isMultiWindowMode);
     }
 
+    public DeviceProfile copy(Context context) {
+        return toBuilder(context).build();
+    }
+
+    /**
+     * TODO: Move this to the builder as part of setMultiWindowMode
+     */
     public DeviceProfile getMultiWindowProfile(Context context, Point mwSize) {
         // We take the minimum sizes of this profile and it's multi-window variant to ensure that
         // the system decor is always excluded.
@@ -253,8 +262,11 @@
         // In multi-window mode, we can have widthPx = availableWidthPx
         // and heightPx = availableHeightPx because Launcher uses the InvariantDeviceProfiles'
         // widthPx and heightPx values where it's needed.
-        DeviceProfile profile = new DeviceProfile(context, inv, mwSize, mwSize, mwSize.x, mwSize.y,
-                isLandscape, true);
+        DeviceProfile profile = toBuilder(context)
+                .setSizeRange(mwSize, mwSize)
+                .setSize(mwSize.x, mwSize.y)
+                .setMultiWindowMode(true)
+                .build();
 
         // If there isn't enough vertical cell padding with the labels displayed, hide the labels.
         float workspaceCellPaddingY = profile.getCellSize().y - profile.iconSizePx
@@ -289,27 +301,30 @@
         iconTextSizePx = 0;
         iconDrawablePaddingPx = 0;
         cellHeightPx = iconSizePx;
+        autoResizeAllAppsCells();
+    }
 
-        // In normal cases, All Apps cell height should equal the Workspace cell height.
-        // Since we are removing labels from the Workspace, we need to manually compute the
-        // All Apps cell height.
+    /**
+     * Re-computes the all-apps cell size to be independent of workspace
+     */
+    public void autoResizeAllAppsCells() {
         int topBottomPadding = allAppsIconDrawablePaddingPx * (isVerticalBarLayout() ? 2 : 1);
         allAppsCellHeightPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx
                 + Utilities.calculateTextHeight(allAppsIconTextSizePx)
                 + topBottomPadding * 2;
     }
 
-    private void updateAvailableDimensions(DisplayMetrics dm, Resources res) {
-        updateIconSize(1f, res, dm);
+    private void updateAvailableDimensions(Resources res) {
+        updateIconSize(1f, res);
 
         // Check to see if the icons fit within the available height.  If not, then scale down.
         float usedHeight = (cellHeightPx * inv.numRows);
         int maxHeight = (availableHeightPx - getTotalWorkspacePadding().y);
         if (usedHeight > maxHeight) {
             float scale = maxHeight / usedHeight;
-            updateIconSize(scale, res, dm);
+            updateIconSize(scale, res);
         }
-        updateAvailableFolderCellDimensions(dm, res);
+        updateAvailableFolderCellDimensions(res);
     }
 
     /**
@@ -317,12 +332,13 @@
      * iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
      * hotseat sizes, workspaceSpringLoadedShrinkFactor, folderIconSizePx, and folderIconOffsetYPx.
      */
-    private void updateIconSize(float scale, Resources res, DisplayMetrics dm) {
+    private void updateIconSize(float scale, Resources res) {
         // Workspace
         final boolean isVerticalLayout = isVerticalBarLayout();
         float invIconSizeDp = isVerticalLayout ? inv.landscapeIconSize : inv.iconSize;
-        iconSizePx = Math.max(1, (int) (ResourceUtils.pxFromDp(invIconSizeDp, dm) * scale));
-        iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, dm) * scale);
+        iconSizePx = Math.max(1, (int) (ResourceUtils.pxFromDp(invIconSizeDp, mInfo.metrics)
+                * scale));
+        iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, mInfo.metrics) * scale);
         iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * scale);
 
         cellHeightPx = iconSizePx + iconDrawablePaddingPx
@@ -340,8 +356,8 @@
 
         // All apps
         if (allAppsHasDifferentNumColumns()) {
-            allAppsIconSizePx = ResourceUtils.pxFromDp(inv.allAppsIconSize, dm);
-            allAppsIconTextSizePx = Utilities.pxFromSp(inv.allAppsIconTextSize, dm);
+            allAppsIconSizePx = ResourceUtils.pxFromDp(inv.allAppsIconSize, mInfo.metrics);
+            allAppsIconTextSizePx = Utilities.pxFromSp(inv.allAppsIconTextSize, mInfo.metrics);
             allAppsCellHeightPx = getCellSize(inv.numAllAppsColumns, inv.numAllAppsColumns).y;
             allAppsIconDrawablePaddingPx = iconDrawablePaddingOriginalPx;
         } else {
@@ -381,12 +397,12 @@
         folderIconOffsetYPx = (iconSizePx - folderIconSizePx) / 2;
     }
 
-    private void updateAvailableFolderCellDimensions(DisplayMetrics dm, Resources res) {
+    private void updateAvailableFolderCellDimensions(Resources res) {
         int folderBottomPanelSize = res.getDimensionPixelSize(R.dimen.folder_label_padding_top)
                 + res.getDimensionPixelSize(R.dimen.folder_label_padding_bottom)
                 + Utilities.calculateTextHeight(res.getDimension(R.dimen.folder_label_text_size));
 
-        updateFolderCellSize(1f, dm, res);
+        updateFolderCellSize(1f, res);
 
         // Don't let the folder get too close to the edges of the screen.
         int folderMargin = edgeMarginPx * 2;
@@ -405,12 +421,12 @@
 
         float scale = Math.min(scaleX, scaleY);
         if (scale < 1f) {
-            updateFolderCellSize(scale, dm, res);
+            updateFolderCellSize(scale, res);
         }
     }
 
-    private void updateFolderCellSize(float scale, DisplayMetrics dm, Resources res) {
-        folderChildIconSizePx = (int) (ResourceUtils.pxFromDp(inv.iconSize, dm) * scale);
+    private void updateFolderCellSize(float scale, Resources res) {
+        folderChildIconSizePx = (int) (ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics) * scale);
         folderChildTextSizePx =
                 (int) (res.getDimensionPixelSize(R.dimen.folder_child_text_size) * scale);
 
@@ -627,4 +643,55 @@
          */
         void onDeviceProfileChanged(DeviceProfile dp);
     }
+
+    public static class Builder {
+        private Context mContext;
+        private InvariantDeviceProfile mInv;
+        private DefaultDisplay.Info mInfo;
+
+        private Point mMinSize, mMaxSize;
+        private int mWidth, mHeight;
+
+        private boolean mIsLandscape;
+        private boolean mIsMultiWindowMode = false;
+        private boolean mTransposeLayoutWithOrientation;
+
+        public Builder(Context context, InvariantDeviceProfile inv, DefaultDisplay.Info info) {
+            mContext = context;
+            mInv = inv;
+            mInfo = info;
+            mTransposeLayoutWithOrientation = context.getResources()
+                    .getBoolean(R.bool.hotseat_transpose_layout_with_orientation);
+        }
+
+        public Builder setSizeRange(Point minSize, Point maxSize) {
+            mMinSize = minSize;
+            mMaxSize = maxSize;
+            return this;
+        }
+
+        public Builder setSize(int width, int height) {
+            mWidth = width;
+            mHeight = height;
+            mIsLandscape = mWidth > mHeight;
+            return this;
+        }
+
+        public Builder setMultiWindowMode(boolean isMultiWindowMode) {
+            mIsMultiWindowMode = isMultiWindowMode;
+            return this;
+        }
+
+        public Builder setTransposeLayoutWithOrientation(boolean transposeLayoutWithOrientation) {
+            mTransposeLayoutWithOrientation = transposeLayoutWithOrientation;
+            return this;
+        }
+
+        public DeviceProfile build() {
+            return new DeviceProfile(mContext, mInv, mInfo, mMinSize, mMaxSize,
+                    mWidth, mHeight, mIsLandscape, mIsMultiWindowMode,
+                    mTransposeLayoutWithOrientation);
+        }
+    }
+
 }
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index d2d0863..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;
@@ -28,6 +29,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -68,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;
@@ -159,10 +164,16 @@
 
     @TargetApi(23)
     private InvariantDeviceProfile(Context context) {
-        String gridName = Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)
-                ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null)
-                : null;
-        initGrid(context, gridName);
+        String gridName = getCurrentGridName(context);
+        String newGridName = initGrid(context, gridName);
+        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);
         mOverlayMonitor = new OverlayMonitor(context);
@@ -178,17 +189,36 @@
         }
     }
 
-/**
+    /**
      * This constructor should NOT have any monitors by design.
      */
     public InvariantDeviceProfile(Context context, Display display) {
-        initGrid(context, null, new Info(display));
+        // Ensure that the main device profile is initialized
+        InvariantDeviceProfile originalProfile = INSTANCE.get(context);
+        String gridName = getCurrentGridName(context);
+
+        // Get the display info based on default display and interpolate it to existing display
+        DisplayOption defaultDisplayOption = invDistWeightedInterpolate(
+                DefaultDisplay.INSTANCE.get(context).getInfo(),
+                getPredefinedDeviceProfiles(context, gridName));
+
+        Info myInfo = new Info(display);
+        DisplayOption myDisplayOption = invDistWeightedInterpolate(
+                myInfo, getPredefinedDeviceProfiles(context, gridName));
+
+        DisplayOption result = new DisplayOption(defaultDisplayOption.grid)
+                .add(myDisplayOption);
+        result.iconSize = defaultDisplayOption.iconSize;
+        result.landscapeIconSize = defaultDisplayOption.landscapeIconSize;
+        result.allAppsIconSize = Math.min(
+                defaultDisplayOption.allAppsIconSize, myDisplayOption.allAppsIconSize);
+        initGrid(context, myInfo, result);
     }
 
     public static String getCurrentGridName(Context context) {
-        return Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)
-                ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null)
-                : null;
+        SharedPreferences prefs = Utilities.getPrefs(context);
+        return prefs.getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)
+                ? prefs.getString(KEY_IDP_GRID_NAME, null) : null;
     }
 
     /**
@@ -203,27 +233,17 @@
     }
 
     private String initGrid(Context context, String gridName) {
-        return initGrid(context, gridName, DefaultDisplay.INSTANCE.get(context).getInfo());
+        DefaultDisplay.Info displayInfo = DefaultDisplay.INSTANCE.get(context).getInfo();
+        ArrayList<DisplayOption> allOptions = getPredefinedDeviceProfiles(context, gridName);
+
+        DisplayOption displayOption = invDistWeightedInterpolate(displayInfo, allOptions);
+        initGrid(context, displayInfo, displayOption);
+        return displayOption.grid.name;
     }
 
-    private String initGrid(Context context, String gridName, DefaultDisplay.Info displayInfo) {
-        Point smallestSize = new Point(displayInfo.smallestSize);
-        Point largestSize = new Point(displayInfo.largestSize);
-
-        ArrayList<DisplayOption> allOptions = getPredefinedDeviceProfiles(context, gridName);
-        // This guarantees that width < height
-        float minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y),
-                displayInfo.metrics);
-        float minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y),
-                displayInfo.metrics);
-        // Sort the profiles based on the closeness to the device size
-        Collections.sort(allOptions, (a, b) ->
-                Float.compare(dist(minWidthDps, minHeightDps, a.minWidthDps, a.minHeightDps),
-                        dist(minWidthDps, minHeightDps, b.minWidthDps, b.minHeightDps)));
-        DisplayOption interpolatedDisplayOption =
-                invDistWeightedInterpolate(minWidthDps,  minHeightDps, allOptions);
-
-        GridOption closestProfile = allOptions.get(0).grid;
+    private void initGrid(
+            Context context, DefaultDisplay.Info displayInfo, DisplayOption displayOption) {
+        GridOption closestProfile = displayOption.grid;
         numRows = closestProfile.numRows;
         numColumns = closestProfile.numColumns;
         numHotseatIcons = closestProfile.numHotseatIcons;
@@ -236,21 +256,16 @@
 
         mExtraAttrs = closestProfile.extraAttrs;
 
-        if (!closestProfile.name.equals(gridName)) {
-            Utilities.getPrefs(context).edit()
-                    .putString(KEY_IDP_GRID_NAME, closestProfile.name).apply();
-        }
-
-        iconSize = interpolatedDisplayOption.iconSize;
+        iconSize = displayOption.iconSize;
         iconShapePath = getIconShapePath(context);
-        landscapeIconSize = interpolatedDisplayOption.landscapeIconSize;
+        landscapeIconSize = displayOption.landscapeIconSize;
         iconBitmapSize = ResourceUtils.pxFromDp(iconSize, displayInfo.metrics);
-        iconTextSize = interpolatedDisplayOption.iconTextSize;
+        iconTextSize = displayOption.iconTextSize;
         fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
 
         if (Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)) {
-            allAppsIconSize = interpolatedDisplayOption.allAppsIconSize;
-            allAppsIconTextSize = interpolatedDisplayOption.allAppsIconTextSize;
+            allAppsIconSize = displayOption.allAppsIconSize;
+            allAppsIconTextSize = displayOption.allAppsIconTextSize;
         } else {
             allAppsIconSize = iconSize;
             allAppsIconTextSize = iconTextSize;
@@ -266,10 +281,12 @@
         int smallSide = Math.min(realSize.x, realSize.y);
         int largeSide = Math.max(realSize.x, realSize.y);
 
-        landscapeProfile = new DeviceProfile(context, this, smallestSize, largestSize,
-                largeSide, smallSide, true /* isLandscape */, false /* isMultiWindowMode */);
-        portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize,
-                smallSide, largeSide, false /* isLandscape */, false /* isMultiWindowMode */);
+        DeviceProfile.Builder builder = new DeviceProfile.Builder(context, this, displayInfo)
+                .setSizeRange(new Point(displayInfo.smallestSize),
+                        new Point(displayInfo.largestSize));
+
+        landscapeProfile = builder.setSize(largeSide, smallSide).build();
+        portraitProfile = builder.setSize(smallSide, largeSide).build();
 
         // We need to ensure that there is enough extra space in the wallpaper
         // for the intended parallax effects
@@ -283,8 +300,6 @@
 
         ComponentName cn = new ComponentName(context.getPackageName(), getClass().getName());
         defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null);
-
-        return closestProfile.name;
     }
 
     @Nullable
@@ -454,6 +469,41 @@
     }
 
     @VisibleForTesting
+    static DisplayOption invDistWeightedInterpolate(
+            DefaultDisplay.Info displayInfo, ArrayList<DisplayOption> points) {
+        Point smallestSize = new Point(displayInfo.smallestSize);
+        Point largestSize = new Point(displayInfo.largestSize);
+
+        // This guarantees that width < height
+        float width = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y),
+                displayInfo.metrics);
+        float height = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y),
+                displayInfo.metrics);
+
+        // Sort the profiles based on the closeness to the device size
+        Collections.sort(points, (a, b) ->
+                Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
+                        dist(width, height, b.minWidthDps, b.minHeightDps)));
+
+        GridOption closestOption = points.get(0).grid;
+        float weights = 0;
+
+        DisplayOption p = points.get(0);
+        if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) {
+            return p;
+        }
+
+        DisplayOption out = new DisplayOption(closestOption);
+        for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) {
+            p = points.get(i);
+            float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);
+            weights += w;
+            out.add(new DisplayOption().add(p).multiply(w));
+        }
+        return out.multiply(1.0f / weights);
+    }
+
+    @VisibleForTesting
     static DisplayOption invDistWeightedInterpolate(float width, float height,
             ArrayList<DisplayOption> points) {
         float weights = 0;
@@ -573,7 +623,6 @@
     private static final class DisplayOption {
         private final GridOption grid;
 
-        private final String name;
         private final float minWidthDps;
         private final float minHeightDps;
         private final boolean canBeDefault;
@@ -590,7 +639,6 @@
             TypedArray a = context.obtainStyledAttributes(
                     attrs, R.styleable.ProfileDisplayOption);
 
-            name = a.getString(R.styleable.ProfileDisplayOption_name);
             minWidthDps = a.getFloat(R.styleable.ProfileDisplayOption_minWidthDps, 0);
             minHeightDps = a.getFloat(R.styleable.ProfileDisplayOption_minHeightDps, 0);
             canBeDefault = a.getBoolean(
@@ -609,8 +657,11 @@
         }
 
         DisplayOption() {
-            grid = null;
-            name = null;
+            this(null);
+        }
+
+        DisplayOption(GridOption grid) {
+            this.grid = grid;
             minWidthDps = 0;
             minHeightDps = 0;
             canBeDefault = false;
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index bf05a24..c4eab8f 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -25,6 +25,8 @@
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.NO_OFFSET;
+import static com.android.launcher3.LauncherState.NO_SCALE;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_PEEK;
 import static com.android.launcher3.Utilities.postAsyncCallback;
@@ -85,7 +87,6 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.allapps.AllAppsContainerView;
@@ -2698,10 +2699,6 @@
         return new TouchController[] {getDragController(), new AllAppsSwipeController(this)};
     }
 
-    protected ScaleAndTranslation getOverviewScaleAndTranslationForNormalState() {
-        return new ScaleAndTranslation(1.1f, 0f, 0f);
-    }
-
     public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) { }
 
     public void onDragLayerHierarchyChanged() { }
@@ -2724,6 +2721,14 @@
         return Stream.of(APP_INFO, WIDGETS, INSTALL);
     }
 
+
+    /**
+     * @see LauncherState#getOverviewScaleAndOffset(Launcher)
+     */
+    public float[] getNormalOverviewScaleAndOffset() {
+        return new float[] {NO_SCALE, NO_OFFSET};
+    }
+
     public static Launcher getLauncher(Context context) {
         return fromContext(context);
     }
@@ -2735,6 +2740,7 @@
         return (T) activityContext;
     }
 
+
     /**
      * Callback for listening for onResume
      */
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 8d20bd6..308d84f 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,
@@ -430,7 +431,8 @@
                                     InvariantDeviceProfile.INSTANCE.get(getContext()).dbFile,
                                     Favorites.TMP_TABLE,
                                     () -> mOpenHelper,
-                                    () -> DatabaseHelper.createDatabaseHelper(getContext())));
+                                    () -> DatabaseHelper.createDatabaseHelper(
+                                            getContext(), true /* forMigration */)));
                     return result;
                 }
             }
@@ -441,7 +443,8 @@
                             prepForMigration(
                                     arg /* dbFile */,
                                     Favorites.PREVIEW_TABLE_NAME,
-                                    () -> DatabaseHelper.createDatabaseHelper(getContext(), arg),
+                                    () -> DatabaseHelper.createDatabaseHelper(
+                                            getContext(), arg, true /* forMigration */),
                                     () -> mOpenHelper));
                     return result;
                 }
@@ -609,20 +612,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 +648,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,7 +676,9 @@
 
             // Fresh and clean launcher DB.
             mMaxItemId = initializeMaxItemId(db);
-            onEmptyDbCreated();
+            if (!mForMigration) {
+                onEmptyDbCreated();
+            }
         }
 
         protected void onAddOrDeleteOp(SQLiteDatabase db) {
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 504666a..54d8f0d 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -90,6 +90,9 @@
     protected static final int FLAG_HIDE_BACK_BUTTON = 1 << 8;
     protected static final int FLAG_HAS_SYS_UI_SCRIM = 1 << 9;
 
+    public static final float NO_OFFSET = 0;
+    public static final float NO_SCALE = 1;
+
     protected static final PageAlphaProvider DEFAULT_ALPHA_PROVIDER =
             new PageAlphaProvider(ACCEL_2) {
                 @Override
@@ -220,7 +223,7 @@
     public abstract int getTransitionDuration(Launcher launcher);
 
     public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
-        return new ScaleAndTranslation(1, 0, 0);
+        return new ScaleAndTranslation(NO_SCALE, NO_OFFSET, NO_OFFSET);
     }
 
     public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) {
@@ -228,12 +231,18 @@
         return getWorkspaceScaleAndTranslation(launcher);
     }
 
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        return launcher.getOverviewScaleAndTranslationForNormalState();
+    /**
+     * Returns an array of two elements.
+     *   The first specifies the scale for the overview
+     *   The second is the factor ([0, 1], 0 => center-screen; 1 => offscreen) by which overview
+     *   should be shifted horizontally.
+     */
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return launcher.getNormalOverviewScaleAndOffset();
     }
 
     public ScaleAndTranslation getQsbScaleAndTranslation(Launcher launcher) {
-        return new ScaleAndTranslation(1, 0, 0);
+        return new ScaleAndTranslation(NO_SCALE, NO_OFFSET, NO_OFFSET);
     }
 
     public float getOverviewFullscreenProgress() {
@@ -276,7 +285,14 @@
      *
      * 0 means completely zoomed in, without blurs. 1 is zoomed out, with blurs.
      */
-    public float getDepth(Context context) {
+    public final float getDepth(Context context) {
+        if (BaseDraggingActivity.fromContext(context).getDeviceProfile().isMultiWindowMode) {
+            return 0;
+        }
+        return getDepthUnchecked(context);
+    }
+
+    protected float getDepthUnchecked(Context context) {
         return 0f;
     }
 
diff --git a/src/com/android/launcher3/Reorderable.java b/src/com/android/launcher3/Reorderable.java
new file mode 100644
index 0000000..047fb01
--- /dev/null
+++ b/src/com/android/launcher3/Reorderable.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+import android.graphics.PointF;
+import android.view.View;
+
+public interface Reorderable {
+
+    /**
+     * Set the offset related to reorder hint and bounce animations
+     */
+    void setReorderBounceOffset(float x, float y);
+
+    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 setReorderBounceScale(float scale);
+    float getReorderBounceScale();
+
+    /**
+     * Get the com.android.view related to this object
+     */
+    View getView();
+}
diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
index 3ee1293..f97eb28 100644
--- a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
+++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
@@ -15,206 +15,90 @@
  */
 package com.android.launcher3.allapps;
 
-import com.android.launcher3.util.Thunk;
+import androidx.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
-import java.util.HashSet;
-import java.util.List;
+import com.android.launcher3.allapps.AlphabeticalAppsList.FastScrollSectionInfo;
 
-import androidx.recyclerview.widget.RecyclerView;
+public class AllAppsFastScrollHelper {
 
-public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallback {
+    private static final int NO_POSITION = -1;
 
-    private static final int INITIAL_TOUCH_SETTLING_DURATION = 100;
-    private static final int REPEAT_TOUCH_SETTLING_DURATION = 200;
+    private int mTargetFastScrollPosition = NO_POSITION;
 
     private AllAppsRecyclerView mRv;
-    private AlphabeticalAppsList mApps;
+    private ViewHolder mLastSelectedViewHolder;
 
-    // Keeps track of the current and targeted fast scroll section (the section to scroll to after
-    // the initial delay)
-    int mTargetFastScrollPosition = -1;
-    @Thunk String mCurrentFastScrollSection;
-    @Thunk String mTargetFastScrollSection;
-
-    // The settled states affect the delay before the fast scroll animation is applied
-    private boolean mHasFastScrollTouchSettled;
-    private boolean mHasFastScrollTouchSettledAtLeastOnce;
-
-    // Set of all views animated during fast scroll.  We keep track of these ourselves since there
-    // is no way to reset a view once it gets scrapped or recycled without other hacks
-    private HashSet<RecyclerView.ViewHolder> mTrackedFastScrollViews = new HashSet<>();
-
-    // Smooth fast-scroll animation frames
-    @Thunk int mFastScrollFrameIndex;
-    @Thunk final int[] mFastScrollFrames = new int[10];
-
-    /**
-     * This runnable runs a single frame of the smooth scroll animation and posts the next frame
-     * if necessary.
-     */
-    @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
-        @Override
-        public void run() {
-            if (mFastScrollFrameIndex < mFastScrollFrames.length) {
-                mRv.scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
-                mFastScrollFrameIndex++;
-                mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
-            }
-        }
-    };
-
-    /**
-     * This runnable updates the current fast scroll section to the target fastscroll section.
-     */
-    Runnable mFastScrollToTargetSectionRunnable = new Runnable() {
-        @Override
-        public void run() {
-            // Update to the target section
-            mCurrentFastScrollSection = mTargetFastScrollSection;
-            mHasFastScrollTouchSettled = true;
-            mHasFastScrollTouchSettledAtLeastOnce = true;
-            updateTrackedViewsFastScrollFocusState();
-        }
-    };
-
-    public AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps) {
+    public AllAppsFastScrollHelper(AllAppsRecyclerView rv) {
         mRv = rv;
-        mApps = apps;
-    }
-
-    public void onSetAdapter(AllAppsGridAdapter adapter) {
-        adapter.setBindViewCallback(this);
     }
 
     /**
      * Smooth scrolls the recycler view to the given section.
-     *
-     * @return whether the fastscroller can scroll to the new section.
      */
-    public boolean smoothScrollToSection(int scrollY, int availableScrollHeight,
-            AlphabeticalAppsList.FastScrollSectionInfo info) {
-        if (mTargetFastScrollPosition != info.fastScrollToItem.position) {
-            mTargetFastScrollPosition = info.fastScrollToItem.position;
-            smoothSnapToPosition(scrollY, availableScrollHeight, info);
-            return true;
+    public void smoothScrollToSection(FastScrollSectionInfo info) {
+        if (mTargetFastScrollPosition == info.fastScrollToItem.position) {
+            return;
         }
-        return false;
-    }
-
-    /**
-     * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
-     * ourselves and animating the scroll on the recycler view.
-     */
-    private void smoothSnapToPosition(int scrollY, int availableScrollHeight,
-            AlphabeticalAppsList.FastScrollSectionInfo info) {
-        mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
-        mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
-
-        trackAllChildViews();
-        if (mHasFastScrollTouchSettled) {
-            // In this case, the user has already settled once (and the fast scroll state has
-            // animated) and they are just fine-tuning their section from the last section, so
-            // we should make it feel fast and update immediately.
-            mCurrentFastScrollSection = info.sectionName;
-            mTargetFastScrollSection = null;
-            updateTrackedViewsFastScrollFocusState();
-        } else {
-            // Otherwise, the user has scrubbed really far, and we don't want to distract the user
-            // with the flashing fast scroll state change animation in addition to the fast scroll
-            // section popup, so reset the views to normal, and wait for the touch to settle again
-            // before animating the fast scroll state.
-            mCurrentFastScrollSection = null;
-            mTargetFastScrollSection = info.sectionName;
-            mHasFastScrollTouchSettled = false;
-            updateTrackedViewsFastScrollFocusState();
-
-            // Delay scrolling to a new section until after some duration.  If the user has been
-            // scrubbing a while and makes multiple big jumps, then reduce the time needed for the
-            // fast scroll to settle so it doesn't feel so long.
-            mRv.postDelayed(mFastScrollToTargetSectionRunnable,
-                    mHasFastScrollTouchSettledAtLeastOnce ?
-                            REPEAT_TOUCH_SETTLING_DURATION :
-                            INITIAL_TOUCH_SETTLING_DURATION);
-        }
-
-        // Calculate the full animation from the current scroll position to the final scroll
-        // position, and then run the animation for the duration.  If we are scrolling to the
-        // first fast scroll section, then just scroll to the top of the list itself.
-        List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
-                mApps.getFastScrollerSections();
-        int newPosition = info.fastScrollToItem.position;
-        int newScrollY = fastScrollSections.size() > 0 && fastScrollSections.get(0) == info
-                        ? 0
-                        : Math.min(availableScrollHeight, mRv.getCurrentScrollY(newPosition, 0));
-        int numFrames = mFastScrollFrames.length;
-        int deltaY = newScrollY - scrollY;
-        float ySign = Math.signum(deltaY);
-        int step = (int) (ySign * Math.ceil((float) Math.abs(deltaY) / numFrames));
-        for (int i = 0; i < numFrames; i++) {
-            // TODO(winsonc): We can interpolate this as well.
-            mFastScrollFrames[i] = (int) (ySign * Math.min(Math.abs(step), Math.abs(deltaY)));
-            deltaY -= step;
-        }
-        mFastScrollFrameIndex = 0;
-        mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
+        mTargetFastScrollPosition = info.fastScrollToItem.position;
+        mRv.getLayoutManager().startSmoothScroll(new MyScroller(mTargetFastScrollPosition));
     }
 
     public void onFastScrollCompleted() {
-        // TODO(winsonc): Handle the case when the user scrolls and releases before the animation
-        //                runs
-
-        // Stop animating the fast scroll position and state
-        mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
-        mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
-
-        // Reset the tracking variables
-        mHasFastScrollTouchSettled = false;
-        mHasFastScrollTouchSettledAtLeastOnce = false;
-        mCurrentFastScrollSection = null;
-        mTargetFastScrollSection = null;
-        mTargetFastScrollPosition = -1;
-
-        updateTrackedViewsFastScrollFocusState();
-        mTrackedFastScrollViews.clear();
+        mTargetFastScrollPosition = NO_POSITION;
+        setLastHolderSelected(false);
+        mLastSelectedViewHolder = null;
     }
 
-    @Override
-    public void onBindView(AllAppsGridAdapter.ViewHolder holder) {
-        // Update newly bound views to the current fast scroll state if we are fast scrolling
-        if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) {
-            mTrackedFastScrollViews.add(holder);
+
+    private void setLastHolderSelected(boolean isSelected) {
+        if (mLastSelectedViewHolder != null) {
+            mLastSelectedViewHolder.itemView.setActivated(isSelected);
+            mLastSelectedViewHolder.setIsRecyclable(!isSelected);
         }
     }
 
-    /**
-     * Starts tracking all the recycler view's children which are FastScrollFocusableViews.
-     */
-    private void trackAllChildViews() {
-        int childCount = mRv.getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder(mRv.getChildAt(i));
-            if (viewHolder != null) {
-                mTrackedFastScrollViews.add(viewHolder);
-            }
-        }
-    }
+    private class MyScroller extends LinearSmoothScroller {
 
-    /**
-     * Updates the fast scroll focus on all the children.
-     */
-    private void updateTrackedViewsFastScrollFocusState() {
-        for (RecyclerView.ViewHolder viewHolder : mTrackedFastScrollViews) {
-            int pos = viewHolder.getAdapterPosition();
-            boolean isActive = false;
-            if (mCurrentFastScrollSection != null
-                    && pos > RecyclerView.NO_POSITION
-                    && pos < mApps.getAdapterItems().size()) {
-                AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos);
-                isActive = item != null &&
-                        mCurrentFastScrollSection.equals(item.sectionName) &&
-                        item.position == mTargetFastScrollPosition;
+        private final int mTargetPosition;
+
+        public MyScroller(int targetPosition) {
+            super(mRv.getContext());
+
+            mTargetPosition = targetPosition;
+            setTargetPosition(targetPosition);
+        }
+
+        @Override
+        protected int getVerticalSnapPreference() {
+            return SNAP_TO_START;
+        }
+
+        @Override
+        protected void onStop() {
+            super.onStop();
+            if (mTargetPosition != mTargetFastScrollPosition) {
+                // Target changed, before the last scroll can finish
+                return;
             }
-            viewHolder.itemView.setActivated(isActive);
+
+            ViewHolder currentHolder = mRv.findViewHolderForAdapterPosition(mTargetPosition);
+            if (currentHolder == mLastSelectedViewHolder) {
+                return;
+            }
+
+            setLastHolderSelected(false);
+            mLastSelectedViewHolder = currentHolder;
+            setLastHolderSelected(true);
+        }
+
+        @Override
+        protected void onStart() {
+            super.onStart();
+            if (mTargetPosition != mTargetFastScrollPosition) {
+                setLastHolderSelected(false);
+                mLastSelectedViewHolder = null;
+            }
         }
     }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index 4aebec0..3afa756 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -71,11 +71,6 @@
     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER;
     public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON;
 
-
-    public interface BindViewCallback {
-        void onBindView(ViewHolder holder);
-    }
-
     /**
      * ViewHolder for each icon.
      */
@@ -186,7 +181,6 @@
 
     private int mAppsPerRow;
 
-    private BindViewCallback mBindViewCallback;
     private OnFocusChangeListener mIconFocusListener;
 
     // The text to show when there are no search results and no market search handler.
@@ -248,13 +242,6 @@
     }
 
     /**
-     * Sets the callback for when views are bound.
-     */
-    public void setBindViewCallback(BindViewCallback cb) {
-        mBindViewCallback = cb;
-    }
-
-    /**
      * Returns the grid layout manager.
      */
     public GridLayoutManager getLayoutManager() {
@@ -319,9 +306,6 @@
                 // nothing to do
                 break;
         }
-        if (mBindViewCallback != null) {
-            mBindViewCallback.onBindView(holder);
-        }
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 069472f..cbf02b7 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -53,12 +53,12 @@
 public class AllAppsRecyclerView extends BaseRecyclerView implements LogContainerProvider {
 
     private AlphabeticalAppsList mApps;
-    private AllAppsFastScrollHelper mFastScrollHelper;
     private final int mNumAppsPerRow;
 
     // The specific view heights that we use to calculate scroll
-    private SparseIntArray mViewHeights = new SparseIntArray();
-    private SparseIntArray mCachedScrollPositions = new SparseIntArray();
+    private final SparseIntArray mViewHeights = new SparseIntArray();
+    private final SparseIntArray mCachedScrollPositions = new SparseIntArray();
+    private final AllAppsFastScrollHelper mFastScrollHelper;
 
     // The empty-search result background
     private AllAppsBackgroundDrawable mEmptySearchBackground;
@@ -85,6 +85,7 @@
         mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize(
                 R.dimen.all_apps_empty_search_bg_top_offset);
         mNumAppsPerRow = LauncherAppState.getIDP(context).numColumns;
+        mFastScrollHelper = new AllAppsFastScrollHelper(this);
     }
 
     /**
@@ -92,7 +93,6 @@
      */
     public void setApps(AlphabeticalAppsList apps) {
         mApps = apps;
-        mFastScrollHelper = new AllAppsFastScrollHelper(this, apps);
     }
 
     public AlphabeticalAppsList getApps() {
@@ -221,9 +221,6 @@
             return "";
         }
 
-        // Stop the scroller if it is scrolling
-        stopScroll();
-
         // Find the fastscroll section that maps to this touch fraction
         List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
                 mApps.getFastScrollerSections();
@@ -236,10 +233,7 @@
             lastInfo = info;
         }
 
-        // Update the fast scroll
-        int scrollY = getCurrentScrollY();
-        int availableScrollHeight = getAvailableScrollHeight();
-        mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo);
+        mFastScrollHelper.smoothScrollToSection(lastInfo);
         return lastInfo.sectionName;
     }
 
@@ -257,7 +251,6 @@
                 mCachedScrollPositions.clear();
             }
         });
-        mFastScrollHelper.onSetAdapter((AllAppsGridAdapter) adapter);
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/DiscoveryBounce.java b/src/com/android/launcher3/allapps/DiscoveryBounce.java
index fc29a30..0648682 100644
--- a/src/com/android/launcher3/allapps/DiscoveryBounce.java
+++ b/src/com/android/launcher3/allapps/DiscoveryBounce.java
@@ -172,8 +172,7 @@
         if (withDelay) {
             new Handler().postDelayed(() -> showForOverviewIfNeeded(launcher, false), DELAY_MS);
             return;
-        } else if (Launcher.ACTIVITY_TRACKER.hasPending()
-                || AbstractFloatingView.getTopOpenView(launcher) != null) {
+        } else if (AbstractFloatingView.getTopOpenView(launcher) != null) {
             // TODO: Move these checks to the top and call this method after invalidate handler.
             return;
         }
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
index 33262b6..6692af5 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
 
 import android.content.Context;
+import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.graphics.Rect;
 import android.os.AsyncTask;
@@ -30,7 +31,6 @@
 import android.widget.Switch;
 
 import com.android.launcher3.Insettable;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.pm.UserCache;
@@ -79,9 +79,8 @@
 
     @Override
     public void toggle() {
-        Launcher launcher = Launcher.getLauncher(getContext());
         // don't show tip if user uses toggle
-        launcher.getSharedPrefs().edit().putInt(KEY_WORK_TIP_COUNTER, -1).apply();
+        Utilities.getPrefs(getContext()).edit().putInt(KEY_WORK_TIP_COUNTER, -1).apply();
         trySetQuietModeEnabledToAllProfilesAsync(isChecked());
     }
 
@@ -203,9 +202,8 @@
     }
 
     private boolean shouldShowWorkSwitch() {
-        Launcher launcher = Launcher.getLauncher(getContext());
-        return Utilities.ATLEAST_P && (hasShortcutsPermission(launcher)
-                || launcher.checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
+        return Utilities.ATLEAST_P && (hasShortcutsPermission(getContext())
+                || getContext().checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
                 == PackageManager.PERMISSION_GRANTED);
     }
 
@@ -213,12 +211,14 @@
      * Shows a work tip on the Nth work tab open
      */
     public void showTipifNeeded() {
-        Launcher launcher = Launcher.getLauncher(getContext());
-        int tipCounter = launcher.getSharedPrefs().getInt(KEY_WORK_TIP_COUNTER, WORK_TIP_THRESHOLD);
+        Context context = getContext();
+        SharedPreferences prefs = Utilities.getPrefs(context);
+        int tipCounter = prefs.getInt(KEY_WORK_TIP_COUNTER, WORK_TIP_THRESHOLD);
         if (tipCounter < 0) return;
         if (tipCounter == 0) {
-            new ArrowTipView(launcher).show(launcher.getString(R.string.work_switch_tip), getTop());
+            new ArrowTipView(context)
+                    .show(context.getString(R.string.work_switch_tip), getTop());
         }
-        launcher.getSharedPrefs().edit().putInt(KEY_WORK_TIP_COUNTER, tipCounter - 1).apply();
+        prefs.edit().putInt(KEY_WORK_TIP_COUNTER, tipCounter - 1).apply();
     }
 }
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index f12789a..e11917b 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -27,7 +27,6 @@
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.content.Context;
-import android.util.FloatProperty;
 
 import androidx.annotation.Nullable;
 
@@ -64,19 +63,6 @@
         return new AnimatorPlaybackController(anim, duration, childAnims);
     }
 
-    private static final FloatProperty<ValueAnimator> CURRENT_PLAY_TIME =
-            new FloatProperty<ValueAnimator>("current-play-time") {
-                @Override
-                public void setValue(ValueAnimator animator, float v) {
-                    animator.setCurrentPlayTime((long) v);
-                }
-
-                @Override
-                public Float get(ValueAnimator animator) {
-                    return (float) animator.getCurrentPlayTime();
-                }
-            };
-
     // Progress factor after which an animation is considered almost completed.
     private static final float ANIMATION_COMPLETE_THRESHOLD = 0.95f;
 
@@ -177,21 +163,22 @@
         long springDuration = animationDuration;
         for (Holder h : mChildAnimations) {
             if ((h.springProperty.flags & springFlag) != 0) {
-                SpringAnimationBuilder s = new SpringAnimationBuilder(h.anim, CURRENT_PLAY_TIME)
-                        .setStartValue(clampDuration(mCurrentFraction))
-                        .setEndValue(goingToEnd ? h.anim.getDuration() : 0)
-                        .setStartVelocity(scaledVelocity * h.anim.getDuration())
+                SpringAnimationBuilder s = new SpringAnimationBuilder(context)
+                        .setStartValue(mCurrentFraction)
+                        .setEndValue(goingToEnd ? 1 : 0)
+                        .setStartVelocity(scaledVelocity)
                         .setMinimumVisibleChange(scaleInverse)
                         .setDampingRatio(h.springProperty.mDampingRatio)
-                        .setStiffness(h.springProperty.mStiffness);
+                        .setStiffness(h.springProperty.mStiffness)
+                        .computeParams();
 
-                long expectedDurationL = s.build(context).getDuration();
+                long expectedDurationL = s.getDuration();
                 springDuration = Math.max(expectedDurationL, springDuration);
 
                 float expectedDuration = expectedDurationL;
-                h.setter = (a, l) ->
-                    s.setValue(a, mAnimationPlayer.getCurrentPlayTime() / expectedDuration);
-                h.anim.setInterpolator(LINEAR);
+                h.setter = (a, l) -> a.setCurrentFraction(
+                        mAnimationPlayer.getCurrentPlayTime() / expectedDuration);
+                h.anim.setInterpolator(s::getInterpolatedValue);
             }
         }
 
diff --git a/src/com/android/launcher3/anim/SpringAnimationBuilder.java b/src/com/android/launcher3/anim/SpringAnimationBuilder.java
index f22a9f0..bc77aab 100644
--- a/src/com/android/launcher3/anim/SpringAnimationBuilder.java
+++ b/src/com/android/launcher3/anim/SpringAnimationBuilder.java
@@ -15,7 +15,9 @@
  */
 package com.android.launcher3.anim;
 
-import android.animation.ObjectAnimator;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.util.FloatProperty;
 
@@ -28,10 +30,9 @@
  * Utility class to build an object animator which follows the same path as a spring animation for
  * an underdamped spring.
  */
-public class SpringAnimationBuilder<T> extends FloatProperty<T> {
+public class SpringAnimationBuilder {
 
-    private final T mTarget;
-    private final FloatProperty<T> mProperty;
+    private final Context mContext;
 
     private float mStartValue;
     private float mEndValue;
@@ -64,27 +65,23 @@
     private double mValueThreshold;
     private double mVelocityThreshold;
 
-    private float mCurrentTime = 0;
+    private float mDuration = 0;
 
-    public SpringAnimationBuilder(T target, FloatProperty<T> property) {
-        super("dynamic-spring-property");
-        mTarget = target;
-        mProperty = property;
-
-        mStartValue = mProperty.get(target);
+    public SpringAnimationBuilder(Context context) {
+        mContext = context;
     }
 
-    public SpringAnimationBuilder<T> setEndValue(float value) {
+    public SpringAnimationBuilder setEndValue(float value) {
         mEndValue = value;
         return this;
     }
 
-    public SpringAnimationBuilder<T> setStartValue(float value) {
+    public SpringAnimationBuilder setStartValue(float value) {
         mStartValue = value;
         return this;
     }
 
-    public SpringAnimationBuilder<T> setValues(float... values) {
+    public SpringAnimationBuilder setValues(float... values) {
         if (values.length > 1) {
             mStartValue = values[0];
             mEndValue = values[values.length - 1];
@@ -94,7 +91,7 @@
         return this;
     }
 
-    public SpringAnimationBuilder<T> setStiffness(
+    public SpringAnimationBuilder setStiffness(
             @FloatRange(from = 0.0, fromInclusive = false) float stiffness) {
         if (stiffness <= 0) {
             throw new IllegalArgumentException("Spring stiffness constant must be positive.");
@@ -103,7 +100,7 @@
         return this;
     }
 
-    public SpringAnimationBuilder<T> setDampingRatio(
+    public SpringAnimationBuilder setDampingRatio(
             @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
                     float dampingRatio) {
         if (dampingRatio <= 0 || dampingRatio >= 1) {
@@ -113,7 +110,7 @@
         return this;
     }
 
-    public SpringAnimationBuilder<T> setMinimumVisibleChange(
+    public SpringAnimationBuilder setMinimumVisibleChange(
             @FloatRange(from = 0.0, fromInclusive = false) float minimumVisibleChange) {
         if (minimumVisibleChange <= 0) {
             throw new IllegalArgumentException("Minimum visible change must be positive.");
@@ -122,25 +119,21 @@
         return this;
     }
 
-    public SpringAnimationBuilder<T> setStartVelocity(float startVelocity) {
+    public SpringAnimationBuilder setStartVelocity(float startVelocity) {
         mVelocity = startVelocity;
         return this;
     }
 
-    @Override
-    public void setValue(T object, float time) {
-        mCurrentTime = time;
-        mProperty.setValue(
-                object, (float) (exponentialComponent(time) * cosSinX(time)) + mEndValue);
+    public float getInterpolatedValue(float fraction) {
+        return getValue(mDuration * fraction);
     }
 
-    @Override
-    public Float get(T t) {
-        return mCurrentTime;
+    private float getValue(float time) {
+        return (float) (exponentialComponent(time) * cosSinX(time)) + mEndValue;
     }
 
-    public ObjectAnimator build(Context context) {
-        int singleFrameMs = DefaultDisplay.getSingleFrameMs(context);
+    public SpringAnimationBuilder computeParams() {
+        int singleFrameMs = DefaultDisplay.getSingleFrameMs(mContext);
         double naturalFreq = Math.sqrt(mStiffness);
         double dampedFreq = naturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio);
 
@@ -187,12 +180,21 @@
             }
         } while (true);
 
+        mDuration = (float) duration;
+        return this;
+    }
 
-        long durationMs = (long) (1000.0 * duration);
-        ObjectAnimator animator = ObjectAnimator.ofFloat(mTarget, this, 0, (float) duration);
-        animator.setDuration(durationMs).setInterpolator(Interpolators.LINEAR);
-        animator.addListener(AnimationSuccessListener.forRunnable(
-                () -> mProperty.setValue(mTarget, mEndValue)));
+    public long getDuration() {
+        return (long) (1000.0 * mDuration);
+    }
+
+    public <T> ValueAnimator build(T target, FloatProperty<T> property) {
+        computeParams();
+
+        ValueAnimator animator = ValueAnimator.ofFloat(0, mDuration);
+        animator.setDuration(getDuration()).setInterpolator(LINEAR);
+        animator.addUpdateListener(anim ->
+                property.set(target, getInterpolatedValue(anim.getAnimatedFraction())));
         return animator;
     }
 
diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
index 737c97b..1d32d1d 100644
--- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
+++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
@@ -75,9 +75,6 @@
     }
 
     public static void sendScrollFinishedEventToTest(Context context) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "sendScrollFinishedEventToTest");
-        }
         final AccessibilityManager accessibilityManager = getAccessibilityManagerForTest(context);
         if (accessibilityManager == null) return;
 
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index bcd91da..65d3cd2 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -159,15 +159,12 @@
             "ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS", false,
             "Always use hardware optimization for folder animations.");
 
-    public static final BooleanFlag ENABLE_FIXED_ROTATION_TRANSFORM = getDebugFlag(
-            FLAG_ENABLE_FIXED_ROTATION_TRANSFORM, true,
-            "Launch/close apps without rotation animation. Fix Launcher to portrait");
-
     public static void initialize(Context context) {
         synchronized (sDebugFlags) {
             for (DebugFlag flag : sDebugFlags) {
                 flag.initialize(context);
             }
+            sDebugFlags.sort((f1, f2) -> f1.key.compareToIgnoreCase(f2.key));
         }
     }
 
@@ -178,10 +175,20 @@
     }
 
     public static void dump(PrintWriter pw) {
-        pw.println("FeatureFlags:");
+        pw.println("DeviceFlags:");
         synchronized (sDebugFlags) {
             for (DebugFlag flag : sDebugFlags) {
-                pw.println("  " + flag.key + "=" + flag.get());
+                if (flag instanceof DeviceFlag) {
+                    pw.println("  " + flag.toString());
+                }
+            }
+        }
+        pw.println("DebugFlags:");
+        synchronized (sDebugFlags) {
+            for (DebugFlag flag : sDebugFlags) {
+                if (!(flag instanceof DeviceFlag)) {
+                    pw.println("  " + flag.toString());
+                }
             }
         }
     }
@@ -202,13 +209,11 @@
 
         @Override
         public String toString() {
-            return appendProps(new StringBuilder()
-                    .append(getClass().getSimpleName()).append('{'))
-                    .append('}').toString();
+            return appendProps(new StringBuilder()).toString();
         }
 
         protected StringBuilder appendProps(StringBuilder src) {
-            return src.append("key=").append(key).append(", defaultValue=").append(defaultValue);
+            return src.append(key).append(", defaultValue=").append(defaultValue);
         }
 
         public void addChangeListener(Context context, Runnable r) { }
@@ -240,8 +245,7 @@
 
         @Override
         protected StringBuilder appendProps(StringBuilder src) {
-            return super.appendProps(src).append(", mCurrentValue=").append(mCurrentValue)
-                    .append(", description=").append(description);
+            return super.appendProps(src).append(", mCurrentValue=").append(mCurrentValue);
         }
     }
 
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index 6c40b8a..0df6713 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -170,16 +170,14 @@
             }
         }, null, View.DRAG_FLAG_GLOBAL);
 
-
-        Intent homeIntent = listener.addToIntent(
-                new Intent(Intent.ACTION_MAIN)
+        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
                         .addCategory(Intent.CATEGORY_HOME)
                         .setPackage(getPackageName())
-                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
-
-        Launcher.ACTIVITY_TRACKER.schedule(listener);
+                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        Launcher.ACTIVITY_TRACKER.runCallbackWhenActivityExists(listener, homeIntent);
         startActivity(homeIntent,
-                ActivityOptions.makeCustomAnimation(this, 0, android.R.anim.fade_out).toBundle());
+                ActivityOptions.makeCustomAnimation(this, 0, android.R.anim.fade_out)
+                        .toBundle());
         mFinishOnPause = true;
         return false;
     }
diff --git a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
index e47a16f..707fd06 100644
--- a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
@@ -161,7 +161,6 @@
     }
 
     protected void postCleanup() {
-        Launcher.ACTIVITY_TRACKER.clearReference(this);
         if (mLauncher != null) {
             // Remove any drag params from the launcher intent since the drag operation is complete.
             Intent newIntent = new Intent(mLauncher.getIntent());
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 4fe1d1a..c287190 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -164,6 +164,7 @@
     @Thunk final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
 
     private AnimatorSet mCurrentAnimator;
+    private boolean mIsAnimatingClosed = false;
 
     protected final Launcher mLauncher;
     protected DragController mDragController;
@@ -331,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;
@@ -729,15 +730,24 @@
     }
 
     private void animateClosed() {
+        if (mIsAnimatingClosed) {
+            return;
+        }
         if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
             mCurrentAnimator.cancel();
         }
         AnimatorSet a = new FolderAnimationManager(this, false /* isOpening */).getAnimator();
         a.addListener(new AnimatorListenerAdapter() {
             @Override
+            public void onAnimationStart(Animator animation) {
+                mIsAnimatingClosed = true;
+            }
+
+            @Override
             public void onAnimationEnd(Animator animation) {
                 closeComplete(true);
                 announceAccessibilityChanges();
+                mIsAnimatingClosed = false;
             }
         });
         startAnimation(a);
@@ -1440,7 +1450,7 @@
             if (hasFocus) {
                 startEditingFolderName();
             } else {
-                logEditFolderLabel();
+                logCurrentFolderLabelState();
                 mFolderName.dispatchBackKey();
             }
         }
@@ -1639,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 e29971e..b875a0b 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -26,6 +26,7 @@
 import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.graphics.Canvas;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
@@ -49,6 +50,7 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.OnAlarmListener;
 import com.android.launcher3.R;
+import com.android.launcher3.Reorderable;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.anim.Interpolators;
@@ -79,7 +81,7 @@
  * An icon that can appear on in the workspace representing an {@link Folder}.
  */
 public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView,
-        DraggableView {
+        DraggableView, Reorderable {
 
     @Thunk ActivityContext mActivity;
     @Thunk Folder mFolder;
@@ -119,6 +121,10 @@
     private float mDotScale;
     private Animator mDotScaleAnim;
 
+    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") {
         @Override
@@ -226,16 +232,6 @@
         mBackground.getBounds(outBounds);
     }
 
-    @Override
-    public int getViewType() {
-        return DRAGGABLE_ICON;
-    }
-
-    @Override
-    public void getVisualDragBounds(Rect bounds) {
-        getPreviewBounds(bounds);
-    }
-
     public float getBackgroundStrokeWidth() {
         return mBackground.getStrokeWidth();
     }
@@ -421,6 +417,7 @@
             mPreviewItemManager.hidePreviewItem(finalIndex, false);
             mFolder.showItem(item);
             setLabelSuggestion(nameInfos);
+            mFolder.logCurrentFolderLabelState();
             invalidate();
         }, DROP_IN_ANIMATION_DURATION);
     }
@@ -443,7 +440,6 @@
         onTitleChanged(mInfo.title);
         mFolder.mFolderName.setText(mInfo.title);
         mFolder.mLauncher.getModelWriter().updateItemInDatabase(mInfo);
-        // TODO: Add logging while folder creation.
     }
 
 
@@ -716,4 +712,53 @@
     public void onFolderClose(int currentPage) {
         mPreviewItemManager.onFolderClose(currentPage);
     }
+
+    private void updateTranslation() {
+        super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x);
+        super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y);
+    }
+
+    public void setReorderBounceOffset(float x, float y) {
+        mTranslationForReorderBounce.set(x, y);
+        updateTranslation();
+    }
+
+    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 getReorderBounceScale() {
+        return mScaleForReorderBounce;
+    }
+
+    public View getView() {
+        return this;
+    }
+
+    @Override
+    public int getViewType() {
+        return DRAGGABLE_ICON;
+    }
+
+    @Override
+    public void getVisualDragBounds(Rect bounds) {
+        getPreviewBounds(bounds);
+    }
 }
diff --git a/src/com/android/launcher3/folder/FolderNameInfo.java b/src/com/android/launcher3/folder/FolderNameInfo.java
index 1287219..1841cd9 100644
--- a/src/com/android/launcher3/folder/FolderNameInfo.java
+++ b/src/com/android/launcher3/folder/FolderNameInfo.java
@@ -50,6 +50,10 @@
         return mLabel;
     }
 
+    public double getScore() {
+        return mScore;
+    }
+
     /**
      * Used to package this object into a {@link Parcel}.
      *
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/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index f1f271f..dd6fc49 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -89,17 +89,16 @@
         if (mDragLayer != null) {
             return;
         }
-        InvariantDeviceProfile mainIdp = LauncherAppState.getIDP(this);
         InvariantDeviceProfile currentDisplayIdp =
                 new InvariantDeviceProfile(this, getWindow().getDecorView().getDisplay());
 
-        // Pick the device profile with the smaller icon size so that the cached icons are
-        // shown properly
-        if (mainIdp.iconBitmapSize <= currentDisplayIdp.iconBitmapSize) {
-            mDeviceProfile = mainIdp.getDeviceProfile(this).copy(this);
-        } else {
-            mDeviceProfile = currentDisplayIdp.getDeviceProfile(this);
-        }
+        // Disable transpose layout and use multi-window mode so that the icons are scaled properly
+        mDeviceProfile = currentDisplayIdp.getDeviceProfile(this)
+                .toBuilder(this)
+                .setMultiWindowMode(true)
+                .setTransposeLayoutWithOrientation(false)
+                .build();
+        mDeviceProfile.autoResizeAllAppsCells();
 
         setContentView(R.layout.secondary_launcher);
         mDragLayer = findViewById(R.id.drag_layer);
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
index e35e884..40630d3 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
@@ -117,10 +117,12 @@
             if (child == mAppsView) {
                 int padding = 2 * (grid.desiredWorkspaceLeftRightMarginPx
                         + grid.cellLayoutPaddingLeftRightPx);
-                int maxWidth = grid.allAppsCellWidthPx * idp.numAllAppsColumns + padding;
 
+                int maxWidth = grid.allAppsCellWidthPx * idp.numAllAppsColumns + padding;
                 int appsWidth = Math.min(width, maxWidth);
-                int appsHeight = Math.round(appsWidth * (float) height / (float) width);
+
+                int maxHeight = grid.allAppsCellHeightPx * idp.numAllAppsColumns + padding;
+                int appsHeight = Math.min(height, maxHeight);
 
                 mAppsView.measure(
                         makeMeasureSpec(appsWidth, EXACTLY), makeMeasureSpec(appsHeight, EXACTLY));
diff --git a/src/com/android/launcher3/states/RotationHelper.java b/src/com/android/launcher3/states/RotationHelper.java
index 2e0521f..5a60f53 100644
--- a/src/com/android/launcher3/states/RotationHelper.java
+++ b/src/com/android/launcher3/states/RotationHelper.java
@@ -15,47 +15,43 @@
  */
 package com.android.launcher3.states;
 
-import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
 
-import static com.android.launcher3.config.FeatureFlags.FLAG_ENABLE_FIXED_ROTATION_TRANSFORM;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.os.Handler;
 import android.provider.Settings;
+import android.util.Log;
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.UiThreadHelper;
 
-import java.util.ArrayList;
-import java.util.List;
-
 /**
  * Utility class to manage launcher rotation
  */
 public class RotationHelper implements OnSharedPreferenceChangeListener {
 
+    private static final String TAG = "RotationHelper";
+
     public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation";
 
-    public static final String FIXED_ROTATION_TRANSFORM_SETTING_NAME = "fixed_rotation_transform";
     private final ContentResolver mContentResolver;
+    private boolean mSystemAutoRotateEnabled;
 
-    /**
-     * Listener to receive changes when {@link #FIXED_ROTATION_TRANSFORM_SETTING_NAME} flag changes.
-     */
-    public interface ForcedRotationChangedListener {
-        void onForcedRotationChanged(boolean isForcedRotation);
-    }
+    private ContentObserver mSystemAutoRotateObserver = new ContentObserver(new Handler()) {
+        @Override
+        public void onChange(boolean selfChange) {
+            updateAutoRotateSetting();
+        }
+    };
 
     public static boolean getAllowRotationDefaultValue() {
         // If the device's pixel density was scaled (usually via settings for A11y), use the
@@ -72,12 +68,9 @@
 
     private final Activity mActivity;
     private final SharedPreferences mSharedPrefs;
-    private final SharedPreferences mFeatureFlagsPrefs;
 
     private boolean mIgnoreAutoRotateSettings;
-    private boolean mAutoRotateEnabled;
-    private boolean mForcedRotation;
-    private List<ForcedRotationChangedListener> mForcedRotationChangedListeners = new ArrayList<>();
+    private boolean mHomeRotationEnabled;
 
     /**
      * Rotation request made by
@@ -108,67 +101,35 @@
         if (!mIgnoreAutoRotateSettings) {
             mSharedPrefs = Utilities.getPrefs(mActivity);
             mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
-            mAutoRotateEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
+            mHomeRotationEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
                     getAllowRotationDefaultValue());
         } else {
             mSharedPrefs = null;
         }
 
         mContentResolver = activity.getContentResolver();
-        mFeatureFlagsPrefs = Utilities.getFeatureFlagsPrefs(mActivity);
-        mFeatureFlagsPrefs.registerOnSharedPreferenceChangeListener(this);
-        updateForcedRotation(true);
     }
 
-    /**
-     * @param setValueFromPrefs If true, then {@link #mForcedRotation} will get set to the value
-     *                          from the home developer settings. Otherwise it will not.
-     *                          This is primarily to allow tests to set their own conditions.
-     */
-    private void updateForcedRotation(boolean setValueFromPrefs) {
-        boolean isForcedRotation = mFeatureFlagsPrefs
-                .getBoolean(FLAG_ENABLE_FIXED_ROTATION_TRANSFORM, true)
-                && !getAllowRotationDefaultValue();
-        if (mForcedRotation == isForcedRotation) {
-            return;
+    private void updateAutoRotateSetting() {
+        int autoRotateEnabled = 0;
+        try {
+            autoRotateEnabled = Settings.System.getInt(mContentResolver,
+                    Settings.System.ACCELEROMETER_ROTATION);
+        } catch (Settings.SettingNotFoundException e) {
+            Log.e(TAG, "autorotate setting not found", e);
         }
-        if (setValueFromPrefs) {
-            mForcedRotation = isForcedRotation;
-        }
-        UI_HELPER_EXECUTOR.execute(() -> {
-            if (mActivity.checkSelfPermission(WRITE_SECURE_SETTINGS) == PERMISSION_GRANTED) {
-                Settings.Global.putInt(mContentResolver, FIXED_ROTATION_TRANSFORM_SETTING_NAME,
-                            mForcedRotation ? 1 : 0);
-            }
-        });
-        for (ForcedRotationChangedListener listener : mForcedRotationChangedListeners) {
-            listener.onForcedRotationChanged(mForcedRotation);
-        }
-    }
 
-    /**
-     * will not be called when first registering the listener.
-     */
-    public void addForcedRotationCallback(ForcedRotationChangedListener listener) {
-        mForcedRotationChangedListeners.add(listener);
-    }
-
-    public void removeForcedRotationCallback(ForcedRotationChangedListener listener) {
-        mForcedRotationChangedListeners.remove(listener);
+        mSystemAutoRotateEnabled = autoRotateEnabled == 1;
     }
 
     @Override
     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
-        if (FLAG_ENABLE_FIXED_ROTATION_TRANSFORM.equals(s)) {
-            updateForcedRotation(true);
-            return;
-        }
-
-        boolean wasRotationEnabled = mAutoRotateEnabled;
-        mAutoRotateEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
+        boolean wasRotationEnabled = mHomeRotationEnabled;
+        mHomeRotationEnabled = mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
                 getAllowRotationDefaultValue());
-        if (mAutoRotateEnabled != wasRotationEnabled) {
+        if (mHomeRotationEnabled != wasRotationEnabled) {
             notifyChange();
+            updateAutoRotateSetting();
         }
     }
 
@@ -197,10 +158,6 @@
     public void forceAllowRotationForTesting(boolean allowRotation) {
         mIgnoreAutoRotateSettings =
                 allowRotation || mActivity.getResources().getBoolean(R.bool.allow_rotation);
-        // TODO(b/150214193) Tests currently expect launcher to be able to be rotated
-        //   Modify tests for this new behavior
-        mForcedRotation = !allowRotation;
-        updateForcedRotation(false);
         notifyChange();
     }
 
@@ -208,6 +165,11 @@
         if (!mInitialized) {
             mInitialized = true;
             notifyChange();
+
+            mContentResolver.registerContentObserver(
+                    Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
+                    false, mSystemAutoRotateObserver);
+            updateAutoRotateSetting();
         }
     }
 
@@ -217,8 +179,7 @@
             if (mSharedPrefs != null) {
                 mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
             }
-            mForcedRotationChangedListeners.clear();
-            mFeatureFlagsPrefs.unregisterOnSharedPreferenceChangeListener(this);
+            mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver);
         }
     }
 
@@ -228,10 +189,7 @@
         }
 
         final int activityFlags;
-        if (mForcedRotation) {
-            // TODO(b/150214193) Properly address this
-            activityFlags = SCREEN_ORIENTATION_PORTRAIT;
-        } else if (mStateHandlerRequest != REQUEST_NONE) {
+        if (mStateHandlerRequest != REQUEST_NONE) {
             activityFlags = mStateHandlerRequest == REQUEST_LOCK ?
                     SCREEN_ORIENTATION_LOCKED : SCREEN_ORIENTATION_UNSPECIFIED;
         } else if (mCurrentTransitionRequest != REQUEST_NONE) {
@@ -240,7 +198,7 @@
         } else if (mCurrentStateRequest == REQUEST_LOCK) {
             activityFlags = SCREEN_ORIENTATION_LOCKED;
         } else if (mIgnoreAutoRotateSettings || mCurrentStateRequest == REQUEST_ROTATE
-                || mAutoRotateEnabled) {
+                || mHomeRotationEnabled) {
             activityFlags = SCREEN_ORIENTATION_UNSPECIFIED;
         } else {
             // If auto rotation is off, allow rotation on the activity, in case the user is using
@@ -253,11 +211,23 @@
         }
     }
 
+    /**
+     * @return how many factors {@param newRotation} is rotated 90 degrees clockwise.
+     * E.g. 1->Rotated by 90 degrees clockwise, 2->Rotated 180 clockwise...
+     * A value of 0 means no rotation has been applied
+     */
+    public static int deltaRotation(int oldRotation, int newRotation) {
+        int delta = newRotation - oldRotation;
+        if (delta < 0) delta += 4;
+        return delta;
+    }
+
     @Override
     public String toString() {
         return String.format("[mStateHandlerRequest=%d, mCurrentStateRequest=%d,"
-                + " mLastActivityFlags=%d, mIgnoreAutoRotateSettings=%b, mAutoRotateEnabled=%b]",
+                + " mLastActivityFlags=%d, mIgnoreAutoRotateSettings=%b, mHomeRotationEnabled=%b,"
+                        + " mSystemAutoRotateEnabled=%b]",
                 mStateHandlerRequest, mCurrentStateRequest, mLastActivityFlags,
-                mIgnoreAutoRotateSettings, mAutoRotateEnabled);
+                mIgnoreAutoRotateSettings, mHomeRotationEnabled, mSystemAutoRotateEnabled);
     }
 }
diff --git a/src/com/android/launcher3/states/SpringLoadedState.java b/src/com/android/launcher3/states/SpringLoadedState.java
index 2ba624c..b2ff69a 100644
--- a/src/com/android/launcher3/states/SpringLoadedState.java
+++ b/src/com/android/launcher3/states/SpringLoadedState.java
@@ -78,7 +78,7 @@
     }
 
     @Override
-    public float getDepth(Context context) {
+    protected float getDepthUnchecked(Context context) {
         return 0.5f;
     }
 
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index e7449bb..a5a06b4 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -96,7 +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";
-    public static final String NO_SCROLL_END_WIDGETS = "b/152354290";
 }
diff --git a/src/com/android/launcher3/touch/HomeRotatedPageHandler.java b/src/com/android/launcher3/touch/HomeRotatedPageHandler.java
new file mode 100644
index 0000000..710b676
--- /dev/null
+++ b/src/com/android/launcher3/touch/HomeRotatedPageHandler.java
@@ -0,0 +1,49 @@
+/*
+ * 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.touch;
+
+import android.graphics.RectF;
+import android.view.Surface;
+
+public class HomeRotatedPageHandler extends PortraitPagedViewHandler {
+    @Override
+    public void offsetTaskRect(RectF rect, float value, int displayRotation, int launcherRotation) {
+        if (launcherRotation == Surface.ROTATION_0) {
+            super.offsetTaskRect(rect, value, displayRotation, launcherRotation);
+        } else if (launcherRotation == Surface.ROTATION_90) {
+            if (displayRotation == Surface.ROTATION_0) {
+                rect.offset(0, value);
+            } else if (displayRotation == Surface.ROTATION_90) {
+                rect.offset(value, 0);
+            } else if (displayRotation == Surface.ROTATION_180) {
+                rect.offset(-value, 0);
+            } else {
+                rect.offset(-value, 0);
+            }
+        } else if (launcherRotation == Surface.ROTATION_270) {
+            if (displayRotation == Surface.ROTATION_0) {
+                rect.offset(0, -value);
+            } else if (displayRotation == Surface.ROTATION_90) {
+                rect.offset(value, 0);
+            } else if (displayRotation == Surface.ROTATION_180) {
+                rect.offset(0, -value);
+            } else {
+                rect.offset(value, 0);
+            }
+        } // TODO (b/149609488) handle 180 case as well
+    }
+}
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index bab5747..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,9 +31,9 @@
 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.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.OverScroller;
@@ -78,6 +79,11 @@
     }
 
     @Override
+    public boolean isLayoutNaturalToLauncher() {
+        return false;
+    }
+
+    @Override
     public void adjustFloatingIconStartVelocity(PointF velocity) {
         float oldX = velocity.x;
         float oldY = velocity.y;
@@ -115,11 +121,6 @@
     }
 
     @Override
-    public int getPrimarySize(Rect rect) {
-        return rect.height();
-    }
-
-    @Override
     public float getPrimarySize(RectF rect) {
         return rect.height();
     }
@@ -130,17 +131,6 @@
     }
 
     @Override
-    public ScaleAndTranslation getScaleAndTranslation(DeviceProfile dp, View view) {
-        float offscreenTranslationY = dp.heightPx - view.getPaddingTop();
-        return new ScaleAndTranslation(1f, 0f, offscreenTranslationY);
-    }
-
-    @Override
-    public float getTranslationValue(ScaleAndTranslation scaleAndTranslation) {
-        return scaleAndTranslation.translationY;
-    }
-
-    @Override
     public FloatProperty<View> getPrimaryViewTranslate() {
         return VIEW_TRANSLATE_Y;
     }
@@ -182,7 +172,7 @@
     }
 
     @Override
-    public void offsetTaskRect(RectF rect, float value, int displayRotation) {
+    public void offsetTaskRect(RectF rect, float value, int displayRotation, int launcherRotation) {
         if (displayRotation == Surface.ROTATION_0) {
             rect.offset(0, value);
         } else if (displayRotation == Surface.ROTATION_90) {
@@ -236,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 50606ec..cc15f99 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -18,6 +18,7 @@
 
 import android.content.res.Resources;
 import android.graphics.Canvas;
+import android.graphics.Matrix;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -26,9 +27,9 @@
 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.LauncherState;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.util.OverScroller;
 
@@ -42,6 +43,7 @@
     PagedOrientationHandler PORTRAIT = new PortraitPagedViewHandler();
     PagedOrientationHandler LANDSCAPE = new LandscapePagedViewHandler();
     PagedOrientationHandler SEASCAPE = new SeascapePagedViewHandler();
+    PagedOrientationHandler HOME_ROTATED = new HomeRotatedPageHandler();
 
     interface Int2DAction<T> {
         void call(T target, int x, int y);
@@ -52,16 +54,15 @@
     Int2DAction<View> VIEW_SCROLL_BY = View::scrollBy;
     Int2DAction<View> VIEW_SCROLL_TO = View::scrollTo;
     Float2DAction<Canvas> CANVAS_TRANSLATE = Canvas::translate;
+    Float2DAction<Matrix> MATRIX_POST_TRANSLATE = Matrix::postTranslate;
+
     <T> void set(T target, Int2DAction<T> action, int param);
     <T> void set(T target, Float2DAction<T> action, float param);
     float getPrimaryDirection(MotionEvent event, int pointerIndex);
     float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId);
     int getMeasuredSize(View view);
-    int getPrimarySize(Rect rect);
     float getPrimarySize(RectF rect);
     int getSecondaryDimension(View view);
-    LauncherState.ScaleAndTranslation getScaleAndTranslation(DeviceProfile dp, View view);
-    float getTranslationValue(LauncherState.ScaleAndTranslation scaleAndTranslation);
     FloatProperty<View> getPrimaryViewTranslate();
     FloatProperty<View> getSecondaryViewTranslate();
     void setPrimaryAndResetSecondaryTranslate(View view, float translation);
@@ -79,7 +80,7 @@
     void setMaxScroll(AccessibilityEvent event, int maxScroll);
     boolean getRecentsRtlSetting(Resources resources);
     float getDegreesRotated();
-    void offsetTaskRect(RectF rect, float value, int delta);
+    void offsetTaskRect(RectF rect, float value, int delta, int launcherRotation);
     int getPrimaryValue(int x, int y);
     int getSecondaryValue(int x, int y);
     void delegateScrollTo(PagedView pagedView, int secondaryScroll, int primaryScroll);
@@ -89,6 +90,12 @@
     void scrollerStartScroll(OverScroller scroller, int newPosition);
     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
@@ -96,7 +103,6 @@
      */
     void adjustFloatingIconStartVelocity(PointF velocity);
 
-
     class CurveProperties {
         public int scroll;
         public int halfPageSize;
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index 245138f..7c30e29 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -30,9 +30,9 @@
 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.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.OverScroller;
@@ -78,6 +78,11 @@
     }
 
     @Override
+    public boolean isLayoutNaturalToLauncher() {
+        return true;
+    }
+
+    @Override
     public void adjustFloatingIconStartVelocity(PointF velocity) {
         //no-op
     }
@@ -113,11 +118,6 @@
     }
 
     @Override
-    public int getPrimarySize(Rect rect) {
-        return rect.width();
-    }
-
-    @Override
     public float getPrimarySize(RectF rect) {
         return rect.width();
     }
@@ -128,17 +128,6 @@
     }
 
     @Override
-    public ScaleAndTranslation getScaleAndTranslation(DeviceProfile dp, View view) {
-        float offscreenTranslationX = dp.widthPx - view.getPaddingStart();
-        return new ScaleAndTranslation(1f, offscreenTranslationX, 0f);
-    }
-
-    @Override
-    public float getTranslationValue(ScaleAndTranslation scaleAndTranslation) {
-        return scaleAndTranslation.translationX;
-    }
-
-    @Override
     public FloatProperty<View> getPrimaryViewTranslate() {
         return VIEW_TRANSLATE_X;
     }
@@ -180,7 +169,7 @@
     }
 
     @Override
-    public void offsetTaskRect(RectF rect, float value, int displayRotation) {
+    public void offsetTaskRect(RectF rect, float value, int displayRotation, int launcherRotation) {
         if (displayRotation == Surface.ROTATION_0) {
             rect.offset(value, 0);
         } else if (displayRotation == Surface.ROTATION_90) {
@@ -234,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 eebd87f..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;
 
@@ -36,7 +37,7 @@
     }
 
     @Override
-    public void offsetTaskRect(RectF rect, float value, int displayRotation) {
+    public void offsetTaskRect(RectF rect, float value, int displayRotation, int launcherRotation) {
         if (displayRotation == Surface.ROTATION_0) {
             rect.offset(0, value);
         } else if (displayRotation == Surface.ROTATION_90) {
@@ -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/util/ActivityTracker.java b/src/com/android/launcher3/util/ActivityTracker.java
index 499f655..59266b4 100644
--- a/src/com/android/launcher3/util/ActivityTracker.java
+++ b/src/com/android/launcher3/util/ActivityTracker.java
@@ -15,27 +15,23 @@
  */
 package com.android.launcher3.util;
 
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.IBinder;
-import android.util.Log;
 
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BaseActivity;
-import com.android.launcher3.testing.TestProtocol;
 
 import java.lang.ref.WeakReference;
 
 /**
  * Helper class to statically track activity creation
+ * @param <T> The activity type to track
  */
-public final class ActivityTracker<T extends BaseActivity> implements Runnable {
+public final class ActivityTracker<T extends BaseActivity> {
 
     private WeakReference<T> mCurrentActivity = new WeakReference<>(null);
-    private WeakReference<SchedulerCallback<T>> mPendingCallback = new WeakReference<>(null);
 
     private static final String EXTRA_SCHEDULER_CALLBACK = "launcher.scheduler_callback";
 
@@ -51,76 +47,32 @@
     }
 
     /**
-     * Schedules the callback to be notified when the activity is created.
-     * @return true if the activity is already created, false otherwise
+     * Call {@link SchedulerCallback#init(BaseActivity, boolean)} when the activity is ready.
+     * If the activity is already created, this is called immediately, otherwise we add the
+     * callback as an extra on the intent, and will call init() when we get handleIntent().
+     * @param callback The callback to call init() on when the activity is ready.
+     * @param intent The intent that will be used to initialize the activity, if the activity
+     *               doesn't already exist. We add the callback as an extra on this intent.
      */
-    public boolean schedule(SchedulerCallback<? extends T> callback) {
-        synchronized (this) {
-            mPendingCallback = new WeakReference<>((SchedulerCallback<T>) callback);
-        }
-        if (!notifyInitIfPending()) {
-            // If the activity doesn't already exist, then post and wait for the activity to start
-            MAIN_EXECUTOR.execute(this);
-            return false;
-        }
-        return true;
-    }
-
-    @Override
-    public void run() {
-        notifyInitIfPending();
-    }
-
-    /**
-     * Notifies the pending callback if the activity is now created.
-     * @return true if the activity is now created.
-     */
-    private boolean notifyInitIfPending() {
+    public void runCallbackWhenActivityExists(SchedulerCallback<T> callback, Intent intent) {
         T activity = mCurrentActivity.get();
         if (activity != null) {
-            notifyInitIfPending(activity, activity.isStarted());
-            return true;
+            callback.init(activity, activity.isStarted());
+        } else {
+            callback.addToIntent(intent);
         }
-        return false;
-    }
-
-    public boolean notifyInitIfPending(T activity, boolean alreadyOnHome) {
-        SchedulerCallback<T> pendingCallback = mPendingCallback.get();
-        if (pendingCallback != null) {
-            if (!pendingCallback.init(activity, alreadyOnHome)) {
-                clearReference(pendingCallback);
-            }
-            return true;
-        }
-        return false;
-    }
-
-    public boolean clearReference(SchedulerCallback<? extends T> handler) {
-        synchronized (this) {
-            if (mPendingCallback.get() == handler) {
-                mPendingCallback.clear();
-                return true;
-            }
-            return false;
-        }
-    }
-
-    public boolean hasPending() {
-        return mPendingCallback.get() != null;
     }
 
     public boolean handleCreate(T activity) {
         mCurrentActivity = new WeakReference<>(activity);
-        return handleIntent(activity, activity.getIntent(), false, false);
+        return handleIntent(activity, activity.getIntent(), false);
     }
 
     public boolean handleNewIntent(T activity, Intent intent) {
-        return handleIntent(activity, intent, activity.isStarted(), true);
+        return handleIntent(activity, intent, activity.isStarted());
     }
 
-    private boolean handleIntent(
-            T activity, Intent intent, boolean alreadyOnHome, boolean explicitIntent) {
-        boolean result = false;
+    private boolean handleIntent(T activity, Intent intent, boolean alreadyOnHome) {
         if (intent != null && intent.getExtras() != null) {
             IBinder stateBinder = intent.getExtras().getBinder(EXTRA_SCHEDULER_CALLBACK);
             if (stateBinder instanceof ObjectWrapper) {
@@ -129,19 +81,26 @@
                 if (!handler.init(activity, alreadyOnHome)) {
                     intent.getExtras().remove(EXTRA_SCHEDULER_CALLBACK);
                 }
-                result = true;
+                return true;
             }
         }
-        if (!result && !explicitIntent) {
-            result = notifyInitIfPending(activity, alreadyOnHome);
-        }
-        return result;
+        return false;
     }
 
     public interface SchedulerCallback<T extends BaseActivity> {
 
+        /**
+         * Called when the activity is ready.
+         * @param alreadyOnHome Whether the activity is already started.
+         * @return Whether to continue receiving callbacks (i.e. if the activity is recreated).
+         */
         boolean init(T activity, boolean alreadyOnHome);
 
+        /**
+         * Adds this callback as an extra on the intent, so we can retrieve it in handleIntent() and
+         * call {@link #init}. The intent should be used to start the activity after calling this
+         * method in order for us to get handleIntent().
+         */
         default Intent addToIntent(Intent intent) {
             Bundle extras = new Bundle();
             extras.putBinder(EXTRA_SCHEDULER_CALLBACK, ObjectWrapper.wrap(this));
diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java
index 07f835d..a8642b0 100644
--- a/src/com/android/launcher3/util/MultiValueAlpha.java
+++ b/src/com/android/launcher3/util/MultiValueAlpha.java
@@ -16,7 +16,7 @@
 
 package com.android.launcher3.util;
 
-import android.util.Property;
+import android.util.FloatProperty;
 import android.view.View;
 
 import java.util.Arrays;
@@ -26,8 +26,8 @@
  */
 public class MultiValueAlpha {
 
-    public static final Property<AlphaProperty, Float> VALUE =
-            new Property<AlphaProperty, Float>(Float.TYPE, "value") {
+    public static final FloatProperty<AlphaProperty> VALUE =
+            new FloatProperty<AlphaProperty>("value") {
 
                 @Override
                 public Float get(AlphaProperty alphaProperty) {
@@ -35,7 +35,7 @@
                 }
 
                 @Override
-                public void set(AlphaProperty object, Float value) {
+                public void setValue(AlphaProperty object, float value) {
                     object.setValue(value);
                 }
             };
diff --git a/src/com/android/launcher3/views/ArrowTipView.java b/src/com/android/launcher3/views/ArrowTipView.java
index 60470dc..a7575d1 100644
--- a/src/com/android/launcher3/views/ArrowTipView.java
+++ b/src/com/android/launcher3/views/ArrowTipView.java
@@ -32,7 +32,7 @@
 import androidx.core.content.ContextCompat;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Launcher;
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.dragndrop.DragLayer;
@@ -48,13 +48,13 @@
     private static final long SHOW_DURATION_MS = 300;
     private static final long HIDE_DURATION_MS = 100;
 
-    protected final Launcher mLauncher;
+    protected final BaseDraggingActivity mActivity;
     private final Handler mHandler = new Handler();
     private Runnable mOnClosed;
 
     public ArrowTipView(Context context) {
         super(context, null, 0);
-        mLauncher = Launcher.getLauncher(context);
+        mActivity = BaseDraggingActivity.fromContext(context);
         init(context);
     }
 
@@ -75,11 +75,11 @@
                         .setStartDelay(0)
                         .setDuration(HIDE_DURATION_MS)
                         .setInterpolator(Interpolators.ACCEL)
-                        .withEndAction(() -> mLauncher.getDragLayer().removeView(this))
+                        .withEndAction(() -> mActivity.getDragLayer().removeView(this))
                         .start();
             } else {
                 animate().cancel();
-                mLauncher.getDragLayer().removeView(this);
+                mActivity.getDragLayer().removeView(this);
             }
             if (mOnClosed != null) mOnClosed.run();
             mIsOpen = false;
@@ -126,12 +126,12 @@
      */
     public ArrowTipView show(String text, int top) {
         ((TextView) findViewById(R.id.text)).setText(text);
-        mLauncher.getDragLayer().addView(this);
+        mActivity.getDragLayer().addView(this);
 
         DragLayer.LayoutParams params = (DragLayer.LayoutParams) getLayoutParams();
         params.gravity = Gravity.CENTER_HORIZONTAL;
-        params.leftMargin = mLauncher.getDeviceProfile().workspacePadding.left;
-        params.rightMargin = mLauncher.getDeviceProfile().workspacePadding.right;
+        params.leftMargin = mActivity.getDeviceProfile().workspacePadding.left;
+        params.rightMargin = mActivity.getDeviceProfile().workspacePadding.right;
         post(() -> setY(top - getHeight()));
         setAlpha(0);
         animate()
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 2fc3eaf..6915953 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -275,9 +275,6 @@
 
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "BaseDragLayer: " + ev);
-        }
         switch (ev.getAction()) {
             case ACTION_DOWN: {
                 mTouchDispatchState |= TOUCH_DISPATCHING_VIEW;
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index e114cf8..6e21512 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -19,7 +19,6 @@
 import static com.android.launcher3.Utilities.getBadge;
 import static com.android.launcher3.Utilities.getFullDrawable;
 import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
-import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.animation.Animator;
@@ -100,6 +99,7 @@
 
     private AnimatorSet mFadeAnimatorSet;
     private ListenerView mListenerView;
+    private Runnable mFastFinishRunnable;
 
     public FloatingIconView(Context context) {
         this(context, null);
@@ -124,7 +124,6 @@
         super.onAttachedToWindow();
         if (!mIsOpening) {
             getViewTreeObserver().addOnGlobalLayoutListener(this);
-            mLauncher.getRotationHelper().setCurrentTransitionRequest(REQUEST_LOCK);
         }
     }
 
@@ -444,9 +443,21 @@
         }
     }
 
+    /**
+     * Sets a runnable that is called after a call to {@link #fastFinish()}.
+     */
+    public void setFastFinishRunnable(Runnable runnable) {
+        mFastFinishRunnable = runnable;
+    }
+
     public void fastFinish() {
+        if (mFastFinishRunnable != null) {
+            mFastFinishRunnable.run();
+            mFastFinishRunnable = null;
+        }
         if (mLoadIconSignal != null) {
             mLoadIconSignal.cancel();
+            mLoadIconSignal = null;
         }
         if (mEndRunnable != null) {
             mEndRunnable.run();
@@ -656,6 +667,7 @@
         sTmpObjArray[0] = null;
         mIconLoadResult = null;
         mClipIconView.recycle();
+        mFastFinishRunnable = null;
     }
 
     private static class IconLoadResult {
diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
index 5653801..6a83332 100644
--- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java
+++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
@@ -67,7 +67,6 @@
 
     private final static int MAX_TRACK_ALPHA = 30;
     private final static int SCROLL_BAR_VIS_DURATION = 150;
-    private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 0.75f;
 
     private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
             Collections.singletonList(new Rect());
@@ -184,7 +183,7 @@
         if (mThumbOffsetY == y) {
             return;
         }
-        updatePopupY((int) y);
+        updatePopupY(y);
         mThumbOffsetY = y;
         invalidate();
     }
@@ -237,7 +236,7 @@
                 } else if (mRv.supportsFastScrolling()
                         && isNearScrollBar(mDownX)) {
                     calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
-                    updateFastScrollSectionNameAndThumbOffset(mLastY, y);
+                    updateFastScrollSectionNameAndThumbOffset(y);
                 }
                 break;
             case MotionEvent.ACTION_MOVE:
@@ -252,7 +251,7 @@
                     calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
                 }
                 if (mIsDragging) {
-                    updateFastScrollSectionNameAndThumbOffset(mLastY, y);
+                    updateFastScrollSectionNameAndThumbOffset(y);
                 }
                 break;
             case MotionEvent.ACTION_UP:
@@ -281,7 +280,7 @@
         showActiveScrollbar(true);
     }
 
-    private void updateFastScrollSectionNameAndThumbOffset(int lastY, int y) {
+    private void updateFastScrollSectionNameAndThumbOffset(int y) {
         // Update the fastscroller section name at this touch position
         int bottom = mRv.getScrollbarTrackHeight() - mThumbHeight;
         float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffsetY));
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 5d33f13..6f2e179 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -19,8 +19,6 @@
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
 import android.content.res.Configuration;
-import android.graphics.PointF;
-import android.graphics.Rect;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.util.SparseBooleanArray;
@@ -40,7 +38,6 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.util.Executors;
@@ -51,7 +48,7 @@
  * {@inheritDoc}
  */
 public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
-        implements TouchCompleteListener, View.OnLongClickListener, DraggableView {
+        implements TouchCompleteListener, View.OnLongClickListener {
 
     // Related to the auto-advancing of widgets
     private static final long ADVANCE_INTERVAL = 20000;
@@ -73,15 +70,7 @@
     private boolean mIsAutoAdvanceRegistered;
     private Runnable mAutoAdvanceRunnable;
 
-    /**
-     * The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY.
-     */
-    private float mScaleToFit = 1f;
 
-    /**
-     * The translation values to center the widget within its cellspans.
-     */
-    private final PointF mTranslationForCentering = new PointF(0, 0);
 
     public LauncherAppWidgetHostView(Context context) {
         super(context);
@@ -307,26 +296,6 @@
         scheduleNextAdvance();
     }
 
-    public void setScaleToFit(float scale) {
-        mScaleToFit = scale;
-        setScaleX(scale);
-        setScaleY(scale);
-    }
-
-    public float getScaleToFit() {
-        return mScaleToFit;
-    }
-
-    public void setTranslationForCentering(float x, float y) {
-        mTranslationForCentering.set(x, y);
-        setTranslationX(x);
-        setTranslationY(y);
-    }
-
-    public PointF getTranslationForCentering() {
-        return mTranslationForCentering;
-    }
-
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
@@ -357,17 +326,4 @@
         }
         return false;
     }
-
-    @Override
-    public int getViewType() {
-        return DRAGGABLE_WIDGET;
-    }
-
-    @Override
-    public void getVisualDragBounds(Rect bounds) {
-        int width = (int) (getMeasuredWidth() * mScaleToFit);
-        int height = (int) (getMeasuredHeight() * mScaleToFit);
-
-        bounds.set(0, 0 , width, height);
-    }
 }
diff --git a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
index 104ad77..a4e7daa 100644
--- a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
@@ -18,12 +18,14 @@
 
 import android.appwidget.AppWidgetHostView;
 import android.content.Context;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
 
+import com.android.launcher3.Reorderable;
 import com.android.launcher3.dragndrop.DraggableView;
 
 import java.util.ArrayList;
@@ -32,7 +34,21 @@
  * Extension of AppWidgetHostView with support for controlled keyboard navigation.
  */
 public abstract class NavigableAppWidgetHostView extends AppWidgetHostView
-        implements DraggableView {
+        implements DraggableView, Reorderable {
+
+    /**
+     * The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY.
+     */
+    private float mScaleToFit = 1f;
+
+    /**
+     * The translation values to center the widget within its cellspans.
+     */
+    private final PointF mTranslationForCentering = new PointF(0, 0);
+
+    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;
@@ -137,6 +153,65 @@
         setSelected(childIsFocused);
     }
 
+    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);
+        updateTranslation();
+    }
+
+    public void setReorderBounceOffset(float x, float y) {
+        mTranslationForReorderBounce.set(x, y);
+        updateTranslation();
+    }
+
+    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);
+    }
+
+    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
     public int getViewType() {
         return DRAGGABLE_WIDGET;
@@ -144,6 +219,9 @@
 
     @Override
     public void getVisualDragBounds(Rect bounds) {
-        bounds.set(0, 0 , getMeasuredWidth(), getMeasuredHeight());
+        int width = (int) (getMeasuredWidth() * mScaleToFit);
+        int height = (int) (getMeasuredHeight() * mScaleToFit);
+
+        bounds.set(0, 0 , width, height);
     }
 }
diff --git a/src/com/android/launcher3/widget/WidgetsFullSheet.java b/src/com/android/launcher3/widget/WidgetsFullSheet.java
index 37a30af..536b766 100644
--- a/src/com/android/launcher3/widget/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsFullSheet.java
@@ -71,14 +71,6 @@
 
     }
 
-    @Override
-    public boolean dispatchTouchEvent(MotionEvent ev) {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "WidgetsFullSheet: " + ev);
-        }
-        return super.dispatchTouchEvent(ev);
-    }
-
     public WidgetsFullSheet(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
index 17baa27..82d4110 100644
--- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
@@ -158,23 +158,13 @@
                     mScrollbar.isHitInParent(e.getX(), e.getY(), mFastScrollerOffset);
         }
         if (mTouchDownOnScroller) {
-            final boolean result = mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
-            if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-                Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onInterceptTouchEvent 1 " + result);
-            }
-            return result;
-        }
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onInterceptTouchEvent 2 false");
+            return mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
         }
         return false;
     }
 
     @Override
     public void onTouchEvent(RecyclerView rv, MotionEvent e) {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "WidgetsRecyclerView.onTouchEvent");
-        }
         if (mTouchDownOnScroller) {
             mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
         }
@@ -182,31 +172,5 @@
 
     @Override
     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onRequestDisallowInterceptTouchEvent "
-                    + disallowIntercept);
-        }
-    }
-
-    @Override
-    public boolean dispatchTouchEvent(MotionEvent ev) {
-        final boolean result = super.dispatchTouchEvent(ev);
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "WidgetsRecyclerView: state: "
-                    + getScrollState()
-                    + " can scroll: " + getLayoutManager().canScrollVertically()
-                    + " result: " + result
-                    + " layout suppressed: " + isLayoutSuppressed()
-                    + " event: " + ev);
-        }
-        return result;
-    }
-
-    @Override
-    public void stopNestedScroll() {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "stopNestedScroll");
-        }
-        super.stopNestedScroll();
     }
 }
\ No newline at end of file
diff --git a/tests/Android.mk b/tests/Android.mk
index a9fff8e..4d1bfa6 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -62,7 +62,11 @@
     LOCAL_STATIC_JAVA_LIBRARIES += ub-launcher-aosp-tapl
 endif
 
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SRC_FILES := \
+	$(call all-java-files-under, src) \
+	$(call all-java-files-under, src_common)
+
+
 LOCAL_FULL_LIBS_MANIFEST_FILES := $(LOCAL_PATH)/AndroidManifest-common.xml
 
 LOCAL_PACKAGE_NAME := Launcher3Tests
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 86faddb..9c8e278 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -17,7 +17,6 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
-import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID;
 import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
@@ -26,7 +25,6 @@
 
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -53,6 +51,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.common.WidgetUtils;
 import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.tapl.LauncherInstrumentation;
@@ -60,7 +59,6 @@
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.testcomponent.TestCommandReceiver;
 import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Wait;
@@ -100,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());
@@ -115,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";
         }
     }
 
@@ -265,7 +285,7 @@
         if (mLauncherPid != 0) {
             assertEquals("Launcher crashed, pid mismatch:", mLauncherPid, mLauncher.getPid());
         }
-        checkDetectedLeaks();
+        checkDetectedLeaks(mLauncher);
     }
 
     protected void clearLauncherData() throws IOException, InterruptedException {
@@ -306,26 +326,7 @@
      * Adds {@param item} on the homescreen on the 0th screen
      */
     protected void addItemToScreen(ItemInfo item) {
-        ContentResolver resolver = mTargetContext.getContentResolver();
-        int screenId = FIRST_SCREEN_ID;
-        // Update the screen id counter for the provider.
-        LauncherSettings.Settings.call(resolver,
-                LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
-
-        if (screenId > FIRST_SCREEN_ID) {
-            screenId = FIRST_SCREEN_ID;
-        }
-
-        // Insert the item
-        ContentWriter writer = new ContentWriter(mTargetContext);
-        item.id = LauncherSettings.Settings.call(
-                resolver, LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
-                .getInt(LauncherSettings.Settings.EXTRA_VALUE);
-        item.screenId = screenId;
-        item.onAddToDatabase(writer);
-        writer.put(LauncherSettings.Favorites._ID, item.id);
-        resolver.insert(LauncherSettings.Favorites.CONTENT_URI,
-                writer.getValues(mTargetContext));
+        WidgetUtils.addItemToScreen(item, mTargetContext);
         resetLoaderState();
 
         // Launch the home activity
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.
diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index 62ce085..df11557 100644
--- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -17,18 +17,16 @@
 
 import static androidx.test.InstrumentationRegistry.getTargetContext;
 
-import static com.android.launcher3.widget.WidgetHostViewLoader.getDefaultOptionsForWidget;
+import static com.android.launcher3.common.WidgetUtils.createWidgetInfo;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
-import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetManager;
 import android.content.ComponentName;
 import android.content.ContentResolver;
-import android.content.Context;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionParams;
 import android.content.pm.PackageManager;
@@ -39,7 +37,6 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.LauncherAppWidgetHost;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
@@ -50,7 +47,6 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.rule.ShellCommandRule;
-import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
 import org.junit.After;
@@ -108,7 +104,7 @@
     @Test
     public void testBindNormalWidget_withConfig() {
         LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, true);
-        LauncherAppWidgetInfo item = createWidgetInfo(info, true);
+        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);
 
         addItemToScreen(item);
         verifyWidgetPresent(info);
@@ -117,7 +113,7 @@
     @Test
     public void testBindNormalWidget_withoutConfig() {
         LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
-        LauncherAppWidgetInfo item = createWidgetInfo(info, true);
+        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);
 
         addItemToScreen(item);
         verifyWidgetPresent(info);
@@ -126,7 +122,7 @@
     @Test
     public void testUnboundWidget_removed() {
         LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
-        LauncherAppWidgetInfo item = createWidgetInfo(info, false);
+        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), false);
         item.appWidgetId = -33;
 
         addItemToScreen(item);
@@ -147,7 +143,7 @@
         LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
 
         // Do not bind the widget
-        LauncherAppWidgetInfo item = createWidgetInfo(info, false);
+        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), false);
         item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
 
         addItemToScreen(item);
@@ -160,7 +156,7 @@
         LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, true);
 
         // Do not bind the widget
-        LauncherAppWidgetInfo item = createWidgetInfo(info, false);
+        LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), false);
         item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
 
         addItemToScreen(item);
@@ -282,47 +278,6 @@
     }
 
     /**
-     * Creates a LauncherAppWidgetInfo corresponding to {@param info}
-     *
-     * @param bindWidget if true the info is bound and a valid widgetId is assigned to
-     *                   the LauncherAppWidgetInfo
-     */
-    public static LauncherAppWidgetInfo createWidgetInfo(
-            LauncherAppWidgetProviderInfo info, boolean bindWidget) {
-        Context targetContext = getTargetContext();
-
-        LauncherAppWidgetInfo item = new LauncherAppWidgetInfo(
-                LauncherAppWidgetInfo.NO_ID, info.provider);
-        item.spanX = info.minSpanX;
-        item.spanY = info.minSpanY;
-        item.minSpanX = info.minSpanX;
-        item.minSpanY = info.minSpanY;
-        item.user = info.getProfile();
-        item.cellX = 0;
-        item.cellY = 1;
-        item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
-
-        if (bindWidget) {
-            PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(info);
-            pendingInfo.spanX = item.spanX;
-            pendingInfo.spanY = item.spanY;
-            pendingInfo.minSpanX = item.minSpanX;
-            pendingInfo.minSpanY = item.minSpanY;
-            Bundle options = getDefaultOptionsForWidget(targetContext, pendingInfo);
-
-            AppWidgetHost host = new LauncherAppWidgetHost(targetContext);
-            int widgetId = host.allocateAppWidgetId();
-            if (!new WidgetManagerHelper(targetContext)
-                    .bindAppWidgetIdIfAllowed(widgetId, info, options)) {
-                host.deleteAppWidgetId(widgetId);
-                throw new IllegalArgumentException("Unable to bind widget id");
-            }
-            item.appWidgetId = widgetId;
-        }
-        return item;
-    }
-
-    /**
      * Returns a LauncherAppWidgetInfo with package name which is not present on the device
      */
     private LauncherAppWidgetInfo getInvalidWidgetInfo() {
diff --git a/tests/src_common/README.md b/tests/src_common/README.md
new file mode 100644
index 0000000..2bc9e73
--- /dev/null
+++ b/tests/src_common/README.md
@@ -0,0 +1 @@
+Common source code used by both android tests and robolectric tests
\ No newline at end of file
diff --git a/tests/src_common/com/android/launcher3/common/WidgetUtils.java b/tests/src_common/com/android/launcher3/common/WidgetUtils.java
new file mode 100644
index 0000000..c0913bf
--- /dev/null
+++ b/tests/src_common/com/android/launcher3/common/WidgetUtils.java
@@ -0,0 +1,104 @@
+/*
+ * 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.common;
+
+import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID;
+import static com.android.launcher3.widget.WidgetHostViewLoader.getDefaultOptionsForWidget;
+
+import android.appwidget.AppWidgetHost;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.launcher3.LauncherAppWidgetHost;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.util.ContentWriter;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
+import com.android.launcher3.widget.WidgetManagerHelper;
+
+/**
+ * Common method for widget binding
+ */
+public class WidgetUtils {
+
+    /**
+     * Creates a LauncherAppWidgetInfo corresponding to {@param info}
+     *
+     * @param bindWidget if true the info is bound and a valid widgetId is assigned to
+     *                   the LauncherAppWidgetInfo
+     */
+    public static LauncherAppWidgetInfo createWidgetInfo(
+            LauncherAppWidgetProviderInfo info, Context targetContext, boolean bindWidget) {
+        LauncherAppWidgetInfo item = new LauncherAppWidgetInfo(
+                LauncherAppWidgetInfo.NO_ID, info.provider);
+        item.spanX = info.minSpanX;
+        item.spanY = info.minSpanY;
+        item.minSpanX = info.minSpanX;
+        item.minSpanY = info.minSpanY;
+        item.user = info.getProfile();
+        item.cellX = 0;
+        item.cellY = 1;
+        item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+
+        if (bindWidget) {
+            PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(info);
+            pendingInfo.spanX = item.spanX;
+            pendingInfo.spanY = item.spanY;
+            pendingInfo.minSpanX = item.minSpanX;
+            pendingInfo.minSpanY = item.minSpanY;
+            Bundle options = getDefaultOptionsForWidget(targetContext, pendingInfo);
+
+            AppWidgetHost host = new LauncherAppWidgetHost(targetContext);
+            int widgetId = host.allocateAppWidgetId();
+            if (!new WidgetManagerHelper(targetContext)
+                    .bindAppWidgetIdIfAllowed(widgetId, info, options)) {
+                host.deleteAppWidgetId(widgetId);
+                throw new IllegalArgumentException("Unable to bind widget id");
+            }
+            item.appWidgetId = widgetId;
+        }
+        return item;
+    }
+
+    /**
+     * Adds {@param item} on the homescreen on the 0th screen
+     */
+    public static void addItemToScreen(ItemInfo item, Context targetContext) {
+        ContentResolver resolver = targetContext.getContentResolver();
+        int screenId = FIRST_SCREEN_ID;
+        // Update the screen id counter for the provider.
+        LauncherSettings.Settings.call(resolver,
+                LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
+
+        if (screenId > FIRST_SCREEN_ID) {
+            screenId = FIRST_SCREEN_ID;
+        }
+
+        // Insert the item
+        ContentWriter writer = new ContentWriter(targetContext);
+        item.id = LauncherSettings.Settings.call(
+                resolver, LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
+                .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+        item.screenId = screenId;
+        item.onAddToDatabase(writer);
+        writer.put(LauncherSettings.Favorites._ID, item.id);
+        resolver.insert(LauncherSettings.Favorites.CONTENT_URI,
+                writer.getValues(targetContext));
+    }
+}