Fix Robolectric test breakage am: 77e29775a9

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Launcher3/+/12442805

Change-Id: Ib8711dce31cc2bbb54776939a402440521b09c5f
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b031ffb..bc0cd97 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -49,7 +49,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index 60afddb..d680507 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -49,7 +49,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index e49f2ec..bb83b76 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -60,7 +60,7 @@
             android:stateNotNeeded="true"
             android:theme="@style/LauncherTheme"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity="" />
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
index c15a596..20e1edc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
@@ -71,10 +71,9 @@
     }
 
     private PrintWriter getWriter() {
-        String fName = FILE_NAME_PREFIX + (LOG_DAYS % 10);
-        if (fName.equals(mFileName)) return mCurrentWriter;
-
         Calendar cal = Calendar.getInstance();
+        String fName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) % 10);
+        if (fName.equals(mFileName)) return mCurrentWriter;
 
         boolean append = false;
         File logFile = new File(mLogsDir, fName);
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
index 8c1db4e..9e7c9fb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
@@ -30,7 +30,6 @@
  */
 public class HotseatRestoreHelper {
     private final Launcher mLauncher;
-    private boolean mBackupRestored = false;
 
     HotseatRestoreHelper(Launcher context) {
         mLauncher = context;
@@ -62,7 +61,6 @@
      * Finds and restores a previously saved snapshow of Favorites table
      */
     public void restoreBackup() {
-        if (mBackupRestored) return;
         MODEL_EXECUTOR.execute(() -> {
             try (LauncherDbUtils.SQLiteTransaction transaction = (LauncherDbUtils.SQLiteTransaction)
                     LauncherSettings.Settings.call(
@@ -78,7 +76,6 @@
                         idp.numRows);
                 backupTable.restoreFromCustomBackupTable(HYBRID_HOTSEAT_BACKUP_TABLE, true);
                 transaction.commit();
-                mBackupRestored = true;
                 mLauncher.getModel().forceReload();
             }
         });
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 085b9b3..5ccc1e8 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
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
 import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
@@ -57,7 +58,7 @@
             mRecentsView.updateEmptyMessage();
             mRecentsView.resetTaskVisuals();
         }
-        setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, state);
+        setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, new StateAnimationConfig(), state);
         mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress());
     }
 
@@ -75,17 +76,19 @@
                     AnimationSuccessListener.forRunnable(mRecentsView::resetTaskVisuals));
         }
 
-        setAlphas(builder, toState);
+        setAlphas(builder, config, toState);
         builder.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
                 toState.getOverviewFullscreenProgress(), LINEAR);
     }
 
-    private void setAlphas(PropertySetter propertySetter, LauncherState state) {
+    private void setAlphas(PropertySetter propertySetter, StateAnimationConfig config,
+            LauncherState state) {
         float buttonAlpha = (state.getVisibleElements(mLauncher) & OVERVIEW_BUTTONS) != 0 ? 1 : 0;
         propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
                 buttonAlpha, LINEAR);
         propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
-                MultiValueAlpha.VALUE, buttonAlpha, LINEAR);
+                MultiValueAlpha.VALUE, buttonAlpha, config.getInterpolator(
+                        ANIM_OVERVIEW_ACTIONS_FADE, LINEAR));
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index a0af797..daa1aad 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -17,7 +17,6 @@
 
 import static android.view.View.VISIBLE;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.HINT_STATE;
 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
@@ -26,6 +25,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW_PEEK;
 import static com.android.launcher3.WorkspaceStateTransitionAnimation.getSpringScaleAnimator;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
@@ -52,6 +52,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -163,10 +164,15 @@
             config.setInterpolator(ANIM_WORKSPACE_FADE, ACCEL);
             config.setInterpolator(ANIM_ALL_APPS_FADE, ACCEL);
             config.setInterpolator(ANIM_OVERVIEW_SCALE, clampToProgress(ACCEL, 0, 0.9f));
-            config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, ACCEL);
-            config.setInterpolator(ANIM_OVERVIEW_FADE, DEACCEL_1_7);
-            Workspace workspace = mActivity.getWorkspace();
+            config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, ACCEL_DEACCEL);
 
+            if (SysUINavigationMode.getMode(mActivity) == NO_BUTTON) {
+                config.setInterpolator(ANIM_OVERVIEW_FADE, FINAL_FRAME);
+            } else {
+                config.setInterpolator(ANIM_OVERVIEW_FADE, DEACCEL_1_7);
+            }
+
+            Workspace workspace = mActivity.getWorkspace();
             // Start from a higher workspace scale, but only if we're invisible so we don't jump.
             boolean isWorkspaceVisible = workspace.getVisibility() == VISIBLE;
             if (isWorkspaceVisible) {
@@ -206,13 +212,15 @@
                 config.setInterpolator(ANIM_WORKSPACE_SCALE,
                         fromState == NORMAL ? ACCEL : OVERSHOOT_1_2);
                 config.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL);
+                config.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
             } else {
                 config.setInterpolator(ANIM_WORKSPACE_SCALE, OVERSHOOT_1_2);
+                config.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
 
                 // Scale up the recents, if it is not coming from the side
                 RecentsView overview = mActivity.getOverviewPanel();
                 if (overview.getVisibility() != VISIBLE || overview.getContentAlpha() == 0) {
-                    SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
+                    RECENTS_SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
                 }
             }
             config.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2);
@@ -225,7 +233,6 @@
                     : OVERSHOOT_1_7;
             config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, translationInterpolator);
             config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, translationInterpolator);
-            config.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
         } else if (fromState == HINT_STATE && toState == NORMAL) {
             config.setInterpolator(ANIM_DEPTH, DEACCEL_3);
             if (mHintToNormalDuration == -1) {
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 c1a585e..9cc3e1a 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
@@ -19,13 +19,13 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
 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.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_EDU;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
 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;
@@ -45,6 +45,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.graphics.OverviewScrim;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.testing.TestProtocol;
@@ -52,7 +53,9 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.TouchController;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.AssistantUtilities;
+import com.android.quickstep.util.OverviewToHomeAnim;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 
@@ -63,6 +66,8 @@
         SingleAxisSwipeDetector.Listener {
 
     private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL_3;
+    // The min amount of overview scrim we keep during the transition.
+    private static final float OVERVIEW_TO_HOME_SCRIM_MULTIPLIER = 0.5f;
 
     private final Launcher mLauncher;
     private final SingleAxisSwipeDetector mSwipeDetector;
@@ -156,8 +161,13 @@
         final PendingAnimation builder = new PendingAnimation(accuracy);
         if (mStartState.overviewUi) {
             RecentsView recentsView = mLauncher.getOverviewPanel();
-            builder.setFloat(recentsView, ADJACENT_PAGE_OFFSET,
-                    -mPullbackDistance / recentsView.getPageOffsetScale(), PULLBACK_INTERPOLATOR);
+            AnimatorControllerWithResistance.createRecentsResistanceFromOverviewAnim(mLauncher,
+                    builder);
+
+            builder.setFloat(mLauncher.getDragLayer().getOverviewScrim(),
+                    OverviewScrim.SCRIM_MULTIPLIER, OVERVIEW_TO_HOME_SCRIM_MULTIPLIER,
+                    PULLBACK_INTERPOLATOR);
+
             if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
                 builder.addOnFrameCallback(
                         () -> recentsView.redrawLiveTile(false /* mightNeedToRefill */));
@@ -212,8 +222,13 @@
                 recentsView.switchToScreenshot(null,
                         () -> recentsView.finishRecentsAnimation(true /* toRecents */, null));
             }
-            mLauncher.getStateManager().goToState(mEndState, true,
-                    () -> onSwipeInteractionCompleted(mEndState));
+            if (mStartState == OVERVIEW) {
+                new OverviewToHomeAnim(mLauncher, () -> onSwipeInteractionCompleted(mEndState))
+                        .animateWithVelocity(velocity);
+            } else {
+                mLauncher.getStateManager().goToState(mEndState, true,
+                        () -> onSwipeInteractionCompleted(mEndState));
+            }
             if (mStartState != mEndState) {
                 logStateChange(mStartState.containerType, logAction);
             }
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 9316938..8f0f683 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
@@ -21,11 +21,8 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
-import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_PEEK;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 
-import android.animation.Animator;
-import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.graphics.PointF;
@@ -35,14 +32,15 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.graphics.OverviewScrim;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.VibratorWrapper;
-import com.android.quickstep.util.StaggeredWorkspaceAnim;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.OverviewToHomeAnim;
 import com.android.quickstep.views.RecentsView;
 
 /**
@@ -62,10 +60,10 @@
 
     private boolean mDidTouchStartInNavBar;
     private boolean mReachedOverview;
-    private boolean mIsOverviewRehidden;
-    private boolean mIsHomeStaggeredAnimFinished;
     // The last recorded displacement before we reached overview.
     private PointF mStartDisplacement = new PointF();
+    private float mStartY;
+    private AnimatorPlaybackController mOverviewResistYAnim;
 
     // Normal to Hint animation has flag SKIP_OVERVIEW, so we update this scrim with this animator.
     private ObjectAnimator mNormalToHintOverviewScrimAnimator;
@@ -123,6 +121,7 @@
                     mToState.getOverviewScrimAlpha(mLauncher));
         }
         mReachedOverview = false;
+        mOverviewResistYAnim = null;
     }
 
     @Override
@@ -137,6 +136,11 @@
     public void onDragEnd(float velocity) {
         super.onDragEnd(velocity);
         mNormalToHintOverviewScrimAnimator = null;
+        if (mLauncher.isInState(OVERVIEW)) {
+            // Normally we would cleanup the state based on mCurrentAnimation, but since we stop
+            // using that when we pause to go to Overview, we need to clean up ourselves.
+            clearState();
+        }
     }
 
     @Override
@@ -160,6 +164,9 @@
         mNormalToHintOverviewScrimAnimator = null;
         mCurrentAnimation.dispatchOnCancelWithoutCancelRunnable(() -> {
             mLauncher.getStateManager().goToState(OVERVIEW, true, () -> {
+                mOverviewResistYAnim = AnimatorControllerWithResistance
+                        .createRecentsResistanceFromOverviewAnim(mLauncher, null)
+                        .createPlaybackController();
                 mReachedOverview = true;
                 maybeSwipeInteractionToOverviewComplete();
             });
@@ -173,13 +180,6 @@
         }
     }
 
-    // Used if flinging back to home after reaching overview
-    private void maybeSwipeInteractionToHomeComplete() {
-        if (mIsHomeStaggeredAnimFinished && mIsOverviewRehidden) {
-            onSwipeInteractionCompleted(NORMAL, Touch.FLING);
-        }
-    }
-
     @Override
     protected boolean handlingOverviewAnim() {
         return mDidTouchStartInNavBar && super.handlingOverviewAnim();
@@ -193,11 +193,17 @@
         if (mMotionPauseDetector.isPaused()) {
             if (!mReachedOverview) {
                 mStartDisplacement.set(xDisplacement, yDisplacement);
+                mStartY = event.getY();
             } else {
                 mRecentsView.setTranslationX((xDisplacement - mStartDisplacement.x)
                         * OVERVIEW_MOVEMENT_FACTOR);
-                mRecentsView.setTranslationY((yDisplacement - mStartDisplacement.y)
-                        * OVERVIEW_MOVEMENT_FACTOR);
+                float yProgress = (mStartDisplacement.y - yDisplacement) / mStartY;
+                if (yProgress > 0 && mOverviewResistYAnim != null) {
+                    mOverviewResistYAnim.setPlayFraction(yProgress);
+                } else {
+                    mRecentsView.setTranslationY((yDisplacement - mStartDisplacement.y)
+                            * OVERVIEW_MOVEMENT_FACTOR);
+                }
             }
             // Stay in Overview.
             return true;
@@ -212,35 +218,8 @@
         StateManager<LauncherState> stateManager = mLauncher.getStateManager();
         boolean goToHomeInsteadOfOverview = isFling;
         if (goToHomeInsteadOfOverview) {
-            if (velocity > 0) {
-                stateManager.goToState(NORMAL, true,
-                        () -> onSwipeInteractionCompleted(NORMAL, Touch.FLING));
-            } else {
-                mIsHomeStaggeredAnimFinished = mIsOverviewRehidden = false;
-
-                StaggeredWorkspaceAnim staggeredWorkspaceAnim = new StaggeredWorkspaceAnim(
-                        mLauncher, velocity, false /* animateOverviewScrim */);
-                staggeredWorkspaceAnim.addAnimatorListener(new AnimationSuccessListener() {
-                    @Override
-                    public void onAnimationSuccess(Animator animator) {
-                        mIsHomeStaggeredAnimFinished = true;
-                        maybeSwipeInteractionToHomeComplete();
-                    }
-                }).start();
-
-                // StaggeredWorkspaceAnim doesn't animate overview, so we handle it here.
-                stateManager.cancelAnimation();
-                StateAnimationConfig config = new StateAnimationConfig();
-                config.duration = OVERVIEW.getTransitionDuration(mLauncher);
-                config.animFlags = PLAY_ATOMIC_OVERVIEW_PEEK;
-                AnimatorSet anim = stateManager.createAtomicAnimation(
-                        stateManager.getState(), NORMAL, config);
-                anim.addListener(AnimationSuccessListener.forRunnable(() -> {
-                    mIsOverviewRehidden = true;
-                    maybeSwipeInteractionToHomeComplete();
-                }));
-                anim.start();
-            }
+            new OverviewToHomeAnim(mLauncher, ()-> onSwipeInteractionCompleted(NORMAL, Touch.FLING))
+                    .animateWithVelocity(velocity);
         }
         if (mReachedOverview) {
             float distanceDp = dpiFromPx(Math.max(
@@ -256,6 +235,13 @@
                     .withEndAction(goToHomeInsteadOfOverview
                             ? null
                             : this::maybeSwipeInteractionToOverviewComplete);
+            if (!goToHomeInsteadOfOverview) {
+                // Return to normal properties for the overview state.
+                StateAnimationConfig config = new StateAnimationConfig();
+                config.duration = duration;
+                LauncherState state = mLauncher.getStateManager().getState();
+                mLauncher.getStateManager().createAtomicAnimation(state, state, config).start();
+            }
         }
     }
 
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 1b439d1..5c9fd47 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
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.uioverrides.touchcontrollers;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
@@ -23,7 +22,6 @@
 import static com.android.launcher3.LauncherState.QUICK_SWITCH;
 import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
 import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_5;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
@@ -47,6 +45,8 @@
 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.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.Animator;
@@ -74,7 +74,9 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.util.VibratorWrapper;
+import com.android.quickstep.AnimatedFloat;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.ShelfPeekAnim;
@@ -90,16 +92,17 @@
         BothAxesSwipeDetector.Listener, MotionPauseDetector.OnMotionPauseListener {
 
     /** The minimum progress of the scale/translationY animation until drag end. */
-    private static final float Y_ANIM_MIN_PROGRESS = 0.15f;
+    private static final float Y_ANIM_MIN_PROGRESS = 0.25f;
     private static final Interpolator FADE_OUT_INTERPOLATOR = DEACCEL_5;
     private static final Interpolator TRANSLATE_OUT_INTERPOLATOR = ACCEL_0_75;
-    private static final Interpolator SCALE_DOWN_INTERPOLATOR = DEACCEL;
+    private static final Interpolator SCALE_DOWN_INTERPOLATOR = LINEAR;
 
     private final BaseQuickstepLauncher mLauncher;
     private final BothAxesSwipeDetector mSwipeDetector;
     private final ShelfPeekAnim mShelfPeekAnim;
     private final float mXRange;
     private final float mYRange;
+    private final float mMaxYProgress;
     private final MotionPauseDetector mMotionPauseDetector;
     private final float mMotionPauseMinDisplacement;
     private final LauncherRecentsView mRecentsView;
@@ -113,7 +116,7 @@
     // and the other two to set overview properties based on x and y progress.
     private AnimatorPlaybackController mNonOverviewAnim;
     private AnimatorPlaybackController mXOverviewAnim;
-    private AnimatorPlaybackController mYOverviewAnim;
+    private AnimatedFloat mYOverviewAnim;
 
     public NoButtonQuickSwitchTouchController(BaseQuickstepLauncher launcher) {
         mLauncher = launcher;
@@ -123,6 +126,7 @@
         mXRange = mLauncher.getDeviceProfile().widthPx / 2f;
         mYRange = LayoutUtils.getShelfTrackingDistance(
             mLauncher, mLauncher.getDeviceProfile(), mRecentsView.getPagedOrientationHandler());
+        mMaxYProgress = mLauncher.getDeviceProfile().heightPx / mYRange;
         mMotionPauseDetector = new MotionPauseDetector(mLauncher);
         mMotionPauseMinDisplacement = mLauncher.getResources().getDimension(
                 R.dimen.motion_pause_detector_min_displacement_from_app);
@@ -244,7 +248,7 @@
         final LauncherState toState = OVERVIEW;
 
         // Set RecentView's initial properties.
-        SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
+        RECENTS_SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
         ADJACENT_PAGE_OFFSET.set(mRecentsView, 1f);
         mRecentsView.setContentAlpha(1);
         mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
@@ -266,11 +270,22 @@
         //   - RecentsView scale
         //   - RecentsView fullscreenProgress
         PendingAnimation yAnim = new PendingAnimation((long) (mYRange * 2));
-        yAnim.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0], SCALE_DOWN_INTERPOLATOR);
+        yAnim.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
+                SCALE_DOWN_INTERPOLATOR);
         yAnim.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
                 toState.getOverviewFullscreenProgress(), SCALE_DOWN_INTERPOLATOR);
-        mYOverviewAnim = yAnim.createPlaybackController();
-        mYOverviewAnim.dispatchOnStart();
+        AnimatorPlaybackController yNormalController = yAnim.createPlaybackController();
+        AnimatorControllerWithResistance yAnimWithResistance = AnimatorControllerWithResistance
+                .createForRecents(yNormalController, mLauncher,
+                        mRecentsView.getPagedViewOrientedState(), mLauncher.getDeviceProfile(),
+                        mRecentsView, RECENTS_SCALE_PROPERTY, mRecentsView,
+                        TASK_SECONDARY_TRANSLATION);
+        mYOverviewAnim = new AnimatedFloat(() -> {
+            if (mYOverviewAnim != null) {
+                yAnimWithResistance.setProgress(mYOverviewAnim.value, mMaxYProgress);
+            }
+        });
+        yNormalController.dispatchOnStart();
     }
 
     @Override
@@ -306,7 +321,7 @@
             mXOverviewAnim.setPlayFraction(xProgress);
         }
         if (mYOverviewAnim != null) {
-            mYOverviewAnim.setPlayFraction(yProgress);
+            mYOverviewAnim.updateValue(yProgress);
         }
         return true;
     }
@@ -353,9 +368,11 @@
         } else if (verticalFling) {
             targetState = velocity.y > 0 ? QUICK_SWITCH : NORMAL;
         } else {
-            // If user isn't flinging, just snap to the closest state based on x progress.
+            // If user isn't flinging, just snap to the closest state.
             boolean passedHorizontalThreshold = mXOverviewAnim.getInterpolatedProgress() > 0.5f;
-            targetState = passedHorizontalThreshold ? QUICK_SWITCH : NORMAL;
+            boolean passedVerticalThreshold = mYOverviewAnim.value > 1f;
+            targetState = passedHorizontalThreshold && !passedVerticalThreshold
+                    ? QUICK_SWITCH : NORMAL;
         }
 
         // Animate the various components to the target state.
@@ -374,9 +391,9 @@
 
         boolean flingUpToNormal = verticalFling && velocity.y < 0 && targetState == NORMAL;
 
-        float yProgress = mYOverviewAnim.getProgressFraction();
+        float yProgress = mYOverviewAnim.value;
         float startYProgress = Utilities.boundToRange(yProgress
-                - velocity.y * getSingleFrameMs(mLauncher) / mYRange, 0f, 1f);
+                - velocity.y * getSingleFrameMs(mLauncher) / mYRange, 0f, mMaxYProgress);
         final float endYProgress;
         if (flingUpToNormal) {
             endYProgress = 1;
@@ -386,12 +403,11 @@
         } else {
             endYProgress = 0;
         }
-        long yDuration = BaseSwipeDetector.calculateDuration(velocity.y,
-                Math.abs(endYProgress - startYProgress));
-        ValueAnimator yOverviewAnim = mYOverviewAnim.getAnimationPlayer();
-        yOverviewAnim.setFloatValues(startYProgress, endYProgress);
+        float yDistanceToCover = Math.abs(endYProgress - startYProgress) * mYRange;
+        long yDuration = (long) (yDistanceToCover / Math.max(1f, Math.abs(velocity.y)));
+        ValueAnimator yOverviewAnim = mYOverviewAnim.animateToValue(startYProgress, endYProgress);
         yOverviewAnim.setDuration(yDuration);
-        mYOverviewAnim.dispatchOnStart();
+        mYOverviewAnim.updateValue(startYProgress);
 
         ValueAnimator nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
         if (flingUpToNormal && !mIsHomeScreenVisible) {
@@ -456,7 +472,7 @@
             mXOverviewAnim.getAnimationPlayer().cancel();
         }
         if (mYOverviewAnim != null) {
-            mYOverviewAnim.getAnimationPlayer().cancel();
+            mYOverviewAnim.cancelAnimation();
         }
         mShelfPeekAnim.setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
         mMotionPauseDetector.clear();
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 0ee5d04..2cfbc2b 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
@@ -25,6 +25,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.animation.Interpolator;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
@@ -82,7 +83,15 @@
         mDetector = new SingleAxisSwipeDetector(activity, this, dir);
     }
 
-    private boolean canInterceptTouch() {
+    private boolean canInterceptTouch(MotionEvent ev) {
+        if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0) {
+            // Don't intercept swipes on the nav bar, as user might be trying to go home
+            // during a task dismiss animation.
+            if (mCurrentAnimation != null) {
+                mCurrentAnimation.getAnimationPlayer().end();
+            }
+            return false;
+        }
         if (mCurrentAnimation != null) {
             mCurrentAnimation.forceFinishIfCloseToEnd();
         }
@@ -118,7 +127,7 @@
             clearState();
         }
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            mNoIntercept = !canInterceptTouch();
+            mNoIntercept = !canInterceptTouch(ev);
             if (mNoIntercept) {
                 return false;
             }
@@ -206,14 +215,19 @@
         long maxDuration = 2 * secondaryLayerDimension;
         int verticalFactor = orientationHandler.getTaskDragDisplacementFactor(mIsRtl);
         int secondaryTaskDimension = orientationHandler.getSecondaryDimension(mTaskBeingDragged);
+        // The interpolator controlling the most prominent visual movement. We use this to determine
+        // whether we passed SUCCESS_TRANSITION_PROGRESS.
+        final Interpolator currentInterpolator;
         if (goingUp) {
+            currentInterpolator = Interpolators.LINEAR;
             mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
                     true /* animateTaskView */, true /* removeTask */, maxDuration);
 
             mEndDisplacement = -secondaryTaskDimension;
         } else {
+            currentInterpolator = Interpolators.ZOOM_IN;
             mPendingAnimation = mRecentsView.createTaskLaunchAnimation(
-                    mTaskBeingDragged, maxDuration, Interpolators.ZOOM_IN);
+                    mTaskBeingDragged, maxDuration, currentInterpolator);
 
             // Since the thumbnail is what is filling the screen, based the end displacement on it.
             View thumbnailView = mTaskBeingDragged.getThumbnail();
@@ -228,6 +242,9 @@
         }
         mCurrentAnimation = mPendingAnimation.createPlaybackController()
                 .setOnCancelRunnable(this::clearState);
+        // Setting this interpolator doesn't affect the visual motion, but is used to determine
+        // whether we successfully reached the target state in onDragEnd().
+        mCurrentAnimation.getTarget().setInterpolator(currentInterpolator);
         onUserControlledAnimationCreated(mCurrentAnimation);
         mCurrentAnimation.getTarget().addListener(this);
         mCurrentAnimation.dispatchOnStart();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index de83caf..55f5424 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -24,6 +24,7 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
+import android.app.ActivityManager.RunningTaskInfo;
 import android.util.Log;
 import android.view.animation.Interpolator;
 
@@ -52,17 +53,17 @@
 
     private final BaseActivityInterface<?, T> mActivityInterface;
     // The id of the currently running task that is transitioning to overview.
-    private final int mTargetTaskId;
+    private final RunningTaskInfo mTargetTask;
     private final RecentsAnimationDeviceState mDeviceState;
 
     private T mActivity;
     private RecentsView mRecentsView;
 
     AppToOverviewAnimationProvider(
-            BaseActivityInterface<?, T> activityInterface, int targetTaskId,
+            BaseActivityInterface<?, T> activityInterface, RunningTaskInfo targetTask,
             RecentsAnimationDeviceState deviceState) {
         mActivityInterface = activityInterface;
-        mTargetTaskId = targetTaskId;
+        mTargetTask = targetTask;
         mDeviceState = deviceState;
     }
 
@@ -73,13 +74,13 @@
      * @param wasVisible true if it was visible before
      */
     boolean onActivityReady(T activity, Boolean wasVisible) {
-        activity.<RecentsView>getOverviewPanel().showCurrentTask(mTargetTaskId);
+        activity.<RecentsView>getOverviewPanel().showCurrentTask(mTargetTask);
         AbstractFloatingView.closeAllOpenViews(activity, wasVisible);
         BaseActivityInterface.AnimationFactory factory = mActivityInterface.prepareRecentsUI(
                 mDeviceState,
                 wasVisible, (controller) -> {
-                    controller.dispatchOnStart();
-                    controller.getAnimationPlayer().end();
+                    controller.getNormalController().dispatchOnStart();
+                    controller.getNormalController().getAnimationPlayer().end();
                 });
         factory.createActivityInterface(RECENTS_LAUNCH_DURATION);
         factory.setRecentsAttachedToAppWindow(true, false);
@@ -122,7 +123,8 @@
                 wallpaperTargets, MODE_CLOSING);
 
         // Use the top closing app to determine the insets for the animation
-        RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(mTargetTaskId);
+        RemoteAnimationTargetCompat runningTaskTarget = mTargetTask == null ? null
+                : targets.findTask(mTargetTask.taskId);
         if (runningTaskTarget == null) {
             Log.e(TAG, "No closing app");
             return pa.buildAnim();
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 aba5ab6..c90f701 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -15,6 +15,8 @@
  */
 package com.android.quickstep;
 
+import static android.widget.Toast.LENGTH_SHORT;
+
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
@@ -27,11 +29,13 @@
 import android.os.Build;
 import android.util.Log;
 import android.view.MotionEvent;
+import android.widget.Toast;
 
 import androidx.annotation.CallSuper;
 import androidx.annotation.UiThread;
 
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.VibratorWrapper;
@@ -140,10 +144,10 @@
             mRecentsView.getNextPageTaskView().launchTask(false /* animate */,
                     true /* freezeTaskList */);
         } else {
-            int taskId = mRecentsView.getNextPageTaskView().getTask().key.id;
             if (!mCanceled) {
-                TaskView nextTask = mRecentsView.getTaskView(taskId);
+                TaskView nextTask = mRecentsView.getNextPageTaskView();
                 if (nextTask != null) {
+                    int taskId = nextTask.getTask().key.id;
                     mGestureState.updateLastStartedTaskId(taskId);
                     boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
                             .contains(taskId);
@@ -160,6 +164,10 @@
                                     mRecentsAnimationController.finish(true /* toRecents */, null);
                                 }
                             }, MAIN_EXECUTOR.getHandler());
+                } else {
+                    mActivityInterface.onLaunchTaskFailed();
+                    Toast.makeText(mContext, R.string.activity_not_available, LENGTH_SHORT).show();
+                    mRecentsAnimationController.finish(true /* toRecents */, null);
                 }
             }
             mCanceled = false;
@@ -357,8 +365,7 @@
      */
     protected void applyWindowTransform() {
         if (mWindowTransitionController != null) {
-            float progress = mCurrentShift.value / mDragLengthFactor;
-            mWindowTransitionController.setPlayFraction(progress);
+            mWindowTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
         }
         if (mRecentsAnimationTargets != null) {
             if (mRecentsViewScrollLinked) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
index f0f3d0f..0aa1486 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
@@ -45,7 +44,6 @@
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
 
 import android.animation.Animator;
-import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
@@ -67,7 +65,6 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.UserEventDispatcher;
@@ -80,6 +77,7 @@
 import com.android.quickstep.GestureState.GestureEndTarget;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
@@ -181,8 +179,7 @@
     private ThumbnailData mTaskSnapshot;
 
     // Used to control launcher components throughout the swipe gesture.
-    private AnimatorPlaybackController mLauncherTransitionController;
-    private boolean mHasLauncherTransitionControllerStarted;
+    private AnimatorControllerWithResistance mLauncherTransitionController;
 
     private AnimationFactory mAnimationFactory = (t) -> { };
 
@@ -266,10 +263,6 @@
         mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED | STATE_RESUME_LAST_TASK,
                 this::notifyTransitionCancelled);
 
-        mGestureState.runOnceAtState(STATE_END_TARGET_SET,
-                () -> mDeviceState.onEndTargetCalculated(mGestureState.getEndTarget(),
-                        mActivityInterface));
-
         if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             mStateCallback.addChangeListener(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT
                             | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT,
@@ -337,7 +330,7 @@
         if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
             return;
         }
-        mTaskViewSimulator.setRecentsConfiguration(mActivity.getResources().getConfiguration());
+        mTaskViewSimulator.setRecentsRotation(mActivity.getDisplay().getRotation());
 
         // If we've already ended the gesture and are going home, don't prepare recents UI,
         // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
@@ -400,6 +393,11 @@
         mGestureState.getActivityInterface().setOnDeferredActivityLaunchCallback(
                 mOnDeferredActivityLaunch);
 
+        mGestureState.runOnceAtState(STATE_END_TARGET_SET,
+                () -> mDeviceState.getRotationTouchHelper().
+                        onEndTargetCalculated(mGestureState.getEndTarget(),
+                        mActivityInterface));
+
         notifyGestureStartedAsync();
     }
 
@@ -423,7 +421,7 @@
     }
 
     protected void notifyGestureAnimationStartToRecents() {
-        mRecentsView.onGestureAnimationStart(mGestureState.getRunningTaskId());
+        mRecentsView.onGestureAnimationStart(mGestureState.getRunningTask());
     }
 
     private void launcherFrameDrawn() {
@@ -490,6 +488,20 @@
             recentsAttachedToAppWindow = mIsShelfPeeking || mIsLikelyToStartNewTask;
         }
         mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
+
+        // Reapply window transform throughout the attach animation, as the animation affects how
+        // much the window is bound by overscroll (vs moving freely).
+        if (animate) {
+            ValueAnimator reapplyWindowTransformAnim = ValueAnimator.ofFloat(0, 1);
+            reapplyWindowTransformAnim.addUpdateListener(anim -> {
+                if (mRunningWindowAnim == null) {
+                    applyWindowTransform();
+                }
+            });
+            reapplyWindowTransformAnim.setDuration(RECENTS_ATTACH_DURATION).start();
+        } else {
+            applyWindowTransform();
+        }
     }
 
     @Override
@@ -527,11 +539,11 @@
 
     /**
      * 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.
+     * (it has its own animation).
      * @return Whether we can create the launcher controller or update its progress.
      */
     private boolean canCreateNewOrUpdateExistingLauncherTransitionController() {
-        return mGestureState.getEndTarget() != HOME && !mHasLauncherTransitionControllerStarted;
+        return mGestureState.getEndTarget() != HOME;
     }
 
     @Override
@@ -541,10 +553,9 @@
         return result;
     }
 
-    private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) {
+    private void onAnimatorPlaybackControllerCreated(AnimatorControllerWithResistance anim) {
         mLauncherTransitionController = anim;
-        mLauncherTransitionController.dispatchSetInterpolator(t -> t * mDragLengthFactor);
-        mLauncherTransitionController.dispatchOnStart();
+        mLauncherTransitionController.getNormalController().dispatchOnStart();
         updateLauncherTransitionProgress();
     }
 
@@ -581,10 +592,7 @@
                 || !canCreateNewOrUpdateExistingLauncherTransitionController()) {
             return;
         }
-        // Normalize the progress to 0 to 1, as the animation controller will clamp it to that
-        // anyway. The controller mimics the drag length factor by applying it to its interpolators.
-        float progress = mCurrentShift.value / mDragLengthFactor;
-        mLauncherTransitionController.setPlayFraction(progress);
+        mLauncherTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
     }
 
     /**
@@ -1027,31 +1035,6 @@
             windowAnim.start();
             mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
         }
-        // Always play the entire launcher animation when going home, since it is separate from
-        // the animation that has been controlled thus far.
-        if (mGestureState.getEndTarget() == HOME) {
-            start = 0;
-        }
-
-        // We want to use the same interpolator as the window, but need to adjust it to
-        // interpolate over the remaining progress (end - start).
-        TimeInterpolator adjustedInterpolator = Interpolators.mapToProgress(
-                interpolator, start, end);
-        if (mLauncherTransitionController == null) {
-            return;
-        }
-        if (start == end || duration <= 0) {
-            mLauncherTransitionController.dispatchSetInterpolator(t -> end);
-        } else {
-            mLauncherTransitionController.dispatchSetInterpolator(adjustedInterpolator);
-        }
-        mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration));
-
-        if (UNSTABLE_SPRINGS.get()) {
-            mLauncherTransitionController.dispatchOnStart();
-        }
-        mLauncherTransitionController.getAnimationPlayer().start();
-        mHasLauncherTransitionControllerStarted = true;
     }
 
     private void computeRecentsScrollIfInvisible() {
@@ -1172,10 +1155,6 @@
     private void cancelCurrentAnimation() {
         mCanceled = true;
         mCurrentShift.cancelAnimation();
-        if (mLauncherTransitionController != null && mLauncherTransitionController
-                .getAnimationPlayer().isStarted()) {
-            mLauncherTransitionController.getAnimationPlayer().cancel();
-        }
     }
 
     private void invalidateHandler() {
@@ -1201,7 +1180,10 @@
     private void endLauncherTransitionController() {
         setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
         if (mLauncherTransitionController != null) {
-            mLauncherTransitionController.getAnimationPlayer().end();
+            // End the animation, but stay at the same visual progress.
+            mLauncherTransitionController.getNormalController().dispatchSetInterpolator(
+                    t -> Utilities.boundToRange(mCurrentShift.value, 0, 1));
+            mLauncherTransitionController.getNormalController().getAnimationPlayer().end();
             mLauncherTransitionController = null;
         }
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
index 33b9cde..3898f0b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
@@ -27,11 +27,11 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.fallback.RecentsState;
 import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
@@ -84,7 +84,7 @@
     /** 6 */
     @Override
     public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
-            boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
+            boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback) {
         DefaultAnimationFactory factory = new DefaultAnimationFactory(callback);
         factory.initUI();
         return factory;
@@ -140,7 +140,7 @@
     }
 
     @Override
-    public void onExitOverview(RecentsAnimationDeviceState deviceState, Runnable exitRunnable) {
+    public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
         // no-op, fake landscape not supported for 3P
     }
 
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 fc7a119..f60a50b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -15,14 +15,35 @@
  */
 package com.android.quickstep;
 
+import static android.content.Intent.EXTRA_COMPONENT_NAME;
+import static android.content.Intent.EXTRA_USER;
+
+import static com.android.launcher3.GestureNavContract.EXTRA_GESTURE_CONTRACT;
+import static com.android.launcher3.GestureNavContract.EXTRA_ICON_POSITION;
+import static com.android.launcher3.GestureNavContract.EXTRA_ICON_SURFACE;
+import static com.android.launcher3.GestureNavContract.EXTRA_REMOTE_CALLBACK;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
 
 import android.animation.ObjectAnimator;
+import android.annotation.TargetApi;
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.ParcelUuid;
+import android.os.UserHandle;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
 
 import androidx.annotation.NonNull;
 
@@ -32,19 +53,33 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.SpringAnimationBuilder;
 import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.util.TransformParams.BuilderProxy;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
 
+import java.lang.ref.WeakReference;
+import java.util.UUID;
+import java.util.function.Consumer;
+
 /**
  * Handles the navigation gestures when a 3rd party launcher is the default home activity.
  */
+@TargetApi(Build.VERSION_CODES.R)
 public class FallbackSwipeHandler extends
         BaseSwipeUpHandlerV2<RecentsActivity, FallbackRecentsView> {
 
+    /**
+     * Message used for receiving gesture nav contract information. We use a static messenger to
+     * avoid leaking too make binders in case the receiving launcher does not handle the contract
+     * properly.
+     */
+    private static StaticMessageReceiver sMessageReceiver = null;
+
     private FallbackHomeAnimationFactory mActiveAnimationFactory;
     private final boolean mRunningOverHome;
 
@@ -89,7 +124,9 @@
     protected HomeAnimationFactory createHomeAnimationFactory(long duration) {
         mActiveAnimationFactory = new FallbackHomeAnimationFactory(duration);
         ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
-        mContext.startActivity(new Intent(mGestureState.getHomeIntent()), options.toBundle());
+        Intent intent = new Intent(mGestureState.getHomeIntent());
+        mActiveAnimationFactory.addGestureContract(intent);
+        mContext.startActivity(intent, options.toBundle());
         return mActiveAnimationFactory;
     }
 
@@ -130,17 +167,20 @@
     }
 
     private class FallbackHomeAnimationFactory extends HomeAnimationFactory {
-
+        private final Rect mTempRect = new Rect();
         private final TransformParams mHomeAlphaParams = new TransformParams();
         private final AnimatedFloat mHomeAlpha;
 
         private final AnimatedFloat mVerticalShiftForScale = new AnimatedFloat();
-
         private final AnimatedFloat mRecentsAlpha = new AnimatedFloat();
 
+        private final RectF mTargetRect = new RectF();
+        private SurfaceControl mSurfaceControl;
+
         private final long mDuration;
+
+        private RectFSpringAnim mSpringAnim;
         FallbackHomeAnimationFactory(long duration) {
-            super(null);
             mDuration = duration;
 
             if (mRunningOverHome) {
@@ -162,6 +202,15 @@
                     this::updateRecentsActivityTransformDuringHomeAnim);
         }
 
+        @NonNull
+        @Override
+        public RectF getWindowTargetRect() {
+            if (mTargetRect.isEmpty()) {
+                mTargetRect.set(super.getWindowTargetRect());
+            }
+            return mTargetRect;
+        }
+
         private void updateRecentsActivityTransformDuringHomeAnim(SurfaceParams.Builder builder,
                 RemoteAnimationTargetCompat app, TransformParams params) {
             builder.withAlpha(mRecentsAlpha.value);
@@ -218,5 +267,87 @@
                         .start();
             }
         }
+
+        @Override
+        public void setAnimation(RectFSpringAnim anim) {
+            mSpringAnim = anim;
+        }
+
+        private void onMessageReceived(Message msg) {
+            try {
+                Bundle data = msg.getData();
+                RectF position = data.getParcelable(EXTRA_ICON_POSITION);
+                if (!position.isEmpty()) {
+                    mSurfaceControl = data.getParcelable(EXTRA_ICON_SURFACE);
+                    mTargetRect.set(position);
+                    if (mSpringAnim != null) {
+                        mSpringAnim.onTargetPositionChanged();
+                    }
+                }
+            } catch (Exception e) {
+                // Ignore
+            }
+        }
+
+        @Override
+        public void update(RectF currentRect, float progress, float radius) {
+            if (mSurfaceControl != null) {
+                currentRect.roundOut(mTempRect);
+                Transaction t = new Transaction();
+                t.setGeometry(mSurfaceControl, null, mTempRect, Surface.ROTATION_0);
+                t.apply();
+            }
+        }
+
+        private void addGestureContract(Intent intent) {
+            if (mRunningOverHome || mGestureState.getRunningTask() == null) {
+                return;
+            }
+
+            TaskKey key = new TaskKey(mGestureState.getRunningTask());
+            if (key.getComponent() != null) {
+                if (sMessageReceiver == null) {
+                    sMessageReceiver = new StaticMessageReceiver();
+                }
+
+                Bundle gestureNavContract = new Bundle();
+                gestureNavContract.putParcelable(EXTRA_COMPONENT_NAME, key.getComponent());
+                gestureNavContract.putParcelable(EXTRA_USER, UserHandle.of(key.userId));
+                gestureNavContract.putParcelable(EXTRA_REMOTE_CALLBACK,
+                        sMessageReceiver.newCallback(this::onMessageReceived));
+                intent.putExtra(EXTRA_GESTURE_CONTRACT, gestureNavContract);
+            }
+        }
+    }
+
+    private static class StaticMessageReceiver implements Handler.Callback {
+
+        private final Messenger mMessenger =
+                new Messenger(new Handler(Looper.getMainLooper(), this));
+
+        private ParcelUuid mCurrentUID = new ParcelUuid(UUID.randomUUID());
+        private WeakReference<Consumer<Message>> mCurrentCallback = new WeakReference<>(null);
+
+        public Message newCallback(Consumer<Message> callback) {
+            mCurrentUID = new ParcelUuid(UUID.randomUUID());
+            mCurrentCallback = new WeakReference<>(callback);
+
+            Message msg = Message.obtain();
+            msg.replyTo = mMessenger;
+            msg.obj = mCurrentUID;
+            return msg;
+        }
+
+        @Override
+        public boolean handleMessage(@NonNull Message message) {
+            if (mCurrentUID.equals(message.obj)) {
+                Consumer<Message> consumer = mCurrentCallback.get();
+                if (consumer != null) {
+                    consumer.accept(message);
+                    return true;
+                }
+            }
+            return false;
+        }
     }
 }
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 edefbe1..cd4be18 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -39,7 +39,6 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.DiscoveryBounce;
-import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.statehandlers.DepthController;
@@ -50,6 +49,7 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
 import com.android.quickstep.views.RecentsView;
@@ -105,7 +105,7 @@
         // recents, we assume the first task is invisible, making translation off by one task.
         launcher.getStateManager().reapplyState();
         launcher.getRootView().setForceHideBackArrow(false);
-        notifyRecentsOfOrientation(deviceState);
+        notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
     }
 
     @Override
@@ -119,8 +119,8 @@
 
     @Override
     public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
-            boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
-        notifyRecentsOfOrientation(deviceState);
+            boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback) {
+        notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
         DefaultAnimationFactory factory = new DefaultAnimationFactory(callback) {
             @Override
             public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator,
@@ -228,7 +228,7 @@
 
 
     @Override
-    public void onExitOverview(RecentsAnimationDeviceState deviceState, Runnable exitRunnable) {
+    public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
         final StateManager<LauncherState> stateManager = getCreatedActivity().getStateManager();
         stateManager.addStateListener(
                 new StateManager.StateListener<LauncherState>() {
@@ -244,11 +244,11 @@
                 });
     }
 
-    private void notifyRecentsOfOrientation(RecentsAnimationDeviceState deviceState) {
+    private void notifyRecentsOfOrientation(RotationTouchHelper rotationTouchHelper) {
         // reset layout on swipe to home
         RecentsView recentsView = getCreatedActivity().getOverviewPanel();
-        recentsView.setLayoutRotation(deviceState.getCurrentActiveRotation(),
-                deviceState.getDisplayRotation());
+        recentsView.setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
+                rotationTouchHelper.getDisplayRotation());
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index fa7d268..052d0a6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -16,6 +16,7 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 
 import android.animation.AnimatorSet;
 import android.content.Context;
@@ -28,6 +29,7 @@
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.views.FloatingIconView;
+import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -72,36 +74,39 @@
             mActivity.getRootView().setForceHideBackArrow(true);
             mActivity.setHintUserWillBeActive();
 
-            homeAnimFactory = new HomeAnimationFactory(floatingIconView) {
-
-                @Override
-                public RectF getWindowTargetRect() {
-                    if (canUseWorkspaceView) {
+            if (canUseWorkspaceView) {
+                // We want the window alpha to be 0 once this threshold is met, so that the
+                // FolderIconView can be seen morphing into the icon shape.
+                float windowAlphaThreshold = 1f - SHAPE_PROGRESS_DURATION;
+                homeAnimFactory = new LauncherHomeAnimationFactory() {
+                    @Override
+                    public RectF getWindowTargetRect() {
                         return iconLocation;
-                    } else {
-                        return super.getWindowTargetRect();
                     }
-                }
 
-                @NonNull
-                @Override
-                public AnimatorPlaybackController createActivityAnimationToHome() {
-                    // Return an empty APC here since we have an non-user controlled animation
-                    // to home.
-                    long accuracy = 2 * Math.max(mDp.widthPx, mDp.heightPx);
-                    return mActivity.getStateManager().createAnimationToNewWorkspace(
-                            NORMAL, accuracy, 0 /* animComponents */);
-                }
+                    @Override
+                    public void setAnimation(RectFSpringAnim anim) {
+                        anim.addAnimatorListener(floatingIconView);
+                        floatingIconView.setOnTargetChangeListener(anim::onTargetPositionChanged);
+                        floatingIconView.setFastFinishRunnable(anim::end);
+                    }
 
-                @Override
-                public void playAtomicAnimation(float velocity) {
-                    new StaggeredWorkspaceAnim(mActivity, velocity,
-                            true /* animateOverviewScrim */).start();
-                }
-            };
+                    @Override
+                    public void update(RectF currentRect, float progress, float radius) {
+                        floatingIconView.update(currentRect, 1f, progress, windowAlphaThreshold,
+                                radius, false);
+                    }
 
+                    @Override
+                    public void onCancel() {
+                        floatingIconView.fastFinish();
+                    }
+                };
+            } else {
+                homeAnimFactory = new LauncherHomeAnimationFactory();
+            }
         } else {
-            homeAnimFactory = new HomeAnimationFactory(null) {
+            homeAnimFactory = new HomeAnimationFactory() {
                 @Override
                 public AnimatorPlaybackController createActivityAnimationToHome() {
                     return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
@@ -118,4 +123,22 @@
         mRecentsAnimationController.finish(
                 true /* toRecents */, callback, true /* sendUserLeaveHint */);
     }
+
+    private class LauncherHomeAnimationFactory extends HomeAnimationFactory {
+        @NonNull
+        @Override
+        public AnimatorPlaybackController createActivityAnimationToHome() {
+            // Return an empty APC here since we have an non-user controlled animation
+            // to home.
+            long accuracy = 2 * Math.max(mDp.widthPx, mDp.heightPx);
+            return mActivity.getStateManager().createAnimationToNewWorkspace(
+                    NORMAL, accuracy, 0 /* animComponents */);
+        }
+
+        @Override
+        public void playAtomicAnimation(float velocity) {
+            new StaggeredWorkspaceAnim(mActivity, velocity,
+                    true /* animateOverviewScrim */).start();
+        }
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverscrollPluginFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverscrollPluginFactory.java
new file mode 100644
index 0000000..4c261ab
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverscrollPluginFactory.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.systemui.plugins.OverscrollPlugin;
+
+/**
+ * Resource overrideable factory for forcing a local overscroll plugin.
+ * Override {@link R.string#overscroll_plugin_factory_class} to set a different class.
+ */
+public class OverscrollPluginFactory implements ResourceBasedOverride {
+    public static final MainThreadInitializedObject<OverscrollPluginFactory> INSTANCE = forOverride(
+            OverscrollPluginFactory.class,
+            R.string.overscroll_plugin_factory_class);
+
+    /**
+     * Get the plugin that is defined locally in launcher, as opposed to a dynamic side loaded one.
+     */
+    public OverscrollPlugin getLocalOverscrollPlugin() {
+        return null;
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
index 434a929..4879db7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
@@ -165,7 +165,7 @@
             mActivityInterface = mOverviewComponentObserver.getActivityInterface();
             mCreateTime = SystemClock.elapsedRealtime();
             mAnimationProvider = new AppToOverviewAnimationProvider<>(mActivityInterface,
-                    RecentsModel.getRunningTaskId(), mDeviceState);
+                    ActivityManagerWrapper.getInstance().getRunningTask(), mDeviceState);
 
             // Preload the plan
             mRecentsModel.getTasks(null);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 6f4d34c..5026f36 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -58,6 +58,12 @@
                         FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get());
                 return response;
             }
+
+            case TestProtocol.REQUEST_OVERVIEW_SHARE_ENABLED: {
+                response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                        FeatureFlags.ENABLE_OVERVIEW_SHARE.get());
+                return response;
+            }
         }
 
         return super.call(method);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
index dc8f1c5..2963b6c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -16,8 +16,7 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
 
 import android.animation.Animator;
 import android.content.Context;
@@ -25,10 +24,8 @@
 import android.graphics.Matrix.ScaleToFit;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.view.animation.Interpolator;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
 import com.android.launcher3.DeviceProfile;
@@ -37,7 +34,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.views.FloatingIconView;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.util.TransformParams;
@@ -48,7 +45,6 @@
 public abstract class SwipeUpAnimationLogic {
 
     protected static final Rect TEMP_RECT = new Rect();
-    private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
 
     protected DeviceProfile mDp;
 
@@ -69,12 +65,8 @@
     protected int mTransitionDragLength;
     // How much further we can drag past recents, as a factor of mTransitionDragLength.
     protected float mDragLengthFactor = 1;
-    // Start resisting when swiping past this factor of mTransitionDragLength.
-    private float mDragLengthFactorStartPullback = 1f;
-    // This is how far down we can scale down, where 0f is full screen and 1f is recents.
-    private float mDragLengthFactorMaxPullback = 1f;
 
-    protected AnimatorPlaybackController mWindowTransitionController;
+    protected AnimatorControllerWithResistance mWindowTransitionController;
 
     public SwipeUpAnimationLogic(Context context, RecentsAnimationDeviceState deviceState,
             GestureState gestureState, TransformParams transformParams) {
@@ -85,7 +77,8 @@
         mTransformParams = transformParams;
 
         mTaskViewSimulator.setLayoutRotation(
-                mDeviceState.getCurrentActiveRotation(), mDeviceState.getDisplayRotation());
+                mDeviceState.getRotationTouchHelper().getCurrentActiveRotation(),
+                mDeviceState.getRotationTouchHelper().getDisplayRotation());
     }
 
     protected void initTransitionEndpoints(DeviceProfile dp) {
@@ -99,19 +92,17 @@
         if (mDeviceState.isFullyGesturalNavMode()) {
             // We can drag all the way to the top of the screen.
             mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
-
-            float startScale = mTaskViewSimulator.getFullScreenScale();
-            // Start pulling back when RecentsView scale is 0.75f, and let it go down to 0.5f.
-            mDragLengthFactorStartPullback = (0.75f - startScale) / (1 - startScale);
-            mDragLengthFactorMaxPullback = (0.5f - startScale) / (1 - startScale);
         } else {
-            mDragLengthFactor = 1;
-            mDragLengthFactorStartPullback = mDragLengthFactorMaxPullback = 1;
+            mDragLengthFactor = 1 + AnimatorControllerWithResistance.TWO_BUTTON_EXTRA_DRAG_FACTOR;
         }
 
         PendingAnimation pa = new PendingAnimation(mTransitionDragLength * 2);
-        mTaskViewSimulator.addAppToOverviewAnim(pa, t -> t * mDragLengthFactor);
-        mWindowTransitionController = pa.createPlaybackController();
+        mTaskViewSimulator.addAppToOverviewAnim(pa, LINEAR);
+        AnimatorPlaybackController normalController = pa.createPlaybackController();
+        mWindowTransitionController = AnimatorControllerWithResistance.createForRecents(
+                normalController, mContext, mTaskViewSimulator.getOrientationState(),
+                mDp, mTaskViewSimulator.recentsViewScale, AnimatedFloat.VALUE,
+                mTaskViewSimulator.recentsViewSecondaryTranslation, AnimatedFloat.VALUE);
     }
 
     @UiThread
@@ -124,13 +115,6 @@
         } else {
             float translation = Math.max(displacement, 0);
             shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
-            if (shift > mDragLengthFactorStartPullback) {
-                float pullbackProgress = Utilities.getProgress(shift,
-                        mDragLengthFactorStartPullback, mDragLengthFactor);
-                pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress);
-                shift = mDragLengthFactorStartPullback + pullbackProgress
-                        * (mDragLengthFactorMaxPullback - mDragLengthFactorStartPullback);
-            }
         }
 
         mCurrentShift.updateValue(shift);
@@ -148,12 +132,6 @@
 
     protected abstract class HomeAnimationFactory {
 
-        public FloatingIconView mIconView;
-
-        public HomeAnimationFactory(@Nullable FloatingIconView iconView) {
-            mIconView = iconView;
-        }
-
         public @NonNull RectF getWindowTargetRect() {
             PagedOrientationHandler orientationHandler = getOrientationHandler();
             DeviceProfile dp = mDp;
@@ -174,6 +152,12 @@
         public void playAtomicAnimation(float velocity) {
             // No-op
         }
+
+        public void setAnimation(RectFSpringAnim anim) { }
+
+        public void update(RectF currentRect, float progress, float radius) { }
+
+        public void onCancel() { }
     }
 
     /**
@@ -184,10 +168,8 @@
     protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
             HomeAnimationFactory homeAnimationFactory) {
         final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
-        final FloatingIconView fiv = homeAnimationFactory.mIconView;
-        final boolean isFloatingIconView = fiv != null;
 
-        mWindowTransitionController.setPlayFraction(startProgress / mDragLengthFactor);
+        mCurrentShift.updateValue(startProgress);
         mTaskViewSimulator.apply(mTransformParams.setProgress(startProgress));
         RectF cropRectF = new RectF(mTaskViewSimulator.getCurrentCropRect());
 
@@ -203,11 +185,7 @@
         windowToHomePositionMap.mapRect(startRect);
 
         RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mContext);
-        if (isFloatingIconView) {
-            anim.addAnimatorListener(fiv);
-            fiv.setOnTargetChangeListener(anim::onTargetPositionChanged);
-            fiv.setFastFinishRunnable(anim::end);
-        }
+        homeAnimationFactory.setAnimation(anim);
 
         SpringAnimationRunner runner = new SpringAnimationRunner(
                 homeAnimationFactory, cropRectF, homeToWindowPositionMap);
@@ -242,32 +220,27 @@
 
         final RectF mWindowCurrentRect = new RectF();
         final Matrix mHomeToWindowPositionMap;
+        final HomeAnimationFactory mAnimationFactory;
 
-        final FloatingIconView mFIV;
         final AnimatorPlaybackController mHomeAnim;
         final RectF mCropRectF;
 
         final float mStartRadius;
         final float mEndRadius;
-        final float mWindowAlphaThreshold;
 
         SpringAnimationRunner(HomeAnimationFactory factory, RectF cropRectF,
                 Matrix homeToWindowPositionMap) {
+            mAnimationFactory = factory;
             mHomeAnim = factory.createActivityAnimationToHome();
             mCropRectF = cropRectF;
             mHomeToWindowPositionMap = homeToWindowPositionMap;
 
             cropRectF.roundOut(mCropRect);
-            mFIV = factory.mIconView;
 
             // End on a "round-enough" radius so that the shape reveal doesn't have to do too much
             // rounding at the end of the animation.
             mStartRadius = mTaskViewSimulator.getCurrentCornerRadius();
             mEndRadius = cropRectF.width() / 2f;
-
-            // We want the window alpha to be 0 once this threshold is met, so that the
-            // FolderIconView can be seen morphing into the icon shape.
-            mWindowAlphaThreshold = mFIV != null ? 1f - SHAPE_PROGRESS_DURATION : 1f;
         }
 
         @Override
@@ -282,10 +255,7 @@
                     .setCornerRadius(cornerRadius);
 
             mTransformParams.applySurfaceParams(mTransformParams.createSurfaceParams(this));
-            if (mFIV != null) {
-                mFIV.update(currentRect, 1f, progress,
-                        mWindowAlphaThreshold, mMatrix.mapRadius(cornerRadius), false);
-            }
+            mAnimationFactory.update(currentRect, progress, mMatrix.mapRadius(cornerRadius));
         }
 
         @Override
@@ -298,9 +268,7 @@
 
         @Override
         public void onCancel() {
-            if (mFIV != null) {
-                mFIV.fastFinish();
-            }
+            mAnimationFactory.onCancel();
         }
 
         @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 e9614d1..36579ec 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
@@ -18,7 +18,7 @@
 
 import static android.view.Surface.ROTATION_0;
 
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
 import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
 
 import android.annotation.SuppressLint;
@@ -38,13 +38,11 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.ResourceBasedOverride;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.TaskThumbnailView;
 import com.android.quickstep.views.TaskView;
-import com.android.systemui.plugins.OverscrollPlugin;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
@@ -90,20 +88,22 @@
         return shortcuts;
     }
 
-    public static final MainThreadInitializedObject<TaskOverlayFactory> INSTANCE =
-            forOverride(TaskOverlayFactory.class, R.string.task_overlay_factory_class);
-
-    /**
-     * @return a launcher-provided OverscrollPlugin if available, otherwise null
-     */
-    public OverscrollPlugin getLocalOverscrollPlugin() {
-        return null;
-    }
-
     public TaskOverlay createOverlay(TaskThumbnailView thumbnailView) {
         return new TaskOverlay(thumbnailView);
     }
 
+    /**
+     * Subclasses can attach any system listeners in this method, must be paired with
+     * {@link #removeListeners()}
+     */
+    public void initListeners() { }
+
+    /**
+     * Subclasses should remove any system listeners in this method, must be paired with
+     * {@link #initListeners()}
+     */
+    public void removeListeners() { }
+
     /** Note that these will be shown in order from top to bottom, if available for the task. */
     private static final TaskShortcutFactory[] MENU_OPTIONS = new TaskShortcutFactory[]{
             TaskShortcutFactory.APP_INFO,
@@ -146,26 +146,29 @@
          */
         public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
                 boolean rotated) {
-            final boolean isAllowedByPolicy = thumbnail.isRealSnapshot;
+            getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null);
 
-            getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
+            if (thumbnail != null) {
+                getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
+                final boolean isAllowedByPolicy = thumbnail.isRealSnapshot;
 
-            getActionsView().setCallbacks(new OverlayUICallbacks() {
-                @Override
-                public void onShare() {
-                    if (isAllowedByPolicy) {
-                        mImageApi.startShareActivity();
-                    } else {
-                        showBlockedByPolicyMessage();
+                getActionsView().setCallbacks(new OverlayUICallbacks() {
+                    @Override
+                    public void onShare() {
+                        if (isAllowedByPolicy) {
+                            mImageApi.startShareActivity();
+                        } else {
+                            showBlockedByPolicyMessage();
+                        }
                     }
-                }
 
-                @SuppressLint("NewApi")
-                @Override
-                public void onScreenshot() {
-                    saveScreenshot(task);
-                }
-            });
+                    @SuppressLint("NewApi")
+                    @Override
+                    public void onScreenshot() {
+                        saveScreenshot(task);
+                    }
+                });
+            }
         }
 
         /**
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 6e0b517..7b91001 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -258,6 +258,7 @@
 
     private static boolean sConnected = false;
     private static boolean sIsInitialized = false;
+    private RotationTouchHelper mRotationTouchHelper;
 
     public static boolean isConnected() {
         return sConnected;
@@ -298,6 +299,7 @@
         mDeviceState = new RecentsAnimationDeviceState(this);
         mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
         mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
+        mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
         ProtoTracer.INSTANCE.get(this).add(this);
 
         sConnected = true;
@@ -326,7 +328,7 @@
         mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(),
                 mMainChoreographer, this::onInputEvent);
 
-        mDeviceState.updateGestureTouchRegions();
+        mRotationTouchHelper.updateGestureTouchRegions();
     }
 
     /**
@@ -470,9 +472,9 @@
             if (TestProtocol.sDebugTracing) {
                 Log.d(TestProtocol.NO_SWIPE_TO_HOME, "TouchInteractionService.onInputEvent:DOWN");
             }
-            mDeviceState.setOrientationTransformIfNeeded(event);
+            mRotationTouchHelper.setOrientationTransformIfNeeded(event);
 
-            if (mDeviceState.isInSwipeUpTouchRegion(event)) {
+            if (mRotationTouchHelper.isInSwipeUpTouchRegion(event)) {
                 if (TestProtocol.sDebugTracing) {
                     Log.d(TestProtocol.NO_SWIPE_TO_HOME,
                             "TouchInteractionService.onInputEvent:isInSwipeUpTouchRegion");
@@ -509,7 +511,7 @@
             // Other events
             if (mUncheckedConsumer != InputConsumer.NO_OP) {
                 // Only transform the event if we are handling it in a proper consumer
-                mDeviceState.setOrientationTransformIfNeeded(event);
+                mRotationTouchHelper.setOrientationTransformIfNeeded(event);
             }
         }
 
@@ -547,7 +549,7 @@
             gestureState.updatePreviouslyAppearedTaskIds(
                     previousGestureState.getPreviouslyAppearedTaskIds());
         } else {
-            gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.0",
+            gestureState.updateRunningTask(TraceHelper.allowIpcs("getRunningTask.0",
                     () -> mAM.getRunningTask(false /* filterOnlyVisibleRecents */)));
         }
         return gestureState;
@@ -595,9 +597,8 @@
             if (FeatureFlags.ENABLE_QUICK_CAPTURE_GESTURE.get()) {
                 OverscrollPlugin plugin = null;
                 if (FeatureFlags.FORCE_LOCAL_OVERSCROLL_PLUGIN.get()) {
-                    TaskOverlayFactory factory =
-                            TaskOverlayFactory.INSTANCE.get(getApplicationContext());
-                    plugin = factory.getLocalOverscrollPlugin();  // may be null
+                    plugin = OverscrollPluginFactory.INSTANCE.get(
+                            getApplicationContext()).getLocalOverscrollPlugin();
                 }
 
                 // If not local plugin was forced, use the actual overscroll plugin if available.
@@ -660,7 +661,7 @@
         if (AssistantUtilities.isExcludedAssistant(gestureState.getRunningTask())) {
             // In the case where we are in the excluded assistant state, ignore it and treat the
             // running activity as the task behind the assistant
-            gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.assistant",
+            gestureState.updateRunningTask(TraceHelper.allowIpcs("getRunningTask.assistant",
                     () -> mAM.getRunningTask(true /* filterOnlyVisibleRecents */)));
             ComponentName homeComponent = mOverviewComponentObserver.getHomeIntent().getComponent();
             ComponentName runningComponent =
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index 3f1e7ba..24a7610 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -15,17 +15,19 @@
  */
 package com.android.quickstep.fallback;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
 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 static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
@@ -82,10 +84,12 @@
                 MultiValueAlpha.VALUE, buttonAlpha, LINEAR);
 
         float[] scaleAndOffset = state.getOverviewScaleAndOffset(mActivity);
-        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0],
+        setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
                 config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
         setter.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1],
                 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
+        setter.setFloat(mRecentsView, TASK_SECONDARY_TRANSLATION, 0f,
+                config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
 
         setter.setFloat(mRecentsView, TASK_MODALNESS, state.getOverviewModalness(),
                 config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
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 d20bbe9..ffe9d6a 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
@@ -76,7 +76,7 @@
      */
     public void onGestureAnimationStartOnHome(RunningTaskInfo homeTaskInfo) {
         mHomeTaskInfo = homeTaskInfo;
-        onGestureAnimationStart(homeTaskInfo == null ? -1 : homeTaskInfo.taskId);
+        onGestureAnimationStart(homeTaskInfo);
     }
 
     /**
@@ -107,14 +107,15 @@
     }
 
     @Override
-    protected boolean shouldAddDummyTaskView(int runningTaskId) {
-        if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == runningTaskId
+    protected boolean shouldAddDummyTaskView(RunningTaskInfo runningTaskInfo) {
+        if (mHomeTaskInfo != null && runningTaskInfo != null &&
+                mHomeTaskInfo.taskId == runningTaskInfo.taskId
                 && getTaskViewCount() == 0) {
             // Do not add a dummy task if we are running over home with empty recents, so that we
             // show the empty recents message instead of showing a dummy task and later removing it.
             return false;
         }
-        return super.shouldAddDummyTaskView(runningTaskId);
+        return super.shouldAddDummyTaskView(runningTaskInfo);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsState.java
index 211a30c..f15a9de 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsState.java
@@ -90,7 +90,6 @@
         return new float[] { NO_SCALE, NO_OFFSET };
     }
 
-
     private static class ModalState extends RecentsState {
 
         public ModalState(int id, int flags) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
index 5ad48eb..0c2c92c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
@@ -99,7 +99,8 @@
             case ACTION_POINTER_DOWN: {
                 if (mState == STATE_INACTIVE) {
                     int pointerIndex = ev.getActionIndex();
-                    if (mDeviceState.isInSwipeUpTouchRegion(ev, pointerIndex)
+                    if (mDeviceState.getRotationTouchHelper()
+                            .isInSwipeUpTouchRegion(ev, pointerIndex)
                             && mDelegate.allowInterceptByParent()) {
                         setActive(ev);
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index 3a97216..a676390 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -147,7 +147,7 @@
                 if (!mThresholdCrossed) {
                     // Cancel interaction in case of multi-touch interaction
                     int ptrIdx = ev.getActionIndex();
-                    if (!mDeviceState.isInSwipeUpTouchRegion(ev, ptrIdx)) {
+                    if (!mDeviceState.getRotationTouchHelper().isInSwipeUpTouchRegion(ev, ptrIdx)) {
                         int action = ev.getAction();
                         ev.setAction(ACTION_CANCEL);
                         finishTouchTracking(ev);
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 26df9c7..6259f1f 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
@@ -59,6 +59,7 @@
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.CachedEventDispatcher;
@@ -86,6 +87,7 @@
     private final NavBarPosition mNavBarPosition;
     private final TaskAnimationManager mTaskAnimationManager;
     private final GestureState mGestureState;
+    private final RotationTouchHelper mRotationTouchHelper;
     private RecentsAnimationCallbacks mActiveCallbacks;
     private final CachedEventDispatcher mRecentsViewDispatcher = new CachedEventDispatcher();
     private final InputMonitorCompat mInputMonitorCompat;
@@ -163,6 +165,7 @@
 
         mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture;
         mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
+        mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
     }
 
     @Override
@@ -230,7 +233,7 @@
                 if (!mPassedPilferInputSlop) {
                     // Cancel interaction in case of multi-touch interaction
                     int ptrIdx = ev.getActionIndex();
-                    if (!mDeviceState.isInSwipeUpTouchRegion(ev, ptrIdx)) {
+                    if (!mRotationTouchHelper.isInSwipeUpTouchRegion(ev, ptrIdx)) {
                         forceCancelGesture(ev);
                     }
                 }
@@ -424,7 +427,7 @@
 
     @Override
     public void notifyOrientationSetup() {
-        mDeviceState.onStartGesture();
+        mRotationTouchHelper.onStartGesture();
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/OverviewToHomeAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/OverviewToHomeAnim.java
new file mode 100644
index 0000000..6278e14
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/OverviewToHomeAnim.java
@@ -0,0 +1,146 @@
+/*
+ * 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 com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
+import static com.android.launcher3.anim.Interpolators.INSTANT;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_COMPONENTS;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
+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 android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.util.Log;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Runs an animation from overview to home. Currently, this animation is just a wrapper around the
+ * normal state transition, in order to keep RecentsView at the same scale and translationY that
+ * it started out at as it translates offscreen. It also scrolls RecentsView to page 0 and may play
+ * a {@link StaggeredWorkspaceAnim} if we're starting from an upward fling.
+ */
+public class OverviewToHomeAnim {
+
+    private static final String TAG = "OverviewToHomeAnim";
+
+    // Constants to specify how to scroll RecentsView to the default page if it's not already there.
+    private static final int DEFAULT_PAGE = 0;
+    private static final int PER_PAGE_SCROLL_DURATION = 150;
+    private static final int MAX_PAGE_SCROLL_DURATION = 750;
+
+    private final Launcher mLauncher;
+    private final Runnable mOnReachedHome;
+
+    // Only run mOnReachedHome when both of these are true.
+    private boolean mIsHomeStaggeredAnimFinished;
+    private boolean mIsOverviewHidden;
+
+    public OverviewToHomeAnim(Launcher launcher, Runnable onReachedHome) {
+        mLauncher = launcher;
+        mOnReachedHome = onReachedHome;
+    }
+
+    /**
+     * Starts the animation. If velocity < 0 (i.e. upwards), also plays a
+     * {@link StaggeredWorkspaceAnim}.
+     */
+    public void animateWithVelocity(float velocity) {
+        StateManager<LauncherState> stateManager = mLauncher.getStateManager();
+        LauncherState startState = stateManager.getState();
+        if (startState != OVERVIEW) {
+            Log.e(TAG, "animateFromOverviewToHome: unexpected start state " + startState);
+        }
+        AnimatorSet anim = new AnimatorSet();
+
+        boolean playStaggeredWorkspaceAnim = velocity < 0;
+        if (playStaggeredWorkspaceAnim) {
+            StaggeredWorkspaceAnim staggeredWorkspaceAnim = new StaggeredWorkspaceAnim(
+                    mLauncher, velocity, false /* animateOverviewScrim */);
+            staggeredWorkspaceAnim.addAnimatorListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    mIsHomeStaggeredAnimFinished = true;
+                    maybeOverviewToHomeAnimComplete();
+                }
+            });
+            anim.play(staggeredWorkspaceAnim.getAnimators());
+        } else {
+            mIsHomeStaggeredAnimFinished = true;
+        }
+
+        RecentsView recentsView = mLauncher.getOverviewPanel();
+        int numPagesToScroll = recentsView.getNextPage() - DEFAULT_PAGE;
+        int scrollDuration = Math.min(MAX_PAGE_SCROLL_DURATION,
+                numPagesToScroll * PER_PAGE_SCROLL_DURATION);
+        int duration = Math.max(scrollDuration, startState.getTransitionDuration(mLauncher));
+
+        StateAnimationConfig config = new UseFirstInterpolatorStateAnimConfig();
+        config.duration = duration;
+        config.animFlags = playStaggeredWorkspaceAnim
+                // StaggeredWorkspaceAnim doesn't animate overview, so we handle it here.
+                ? PLAY_ATOMIC_OVERVIEW_PEEK
+                : ANIM_ALL_COMPONENTS;
+        config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, DEACCEL);
+        config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, FINAL_FRAME);
+        config.setInterpolator(ANIM_OVERVIEW_SCALE, FINAL_FRAME);
+        config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, INSTANT);
+        AnimatorSet stateAnim = stateManager.createAtomicAnimation(
+                startState, NORMAL, config);
+        stateAnim.addListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                mIsOverviewHidden = true;
+                maybeOverviewToHomeAnimComplete();
+            }
+        });
+        anim.play(stateAnim);
+        stateManager.setCurrentAnimation(anim, NORMAL);
+        anim.start();
+        recentsView.snapToPage(DEFAULT_PAGE, duration);
+    }
+
+    private void maybeOverviewToHomeAnimComplete() {
+        if (mIsHomeStaggeredAnimFinished && mIsOverviewHidden) {
+            mOnReachedHome.run();
+        }
+    }
+
+    /**
+     * Wrapper around StateAnimationConfig that doesn't allow interpolators to be set if they are
+     * already set. This ensures they aren't overridden before being used.
+     */
+    private static class UseFirstInterpolatorStateAnimConfig extends StateAnimationConfig {
+        @Override
+        public void setInterpolator(int animId, Interpolator interpolator) {
+            if (mInterpolators[animId] == null || interpolator == null) {
+                super.setInterpolator(animId, interpolator);
+            }
+        }
+    }
+}
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 3cafd42..4120331 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
@@ -205,13 +205,15 @@
         ResourceProvider rp = DynamicResource.provider(v.getContext());
         float stiffness = rp.getFloat(R.dimen.staggered_stiffness);
         float damping = rp.getFloat(R.dimen.staggered_damping_ratio);
+        float endTransY = 0;
+        float springVelocity = Math.abs(mVelocity) * Math.signum(endTransY - mSpringTransY);
         ValueAnimator springTransY = new SpringAnimationBuilder(v.getContext())
                 .setStiffness(stiffness)
                 .setDampingRatio(damping)
                 .setMinimumVisibleChange(1f)
                 .setStartValue(mSpringTransY)
-                .setEndValue(0)
-                .setStartVelocity(mVelocity)
+                .setEndValue(endTransY)
+                .setStartVelocity(springVelocity)
                 .build(v, VIEW_TRANSLATE_Y);
         springTransY.setStartDelay(startDelay);
         springTransY.addListener(new AnimatorListenerAdapter() {
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
index c9ed498..db64464 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -31,7 +31,6 @@
 import android.util.IntProperty;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.PagedOrientationHandler;
@@ -91,8 +90,8 @@
     // RecentsView properties
     public final AnimatedFloat recentsViewScale = new AnimatedFloat();
     public final AnimatedFloat fullScreenProgress = new AnimatedFloat();
+    public final AnimatedFloat recentsViewSecondaryTranslation = new AnimatedFloat();
     private final ScrollState mScrollState = new ScrollState();
-    private final int mPageSpacing;
 
     // Cached calculations
     private boolean mLayoutValid = false;
@@ -106,7 +105,6 @@
         mOrientationState.setGestureActive(true);
 
         mCurrentFullscreenParams = new FullscreenDrawParams(context);
-        mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
     }
 
     /**
@@ -129,8 +127,8 @@
     /**
      * @see com.android.quickstep.views.RecentsView#onConfigurationChanged(Configuration)
      */
-    public void setRecentsConfiguration(Configuration configuration) {
-        mOrientationState.setActivityConfiguration(configuration);
+    public void setRecentsRotation(int recentsRotation) {
+        mOrientationState.setRecentsRotation(recentsRotation);
         mLayoutValid = false;
     }
 
@@ -252,7 +250,8 @@
             int start = mOrientationState.getOrientationHandler()
                     .getPrimaryValue(mTaskRect.left, mTaskRect.top);
             mScrollState.screenCenter = start + mScrollState.scroll + mScrollState.halfPageSize;
-            mScrollState.updateInterpolation(start, mPageSpacing);
+            mScrollState.pageParentScale = recentsViewScale.value;
+            mScrollState.updateInterpolation(start);
             mCurveScale = TaskView.getCurveScaleForInterpolation(mScrollState.linearInterpolation);
         }
 
@@ -276,8 +275,10 @@
         mOrientationState.getOrientationHandler().set(
                 mMatrix, MATRIX_POST_TRANSLATE, mScrollState.scroll);
 
-        // Apply recensView matrix
+        // Apply RecentsView matrix
         mMatrix.postScale(recentsViewScale.value, recentsViewScale.value, mPivot.x, mPivot.y);
+        mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
+                recentsViewSecondaryTranslation.value);
         applyWindowToHomeRotation(mMatrix);
 
         // Crop rect is the inverse of thumbnail matrix
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
index 0979c07..c06dd9c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
@@ -16,6 +16,7 @@
 package com.android.quickstep.views;
 
 import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
@@ -42,8 +43,10 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.util.Themes;
@@ -51,6 +54,7 @@
 
 /**
  * View used to educate the user on how to access All Apps when in No Nav Button navigation mode.
+ * Consumes all touches until after the animation is completed and the view is removed.
  */
 public class AllAppsEduView extends AbstractFloatingView {
 
@@ -111,8 +115,18 @@
     }
 
     @Override
+    public boolean onBackPressed() {
+        return true;
+    }
+
+    @Override
+    public boolean canInterceptEventsInSystemGestureRegion() {
+        return true;
+    }
+
+    @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        return mAnimation != null && mAnimation.isRunning();
+        return true;
     }
 
     private void playAnimation() {
@@ -139,7 +153,12 @@
         config.userControlled = false;
         AnimatorPlaybackController stateAnimationController =
                 mLauncher.getStateManager().createAnimationToNewWorkspace(ALL_APPS, config);
-        float maxAllAppsProgress = 0.15f;
+        float maxAllAppsProgress = mLauncher.getDeviceProfile().isLandscape ? 0.35f : 0.15f;
+
+        AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
+        PendingAnimation allAppsAlpha = new PendingAnimation(config.duration);
+        allAppsController.setAlphas(ALL_APPS, config, allAppsAlpha);
+        mAnimation.play(allAppsAlpha.buildAnim());
 
         ValueAnimator intro = ValueAnimator.ofFloat(0, 1f);
         intro.setInterpolator(LINEAR);
@@ -191,7 +210,8 @@
             @Override
             public void onAnimationEnd(Animator animation) {
                 mAnimation = null;
-                stateAnimationController.dispatchOnCancel();
+                // Handles cancelling the animation used to hint towards All Apps.
+                mLauncher.getStateManager().goToState(NORMAL, false);
                 handleClose(false);
             }
         });
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 846b944..b934c29 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
@@ -51,6 +51,7 @@
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.util.OverviewToHomeAnim;
 import com.android.quickstep.util.TransformParams;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.RecentsExtraCard;
@@ -105,12 +106,14 @@
 
     @Override
     public void startHome() {
+        Runnable onReachedHome = () -> mActivity.getStateManager().goToState(NORMAL, false);
+        OverviewToHomeAnim overviewToHomeAnim = new OverviewToHomeAnim(mActivity, onReachedHome);
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             switchToScreenshot(null,
                     () -> finishRecentsAnimation(true /* toRecents */,
-                            () -> mActivity.getStateManager().goToState(NORMAL)));
+                            () -> overviewToHomeAnim.animateWithVelocity(0)));
         } else {
-            mActivity.getStateManager().goToState(NORMAL);
+            overviewToHomeAnim.animateWithVelocity(0);
         }
     }
 
@@ -189,7 +192,7 @@
 
     @Override
     public boolean shouldUseMultiWindowTaskSizeStrategy() {
-        return TraceHelper.whitelistIpcs("isInMultiWindowMode", mActivity::isInMultiWindowMode);
+        return TraceHelper.allowIpcs("isInMultiWindowMode", mActivity::isInMultiWindowMode);
     }
 
     @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 a2da398..1bf2fbf 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
@@ -70,12 +70,14 @@
 
     @IntDef(flag = true, value = {
             DISABLED_SCROLLING,
-            DISABLED_ROTATED})
+            DISABLED_ROTATED,
+            DISABLED_NO_THUMBNAIL})
     @Retention(RetentionPolicy.SOURCE)
     public @interface ActionsDisabledFlags { }
 
     public static final int DISABLED_SCROLLING = 1 << 0;
     public static final int DISABLED_ROTATED = 1 << 1;
+    public static final int DISABLED_NO_THUMBNAIL = 1 << 2;
 
     private static final int INDEX_CONTENT_ALPHA = 0;
     private static final int INDEX_VISIBILITY_ALPHA = 1;
@@ -103,6 +105,7 @@
     public OverviewActionsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr, 0);
         mMultiValueAlpha = new MultiValueAlpha(this, 4);
+        mMultiValueAlpha.setUpdateVisibility(true);
     }
 
     @Override
@@ -166,7 +169,6 @@
         }
         boolean isHidden = mHiddenFlags != 0;
         mMultiValueAlpha.getProperty(INDEX_HIDDEN_FLAGS_ALPHA).setValue(isHidden ? 0 : 1);
-        setVisibility(isHidden ? INVISIBLE : VISIBLE);
     }
 
     /**
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 7b24b03..1d17f5a 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
@@ -22,9 +22,9 @@
 
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
 import static com.android.launcher3.Utilities.mapToRange;
 import static com.android.launcher3.Utilities.squaredHypot;
@@ -55,15 +55,14 @@
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
-import android.app.ActivityManager;
-import android.content.ComponentName;
+import android.app.ActivityManager.RunningTaskInfo;
 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;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
@@ -80,6 +79,7 @@
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
+import android.view.TouchDelegate;
 import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
@@ -94,6 +94,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 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;
@@ -116,6 +117,7 @@
 import com.android.launcher3.util.DynamicResource;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.OverScroller;
+import com.android.launcher3.util.ResourceBasedOverride.Overrides;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.ViewPool;
 import com.android.quickstep.BaseActivityInterface;
@@ -124,6 +126,7 @@
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.RecentsModel.TaskVisualsChangeListener;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.ViewUtils;
@@ -135,6 +138,7 @@
 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.Task.TaskKey;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.LauncherEventUtil;
@@ -147,7 +151,7 @@
 /**
  * A list of recent tasks.
  */
-@TargetApi(Build.VERSION_CODES.P)
+@TargetApi(Build.VERSION_CODES.R)
 public abstract class RecentsView<T extends StatefulActivity> extends PagedView implements
         Insettable, TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
         InvariantDeviceProfile.OnIDPChangeListener, TaskVisualsChangeListener,
@@ -210,6 +214,37 @@
                 }
             };
 
+    public static final FloatProperty<RecentsView> TASK_SECONDARY_TRANSLATION =
+            new FloatProperty<RecentsView>("taskSecondaryTranslation") {
+                @Override
+                public void setValue(RecentsView recentsView, float v) {
+                    recentsView.setTaskViewsSecondaryTranslation(v);
+                }
+
+                @Override
+                public Float get(RecentsView recentsView) {
+                    return recentsView.mTaskViewsSecondaryTranslation;
+                }
+            };
+
+    /** Same as normal SCALE_PROPERTY, but also updates page offsets that depend on this scale. */
+    public static final FloatProperty<RecentsView> RECENTS_SCALE_PROPERTY =
+            new FloatProperty<RecentsView>("recentsScale") {
+                @Override
+                public void setValue(RecentsView view, float scale) {
+                    view.setScaleX(scale);
+                    view.setScaleY(scale);
+                    view.mLastComputedTaskPushOutDistance = null;
+                    view.updatePageOffsets();
+                    view.setTaskViewsSecondaryTranslation(view.mTaskViewsSecondaryTranslation);
+                }
+
+                @Override
+                public Float get(RecentsView view) {
+                    return view.getScaleX();
+                }
+            };
+
     protected RecentsOrientedState mOrientationState;
     protected final BaseActivityInterface mSizeStrategy;
     protected RecentsAnimationController mRecentsAnimationController;
@@ -217,8 +252,12 @@
     protected SurfaceTransactionApplier mSyncTransactionApplier;
     protected int mTaskWidth;
     protected int mTaskHeight;
+    protected final Rect mLastComputedTaskSize = new Rect();
+    // How much a task that is directly offscreen will be pushed out due to RecentsView scale/pivot.
+    protected Float mLastComputedTaskPushOutDistance = null;
     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;
@@ -242,12 +281,15 @@
 
     private final ViewPool<TaskView> mTaskViewPool;
 
+    private final TaskOverlayFactory mTaskOverlayFactory;
+
     private boolean mDwbToastShown;
     protected boolean mDisallowScrollToClearAll;
     private boolean mOverlayEnabled;
     protected boolean mFreezeViewVisibility;
 
     private float mAdjacentPageOffset = 0;
+    private float mTaskViewsSecondaryTranslation = 0;
 
     /**
      * TODO: Call reloadIdNeeded in onTaskStackChanged.
@@ -377,7 +419,7 @@
                     mOrientationState.setMultiWindowMode(inMultiWindowMode);
                     setLayoutRotation(mOrientationState.getTouchRotation(),
                             mOrientationState.getDisplayRotation());
-                    rotateAllChildTasks();
+                    updateChildTaskOrientations();
                 }
                 if (!inMultiWindowMode && mOverviewStateEnabled) {
                     // TODO: Re-enable layout transitions for addition of the unpinned task
@@ -394,7 +436,7 @@
         mActivity = BaseActivity.fromContext(context);
         mOrientationState = new RecentsOrientedState(
                 context, mSizeStrategy, this::animateRecentsRotationInPlace);
-        mOrientationState.setActivityConfiguration(context.getResources().getConfiguration());
+        mOrientationState.setRecentsRotation(mActivity.getDisplay().getRotation());
 
         mFastFlingVelocity = getResources()
                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
@@ -428,6 +470,11 @@
         updateEmptyMessage();
         mOrientationHandler = mOrientationState.getOrientationHandler();
 
+        mTaskOverlayFactory = Overrides.getObject(
+                TaskOverlayFactory.class,
+                context.getApplicationContext(),
+                R.string.task_overlay_factory_class);
+
         // Initialize quickstep specific cache params here, as this is constructed only once
         mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
     }
@@ -521,6 +568,7 @@
                 mIPinnedStackAnimationListener);
         mOrientationState.initListeners();
         SplitScreenBounds.INSTANCE.addOnChangeListener(this);
+        mTaskOverlayFactory.initListeners();
     }
 
     @Override
@@ -537,6 +585,7 @@
         SplitScreenBounds.INSTANCE.removeOnChangeListener(this);
         mIPinnedStackAnimationListener.setActivity(null);
         mOrientationState.destroyListeners();
+        mTaskOverlayFactory.removeListeners();
     }
 
     @Override
@@ -649,6 +698,16 @@
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
         super.onTouchEvent(ev);
+
+        TaskView taskView = getCurrentPageTaskView();
+        if (taskView != null) {
+            TouchDelegate mChildTouchDelegate = taskView.getIconTouchDelegate(ev);
+            if (mChildTouchDelegate != null && mChildTouchDelegate.onTouchEvent(ev)) {
+                // Keep consuming events to pass to delegate
+                return true;
+            }
+        }
+
         final int x = (int) ev.getX();
         final int y = (int) ev.getY();
         switch (ev.getAction()) {
@@ -865,6 +924,7 @@
     public void getTaskSize(Rect outRect) {
         mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), outRect,
                 mOrientationHandler);
+        mLastComputedTaskSize.set(outRect);
     }
 
     /** Gets the task size for modal state. */
@@ -906,8 +966,8 @@
         final int pageCount = getPageCount();
         for (int i = 0; i < pageCount; i++) {
             View page = getPageAt(i);
-            mScrollState.updateInterpolation(mOrientationHandler.getChildStartWithTranslation(page),
-                    mPageSpacing);
+            mScrollState.updateInterpolation(
+                    mOrientationHandler.getChildStartWithTranslation(page));
             ((PageCallbacks) page).onPageScroll(mScrollState);
         }
     }
@@ -1041,13 +1101,13 @@
     /**
      * Called when a gesture from an app is starting.
      */
-    public void onGestureAnimationStart(int runningTaskId) {
+    public void onGestureAnimationStart(RunningTaskInfo runningTaskInfo) {
         // This needs to be called before the other states are set since it can create the task view
         if (mOrientationState.setGestureActive(true)) {
             updateOrientationHandler();
         }
 
-        showCurrentTask(runningTaskId);
+        showCurrentTask(runningTaskInfo);
         setEnableFreeScroll(false);
         setEnableDrawingLiveTile(false);
         setRunningTaskHidden(true);
@@ -1078,7 +1138,7 @@
         pa.addListener(AnimationSuccessListener.forRunnable(() -> {
             setLayoutRotation(newRotation, mOrientationState.getDisplayRotation());
             mActivity.getDragLayer().recreateControllers();
-            rotateAllChildTasks();
+            updateChildTaskOrientations();
             setRecentsChangedOrientation(false).start();
         }));
         pa.start();
@@ -1099,7 +1159,7 @@
     }
 
 
-    private void rotateAllChildTasks() {
+    private void updateChildTaskOrientations() {
         for (int i = 0; i < getTaskViewCount(); i++) {
             getTaskViewAt(i).setOrientationState(mOrientationState);
         }
@@ -1127,8 +1187,8 @@
     /**
      * Returns true if we should add a dummy taskView for the running task id
      */
-    protected boolean shouldAddDummyTaskView(int runningTaskId) {
-        return getTaskView(runningTaskId) == null;
+    protected boolean shouldAddDummyTaskView(RunningTaskInfo runningTaskInfo) {
+        return runningTaskInfo != null && getTaskView(runningTaskInfo.taskId) == null;
     }
 
     /**
@@ -1137,8 +1197,8 @@
      * All subsequent calls to reload will keep the task as the first item until {@link #reset()}
      * is called.  Also scrolls the view to this task.
      */
-    public void showCurrentTask(int runningTaskId) {
-        if (shouldAddDummyTaskView(runningTaskId)) {
+    public void showCurrentTask(RunningTaskInfo runningTaskInfo) {
+        if (shouldAddDummyTaskView(runningTaskInfo)) {
             boolean wasEmpty = getChildCount() == 0;
             // Add an empty view for now until the task plan is loaded and applied
             final TaskView taskView = mTaskViewPool.getView();
@@ -1148,10 +1208,7 @@
             }
             // The temporary running task is only used for the duration between the start of the
             // gesture and the task list is loaded and applied
-            mTmpRunningTask = new Task(new Task.TaskKey(runningTaskId, 0, new Intent(),
-                    new ComponentName(getContext(), getClass()), 0, 0), null, null, "", "", 0, 0,
-                    false, true, false, false, new ActivityManager.TaskDescription(), 0,
-                    new ComponentName("", ""), false);
+            mTmpRunningTask = Task.from(new TaskKey(runningTaskInfo), runningTaskInfo, false);
             taskView.bind(mTmpRunningTask, mOrientationState);
 
             // Measure and layout immediately so that the scroll values is updated instantly
@@ -1162,7 +1219,7 @@
         }
 
         boolean runningTaskTileHidden = mRunningTaskTileHidden;
-        setCurrentTask(runningTaskId);
+        setCurrentTask(runningTaskInfo == null ? -1 : runningTaskInfo.taskId);
         setCurrentPage(getRunningTaskIndex());
         setRunningTaskViewShowScreenshot(false);
         setRunningTaskHidden(runningTaskTileHidden);
@@ -1332,10 +1389,14 @@
         /**
          * Updates linearInterpolation for the provided child position
          */
-        public void updateInterpolation(float childStart, int pageSpacing) {
-            float pageCenter = childStart + halfPageSize;
+        public void updateInterpolation(float childStart) {
+            float scaledHalfPageSize = halfPageSize / pageParentScale;
+            float pageCenter = childStart + scaledHalfPageSize;
             float distanceFromScreenCenter = screenCenter - pageCenter;
-            float distanceToReachEdge = halfScreenSize + halfPageSize + pageSpacing;
+            // How far the page has to move from the center to be offscreen, taking into account
+            // the EDGE_SCALE_DOWN_FACTOR that will be applied at that position.
+            float distanceToReachEdge = halfScreenSize
+                    + scaledHalfPageSize * (1 - TaskView.EDGE_SCALE_DOWN_FACTOR);
             linearInterpolation = Math.min(1,
                     Math.abs(distanceFromScreenCenter) / distanceToReachEdge);
         }
@@ -1358,7 +1419,7 @@
         FloatProperty<View> secondaryViewTranslate =
             mOrientationHandler.getSecondaryViewTranslate();
         int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView);
-        int verticalFactor = mOrientationHandler.getTaskDismissDirectionFactor();
+        int verticalFactor = mOrientationHandler.getSecondaryTranslationDirectionFactor();
 
         ResourceProvider rp = DynamicResource.provider(mActivity);
         SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_START)
@@ -1652,15 +1713,24 @@
         super.setVisibility(visibility);
         if (mActionsView != null) {
             mActionsView.updateHiddenFlags(HIDDEN_NO_RECENTS, visibility != VISIBLE);
+            if (visibility != VISIBLE) {
+                mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false);
+            }
         }
     }
 
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
-        if (mOrientationState.setActivityConfiguration(newConfig)) {
+        if (mOrientationState.setRecentsRotation(mActivity.getDisplay().getRotation())) {
             updateOrientationHandler();
         }
+        // If overview is in modal state when rotate, reset it to overview state without running
+        // animation.
+        if (mActivity.isInState(OVERVIEW_MODAL_TASK)) {
+            mActivity.getStateManager().goToState(LauncherState.OVERVIEW, false);
+            resetModalVisuals();
+        }
     }
 
     public void setLayoutRotation(int touchRotation, int displayRotation) {
@@ -1680,10 +1750,11 @@
                 : View.LAYOUT_DIRECTION_RTL);
         mClearAllButton.setRotation(mOrientationHandler.getDegreesRotated());
         mActivity.getDragLayer().recreateControllers();
-        boolean isInLandscape = mOrientationState.getTouchRotation() != 0
+        boolean isInLandscape = mOrientationState.getTouchRotation() != ROTATION_0
                 || mOrientationState.getRecentsActivityRotation() != ROTATION_0;
         mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION,
                 !mOrientationState.canRecentsActivityRotate() && isInLandscape);
+        updateChildTaskOrientations();
         resetPaddingFromTaskSize();
         requestLayout();
         // Reapply the current page to update page scrolls.
@@ -1769,14 +1840,15 @@
         setPivotX(mTempPointF.x);
         setPivotY(mTempPointF.y);
         setTaskModalness(mTaskModalness);
+        mLastComputedTaskPushOutDistance = null;
         updatePageOffsets();
         setImportantForAccessibility(isModal() ? IMPORTANT_FOR_ACCESSIBILITY_NO
                 : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
     }
 
     private void updatePageOffsets() {
-        float offset = mAdjacentPageOffset * getWidth();
-        float modalOffset = ACCEL_0_75.getInterpolation(mTaskModalness) * getWidth();
+        float offset = mAdjacentPageOffset;
+        float modalOffset = ACCEL_0_75.getInterpolation(mTaskModalness);
         if (mIsRtl) {
             offset = -offset;
             modalOffset = -modalOffset;
@@ -1785,19 +1857,100 @@
 
         TaskView runningTask = mRunningTaskId == -1 || !mRunningTaskTileHidden
                 ? null : getTaskView(mRunningTaskId);
-        int midPoint = runningTask == null ? -1 : indexOfChild(runningTask);
-        int currentPage = getCurrentPage();
+        int midpoint = runningTask == null ? -1 : indexOfChild(runningTask);
+        int modalMidpoint = getCurrentPage();
+
+        float midpointOffsetSize = 0;
+        float leftOffsetSize = midpoint - 1 >= 0
+                ? -getOffsetSize(midpoint - 1, midpoint, offset)
+                : 0;
+        float rightOffsetSize = midpoint + 1 < count
+                ? getOffsetSize(midpoint + 1, midpoint, offset)
+                : 0;
+
+        float modalMidpointOffsetSize = 0;
+        float modalLeftOffsetSize = modalMidpoint - 1 >= 0
+                ? -getOffsetSize(modalMidpoint - 1, modalMidpoint, modalOffset)
+                : 0;
+        float modalRightOffsetSize = modalMidpoint + 1 < count
+                ? getOffsetSize(modalMidpoint + 1, modalMidpoint, modalOffset)
+                : 0;
 
         for (int i = 0; i < count; i++) {
-            float translation = i == midPoint ? 0 : (i < midPoint ? -offset : offset);
-            float modalTranslation =
-                    i == currentPage ? 0 : (i < currentPage ? -modalOffset : modalOffset);
-            getChildAt(i).setTranslationX(translation + modalTranslation);
+            float translation = i == midpoint
+                    ? midpointOffsetSize
+                    : i < midpoint
+                            ? leftOffsetSize
+                            : rightOffsetSize;
+            float modalTranslation = i == modalMidpoint
+                    ? modalMidpointOffsetSize
+                    : i < modalMidpoint
+                            ? modalLeftOffsetSize
+                            : modalRightOffsetSize;
+            float totalTranslation = translation + modalTranslation;
+            mOrientationHandler.getPrimaryViewTranslate().set(getChildAt(i),
+                    totalTranslation * mOrientationHandler.getPrimaryTranslationDirectionFactor());
         }
         updateCurveProperties();
     }
 
     /**
+     * Computes the distance to offset the given child such that it is completely offscreen when
+     * translating away from the given midpoint.
+     * @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen.
+     */
+    private float getOffsetSize(int childIndex, int midpointIndex, float offsetProgress) {
+        if (offsetProgress == 0) {
+            // Don't bother calculating everything below if we won't offset anyway.
+            return 0;
+        }
+        // First, get the position of the task relative to the midpoint. If there is no midpoint
+        // then we just use the normal (centered) task position.
+        mTempRectF.set(mLastComputedTaskSize);
+        RectF taskPosition = mTempRectF;
+        float desiredLeft = getWidth();
+        float distanceToOffscreen = desiredLeft - taskPosition.left;
+        // Used to calculate the scale of the task view based on its new offset.
+        float centerToOffscreenProgress = Math.abs(offsetProgress);
+        if (midpointIndex > -1) {
+            // When there is a midpoint reference task, adjacent tasks have less distance to travel
+            // to reach offscreen. Offset the task position to the task's starting point.
+            View child = getChildAt(childIndex);
+            View midpointChild = getChildAt(midpointIndex);
+            int distanceFromMidpoint = Math.abs(mOrientationHandler.getChildStart(child)
+                    - mOrientationHandler.getChildStart(midpointChild)
+                    + getDisplacementFromScreenCenter(midpointIndex));
+            taskPosition.offset(distanceFromMidpoint, 0);
+            centerToOffscreenProgress = Utilities.mapRange(centerToOffscreenProgress,
+                    distanceFromMidpoint / distanceToOffscreen, 1);
+        }
+        // Find the task's scale based on its offscreen progress, then see how far it still needs to
+        // move to be completely offscreen.
+        Utilities.scaleRectFAboutCenter(taskPosition,
+                TaskView.getCurveScaleForInterpolation(centerToOffscreenProgress));
+        distanceToOffscreen = desiredLeft - taskPosition.left;
+        // Finally, we need to account for RecentsView scale, because it moves tasks based on its
+        // pivot. To do this, we move the task position to where it would be offscreen at scale = 1
+        // (computed above), then we apply the scale via getMatrix() to determine how much that
+        // moves the task from its desired position, and adjust the computed distance accordingly.
+        if (mLastComputedTaskPushOutDistance == null) {
+            taskPosition.offsetTo(desiredLeft, 0);
+            getMatrix().mapRect(taskPosition);
+            mLastComputedTaskPushOutDistance = (taskPosition.left - desiredLeft) / getScaleX();
+        }
+        distanceToOffscreen -= mLastComputedTaskPushOutDistance;
+        return distanceToOffscreen * offsetProgress;
+    }
+
+    private void setTaskViewsSecondaryTranslation(float translation) {
+        mTaskViewsSecondaryTranslation = translation;
+        for (int i = 0; i < getTaskViewCount(); i++) {
+            TaskView task = getTaskViewAt(i);
+            mOrientationHandler.getSecondaryViewTranslate().set(task, translation / getScaleY());
+        }
+    }
+
+    /**
      * TODO: Do not assume motion across X axis for adjacent page
      */
     public float getPageOffsetScale() {
@@ -1895,7 +2048,7 @@
         float toScale = getMaxScaleForFullScreen();
         if (launchingCenterTask) {
             RecentsView recentsView = tv.getRecentsView();
-            anim.play(ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, toScale));
+            anim.play(ObjectAnimator.ofFloat(recentsView, RECENTS_SCALE_PROPERTY, toScale));
             anim.play(ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, 1));
         } else {
             // We are launching an adjacent task, so parallax the center and other adjacent task.
@@ -2181,7 +2334,14 @@
         if (pageIndex == -1) {
             return 0;
         }
-        return getScrollForPage(pageIndex) - mOrientationHandler.getPrimaryScroll(this);
+        // Unbound the scroll (due to overscroll) if the adjacent tasks are offset away from it.
+        // This allows the page to move freely, given there's no visual indication why it shouldn't.
+        int boundedScroll = mOrientationHandler.getPrimaryScroll(this);
+        int unboundedScroll = getUnboundedScroll();
+        float unboundedProgress = mAdjacentPageOffset;
+        int scroll = Math.round(unboundedScroll * unboundedProgress
+                + boundedScroll * (1 - unboundedProgress));
+        return getScrollForPage(pageIndex) - scroll;
     }
 
     public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
@@ -2283,6 +2443,10 @@
      */
     public void setModalStateEnabled(boolean isModalState) { }
 
+    public TaskOverlayFactory getTaskOverlayFactory() {
+        return mTaskOverlayFactory;
+    }
+
     public BaseActivityInterface getSizeStrategy() {
         return mSizeStrategy;
     }
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 ef66b7a..8b49f2c 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
@@ -169,7 +169,9 @@
         }
         if (mIsOpen) {
             mOptionLayout.removeAllViews();
-            populateAndLayoutMenu();
+            if (!populateAndLayoutMenu()) {
+                close(false);
+            }
         }
     }
 
@@ -186,14 +188,22 @@
         }
         mActivity.getDragLayer().addView(this);
         mTaskView = taskView;
-        populateAndLayoutMenu();
+        if (!populateAndLayoutMenu()) {
+            return false;
+        }
         post(this::animateOpen);
         return true;
     }
 
-    private void populateAndLayoutMenu() {
+    /** @return true if successfully able to populate task view menu, false otherwise */
+    private boolean populateAndLayoutMenu() {
+        if (mTaskView.getTask().icon == null) {
+            // Icon may not be loaded
+            return false;
+        }
         addMenuOptions(mTaskView);
         orientAroundTaskView(mTaskView);
+        return true;
     }
 
     private void addMenuOptions(TaskView taskView) {
@@ -240,8 +250,10 @@
         setLayoutParams(params);
         setScaleX(taskView.getScaleX());
         setScaleY(taskView.getScaleY());
+        boolean canActivityRotate = taskView.getRecentsView()
+            .mOrientationState.canRecentsActivityRotate();
         mOptionLayout.setOrientation(orientationHandler
-                .getTaskMenuLayoutOrientation(mOptionLayout));
+                .getTaskMenuLayoutOrientation(canActivityRotate, mOptionLayout));
         setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top,
             taskView.getPagedOrientationHandler());
     }
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 b2f937f..607672a 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
@@ -52,14 +52,12 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.Themes;
-import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
 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;
 import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ConfigurationCompat;
 
 /**
  * A task in the Recents view.
@@ -86,7 +84,7 @@
             };
 
     private final BaseActivity mActivity;
-    private final TaskOverlay mOverlay;
+    private TaskOverlay mOverlay;
     private final boolean mIsDarkTextTheme;
     private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
     private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
@@ -119,7 +117,6 @@
 
     public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mOverlay = TaskOverlayFactory.INSTANCE.get(context).createOverlay(this);
         mPaint.setFilterBitmap(true);
         mBackgroundPaint.setColor(Color.WHITE);
         mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
@@ -135,7 +132,7 @@
      * @param task
      */
     public void bind(Task task) {
-        mOverlay.reset();
+        getTaskOverlay().reset();
         mTask = task;
         int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000;
         mPaint.setColor(color);
@@ -177,7 +174,7 @@
             mBitmapShader = null;
             mThumbnailData = null;
             mPaint.setShader(null);
-            mOverlay.reset();
+            getTaskOverlay().reset();
         }
         if (mOverviewScreenshotActionsPlugin != null) {
             mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
@@ -201,6 +198,9 @@
     }
 
     public TaskOverlay getTaskOverlay() {
+        if (mOverlay == null) {
+            mOverlay = getTaskView().getRecentsView().getTaskOverlayFactory().createOverlay(this);
+        }
         return mOverlay;
     }
 
@@ -357,11 +357,11 @@
     }
 
     private void updateOverlay() {
-        if (mOverlayEnabled && mBitmapShader != null && mThumbnailData != null) {
-            mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix,
+        if (mOverlayEnabled) {
+            getTaskOverlay().initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix,
                     mPreviewPositionHelper.mIsOrientationChanged);
         } else {
-            mOverlay.reset();
+            getTaskOverlay().reset();
         }
     }
 
@@ -385,8 +385,8 @@
         if (mBitmapShader != null && mThumbnailData != null) {
             mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
                     mThumbnailData.thumbnail.getHeight());
-            int currentRotation = ConfigurationCompat.getWindowConfigurationRotation(
-                    mActivity.getResources().getConfiguration());
+            int currentRotation = getTaskView().getRecentsView().getPagedViewOrientedState()
+                    .getRecentsActivityRotation();
             mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
                     getMeasuredWidth(), getMeasuredHeight(), mActivity.getDeviceProfile(),
                     currentRotation);
@@ -480,17 +480,18 @@
             boolean isOrientationDifferent;
             mClipBottom = -1;
 
+            int thumbnailRotation = thumbnailData.rotation;
+            int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
+            Rect thumbnailInsets = getBoundedInsets(
+                    dp.getInsets(), thumbnailData.insets, deltaRotate);
+
             float scale = thumbnailData.scale;
-            Rect activityInsets = dp.getInsets();
-            Rect thumbnailInsets = getBoundedInsets(activityInsets, 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 deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
 
             // Landscape vs portrait change
             boolean windowingModeSupportsRotation = !dp.isMultiWindowMode
@@ -559,7 +560,10 @@
             mIsOrientationChanged = isOrientationDifferent;
         }
 
-        private Rect getBoundedInsets(Rect activityInsets, Rect insets) {
+        private Rect getBoundedInsets(Rect activityInsets, Rect insets, int deltaRotation) {
+            if (deltaRotation != 0) {
+                return insets;
+            }
             return new Rect(Math.min(insets.left, activityInsets.left),
                     Math.min(insets.top, activityInsets.top),
                     Math.min(insets.right, activityInsets.right),
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 222f6e6..2058a7f 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
@@ -22,16 +22,19 @@
 import static android.view.Gravity.END;
 import static android.view.Gravity.START;
 import static android.view.Gravity.TOP;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
 import static android.widget.Toast.LENGTH_SHORT;
 
 import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
 import static com.android.launcher3.Utilities.comp;
+import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent
-        .LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
 
 import android.animation.Animator;
@@ -53,7 +56,9 @@
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.Log;
+import android.view.MotionEvent;
 import android.view.Surface;
+import android.view.TouchDelegate;
 import android.view.View;
 import android.view.ViewOutlineProvider;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -78,6 +83,7 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.TransformingTouchDelegate;
 import com.android.launcher3.util.ViewPool.Reusable;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskIconCache;
@@ -122,6 +128,13 @@
 
     public static final long SCALE_ICON_DURATION = 120;
     private static final long DIM_ANIM_DURATION = 700;
+    /**
+     * This technically can be a vanilla {@link TouchDelegate} class, however that class requires
+     * setting the touch bounds at construction, so we'd repeatedly be created many instances
+     * unnecessarily as scrolling occurs, whereas {@link TransformingTouchDelegate} allows touch
+     * delegated bounds only to be updated.
+     */
+    private TransformingTouchDelegate mIconTouchDelegate;
 
     private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
             Collections.singletonList(new Rect());
@@ -186,6 +199,7 @@
     private int mStackHeight;
     private View mContextualChipWrapper;
     private View mContextualChip;
+    private final float[] mIconCenterCoords = new float[2];
 
     public TaskView(Context context) {
         this(context, null);
@@ -246,6 +260,26 @@
         super.onFinishInflate();
         mSnapshotView = findViewById(R.id.snapshot);
         mIconView = findViewById(R.id.icon);
+        mIconTouchDelegate = new TransformingTouchDelegate(mIconView);
+    }
+
+    public TouchDelegate getIconTouchDelegate(MotionEvent event) {
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            computeAndSetIconTouchDelegate();
+        }
+        return mIconTouchDelegate;
+    }
+
+    private void computeAndSetIconTouchDelegate() {
+        float iconHalfSize = mIconView.getWidth() / 2f;
+        mIconCenterCoords[0] = mIconCenterCoords[1] = iconHalfSize;
+        getDescendantCoordRelativeToAncestor(mIconView, mActivity.getDragLayer(), mIconCenterCoords,
+                false);
+        mIconTouchDelegate.setBounds(
+                (int) (mIconCenterCoords[0] - iconHalfSize),
+                (int) (mIconCenterCoords[1] - iconHalfSize),
+                (int) (mIconCenterCoords[0] + iconHalfSize),
+                (int) (mIconCenterCoords[1] + iconHalfSize));
     }
 
     /**
@@ -468,18 +502,18 @@
         int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
         LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
         switch (orientationHandler.getRotation()) {
-            case Surface.ROTATION_90:
+            case ROTATION_90:
                 iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL;
                 iconParams.rightMargin = -thumbnailPadding;
                 iconParams.leftMargin = 0;
                 iconParams.topMargin = snapshotParams.topMargin / 2;
                 break;
-            case Surface.ROTATION_180:
+            case ROTATION_180:
                 iconParams.gravity = BOTTOM | CENTER_HORIZONTAL;
                 iconParams.bottomMargin = -thumbnailPadding;
                 iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
                 break;
-            case Surface.ROTATION_270:
+            case ROTATION_270:
                 iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
                 iconParams.leftMargin = -thumbnailPadding;
                 iconParams.rightMargin = 0;
diff --git a/quickstep/res/drawable/bg_circle.xml b/quickstep/res/drawable/bg_circle.xml
new file mode 100644
index 0000000..506177b
--- /dev/null
+++ b/quickstep/res/drawable/bg_circle.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid android:color="#FFFFFFFF" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/layout/gesture_tutorial_fragment.xml b/quickstep/res/layout/gesture_tutorial_fragment.xml
index 459d65f..43bf0ea 100644
--- a/quickstep/res/layout/gesture_tutorial_fragment.xml
+++ b/quickstep/res/layout/gesture_tutorial_fragment.xml
@@ -24,6 +24,14 @@
         android:layout_height="match_parent"
         android:background="@drawable/gesture_tutorial_ripple"/>
 
+    <com.android.launcher3.views.ClipIconView
+        android:id="@+id/gesture_tutorial_fake_icon_view"
+        android:layout_width="20dp"
+        android:layout_height="20dp"
+        android:background="@drawable/bg_circle"
+        android:backgroundTint="@color/gesture_tutorial_fake_task_view_color"
+        android:visibility="invisible" />
+
     <View
         android:id="@+id/gesture_tutorial_fake_task_view"
         android:layout_width="match_parent"
@@ -41,81 +49,81 @@
         android:id="@+id/gesture_tutorial_fragment_close_button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:padding="18dp"
-        android:layout_marginTop="30dp"
-        android:layout_marginStart="4dp"
         android:layout_alignParentStart="true"
         android:layout_alignParentTop="true"
-        android:background="@android:color/transparent"
+        android:layout_marginStart="4dp"
+        android:layout_marginTop="30dp"
         android:accessibilityTraversalAfter="@id/gesture_tutorial_fragment_titles_container"
+        android:background="@android:color/transparent"
         android:contentDescription="@string/gesture_tutorial_close_button_content_description"
-        android:tint="?android:attr/textColorPrimary"
-        android:src="@drawable/gesture_tutorial_close_button"/>
+        android:padding="18dp"
+        android:src="@drawable/gesture_tutorial_close_button"
+        android:tint="?android:attr/textColorPrimary"/>
 
     <LinearLayout
         android:id="@+id/gesture_tutorial_fragment_titles_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_marginTop="70dp"
         android:layout_alignParentTop="true"
+        android:layout_marginTop="70dp"
         android:focusable="true"
         android:gravity="center_horizontal"
         android:orientation="vertical">
 
         <TextView
             android:id="@+id/gesture_tutorial_fragment_title_view"
+            style="@style/TextAppearance.GestureTutorial.Title"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginStart="@dimen/gesture_tutorial_title_margin_start_end"
-            android:layout_marginEnd="@dimen/gesture_tutorial_title_margin_start_end"
-            style="@style/TextAppearance.GestureTutorial.Title"/>
+            android:layout_marginEnd="@dimen/gesture_tutorial_title_margin_start_end"/>
 
         <TextView
             android:id="@+id/gesture_tutorial_fragment_subtitle_view"
+            style="@style/TextAppearance.GestureTutorial.Subtitle"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_marginTop="10dp"
             android:layout_marginStart="@dimen/gesture_tutorial_subtitle_margin_start_end"
-            android:layout_marginEnd="@dimen/gesture_tutorial_subtitle_margin_start_end"
-            style="@style/TextAppearance.GestureTutorial.Subtitle"/>
+            android:layout_marginTop="10dp"
+            android:layout_marginEnd="@dimen/gesture_tutorial_subtitle_margin_start_end"/>
     </LinearLayout>
 
     <TextView
         android:id="@+id/gesture_tutorial_fragment_feedback_view"
+        style="@style/TextAppearance.GestureTutorial.Feedback"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginBottom="10dp"
-        android:layout_centerHorizontal="true"
         android:layout_above="@id/gesture_tutorial_fragment_action_button"
+        android:layout_centerHorizontal="true"
         android:layout_marginStart="@dimen/gesture_tutorial_feedback_margin_start_end"
         android:layout_marginEnd="@dimen/gesture_tutorial_feedback_margin_start_end"
-        style="@style/TextAppearance.GestureTutorial.Feedback"/>
+        android:layout_marginBottom="10dp"/>
 
     <!-- android:stateListAnimator="@null" removes shadow and normal on click behavior (increase
          of elevation and shadow) which is replaced by ripple effect in android:foreground -->
     <Button
         android:id="@+id/gesture_tutorial_fragment_action_button"
+        style="@style/TextAppearance.GestureTutorial.ButtonLabel"
         android:layout_width="142dp"
         android:layout_height="49dp"
-        android:layout_marginEnd="@dimen/gesture_tutorial_button_margin_start_end"
-        android:layout_marginBottom="48dp"
         android:layout_alignParentEnd="true"
         android:layout_alignParentBottom="true"
-        android:stateListAnimator="@null"
+        android:layout_marginEnd="@dimen/gesture_tutorial_button_margin_start_end"
+        android:layout_marginBottom="48dp"
         android:background="@drawable/gesture_tutorial_action_button_background"
         android:foreground="?android:attr/selectableItemBackgroundBorderless"
-        style="@style/TextAppearance.GestureTutorial.ButtonLabel"/>
+        android:stateListAnimator="@null"/>
 
     <Button
         android:id="@+id/gesture_tutorial_fragment_action_text_button"
+        style="@style/TextAppearance.GestureTutorial.TextButtonLabel"
         android:layout_width="142dp"
         android:layout_height="49dp"
-        android:layout_marginStart="@dimen/gesture_tutorial_button_margin_start_end"
-        android:layout_marginBottom="48dp"
         android:layout_alignParentStart="true"
         android:layout_alignParentBottom="true"
-        android:stateListAnimator="@null"
+        android:layout_marginStart="@dimen/gesture_tutorial_button_margin_start_end"
+        android:layout_marginBottom="48dp"
         android:background="@null"
         android:foreground="?android:attr/selectableItemBackgroundBorderless"
-        style="@style/TextAppearance.GestureTutorial.TextButtonLabel"/>
+        android:stateListAnimator="@null"/>
 </RelativeLayout>
\ No newline at end of file
diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml
index e05688e..258f24a 100644
--- a/quickstep/res/layout/overview_actions_container.xml
+++ b/quickstep/res/layout/overview_actions_container.xml
@@ -17,9 +17,7 @@
 <com.android.quickstep.views.OverviewActionsView xmlns:android="http://schemas.android.com/apk/res/android"
     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">
+    android:layout_gravity="center_horizontal|bottom">
 
     <LinearLayout
         android:id="@+id/action_buttons"
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 0f2955b..a57112b 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -16,6 +16,8 @@
 <resources>
     <string name="task_overlay_factory_class" translatable="false"/>
 
+    <string name="overscroll_plugin_factory_class" translatable="false" />
+
     <!-- Activities which block home gesture -->
     <string-array name="gesture_blocking_activities" translatable="false">
         <item>com.android.launcher3/com.android.quickstep.interaction.GestureSandboxActivity</item>
diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
index 93b64e6..c148a4b 100644
--- a/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.util.LauncherUIHelper.doLayout;
 
+import android.app.ActivityManager.RunningTaskInfo;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 
@@ -50,7 +51,7 @@
     }
 
     @Test
-    public void testRecets_showCurrentTask() {
+    public void testRecents_showCurrentTask() {
         ActivityController<RecentsActivity> controller =
                 Robolectric.buildActivity(RecentsActivity.class);
 
@@ -58,7 +59,10 @@
         doLayout(activity);
 
         FallbackRecentsView frv = activity.getOverviewPanel();
-        frv.showCurrentTask(22);
+
+        RunningTaskInfo dummyTask = new RunningTaskInfo();
+        dummyTask.taskId = 22;
+        frv.showCurrentTask(dummyTask);
         doLayout(activity);
 
         ThumbnailData thumbnailData = new ThumbnailData();
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 47ce320..235df42 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -111,6 +111,13 @@
     }
 
     @Override
+    protected void handleGestureContract(Intent intent) {
+        if (FeatureFlags.SEPARATE_RECENTS_ACTIVITY.get()) {
+            super.handleGestureContract(intent);
+        }
+    }
+
+    @Override
     public void onTrimMemory(int level) {
         super.onTrimMemory(level);
         RecentsModel.INSTANCE.get(this).onTrimMemory(level);
@@ -157,6 +164,12 @@
     @Override
     protected void onDeferredResumed() {
         super.onDeferredResumed();
+        handlePendingActivityRequest();
+    }
+
+    @Override
+    protected void handlePendingActivityRequest() {
+        super.handlePendingActivityRequest();
         if (mPendingActivityRequestCode != -1 && isInState(NORMAL)) {
             // Remove any active ProxyActivityStarter task and send RESULT_CANCELED to Launcher.
             onActivityResult(mPendingActivityRequestCode, RESULT_CANCELED, null);
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index fe7b946..bd97f65 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3;
 
-import static android.util.TypedValue.COMPLEX_UNIT_DIP;
-
 import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
@@ -62,7 +60,6 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.util.Pair;
-import android.util.TypedValue;
 import android.view.View;
 
 import androidx.annotation.NonNull;
@@ -875,10 +872,8 @@
                             }
                         });
                     } else {
-                        float velocityDpPerS = DynamicResource.provider(mLauncher)
+                        float velocityPxPerS = DynamicResource.provider(mLauncher)
                                 .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s);
-                        float velocityPxPerS = TypedValue.applyDimension(COMPLEX_UNIT_DIP,
-                                velocityDpPerS, mLauncher.getResources().getDisplayMetrics());
                         anim.play(new StaggeredWorkspaceAnim(mLauncher, velocityPxPerS, false)
                                 .getAnimators());
                     }
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
index f42b124..810f4e3 100644
--- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -43,9 +43,15 @@
 import android.util.Log;
 
 import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherProvider;
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.popup.RemoteActionShortcut;
 import com.android.launcher3.popup.SystemShortcut;
@@ -57,6 +63,7 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.Consumer;
 
 /**
  * Data model for digital wellbeing status of apps.
@@ -71,13 +78,21 @@
     private static final int MSG_PACKAGE_REMOVED = 2;
     private static final int MSG_FULL_REFRESH = 3;
 
+    private static final int UNKNOWN_MINIMAL_DEVICE_STATE = 0;
+    private static final int IN_MINIMAL_DEVICE = 2;
+
     // Welbeing contract
+    private static final String PATH_ACTIONS = "actions";
+    private static final String PATH_MINIMAL_DEVICE = "minimal_device";
+    private static final String METHOD_GET_MINIMAL_DEVICE_CONFIG = "get_minimal_device_config";
     private static final String METHOD_GET_ACTIONS = "get_actions";
     private static final String EXTRA_ACTIONS = "actions";
     private static final String EXTRA_ACTION = "action";
     private static final String EXTRA_MAX_NUM_ACTIONS_SHOWN = "max_num_actions_shown";
     private static final String EXTRA_PACKAGES = "packages";
     private static final String EXTRA_SUCCESS = "success";
+    private static final String EXTRA_MINIMAL_DEVICE_STATE = "minimal_device_state";
+    private static final String DB_NAME_MINIMAL_DEVICE = "minimal.db";
 
     public static final MainThreadInitializedObject<WellbeingModel> INSTANCE =
             new MainThreadInitializedObject<>(WellbeingModel::new);
@@ -104,15 +119,23 @@
         mContentObserver = new ContentObserver(MAIN_EXECUTOR.getHandler()) {
             @Override
             public void onChange(boolean selfChange, Uri uri) {
-                // Wellbeing reports that app actions have changed.
                 if (DEBUG || mIsInTest) {
-                    Log.d(TAG, "ContentObserver.onChange() called with: selfChange = [" + selfChange
-                            + "], uri = [" + uri + "]");
+                    Log.d(TAG, "ContentObserver.onChange() called with: selfChange = ["
+                            + selfChange + "], uri = [" + uri + "]");
                 }
                 Preconditions.assertUIThread();
-                updateWellbeingData();
+
+                if (uri.getPath().contains(PATH_ACTIONS)) {
+                    // Wellbeing reports that app actions have changed.
+                    updateWellbeingData();
+                } else if (uri.getPath().contains(PATH_MINIMAL_DEVICE)) {
+                    // Wellbeing reports that minimal device state or config is changed.
+                    updateLauncherModel(context);
+                }
             }
         };
+        FeatureFlags.ENABLE_MINIMAL_DEVICE.addChangeListener(mContext, () ->
+                updateLauncherModel(context));
 
         if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
             context.registerReceiver(
@@ -146,10 +169,13 @@
     private void restartObserver() {
         final ContentResolver resolver = mContext.getContentResolver();
         resolver.unregisterContentObserver(mContentObserver);
-        Uri actionsUri = apiBuilder().path("actions").build();
+        Uri actionsUri = apiBuilder().path(PATH_ACTIONS).build();
+        Uri minimalDeviceUri = apiBuilder().path(PATH_MINIMAL_DEVICE).build();
         try {
             resolver.registerContentObserver(
                     actionsUri, true /* notifyForDescendants */, mContentObserver);
+            resolver.registerContentObserver(
+                    minimalDeviceUri, true /* notifyForDescendants */, mContentObserver);
         } catch (Exception e) {
             Log.e(TAG, "Failed to register content observer for " + actionsUri + ": " + e);
             if (mIsInTest) throw new RuntimeException(e);
@@ -191,12 +217,69 @@
         mWorkerHandler.sendEmptyMessage(MSG_FULL_REFRESH);
     }
 
+    private void updateLauncherModel(@NonNull final Context context) {
+        if (!FeatureFlags.ENABLE_MINIMAL_DEVICE.get()) {
+            reloadLauncherInNormalMode(context);
+            return;
+        }
+        runWithMinimalDeviceConfigs((bundle) -> {
+            if (bundle.getInt(EXTRA_MINIMAL_DEVICE_STATE, UNKNOWN_MINIMAL_DEVICE_STATE)
+                    == IN_MINIMAL_DEVICE) {
+                reloadLauncherInMinimalMode(context);
+            } else {
+                reloadLauncherInNormalMode(context);
+            }
+        });
+    }
+
+    private void reloadLauncherInNormalMode(@NonNull final Context context) {
+        LauncherSettings.Settings.call(context.getContentResolver(),
+                LauncherSettings.Settings.METHOD_SWITCH_DATABASE,
+                InvariantDeviceProfile.INSTANCE.get(context).dbFile);
+    }
+
+    private void reloadLauncherInMinimalMode(@NonNull final Context context) {
+        final Bundle extras = new Bundle();
+        extras.putString(LauncherProvider.KEY_LAYOUT_PROVIDER_AUTHORITY,
+                mWellbeingProviderPkg + ".api");
+        LauncherSettings.Settings.call(context.getContentResolver(),
+                LauncherSettings.Settings.METHOD_SWITCH_DATABASE,
+                DB_NAME_MINIMAL_DEVICE, extras);
+    }
+
     private Uri.Builder apiBuilder() {
         return new Uri.Builder()
                 .scheme(SCHEME_CONTENT)
                 .authority(mWellbeingProviderPkg + ".api");
     }
 
+    /**
+     * Fetch most up-to-date minimal device config.
+     */
+    @WorkerThread
+    private void runWithMinimalDeviceConfigs(Consumer<Bundle> consumer) {
+        if (!FeatureFlags.ENABLE_MINIMAL_DEVICE.get()) {
+            return;
+        }
+        if (DEBUG || mIsInTest) {
+            Log.d(TAG, "runWithMinimalDeviceConfigs() called");
+        }
+        Preconditions.assertNonUiThread();
+
+        final Uri contentUri = apiBuilder().build();
+        final Bundle remoteBundle;
+        try (ContentProviderClient client = mContext.getContentResolver()
+                .acquireUnstableContentProviderClient(contentUri)) {
+            remoteBundle = client.call(
+                    METHOD_GET_MINIMAL_DEVICE_CONFIG, null /* args */, null /* extras */);
+            consumer.accept(remoteBundle);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to retrieve data from " + contentUri + ": " + e);
+            if (mIsInTest) throw new RuntimeException(e);
+        }
+        if (DEBUG || mIsInTest) Log.i(TAG, "runWithMinimalDeviceConfigs(): finished");
+    }
+
     private boolean updateActions(String... packageNames) {
         if (packageNames.length == 0) {
             return true;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index ec3a490..aad7e17 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -16,19 +16,22 @@
 
 package com.android.launcher3.uioverrides;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 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.OverviewScrim.SCRIM_MULTIPLIER;
 import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
 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 static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 
 import android.util.FloatProperty;
 
@@ -61,12 +64,14 @@
     @Override
     public void setState(@NonNull LauncherState state) {
         float[] scaleAndOffset = state.getOverviewScaleAndOffset(mLauncher);
-        SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
+        RECENTS_SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
         ADJACENT_PAGE_OFFSET.set(mRecentsView, scaleAndOffset[1]);
+        TASK_SECONDARY_TRANSLATION.set(mRecentsView, 0f);
 
         getContentAlphaProperty().set(mRecentsView, state.overviewUi ? 1f : 0);
         OverviewScrim scrim = mLauncher.getDragLayer().getOverviewScrim();
         SCRIM_PROGRESS.set(scrim, state.getOverviewScrimAlpha(mLauncher));
+        SCRIM_MULTIPLIER.set(scrim, 1f);
         getTaskModalnessProperty().set(mRecentsView, state.getOverviewModalness());
     }
 
@@ -93,16 +98,20 @@
     void setStateWithAnimationInternal(@NonNull final LauncherState toState,
             @NonNull StateAnimationConfig config, @NonNull PendingAnimation setter) {
         float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
-        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0],
+        setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
                 config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
         setter.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1],
                 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
+        setter.setFloat(mRecentsView, TASK_SECONDARY_TRANSLATION, 0f,
+                config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
 
         setter.setFloat(mRecentsView, getContentAlphaProperty(), toState.overviewUi ? 1 : 0,
                 config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
         OverviewScrim scrim = mLauncher.getDragLayer().getOverviewScrim();
         setter.setFloat(scrim, SCRIM_PROGRESS, toState.getOverviewScrimAlpha(mLauncher),
                 config.getInterpolator(ANIM_OVERVIEW_SCRIM_FADE, LINEAR));
+        setter.setFloat(scrim, SCRIM_MULTIPLIER, 1f,
+                config.getInterpolator(ANIM_OVERVIEW_SCRIM_FADE, LINEAR));
 
         setter.setFloat(
                 mRecentsView, getTaskModalnessProperty(),
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index e7cd393..29a6be0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
 
 import android.content.Context;
 
@@ -92,7 +93,8 @@
 
     @Override
     public float[] getOverviewScaleAndOffset(Launcher launcher) {
-        return new float[] {0.9f, 0};
+        float offset = ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(launcher) ? 1 : 0;
+        return new float[] {0.9f, offset};
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 20ee61d..a684b9d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -142,6 +142,10 @@
                 Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
                         "PortraitStatesTouchController.getTargetState 1");
             }
+            if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(mLauncher)) {
+                // Don't allow swiping down to overview.
+                return NORMAL;
+            }
             return TouchInteractionService.isConnected() ?
                     mLauncher.getStateManager().getLastState() : NORMAL;
         } else if (fromState == OVERVIEW) {
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 2b698bd..bdf525d 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -15,7 +15,6 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.INSTANT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
@@ -28,6 +27,8 @@
 import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_TRANSLATE_X_ANIM;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 
 import android.animation.Animator;
 import android.annotation.TargetApi;
@@ -52,6 +53,7 @@
 import com.android.launcher3.util.WindowBounds;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.quickstep.util.SplitScreenBounds;
 import com.android.quickstep.views.RecentsView;
@@ -106,7 +108,7 @@
     public abstract void onAssistantVisibilityChanged(float visibility);
 
     public abstract AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
-            boolean activityVisible, Consumer<AnimatorPlaybackController> callback);
+            boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback);
 
     public abstract ActivityInitListener createActivityInitListener(
             Predicate<Boolean> onInitListener);
@@ -150,7 +152,7 @@
         return deviceState.isInDeferredGestureRegion(ev);
     }
 
-    public abstract void onExitOverview(RecentsAnimationDeviceState deviceState,
+    public abstract void onExitOverview(RotationTouchHelper deviceState,
             Runnable exitRunnable);
 
     /**
@@ -319,11 +321,11 @@
 
         protected final ACTIVITY_TYPE mActivity;
         private final STATE_TYPE mStartState;
-        private final Consumer<AnimatorPlaybackController> mCallback;
+        private final Consumer<AnimatorControllerWithResistance> mCallback;
 
         private boolean mIsAttachedToWindow;
 
-        DefaultAnimationFactory(Consumer<AnimatorPlaybackController> callback) {
+        DefaultAnimationFactory(Consumer<AnimatorControllerWithResistance> callback) {
             mCallback = callback;
 
             mActivity = getCreatedActivity();
@@ -351,7 +353,14 @@
             controller.setEndAction(() -> mActivity.getStateManager().goToState(
                     controller.getInterpolatedProgress() > 0.5 ? mOverviewState : mBackgroundState,
                     false));
-            mCallback.accept(controller);
+
+            RecentsView recentsView = mActivity.getOverviewPanel();
+            AnimatorControllerWithResistance controllerWithResistance =
+                    AnimatorControllerWithResistance.createForRecents(controller, mActivity,
+                            recentsView.getPagedViewOrientedState(), mActivity.getDeviceProfile(),
+                            recentsView, RECENTS_SCALE_PROPERTY, recentsView,
+                            TASK_SECONDARY_TRANSLATION);
+            mCallback.accept(controllerWithResistance);
 
             // Creating the activity controller animation sometimes reapplies the launcher state
             // (because we set the animation as the current state animation), so we reapply the
@@ -400,7 +409,7 @@
         protected void createBackgroundToOverviewAnim(ACTIVITY_TYPE activity, PendingAnimation pa) {
             //  Scale down recents from being full screen to being in overview.
             RecentsView recentsView = activity.getOverviewPanel();
-            pa.addFloat(recentsView, SCALE_PROPERTY,
+            pa.addFloat(recentsView, RECENTS_SCALE_PROPERTY,
                     recentsView.getMaxScaleForFullScreen(), 1, LINEAR);
             pa.addFloat(recentsView, FULLSCREEN_PROGRESS, 1, 0, LINEAR);
         }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 0a70bd6..7a6bbb4 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -16,11 +16,9 @@
 package com.android.quickstep;
 
 import static android.content.Intent.ACTION_USER_UNLOCKED;
-import static android.view.Surface.ROTATION_0;
 
 import static com.android.launcher3.util.DefaultDisplay.CHANGE_ALL;
 import static com.android.launcher3.util.DefaultDisplay.CHANGE_FRAME_DELAY;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 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;
@@ -49,23 +47,19 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.view.MotionEvent;
-import android.view.OrientationEventListener;
 
 import androidx.annotation.BinderThread;
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.DefaultDisplay;
 import com.android.launcher3.util.SecureSettingsObserver;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
 import com.android.quickstep.util.NavBarPosition;
-import com.android.quickstep.util.RecentsOrientedState;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 import com.android.systemui.shared.system.SystemGestureExclusionListenerCompat;
-import com.android.systemui.shared.system.TaskStackChangeListener;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -83,7 +77,7 @@
     private final SysUINavigationMode mSysUiNavMode;
     private final DefaultDisplay mDefaultDisplay;
     private final int mDisplayId;
-    private int mDisplayRotation;
+    private final RotationTouchHelper mRotationTouchHelper;
 
     private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
 
@@ -107,76 +101,10 @@
         }
     };
 
-    private TaskStackChangeListener mFrozenTaskListener = new TaskStackChangeListener() {
-        @Override
-        public void onRecentTaskListFrozenChanged(boolean frozen) {
-            mTaskListFrozen = frozen;
-            if (frozen || mInOverview) {
-                return;
-            }
-            enableMultipleRegions(false);
-        }
-
-        @Override
-        public void onActivityRotation(int displayId) {
-            super.onActivityRotation(displayId);
-            // This always gets called before onDisplayInfoChanged() so we know how to process
-            // the rotation in that method. This is done to avoid having a race condition between
-            // the sensor readings and onDisplayInfoChanged() call
-            if (displayId != mDisplayId) {
-                return;
-            }
-
-            mPrioritizeDeviceRotation = true;
-            if (mInOverview) {
-                // reset, launcher must be rotating
-                mExitOverviewRunnable.run();
-            }
-        }
-    };
-
-    private Runnable mExitOverviewRunnable = new Runnable() {
-        @Override
-        public void run() {
-            mInOverview = false;
-            enableMultipleRegions(false);
-        }
-    };
-
-    private OrientationTouchTransformer mOrientationTouchTransformer;
-    /**
-     * Used to listen for when the device rotates into the orientation of the current
-     * foreground app. For example, if a user quickswitches from a portrait to a fixed landscape
-     * app and then rotates rotates the device to match that orientation, this triggers calls to
-     * sysui to adjust the navbar.
-     */
-    private OrientationEventListener mOrientationListener;
-    private int mSensorRotation = ROTATION_0;
-    /**
-     * This is the configuration of the foreground app or the app that will be in the foreground
-     * once a quickstep gesture finishes.
-     */
-    private int mCurrentAppRotation = -1;
-    /**
-     * This flag is set to true when the device physically changes orientations. When true,
-     * we will always report the current rotation of the foreground app whenever the display
-     * changes, as it would indicate the user's intention to rotate the foreground app.
-     */
-    private boolean mPrioritizeDeviceRotation = false;
-
     private Region mExclusionRegion;
     private SystemGestureExclusionListenerCompat mExclusionListener;
 
     private final List<ComponentName> mGestureBlockedActivities;
-    private Runnable mOnDestroyFrozenTaskRunnable;
-    /**
-     * Set to true when user swipes to recents. In recents, we ignore the state of the recents
-     * task list being frozen or not to allow the user to keep interacting with nav bar rotation
-     * they went into recents with as opposed to defaulting to the default display rotation.
-     * TODO: (b/156984037) For when user rotates after entering overview
-     */
-    private boolean mInOverview;
-    private boolean mTaskListFrozen;
 
     private boolean mIsUserSetupComplete;
 
@@ -186,6 +114,8 @@
         mDefaultDisplay = DefaultDisplay.INSTANCE.get(context);
         mDisplayId = mDefaultDisplay.getInfo().id;
         runOnDestroy(() -> mDefaultDisplay.removeChangeListener(this));
+        mRotationTouchHelper = new RotationTouchHelper(context);
+        runOnDestroy(mRotationTouchHelper::destroy);
 
         // Register for user unlocked if necessary
         mIsUserUnlocked = context.getSystemService(UserManager.class)
@@ -207,10 +137,6 @@
         };
         runOnDestroy(mExclusionListener::unregister);
 
-        Resources resources = mContext.getResources();
-        mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode,
-                () -> QuickStepContract.getWindowCornerRadius(resources));
-
         // Register for navigation mode changes
         onNavigationModeChanged(mSysUiNavMode.addModeChangeListener(this));
         runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(this));
@@ -241,38 +167,6 @@
             userSetupObserver.register();
             runOnDestroy(userSetupObserver::unregister);
         }
-
-        mOrientationListener = new OrientationEventListener(context) {
-            @Override
-            public void onOrientationChanged(int degrees) {
-                int newRotation = RecentsOrientedState.getRotationForUserDegreesRotated(degrees,
-                        mSensorRotation);
-                if (newRotation == mSensorRotation) {
-                    return;
-                }
-
-                mSensorRotation = newRotation;
-                mPrioritizeDeviceRotation = true;
-
-                if (newRotation == mCurrentAppRotation) {
-                    // When user rotates device to the orientation of the foreground app after
-                    // quickstepping
-                    toggleSecondaryNavBarsForRotation();
-                }
-            }
-        };
-    }
-
-    private void setupOrientationSwipeHandler() {
-        ActivityManagerWrapper.getInstance().registerTaskStackListener(mFrozenTaskListener);
-        mOnDestroyFrozenTaskRunnable = () -> ActivityManagerWrapper.getInstance()
-                .unregisterTaskStackListener(mFrozenTaskListener);
-        runOnDestroy(mOnDestroyFrozenTaskRunnable);
-    }
-
-    private void destroyOrientationSwipeHandlerCallback() {
-        ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mFrozenTaskListener);
-        mOnDestroyActions.remove(mOnDestroyFrozenTaskRunnable);
     }
 
     private void runOnDestroy(Runnable action) {
@@ -311,13 +205,6 @@
 
         mNavBarPosition = new NavBarPosition(newMode, mDefaultDisplay.getInfo());
 
-        mOrientationTouchTransformer.setNavigationMode(newMode, mDefaultDisplay.getInfo());
-        if (!mMode.hasGestures && newMode.hasGestures) {
-            setupOrientationSwipeHandler();
-        } else if (mMode.hasGestures && !newMode.hasGestures){
-            destroyOrientationSwipeHandlerCallback();
-        }
-
         mMode = newMode;
     }
 
@@ -328,28 +215,10 @@
             return;
         }
 
-        mDisplayRotation = info.rotation;
-
         if (!mMode.hasGestures) {
             return;
         }
         mNavBarPosition = new NavBarPosition(mMode, info);
-        updateGestureTouchRegions();
-        mOrientationTouchTransformer.createOrAddTouchRegion(info);
-        mCurrentAppRotation = mDisplayRotation;
-
-        /* Update nav bars on the following:
-         * a) if this is coming from an activity rotation OR
-         *   aa) we launch an app in the orientation that user is already in
-         * b) We're not in overview, since overview will always be portrait (w/o home rotation)
-         * c) We're actively in quickswitch mode
-         */
-        if ((mPrioritizeDeviceRotation
-                || mCurrentAppRotation == mSensorRotation) // switch to an app of orientation user is in
-                && !mInOverview
-                && mTaskListFrozen) {
-            toggleSecondaryNavBarsForRotation();
-        }
     }
 
     /**
@@ -464,7 +333,7 @@
      */
     public boolean canStartSystemGesture() {
         boolean canStartWithNavHidden = (mSystemUiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0
-                || mTaskListFrozen;
+                || mRotationTouchHelper.isTaskListFrozen();
         return canStartWithNavHidden
                 && (mSystemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0
                 && (mSystemUiStateFlags & SYSUI_STATE_QUICK_SETTINGS_EXPANDED) == 0
@@ -537,33 +406,6 @@
     }
 
     /**
-     * Updates the regions for detecting the swipe up/quickswitch and assistant gestures.
-     */
-    public void updateGestureTouchRegions() {
-        if (!mMode.hasGestures) {
-            return;
-        }
-
-        mOrientationTouchTransformer.createOrAddTouchRegion(mDefaultDisplay.getInfo());
-    }
-
-    /**
-     * @return whether the coordinates of the {@param event} is in the swipe up gesture region.
-     */
-    public boolean isInSwipeUpTouchRegion(MotionEvent event) {
-        return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY());
-    }
-
-    /**
-     * @return whether the coordinates of the {@param event} with the given {@param pointerIndex}
-     *         is in the swipe up gesture region.
-     */
-    public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex) {
-        return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(pointerIndex),
-                event.getY(pointerIndex));
-    }
-
-    /**
      * Sets the region in screen space where the gestures should be deferred (ie. due to specific
      * nav bar ui).
      */
@@ -620,101 +462,13 @@
     public boolean canTriggerAssistantAction(MotionEvent ev, ActivityManager.RunningTaskInfo task) {
         return mAssistantAvailable
                 && !QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags)
-                && mOrientationTouchTransformer.touchInAssistantRegion(ev)
+                && mRotationTouchHelper.touchInAssistantRegion(ev)
                 && !isLockToAppActive()
                 && !isGestureBlockedActivity(task);
     }
 
-    /**
-     * *May* apply a transform on the motion event if it lies in the nav bar region for another
-     * orientation that is currently being tracked as a part of quickstep
-     */
-    void setOrientationTransformIfNeeded(MotionEvent event) {
-        // negative coordinates bug b/143901881
-        if (event.getX() < 0 || event.getY() < 0) {
-            event.setLocation(Math.max(0, event.getX()), Math.max(0, event.getY()));
-        }
-        mOrientationTouchTransformer.transform(event);
-    }
-
-    private void enableMultipleRegions(boolean enable) {
-        mOrientationTouchTransformer.enableMultipleRegions(enable, mDefaultDisplay.getInfo());
-        notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getQuickStepStartingRotation());
-        if (enable && !mInOverview && !TestProtocol.sDisableSensorRotation) {
-            // Clear any previous state from sensor manager
-            mSensorRotation = mCurrentAppRotation;
-            mOrientationListener.enable();
-        } else {
-            mOrientationListener.disable();
-        }
-    }
-
-    public void onStartGesture() {
-        if (mTaskListFrozen) {
-            // Prioritize whatever nav bar user touches once in quickstep
-            // This case is specifically when user changes what nav bar they are using mid
-            // quickswitch session before tasks list is unfrozen
-            notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
-        }
-    }
-
-    void onEndTargetCalculated(GestureState.GestureEndTarget endTarget,
-            BaseActivityInterface activityInterface) {
-        if (endTarget == GestureState.GestureEndTarget.RECENTS) {
-            mInOverview = true;
-            if (!mTaskListFrozen) {
-                // If we're in landscape w/o ever quickswitching, show the navbar in landscape
-                enableMultipleRegions(true);
-            }
-            activityInterface.onExitOverview(this, mExitOverviewRunnable);
-        } else if (endTarget == GestureState.GestureEndTarget.HOME) {
-            enableMultipleRegions(false);
-        } else if (endTarget == GestureState.GestureEndTarget.NEW_TASK) {
-            if (mOrientationTouchTransformer.getQuickStepStartingRotation() == -1) {
-                // First gesture to start quickswitch
-                enableMultipleRegions(true);
-            } else {
-                notifySysuiOfCurrentRotation(
-                        mOrientationTouchTransformer.getCurrentActiveRotation());
-            }
-
-            // A new gesture is starting, reset the current device rotation
-            // This is done under the assumption that the user won't rotate the phone and then
-            // quickswitch in the old orientation.
-            mPrioritizeDeviceRotation = false;
-        } else if (endTarget == GestureState.GestureEndTarget.LAST_TASK) {
-            if (!mTaskListFrozen) {
-                // touched nav bar but didn't go anywhere and not quickswitching, do nothing
-                return;
-            }
-            notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
-        }
-    }
-
-    private void notifySysuiOfCurrentRotation(int rotation) {
-        UI_HELPER_EXECUTOR.execute(() -> SystemUiProxy.INSTANCE.get(mContext)
-                .onQuickSwitchToNewTask(rotation));
-    }
-
-    /**
-     * Disables/Enables multiple nav bars on {@link OrientationTouchTransformer} and then
-     * notifies system UI of the primary rotation the user is interacting with
-     */
-    private void toggleSecondaryNavBarsForRotation() {
-        mOrientationTouchTransformer.setSingleActiveRegion(mDefaultDisplay.getInfo());
-        notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
-    }
-
-    public int getCurrentActiveRotation() {
-        if (!mMode.hasGestures) {
-            // touch rotation should always match that of display for 3 button
-            return mDisplayRotation;
-        }
-        return mOrientationTouchTransformer.getCurrentActiveRotation();
-    }
-
-    public int getDisplayRotation() {
-        return mDisplayRotation;
+    public RotationTouchHelper getRotationTouchHelper() {
+        return mRotationTouchHelper;
     }
 
     public void dump(PrintWriter pw) {
@@ -726,9 +480,7 @@
         pw.println("  assistantAvailable=" + mAssistantAvailable);
         pw.println("  assistantDisabled="
                 + QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags));
-        pw.println("  currentActiveRotation=" + getCurrentActiveRotation());
-        pw.println("  displayRotation=" + getDisplayRotation());
         pw.println("  isUserUnlocked=" + mIsUserUnlocked);
-        mOrientationTouchTransformer.dump(pw);
+        mRotationTouchHelper.dump(pw);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 517501a..6f54ba2 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -93,15 +93,6 @@
     }
 
     /**
-     * @return The task id of the running task, or -1 if there is no current running task.
-     */
-    public static int getRunningTaskId() {
-        ActivityManager.RunningTaskInfo runningTask =
-                ActivityManagerWrapper.getInstance().getRunningTask();
-        return runningTask != null ? runningTask.id : -1;
-    }
-
-    /**
      * @return Whether the provided {@param changeId} is the latest recent tasks list id.
      */
     public boolean isTaskListValid(int changeId) {
@@ -140,7 +131,9 @@
         }
 
         // Keep the cache up to date with the latest thumbnails
-        int runningTaskId = RecentsModel.getRunningTaskId();
+        ActivityManager.RunningTaskInfo runningTask =
+                ActivityManagerWrapper.getInstance().getRunningTask();
+        int runningTaskId = runningTask != null ? runningTask.id : -1;
         mTaskList.getTaskKeys(mThumbnailCache.getCacheSize(), tasks -> {
             for (Task task : tasks) {
                 if (task.key.id == runningTaskId) {
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
new file mode 100644
index 0000000..5f3c022
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -0,0 +1,363 @@
+/*
+ * 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;
+
+import static android.view.Surface.ROTATION_0;
+
+import static com.android.launcher3.util.DefaultDisplay.CHANGE_ALL;
+import static com.android.launcher3.util.DefaultDisplay.CHANGE_FRAME_DELAY;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.quickstep.util.RecentsOrientedState;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+public class RotationTouchHelper implements
+        SysUINavigationMode.NavigationModeChangeListener,
+        DefaultDisplay.DisplayInfoChangeListener {
+
+    private final OrientationTouchTransformer mOrientationTouchTransformer;
+    private final DefaultDisplay mDefaultDisplay;
+    private final SysUINavigationMode mSysUiNavMode;
+    private final int mDisplayId;
+    private int mDisplayRotation;
+
+    private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
+
+    private SysUINavigationMode.Mode mMode = THREE_BUTTONS;
+
+    private TaskStackChangeListener mFrozenTaskListener = new TaskStackChangeListener() {
+        @Override
+        public void onRecentTaskListFrozenChanged(boolean frozen) {
+            mTaskListFrozen = frozen;
+            if (frozen || mInOverview) {
+                return;
+            }
+            enableMultipleRegions(false);
+        }
+
+        @Override
+        public void onActivityRotation(int displayId) {
+            super.onActivityRotation(displayId);
+            // This always gets called before onDisplayInfoChanged() so we know how to process
+            // the rotation in that method. This is done to avoid having a race condition between
+            // the sensor readings and onDisplayInfoChanged() call
+            if (displayId != mDisplayId) {
+                return;
+            }
+
+            mPrioritizeDeviceRotation = true;
+            if (mInOverview) {
+                // reset, launcher must be rotating
+                mExitOverviewRunnable.run();
+            }
+        }
+    };
+
+    private Runnable mExitOverviewRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mInOverview = false;
+            enableMultipleRegions(false);
+        }
+    };
+
+    /**
+     * Used to listen for when the device rotates into the orientation of the current foreground
+     * app. For example, if a user quickswitches from a portrait to a fixed landscape app and then
+     * rotates rotates the device to match that orientation, this triggers calls to sysui to adjust
+     * the navbar.
+     */
+    private OrientationEventListener mOrientationListener;
+    private int mSensorRotation = ROTATION_0;
+    /**
+     * This is the configuration of the foreground app or the app that will be in the foreground
+     * once a quickstep gesture finishes.
+     */
+    private int mCurrentAppRotation = -1;
+    /**
+     * This flag is set to true when the device physically changes orientations. When true, we will
+     * always report the current rotation of the foreground app whenever the display changes, as it
+     * would indicate the user's intention to rotate the foreground app.
+     */
+    private boolean mPrioritizeDeviceRotation = false;
+    private Runnable mOnDestroyFrozenTaskRunnable;
+    /**
+     * Set to true when user swipes to recents. In recents, we ignore the state of the recents
+     * task list being frozen or not to allow the user to keep interacting with nav bar rotation
+     * they went into recents with as opposed to defaulting to the default display rotation.
+     * TODO: (b/156984037) For when user rotates after entering overview
+     */
+    private boolean mInOverview;
+    private boolean mTaskListFrozen;
+
+
+    private final Context mContext;
+
+    public RotationTouchHelper(Context context) {
+        mContext = context;
+        Resources resources = mContext.getResources();
+        mSysUiNavMode = SysUINavigationMode.INSTANCE.get(context);
+        mDefaultDisplay = DefaultDisplay.INSTANCE.get(context);
+        mDisplayId = mDefaultDisplay.getInfo().id;
+
+        mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode,
+                () -> QuickStepContract.getWindowCornerRadius(resources));
+
+        // Register for navigation mode changes
+        onNavigationModeChanged(mSysUiNavMode.addModeChangeListener(this));
+        runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(this));
+
+        mOrientationListener = new OrientationEventListener(context) {
+            @Override
+            public void onOrientationChanged(int degrees) {
+                int newRotation = RecentsOrientedState.getRotationForUserDegreesRotated(degrees,
+                        mSensorRotation);
+                if (newRotation == mSensorRotation) {
+                    return;
+                }
+
+                mSensorRotation = newRotation;
+                mPrioritizeDeviceRotation = true;
+
+                if (newRotation == mCurrentAppRotation) {
+                    // When user rotates device to the orientation of the foreground app after
+                    // quickstepping
+                    toggleSecondaryNavBarsForRotation();
+                }
+            }
+        };
+    }
+
+    private void setupOrientationSwipeHandler() {
+        ActivityManagerWrapper.getInstance().registerTaskStackListener(mFrozenTaskListener);
+        mOnDestroyFrozenTaskRunnable = () -> ActivityManagerWrapper.getInstance()
+                .unregisterTaskStackListener(mFrozenTaskListener);
+        runOnDestroy(mOnDestroyFrozenTaskRunnable);
+    }
+
+    private void destroyOrientationSwipeHandlerCallback() {
+        ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mFrozenTaskListener);
+        mOnDestroyActions.remove(mOnDestroyFrozenTaskRunnable);
+    }
+
+    private void runOnDestroy(Runnable action) {
+        mOnDestroyActions.add(action);
+    }
+
+    /**
+     * Cleans up all the registered listeners and receivers.
+     */
+    public void destroy() {
+        for (Runnable r : mOnDestroyActions) {
+            r.run();
+        }
+    }
+
+    public boolean isTaskListFrozen() {
+        return mTaskListFrozen;
+    }
+
+    public boolean touchInAssistantRegion(MotionEvent ev) {
+        return mOrientationTouchTransformer.touchInAssistantRegion(ev);
+    }
+
+    /**
+     * Updates the regions for detecting the swipe up/quickswitch and assistant gestures.
+     */
+    public void updateGestureTouchRegions() {
+        if (!mMode.hasGestures) {
+            return;
+        }
+
+        mOrientationTouchTransformer.createOrAddTouchRegion(mDefaultDisplay.getInfo());
+    }
+
+    /**
+     * @return whether the coordinates of the {@param event} is in the swipe up gesture region.
+     */
+    public boolean isInSwipeUpTouchRegion(MotionEvent event) {
+        return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY());
+    }
+
+    /**
+     * @return whether the coordinates of the {@param event} with the given {@param pointerIndex}
+     *         is in the swipe up gesture region.
+     */
+    public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex) {
+        return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(pointerIndex),
+                event.getY(pointerIndex));
+    }
+
+
+    @Override
+    public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
+        mDefaultDisplay.removeChangeListener(this);
+        mDefaultDisplay.addChangeListener(this);
+        onDisplayInfoChanged(mDefaultDisplay.getInfo(), CHANGE_ALL);
+
+        mOrientationTouchTransformer.setNavigationMode(newMode, mDefaultDisplay.getInfo());
+        if (!mMode.hasGestures && newMode.hasGestures) {
+            setupOrientationSwipeHandler();
+        } else if (mMode.hasGestures && !newMode.hasGestures){
+            destroyOrientationSwipeHandlerCallback();
+        }
+
+        mMode = newMode;
+    }
+
+    public int getDisplayRotation() {
+        return mDisplayRotation;
+    }
+
+    @Override
+    public void onDisplayInfoChanged(DefaultDisplay.Info info, int flags) {
+        if (info.id != mDisplayId|| flags == CHANGE_FRAME_DELAY) {
+            // ignore displays that aren't running launcher and frame refresh rate changes
+            return;
+        }
+
+        mDisplayRotation = info.rotation;
+
+        if (!mMode.hasGestures) {
+            return;
+        }
+        updateGestureTouchRegions();
+        mOrientationTouchTransformer.createOrAddTouchRegion(info);
+        mCurrentAppRotation = mDisplayRotation;
+
+        /* Update nav bars on the following:
+         * a) if this is coming from an activity rotation OR
+         *   aa) we launch an app in the orientation that user is already in
+         * b) We're not in overview, since overview will always be portrait (w/o home rotation)
+         * c) We're actively in quickswitch mode
+         */
+        if ((mPrioritizeDeviceRotation
+                || mCurrentAppRotation == mSensorRotation) // switch to an app of orientation user is in
+                && !mInOverview
+                && mTaskListFrozen) {
+            toggleSecondaryNavBarsForRotation();
+        }
+    }
+
+    /**
+     * *May* apply a transform on the motion event if it lies in the nav bar region for another
+     * orientation that is currently being tracked as a part of quickstep
+     */
+    void setOrientationTransformIfNeeded(MotionEvent event) {
+        // negative coordinates bug b/143901881
+        if (event.getX() < 0 || event.getY() < 0) {
+            event.setLocation(Math.max(0, event.getX()), Math.max(0, event.getY()));
+        }
+        mOrientationTouchTransformer.transform(event);
+    }
+
+    private void enableMultipleRegions(boolean enable) {
+        mOrientationTouchTransformer.enableMultipleRegions(enable, mDefaultDisplay.getInfo());
+        notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getQuickStepStartingRotation());
+        if (enable && !mInOverview && !TestProtocol.sDisableSensorRotation) {
+            // Clear any previous state from sensor manager
+            mSensorRotation = mCurrentAppRotation;
+            mOrientationListener.enable();
+        } else {
+            mOrientationListener.disable();
+        }
+    }
+
+    public void onStartGesture() {
+        if (mTaskListFrozen) {
+            // Prioritize whatever nav bar user touches once in quickstep
+            // This case is specifically when user changes what nav bar they are using mid
+            // quickswitch session before tasks list is unfrozen
+            notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
+        }
+    }
+
+    void onEndTargetCalculated(GestureState.GestureEndTarget endTarget,
+            BaseActivityInterface activityInterface) {
+        if (endTarget == GestureState.GestureEndTarget.RECENTS) {
+            mInOverview = true;
+            if (!mTaskListFrozen) {
+                // If we're in landscape w/o ever quickswitching, show the navbar in landscape
+                enableMultipleRegions(true);
+            }
+            activityInterface.onExitOverview(this, mExitOverviewRunnable);
+        } else if (endTarget == GestureState.GestureEndTarget.HOME) {
+            enableMultipleRegions(false);
+        } else if (endTarget == GestureState.GestureEndTarget.NEW_TASK) {
+            if (mOrientationTouchTransformer.getQuickStepStartingRotation() == -1) {
+                // First gesture to start quickswitch
+                enableMultipleRegions(true);
+            } else {
+                notifySysuiOfCurrentRotation(
+                        mOrientationTouchTransformer.getCurrentActiveRotation());
+            }
+
+            // A new gesture is starting, reset the current device rotation
+            // This is done under the assumption that the user won't rotate the phone and then
+            // quickswitch in the old orientation.
+            mPrioritizeDeviceRotation = false;
+        } else if (endTarget == GestureState.GestureEndTarget.LAST_TASK) {
+            if (!mTaskListFrozen) {
+                // touched nav bar but didn't go anywhere and not quickswitching, do nothing
+                return;
+            }
+            notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
+        }
+    }
+
+    private void notifySysuiOfCurrentRotation(int rotation) {
+        UI_HELPER_EXECUTOR.execute(() -> SystemUiProxy.INSTANCE.get(mContext)
+                .onQuickSwitchToNewTask(rotation));
+    }
+
+    /**
+     * Disables/Enables multiple nav bars on {@link OrientationTouchTransformer} and then
+     * notifies system UI of the primary rotation the user is interacting with
+     */
+    private void toggleSecondaryNavBarsForRotation() {
+        mOrientationTouchTransformer.setSingleActiveRegion(mDefaultDisplay.getInfo());
+        notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
+    }
+
+    public int getCurrentActiveRotation() {
+        if (!mMode.hasGestures) {
+            // touch rotation should always match that of display for 3 button
+            return mDisplayRotation;
+        }
+        return mOrientationTouchTransformer.getCurrentActiveRotation();
+    }
+
+    public void dump(PrintWriter pw) {
+        pw.println("RotationTouchHelper:");
+        pw.println("  currentActiveRotation=" + getCurrentActiveRotation());
+        pw.println("  displayRotation=" + getDisplayRotation());
+        mOrientationTouchTransformer.dump(pw);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/SysUINavigationMode.java b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
index 05ce2a2..f2675f2 100644
--- a/quickstep/src/com/android/quickstep/SysUINavigationMode.java
+++ b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
@@ -16,6 +16,9 @@
 
 package com.android.quickstep;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_2_BUTTON;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_3_BUTTON;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON;
 import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
 
 import android.content.BroadcastReceiver;
@@ -24,6 +27,7 @@
 import android.content.res.Resources;
 import android.util.Log;
 
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.MainThreadInitializedObject;
 
@@ -37,16 +41,18 @@
 public class SysUINavigationMode {
 
     public enum Mode {
-        THREE_BUTTONS(false, 0),
-        TWO_BUTTONS(true, 1),
-        NO_BUTTON(true, 2);
+        THREE_BUTTONS(false, 0, LAUNCHER_NAVIGATION_MODE_3_BUTTON),
+        TWO_BUTTONS(true, 1, LAUNCHER_NAVIGATION_MODE_2_BUTTON),
+        NO_BUTTON(true, 2, LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON);
 
         public final boolean hasGestures;
         public final int resValue;
+        public final LauncherEvent launcherEvent;
 
-        Mode(boolean hasGestures, int resValue) {
+        Mode(boolean hasGestures, int resValue, LauncherEvent launcherEvent) {
             this.hasGestures = hasGestures;
             this.resValue = resValue;
+            this.launcherEvent = launcherEvent;
         }
     }
 
@@ -147,7 +153,6 @@
     }
 
     public interface NavigationModeChangeListener {
-
         void onNavigationModeChanged(Mode newMode);
     }
 }
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
index 0e2312b..81c4d0c 100644
--- a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
@@ -16,6 +16,7 @@
 package com.android.quickstep.interaction;
 
 import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_COMPLETED;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_BAD_ANGLE;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT;
@@ -48,6 +49,7 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.NavBarPosition;
 import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -74,6 +76,7 @@
     private final PointF mAssistantStartDragPos = new PointF();
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
+    private final MotionPauseDetector mMotionPauseDetector;
     private boolean mTouchCameFromAssistantCorner;
     private boolean mTouchCameFromNavBar;
     private boolean mPassedAssistantSlop;
@@ -100,6 +103,7 @@
                 new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/,
                         new NavBarPosition(Mode.NO_BUTTON, displayRotation),
                         null /*onInterceptTouch*/, this);
+        mMotionPauseDetector = new MotionPauseDetector(context);
 
         final Resources resources = context.getResources();
         mBottomGestureHeight =
@@ -177,12 +181,14 @@
                 }
                 mLaunchedAssistant = false;
                 mSwipeUpTouchTracker.init();
+                mMotionPauseDetector.clear();
+                mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseChanged);
                 break;
             case MotionEvent.ACTION_MOVE:
+                mLastPos.set(event.getX(), event.getY());
                 if (!mAssistantGestureActive) {
                     break;
                 }
-                mLastPos.set(event.getX(), event.getY());
 
                 if (!mPassedAssistantSlop) {
                     // Normal gesture, ensure we pass the slop before we start tracking the gesture
@@ -213,6 +219,8 @@
                 break;
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
+                mMotionPauseDetector.clear();
+                mMotionPauseDetector.setOnMotionPauseListener(null);
                 if (mGestureCallback != null && !intercepted && mTouchCameFromNavBar) {
                     mGestureCallback.onNavBarGestureAttempted(
                             HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, new PointF());
@@ -239,9 +247,17 @@
         }
         mSwipeUpTouchTracker.onMotionEvent(event);
         mAssistantGestureDetector.onTouchEvent(event);
+        mMotionPauseDetector.addPosition(event);
+        mMotionPauseDetector.setDisallowPause(mLastPos.y >= mDisplaySize.y - mBottomGestureHeight);
         return intercepted;
     }
 
+    protected void onMotionPauseChanged(boolean isPaused) {
+        if (isPaused) {
+            VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
+        }
+    }
+
     /**
      * Determine if angle is larger than threshold for assistant detection
      */
diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
index 14e00dc..a157314 100644
--- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
+import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 import static com.android.quickstep.BaseSwipeUpHandlerV2.MAX_SWIPE_DURATION;
 import static com.android.quickstep.interaction.TutorialController.TutorialType.HOME_NAVIGATION_COMPLETE;
 import static com.android.quickstep.interaction.TutorialController.TutorialType.OVERVIEW_NAVIGATION_COMPLETE;
@@ -110,6 +111,7 @@
         AnimatorListenerAdapter resetTaskView = new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation, boolean isReverse) {
+                mFakeIconView.setVisibility(View.INVISIBLE);
                 mFakeTaskView.setVisibility(View.INVISIBLE);
                 mFakeTaskView.setAlpha(1);
                 mRunningWindowAnim = null;
@@ -131,6 +133,7 @@
             });
         } else {
             anim.setViewAlpha(mFakeTaskView, 0, ACCEL);
+            anim.setViewAlpha(mFakeIconView, 0, ACCEL);
             anim.addListener(resetTaskView);
         }
         if (onEndRunnable != null) {
@@ -181,8 +184,7 @@
 
         @Override
         public void updateFinalShift() {
-            float progress = mCurrentShift.value / mDragLengthFactor;
-            mWindowTransitionController.setPlayFraction(progress);
+            mWindowTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
             mTaskViewSimulator.apply(mTransformParams);
         }
 
@@ -202,7 +204,7 @@
             // derivative of the scroll interpolator at zero, ie. 2.
             long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y));
             long duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
-            HomeAnimationFactory homeAnimFactory = new HomeAnimationFactory(null) {
+            HomeAnimationFactory homeAnimFactory = new HomeAnimationFactory() {
                 @Override
                 public AnimatorPlaybackController createActivityAnimationToHome() {
                     return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
@@ -218,6 +220,24 @@
                             fakeHomeIconLeft + fakeHomeIconSizePx,
                             fakeHomeIconTop + fakeHomeIconSizePx);
                 }
+
+                @Override
+                public void update(RectF rect, float progress, float radius) {
+                    mFakeIconView.setVisibility(View.VISIBLE);
+                    mFakeIconView.update(rect, progress,
+                            1f - SHAPE_PROGRESS_DURATION /* shapeProgressStart */,
+                            radius,
+                            false, /* isOpening */
+                            mFakeIconView, mDp,
+                            false /* isVerticalBarLayout */);
+                    mFakeIconView.setAlpha(1);
+                    mFakeTaskView.setAlpha(getWindowAlpha(progress));
+                }
+
+                @Override
+                public void onCancel() {
+                    mFakeIconView.setVisibility(View.INVISIBLE);
+                }
             };
             RectFSpringAnim windowAnim = createWindowAnimationToHome(startShift, homeAnimFactory);
             windowAnim.start(mContext, velocityPxPerMs);
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index c1918c2..73f1f8c 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -28,6 +28,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.R;
+import com.android.launcher3.views.ClipIconView;
 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback;
 
@@ -46,6 +47,7 @@
     final TextView mTitleTextView;
     final TextView mSubtitleTextView;
     final TextView mFeedbackView;
+    final ClipIconView mFakeIconView;
     final View mFakeTaskView;
     final View mRippleView;
     final RippleDrawable mRippleDrawable;
@@ -66,6 +68,7 @@
         mTitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_title_view);
         mSubtitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_subtitle_view);
         mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view);
+        mFakeIconView = rootView.findViewById(R.id.gesture_tutorial_fake_icon_view);
         mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view);
         mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view);
         mRippleDrawable = (RippleDrawable) mRippleView.getBackground();
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index eac45e9..256ad82 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -16,6 +16,10 @@
 
 package com.android.quickstep.logging;
 
+import static android.text.format.DateUtils.DAY_IN_MILLIS;
+import static android.text.format.DateUtils.formatElapsedTime;
+
+import static com.android.launcher3.Utilities.getDevicePrefs;
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.FOLDER;
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.SEARCH_RESULT_CONTAINER;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_SNAPSHOT;
@@ -24,6 +28,8 @@
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__HOME;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__OVERVIEW;
 
+import static java.lang.System.currentTimeMillis;
+
 import android.content.Context;
 import android.util.Log;
 
@@ -33,6 +39,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
+import com.android.launcher3.logger.LauncherAtom.FolderContainer.ParentContainerCase;
 import com.android.launcher3.logger.LauncherAtom.FolderIcon;
 import com.android.launcher3.logger.LauncherAtom.FromState;
 import com.android.launcher3.logger.LauncherAtom.ToState;
@@ -52,6 +59,7 @@
 import com.android.systemui.shared.system.SysUiStatsLog;
 
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Optional;
 import java.util.OptionalInt;
 
@@ -59,17 +67,18 @@
  * This class calls StatsLog compile time generated methods.
  *
  * To see if the logs are properly sent to statsd, execute following command.
+ * <ul>
  * $ wwdebug (to turn on the logcat printout)
  * $ wwlogcat (see logcat with grep filter on)
  * $ statsd_testdrive (see how ww is writing the proto to statsd buffer)
+ * </ul>
  */
 public class StatsLogCompatManager extends StatsLogManager {
 
     private static final String TAG = "StatsLog";
     private static final boolean IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.STATSLOG);
 
-    private static Context sContext;
-
+    private static final String LAST_SNAPSHOT_TIME_MILLIS = "LAST_SNAPSHOT_TIME_MILLIS";
     private static final InstanceId DEFAULT_INSTANCE_ID = InstanceId.fakeInstanceId(0);
     // LauncherAtom.ItemInfo.getDefaultInstance() should be used but until launcher proto migrates
     // from nano to lite, bake constant to prevent robo test failure.
@@ -77,8 +86,10 @@
     private static final int FOLDER_HIERARCHY_OFFSET = 100;
     private static final int SEARCH_RESULT_HIERARCHY_OFFSET = 200;
 
+    private final Context mContext;
+
     public StatsLogCompatManager(Context context) {
-        sContext = context;
+        mContext = context;
     }
 
     @Override
@@ -100,23 +111,40 @@
     }
 
     /**
-     * Logs the workspace layout information on the model thread.
+     * Logs impression of the current workspace with additional launcher events.
      */
     @Override
-    public void logSnapshot() {
-        LauncherAppState.getInstance(sContext).getModel().enqueueModelUpdateTask(
-                new SnapshotWorker());
+    public void logSnapshot(List<EventEnum> extraEvents) {
+        LauncherAppState.getInstance(mContext).getModel().enqueueModelUpdateTask(
+                new SnapshotWorker(extraEvents));
     }
 
     private class SnapshotWorker extends BaseModelUpdateTask {
         private final InstanceId mInstanceId;
-        SnapshotWorker() {
-            mInstanceId = new InstanceIdSequence(
-                    1 << 20 /*InstanceId.INSTANCE_ID_MAX*/).newInstanceId();
+        private final List<EventEnum> mExtraEvents;
+
+        SnapshotWorker(List<EventEnum> extraEvents) {
+            mInstanceId = new InstanceIdSequence(1 << 20 /*InstanceId.INSTANCE_ID_MAX*/)
+                    .newInstanceId();
+            this.mExtraEvents = extraEvents;
         }
 
         @Override
         public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+            long lastSnapshotTimeMillis = getDevicePrefs(mContext)
+                    .getLong(LAST_SNAPSHOT_TIME_MILLIS, 0);
+            // Log snapshot only if previous snapshot was older than a day
+            if (currentTimeMillis() - lastSnapshotTimeMillis < DAY_IN_MILLIS) {
+                if (IS_VERBOSE) {
+                    String elapsedTime = formatElapsedTime(
+                            (currentTimeMillis() - lastSnapshotTimeMillis) / 1000);
+                    Log.d(TAG, String.format(
+                            "Skipped snapshot logging since previous snapshot was %s old.",
+                            elapsedTime));
+                }
+                return;
+            }
+
             IntSparseArrayMap<FolderInfo> folders = dataModel.folders.clone();
             ArrayList<ItemInfo> workspaceItems = (ArrayList) dataModel.workspaceItems.clone();
             ArrayList<LauncherAppWidgetInfo> appWidgets = (ArrayList) dataModel.appWidgets.clone();
@@ -132,16 +160,22 @@
                         LauncherAtom.ItemInfo atomInfo = info.buildProto(fInfo);
                         writeSnapshot(atomInfo, mInstanceId);
                     }
-                } catch (Exception e) { }
+                } catch (Exception e) {
+                }
             }
             for (ItemInfo info : appWidgets) {
                 LauncherAtom.ItemInfo atomInfo = info.buildProto(null);
                 writeSnapshot(atomInfo, mInstanceId);
             }
+            mExtraEvents
+                    .forEach(eventName -> logger().withInstanceId(mInstanceId).log(eventName));
+
+            getDevicePrefs(mContext).edit()
+                    .putLong(LAST_SNAPSHOT_TIME_MILLIS, currentTimeMillis()).apply();
         }
     }
 
-    private static void writeSnapshot(LauncherAtom.ItemInfo info, InstanceId instanceId) {
+    private void writeSnapshot(LauncherAtom.ItemInfo info, InstanceId instanceId) {
         if (IS_VERBOSE) {
             Log.d(TAG, String.format("\nwriteSnapshot(%d):\n%s", instanceId.getId(), info));
         }
@@ -260,7 +294,7 @@
             } else {
                 // Item is inside the folder, fetch folder info in a BG thread
                 // and then write to StatsLog.
-                LauncherAppState.getInstance(sContext).getModel().enqueueModelUpdateTask(
+                LauncherAppState.getInstanceNoCreate().getModel().enqueueModelUpdateTask(
                         new BaseModelUpdateTask() {
                             @Override
                             public void execute(LauncherAppState app, BgDataModel dataModel,
@@ -337,7 +371,7 @@
     }
 
     private static int getCardinality(LauncherAtom.ItemInfo info) {
-        switch (info.getContainerInfo().getContainerCase()){
+        switch (info.getContainerInfo().getContainerCase()) {
             case PREDICTED_HOTSEAT_CONTAINER:
                 return info.getContainerInfo().getPredictedHotseatContainer().getCardinality();
             case SEARCH_RESULT_CONTAINER:
@@ -402,9 +436,16 @@
     }
 
     private static int getPageId(LauncherAtom.ItemInfo info) {
+        if (info.hasTask()) {
+            return info.getTask().getIndex();
+        }
         switch (info.getContainerInfo().getContainerCase()) {
             case FOLDER:
                 return info.getContainerInfo().getFolder().getPageIndex();
+            case HOTSEAT:
+                return info.getContainerInfo().getHotseat().getIndex();
+            case PREDICTED_HOTSEAT_CONTAINER:
+                return info.getContainerInfo().getPredictedHotseatContainer().getIndex();
             default:
                 return info.getContainerInfo().getWorkspace().getPageIndex();
         }
@@ -413,6 +454,10 @@
     private static int getParentPageId(LauncherAtom.ItemInfo info) {
         switch (info.getContainerInfo().getContainerCase()) {
             case FOLDER:
+                if (info.getContainerInfo().getFolder().getParentContainerCase()
+                        == ParentContainerCase.HOTSEAT) {
+                    return info.getContainerInfo().getFolder().getHotseat().getIndex();
+                }
                 return info.getContainerInfo().getFolder().getWorkspace().getPageIndex();
             case SEARCH_RESULT_CONTAINER:
                 return info.getContainerInfo().getSearchResultContainer().getWorkspace()
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
new file mode 100644
index 0000000..a19a67c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -0,0 +1,230 @@
+/*
+ * 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 com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
+
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.FloatProperty;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.quickstep.LauncherActivityInterface;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Controls an animation that can go beyond progress = 1, at which point resistance should be
+ * applied. Internally, this is a wrapper around 2 {@link AnimatorPlaybackController}s, one that
+ * runs from progress 0 to 1 like normal, then one that seamlessly continues that animation but
+ * starts applying resistance as well.
+ */
+public class AnimatorControllerWithResistance {
+
+    /**
+     * How much farther we can drag past overview in 2-button mode, as a factor of the distance
+     * it takes to drag from an app to overview.
+     */
+    public static final float TWO_BUTTON_EXTRA_DRAG_FACTOR = 0.25f;
+
+    private enum RecentsParams {
+        FROM_APP(0.75f, 0.5f, 1f),
+        FROM_OVERVIEW(1f, 0.75f, 0.5f);
+
+        RecentsParams(float scaleStartResist, float scaleMaxResist, float translationFactor) {
+            this.scaleStartResist = scaleStartResist;
+            this.scaleMaxResist = scaleMaxResist;
+            this.translationFactor = translationFactor;
+        }
+
+        /**
+         * Start slowing down the rate of scaling down when recents view is smaller than this scale.
+         */
+        public final float scaleStartResist;
+
+        /**
+         * Recents view will reach this scale at the very end of the drag.
+         */
+        public final float scaleMaxResist;
+
+        /**
+         * How much translation to apply to RecentsView when the drag reaches the top of the screen,
+         * where 0 will keep it centered and 1 will have it barely touch the top of the screen.
+         */
+        public final float translationFactor;
+    }
+
+    private static final TimeInterpolator RECENTS_SCALE_RESIST_INTERPOLATOR = DEACCEL;
+    private static final TimeInterpolator RECENTS_TRANSLATE_RESIST_INTERPOLATOR = LINEAR;
+
+    private final AnimatorPlaybackController mNormalController;
+    private final AnimatorPlaybackController mResistanceController;
+
+    // Initialize to -1 so the first 0 gets applied.
+    private float mLastNormalProgress = -1;
+    private float mLastResistProgress;
+
+    public AnimatorControllerWithResistance(AnimatorPlaybackController normalController,
+            AnimatorPlaybackController resistanceController) {
+        mNormalController = normalController;
+        mResistanceController = resistanceController;
+    }
+
+    public AnimatorPlaybackController getNormalController() {
+        return mNormalController;
+    }
+
+    /**
+     * Applies the current progress of the animation.
+     * @param progress From 0 to maxProgress, where 1 is the target we are animating towards.
+     * @param maxProgress > 1, this is where the resistance will be applied.
+     */
+    public void setProgress(float progress, float maxProgress) {
+        float normalProgress = Utilities.boundToRange(progress, 0, 1);
+        if (normalProgress != mLastNormalProgress) {
+            mLastNormalProgress = normalProgress;
+            mNormalController.setPlayFraction(normalProgress);
+        }
+        if (maxProgress <= 1) {
+            return;
+        }
+        float resistProgress = progress <= 1 ? 0 : Utilities.getProgress(progress, 1, maxProgress);
+        if (resistProgress != mLastResistProgress) {
+            mLastResistProgress = resistProgress;
+            mResistanceController.setPlayFraction(resistProgress);
+        }
+    }
+
+    /**
+     * Applies resistance to recents when swiping up past its target position.
+     * @param normalController The controller to run from 0 to 1 before this resistance applies.
+     * @param context Used to compute start and end values.
+     * @param recentsOrientedState Used to compute start and end values.
+     * @param dp Used to compute start and end values.
+     * @param scaleTarget The target for the scaleProperty.
+     * @param scaleProperty Animate the value to change the scale of the window/recents view.
+     * @param translationTarget The target for the translationProperty.
+     * @param translationProperty Animate the value to change the translation of the recents view.
+     */
+    public static <SCALE, TRANSLATION> AnimatorControllerWithResistance createForRecents(
+            AnimatorPlaybackController normalController, Context context,
+            RecentsOrientedState recentsOrientedState, DeviceProfile dp, SCALE scaleTarget,
+            FloatProperty<SCALE> scaleProperty, TRANSLATION translationTarget,
+            FloatProperty<TRANSLATION> translationProperty) {
+
+        PendingAnimation resistAnim = createRecentsResistanceAnim(null, context,
+                recentsOrientedState, dp, scaleTarget, scaleProperty, translationTarget,
+                translationProperty, RecentsParams.FROM_APP);
+
+        AnimatorPlaybackController resistanceController = resistAnim.createPlaybackController();
+        return new AnimatorControllerWithResistance(normalController, resistanceController);
+    }
+
+    /**
+     * Creates the resistance animation for {@link #createForRecents}, or can be used separately
+     * when starting from recents, i.e. {@link #createRecentsResistanceFromOverviewAnim}.
+     */
+    public static <SCALE, TRANSLATION> PendingAnimation createRecentsResistanceAnim(
+            @Nullable PendingAnimation resistAnim, Context context,
+            RecentsOrientedState recentsOrientedState, DeviceProfile dp, SCALE scaleTarget,
+            FloatProperty<SCALE> scaleProperty, TRANSLATION translationTarget,
+            FloatProperty<TRANSLATION> translationProperty, RecentsParams params) {
+        Rect startRect = new Rect();
+        LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, startRect,
+                recentsOrientedState.getOrientationHandler());
+        long distanceToCover = startRect.bottom;
+        boolean isTwoButtonMode = SysUINavigationMode.getMode(context) == TWO_BUTTONS;
+        if (isTwoButtonMode) {
+            // We can only drag a small distance past overview, not to the top of the screen.
+            distanceToCover = (long)
+                    ((dp.heightPx - startRect.bottom) * TWO_BUTTON_EXTRA_DRAG_FACTOR);
+        }
+        if (resistAnim == null) {
+            resistAnim = new PendingAnimation(distanceToCover * 2);
+        }
+
+        PointF pivot = new PointF();
+        float fullscreenScale = recentsOrientedState.getFullScreenScaleAndPivot(
+                startRect, dp, pivot);
+        float startScale = 1;
+        float prevScaleRate = (fullscreenScale - startScale) / (dp.heightPx - startRect.bottom);
+        // This is what the scale would be at the end of the drag if we didn't apply resistance.
+        float endScale = startScale - prevScaleRate * distanceToCover;
+        final TimeInterpolator scaleInterpolator;
+        if (isTwoButtonMode) {
+            // We are bounded by the distance of the drag, so we don't need to apply resistance.
+            scaleInterpolator = LINEAR;
+        } else {
+            // Create an interpolator that resists the scale so the scale doesn't get smaller than
+            // RECENTS_SCALE_MAX_RESIST.
+            float startResist = Utilities.getProgress(params.scaleStartResist , startScale,
+                    endScale);
+            float maxResist = Utilities.getProgress(params.scaleMaxResist, startScale, endScale);
+            scaleInterpolator = t -> {
+                if (t < startResist) {
+                    return t;
+                }
+                float resistProgress = Utilities.getProgress(t, startResist, 1);
+                resistProgress = RECENTS_SCALE_RESIST_INTERPOLATOR.getInterpolation(resistProgress);
+                return startResist + resistProgress * (maxResist - startResist);
+            };
+        }
+        resistAnim.addFloat(scaleTarget, scaleProperty, startScale, endScale,
+                scaleInterpolator);
+
+        if (!isTwoButtonMode) {
+            // Compute where the task view would be based on the end scale, if we didn't translate.
+            RectF endRectF = new RectF(startRect);
+            Matrix temp = new Matrix();
+            temp.setScale(params.scaleMaxResist, params.scaleMaxResist, pivot.x, pivot.y);
+            temp.mapRect(endRectF);
+            // Translate such that the task view touches the top of the screen when drag does.
+            float endTranslation = endRectF.top * recentsOrientedState.getOrientationHandler()
+                    .getSecondaryTranslationDirectionFactor() * params.translationFactor;
+            resistAnim.addFloat(translationTarget, translationProperty, 0, endTranslation,
+                    RECENTS_TRANSLATE_RESIST_INTERPOLATOR);
+        }
+
+        return resistAnim;
+    }
+
+    /**
+     * Helper method to update or create a PendingAnimation suitable for animating
+     * a RecentsView interaction that started from the overview state.
+     */
+    public static PendingAnimation createRecentsResistanceFromOverviewAnim(
+            BaseDraggingActivity activity, @Nullable PendingAnimation resistanceAnim) {
+        RecentsView recentsView = activity.getOverviewPanel();
+        return createRecentsResistanceAnim(resistanceAnim, activity,
+                recentsView.getPagedViewOrientedState(), activity.getDeviceProfile(),
+                recentsView, RECENTS_SCALE_PROPERTY, recentsView, TASK_SECONDARY_TRANSLATION,
+                RecentsParams.FROM_OVERVIEW);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index ae19d73..f7bd1e2 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -49,7 +49,7 @@
             Rect taskSize = new Rect();
             LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, taskSize,
                     orientationHandler);
-            return (dp.heightPx - taskSize.height()) / 2;
+            return orientationHandler.getDistanceToBottomOfRect(dp, taskSize);
         }
         int shelfHeight = dp.hotseatBarSizePx + dp.getInsets().bottom;
         int spaceBetweenShelfAndRecents = (int) context.getResources().getDimension(
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index a5d4568..f60f7ad 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -276,7 +276,7 @@
         private static final int HISTORY_SIZE = 20;
 
         // Position history are stored in a circular array
-        private final float[] mHistoricTimes = new float[HISTORY_SIZE];
+        private final long[] mHistoricTimes = new long[HISTORY_SIZE];
         private final float[] mHistoricPos = new float[HISTORY_SIZE];
         private int mHistoryCount = 0;
         private int mHistoryStart = 0;
@@ -292,7 +292,7 @@
             mHistoryCount = mHistoryStart = 0;
         }
 
-        private void addPositionAndTime(float eventTime, float eventPosition) {
+        private void addPositionAndTime(long eventTime, float eventPosition) {
             mHistoricTimes[mHistoryStart] = eventTime;
             mHistoricPos[mHistoryStart] = eventPosition;
             mHistoryStart++;
@@ -322,7 +322,7 @@
          * Based on solveUnweightedLeastSquaresDeg2 in VelocityTracker.cpp
          */
         private Float solveUnweightedLeastSquaresDeg2(final int pointPos) {
-            final float eventTime = mHistoricTimes[pointPos];
+            final long eventTime = mHistoricTimes[pointPos];
 
             float sxi = 0, sxiyi = 0, syi = 0, sxi2 = 0, sxi3 = 0, sxi2yi = 0, sxi4 = 0;
             int count = 0;
@@ -332,8 +332,8 @@
                     index += HISTORY_SIZE;
                 }
 
-                float time = mHistoricTimes[index];
-                float age = eventTime - time;
+                long time = mHistoricTimes[index];
+                long age = eventTime - time;
                 if (age > HORIZON_MS) {
                     break;
                 }
@@ -358,18 +358,23 @@
 
             if (count < 3) {
                 // Too few samples
-                if (count == 2) {
-                    int endPos = pointPos - 1;
-                    if (endPos < 0) {
-                        endPos += HISTORY_SIZE;
+                switch (count) {
+                    case 2: {
+                        int endPos = pointPos - 1;
+                        if (endPos < 0) {
+                            endPos += HISTORY_SIZE;
+                        }
+                        long denominator = eventTime - mHistoricTimes[endPos];
+                        if (denominator != 0) {
+                            return (mHistoricPos[pointPos] - mHistoricPos[endPos]) / denominator;
+                        }
                     }
-                    float denominator = eventTime - mHistoricTimes[endPos];
-                    if (denominator != 0) {
-                        return (eventTime - mHistoricPos[endPos]) / denominator;
-
-                    }
+                    // fall through
+                    case 1:
+                        return 0f;
+                    default:
+                        return null;
                 }
-                return null;
             }
 
             float Sxx = sxi2 - sxi * sxi / count;
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index d822b6c..81d24d7 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -33,7 +33,6 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
-import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.graphics.Matrix;
@@ -48,7 +47,6 @@
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
@@ -58,7 +56,6 @@
 import com.android.launcher3.util.WindowBounds;
 import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.SysUINavigationMode;
-import com.android.systemui.shared.system.ConfigurationCompat;
 
 import java.lang.annotation.Retention;
 import java.util.function.IntConsumer;
@@ -91,6 +88,7 @@
     private @SurfaceRotation int mTouchRotation = ROTATION_0;
     private @SurfaceRotation int mDisplayRotation = ROTATION_0;
     private @SurfaceRotation int mRecentsActivityRotation = ROTATION_0;
+    private @SurfaceRotation int mRecentsRotation = ROTATION_0 - 1;
 
     // Launcher activity supports multiple orientation, but fallback activity does not
     private static final int FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY = 1 << 0;
@@ -133,8 +131,6 @@
     private int mFlags;
     private int mPreviousRotation = ROTATION_0;
 
-    @Nullable private Configuration mActivityConfiguration;
-
     /**
      * @param rotationChangeListener Callback for receiving rotation events when rotation watcher
      *                              is enabled
@@ -170,11 +166,11 @@
     }
 
     /**
-     * Sets the configuration for the recents activity, which could affect the activity's rotation
+     * Sets the rotation for the recents activity, which could affect the appearance of task view.
      * @see #update(int, int)
      */
-    public boolean setActivityConfiguration(Configuration activityConfiguration) {
-        mActivityConfiguration = activityConfiguration;
+    public boolean setRecentsRotation(@SurfaceRotation int recentsRotation) {
+        mRecentsRotation = recentsRotation;
         return update(mTouchRotation, mDisplayRotation);
     }
 
@@ -231,9 +227,7 @@
     @SurfaceRotation
     private int inferRecentsActivityRotation(@SurfaceRotation int displayRotation) {
         if (isRecentsActivityRotationAllowed()) {
-            return mActivityConfiguration == null
-                    ? displayRotation
-                    : ConfigurationCompat.getWindowConfigurationRotation(mActivityConfiguration);
+            return mRecentsRotation < ROTATION_0 ? displayRotation : mRecentsRotation;
         } else {
             return ROTATION_0;
         }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index bf093fd..ecd4e2b 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.tapl.Background;
 import com.android.launcher3.tapl.LauncherInstrumentation.NavigationModel;
 import com.android.launcher3.tapl.Overview;
+import com.android.launcher3.tapl.OverviewActions;
 import com.android.launcher3.tapl.OverviewTask;
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.ui.TaplTestsLauncher3;
@@ -68,11 +69,14 @@
         });
     }
 
-    private void startTestApps() throws Exception {
+    public static void startTestApps() throws Exception {
         startAppFast(getAppPackageName());
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
         startTestActivity(2);
+    }
 
+    private void startTestAppsWithCheck() throws Exception {
+        startTestApps();
         executeOnLauncher(launcher -> assertTrue(
                 "Launcher activity is the top activity; expecting another activity to be the top "
                         + "one",
@@ -105,7 +109,7 @@
     @Test
     @PortraitLandscape
     public void testOverview() throws Exception {
-        startTestApps();
+        startTestAppsWithCheck();
         // mLauncher.pressHome() also tests an important case of pressing home while in background.
         Overview overview = mLauncher.pressHome().switchToOverview();
         assertTrue("Launcher internal state didn't switch to Overview",
@@ -189,6 +193,22 @@
                         0, getTaskCount(launcher)));
     }
 
+    /**
+     * Smoke test for action buttons: Presses all the buttons and makes sure no crashes occur.
+     */
+    @Test
+    @NavigationModeSwitch
+    @PortraitLandscape
+    public void testOverviewActions() throws Exception {
+        if (mLauncher.getNavigationModel() != NavigationModel.TWO_BUTTON) {
+            startTestAppsWithCheck();
+            OverviewActions actionsView =
+                    mLauncher.pressHome().switchToOverview().getOverviewActions();
+            actionsView.clickAndDismissScreenshot();
+            actionsView.clickAndDismissShare();
+        }
+    }
+
     private int getCurrentOverviewPage(Launcher launcher) {
         return launcher.<RecentsView>getOverviewPanel().getCurrentPage();
     }
diff --git a/res/layout/floating_surface_view.xml b/res/layout/floating_surface_view.xml
new file mode 100644
index 0000000..434e84f
--- /dev/null
+++ b/res/layout/floating_surface_view.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.launcher3.views.FloatingSurfaceView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content" />
diff --git a/res/values/config.xml b/res/values/config.xml
index ca25325..75fcc90 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -142,7 +142,7 @@
 
     <item name="staggered_damping_ratio" type="dimen" format="float">0.7</item>
     <item name="staggered_stiffness" type="dimen" format="float">150</item>
-    <dimen name="unlock_staggered_velocity_dp_per_s">3dp</dimen>
+    <dimen name="unlock_staggered_velocity_dp_per_s">4dp</dimen>
 
     <item name="hint_scale_damping_ratio" type="dimen" format="float">0.7</item>
     <item name="hint_scale_stiffness" type="dimen" format="float">200</item>
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index cd27a2d..ce37a30 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -62,7 +62,8 @@
             TYPE_ALL_APPS_EDU,
 
             TYPE_TASK_MENU,
-            TYPE_OPTIONS_POPUP
+            TYPE_OPTIONS_POPUP,
+            TYPE_ICON_SURFACE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface FloatingViewType {}
@@ -80,16 +81,18 @@
     // Popups related to quickstep UI
     public static final int TYPE_TASK_MENU = 1 << 10;
     public static final int TYPE_OPTIONS_POPUP = 1 << 11;
+    public static final int TYPE_ICON_SURFACE = 1 << 12;
 
     public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP
             | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET
             | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU
-            | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU;
+            | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU
+            | TYPE_ICON_SURFACE;
 
     // Type of popups which should be kept open during launcher rebind
     public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET
             | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE
-            | TYPE_ALL_APPS_EDU;
+            | TYPE_ALL_APPS_EDU | TYPE_ICON_SURFACE;
 
     // Usually we show the back button when a floating view is open. Instead, hide for these types.
     public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 9cb8cf2..833ce15 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -42,6 +42,7 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.Launcher.OnResumeCallback;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.InstanceIdSequence;
@@ -82,7 +83,7 @@
         super.onCreate(savedInstanceState);
 
 
-        mIsSafeModeEnabled = TraceHelper.whitelistIpcs("isSafeMode",
+        mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
                 () -> getPackageManager().isSafeMode());
         DefaultDisplay.INSTANCE.get(this).addChangeListener(this);
 
@@ -108,10 +109,20 @@
 
     private void updateTheme() {
         if (mThemeRes != Themes.getActivityThemeRes(this)) {
-            recreate();
+            // Workaround (b/162812884): The system currently doesn't allow recreating an activity
+            // when it is not resumed, in such a case defer recreation until it is possible
+            if (hasBeenResumed()) {
+                recreate();
+            } else {
+                addOnResumeCallback(this::recreate);
+            }
         }
     }
 
+    protected void addOnResumeCallback(OnResumeCallback callback) {
+        // To be overridden
+    }
+
     @Override
     public void onActionModeStarted(ActionMode mode) {
         super.onActionModeStarted(mode);
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 60b6da6..198f13d 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -331,10 +331,7 @@
     public boolean onTouchEvent(MotionEvent event) {
         // ignore events if they happen in padding area
         if (event.getAction() == MotionEvent.ACTION_DOWN
-                && (event.getY() < getPaddingTop()
-                || event.getX() < getPaddingLeft()
-                || event.getY() > getHeight() - getPaddingBottom()
-                || event.getX() > getWidth() - getPaddingRight())) {
+                && shouldIgnoreTouchDown(event.getX(), event.getY())) {
             return false;
         }
         if (isLongClickable()) {
@@ -347,6 +344,16 @@
         }
     }
 
+    /**
+     * Returns true if the touch down at the provided position be ignored
+     */
+    protected boolean shouldIgnoreTouchDown(float x, float y) {
+        return y < getPaddingTop()
+                || x < getPaddingLeft()
+                || y > getHeight() - getPaddingBottom()
+                || x > getWidth() - getPaddingRight();
+    }
+
     void setStayPressed(boolean stayPressed) {
         mStayPressed = stayPressed;
         refreshDrawableState();
@@ -607,6 +614,9 @@
     @Override
     public void setIconVisible(boolean visible) {
         mIsIconVisible = visible;
+        if (!mIsIconVisible) {
+            resetIconScale();
+        }
         Drawable icon = visible ? mIcon : new ColorDrawable(Color.TRANSPARENT);
         applyCompoundDrawables(icon);
     }
@@ -746,11 +756,14 @@
 
     @Override
     public SafeCloseable prepareDrawDragView() {
-        if (getIcon() instanceof FastBitmapDrawable) {
-            FastBitmapDrawable icon = (FastBitmapDrawable) getIcon();
-            icon.setScale(1f);
-        }
+        resetIconScale();
         setForceHideDot(true);
         return () -> { };
     }
+
+    private void resetIconScale() {
+        if (mIcon instanceof FastBitmapDrawable) {
+            ((FastBitmapDrawable) mIcon).setScale(1f);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/GestureNavContract.java b/src/com/android/launcher3/GestureNavContract.java
new file mode 100644
index 0000000..2a7e629
--- /dev/null
+++ b/src/com/android/launcher3/GestureNavContract.java
@@ -0,0 +1,101 @@
+/*
+ * 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 static android.content.Intent.EXTRA_COMPONENT_NAME;
+import static android.content.Intent.EXTRA_USER;
+
+import android.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.SurfaceControl;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Class to encapsulate the handshake protocol between Launcher and gestureNav.
+ */
+public class GestureNavContract {
+
+    private static final String TAG = "GestureNavContract";
+
+    public static final String EXTRA_GESTURE_CONTRACT = "gesture_nav_contract_v1";
+    public static final String EXTRA_ICON_POSITION = "gesture_nav_contract_icon_position";
+    public static final String EXTRA_ICON_SURFACE = "gesture_nav_contract_surface_control";
+    public static final String EXTRA_REMOTE_CALLBACK = "android.intent.extra.REMOTE_CALLBACK";
+
+    public final ComponentName componentName;
+    public final UserHandle user;
+
+    private final Message mCallback;
+
+    public GestureNavContract(ComponentName componentName, UserHandle user, Message callback) {
+        this.componentName = componentName;
+        this.user = user;
+        this.mCallback = callback;
+    }
+
+    /**
+     * Sends the position information to the receiver
+     */
+    @TargetApi(Build.VERSION_CODES.R)
+    public void sendEndPosition(RectF position, @Nullable SurfaceControl surfaceControl) {
+        Bundle result = new Bundle();
+        result.putParcelable(EXTRA_ICON_POSITION, position);
+        result.putParcelable(EXTRA_ICON_SURFACE, surfaceControl);
+
+        Message callback = Message.obtain();
+        callback.copyFrom(mCallback);
+        callback.setData(result);
+
+        try {
+            callback.replyTo.send(callback);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending icon position", e);
+        }
+    }
+
+    /**
+     * Clears and returns the GestureNavContract if it was present in the intent.
+     */
+    public static GestureNavContract fromIntent(Intent intent) {
+        if (!Utilities.ATLEAST_R) {
+            return null;
+        }
+        Bundle extras = intent.getBundleExtra(EXTRA_GESTURE_CONTRACT);
+        if (extras == null) {
+            return null;
+        }
+        intent.removeExtra(EXTRA_GESTURE_CONTRACT);
+
+        ComponentName componentName = extras.getParcelable(EXTRA_COMPONENT_NAME);
+        UserHandle userHandle = extras.getParcelable(EXTRA_USER);
+        Message callback = extras.getParcelable(EXTRA_REMOTE_CALLBACK);
+
+        if (componentName != null && userHandle != null && callback != null
+                && callback.replyTo != null) {
+            return new GestureNavContract(componentName, userHandle, callback);
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 52583d4..dede954 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -21,6 +21,7 @@
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ICON_SURFACE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_SNACKBAR;
 import static com.android.launcher3.InstallShortcutReceiver.FLAG_DRAG_AND_DROP;
@@ -168,6 +169,7 @@
 import com.android.launcher3.util.UiThreadHelper;
 import com.android.launcher3.util.ViewOnDrawExecutor;
 import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.FloatingSurfaceView;
 import com.android.launcher3.views.OptionsPopupView;
 import com.android.launcher3.views.ScrimView;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
@@ -509,6 +511,7 @@
     public void onEnterAnimationComplete() {
         super.onEnterAnimationComplete();
         mRotationHelper.setCurrentTransitionRequest(REQUEST_NONE);
+        AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE);
     }
 
     @Override
@@ -923,6 +926,7 @@
         DiscoveryBounce.showForHomeIfNeeded(this);
     }
 
+    protected void handlePendingActivityRequest() { }
 
     private void logStopAndResume(int command) {
         if (mPendingExecutor != null) return;
@@ -1424,7 +1428,8 @@
                 if (!isInState(NORMAL)) {
                     // Only change state, if not already the same. This prevents cancelling any
                     // animations running as part of resume
-                    mStateManager.goToState(NORMAL);
+                    mStateManager.goToState(NORMAL, mStateManager.shouldAnimateStateChange(),
+                            this::handlePendingActivityRequest);
                 }
 
                 // Reset the apps view
@@ -1449,6 +1454,7 @@
                 mLauncherCallbacks.onHomeIntent(internalStateHandled);
             }
             mOverlayManager.hideOverlay(isStarted() && !isForceInvisible());
+            handleGestureContract(intent);
         } else if (Intent.ACTION_ALL_APPS.equals(intent.getAction())) {
             getStateManager().goToState(ALL_APPS, alreadyOnHome);
         }
@@ -1457,6 +1463,17 @@
     }
 
     /**
+     * Handles gesture nav contract
+     */
+    protected void handleGestureContract(Intent intent) {
+        GestureNavContract gnc = GestureNavContract.fromIntent(intent);
+        if (gnc != null) {
+            AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE);
+            FloatingSurfaceView.show(this, gnc);
+        }
+    }
+
+    /**
      * Hides the keyboard if visible
      */
     public void hideKeyboard() {
@@ -1927,6 +1944,7 @@
         return result;
     }
 
+    @Override
     public void addOnResumeCallback(OnResumeCallback callback) {
         mOnResumeCallbacks.add(callback);
     }
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index e8b5568..19a314f 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -100,10 +100,12 @@
     public static final int SCHEMA_VERSION = 28;
 
     public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".settings";
+    public static final String KEY_LAYOUT_PROVIDER_AUTHORITY = "KEY_LAYOUT_PROVIDER_AUTHORITY";
 
     static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
 
     protected DatabaseHelper mOpenHelper;
+    protected String mProviderAuthority;
 
     private long mLastRestoreTimestamp = 0L;
 
@@ -367,7 +369,8 @@
             case LauncherSettings.Settings.METHOD_WAS_EMPTY_DB_CREATED : {
                 Bundle result = new Bundle();
                 result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
-                        Utilities.getPrefs(getContext()).getBoolean(EMPTY_DATABASE_CREATED, false));
+                        Utilities.getPrefs(getContext()).getBoolean(
+                                mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false));
                 return result;
             }
             case LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS: {
@@ -437,6 +440,7 @@
                                             getContext(), true /* forMigration */)));
                     return result;
                 }
+                return null;
             }
             case LauncherSettings.Settings.METHOD_PREP_FOR_PREVIEW: {
                 if (MULTI_DB_GRID_MIRATION_ALGO.get()) {
@@ -450,6 +454,23 @@
                                     () -> mOpenHelper));
                     return result;
                 }
+                return null;
+            }
+            case LauncherSettings.Settings.METHOD_SWITCH_DATABASE: {
+                if (TextUtils.equals(arg, mOpenHelper.getDatabaseName())) return null;
+                final DatabaseHelper helper = mOpenHelper;
+                if (extras == null || !extras.containsKey(KEY_LAYOUT_PROVIDER_AUTHORITY)) {
+                    mProviderAuthority = null;
+                } else {
+                    mProviderAuthority = extras.getString(KEY_LAYOUT_PROVIDER_AUTHORITY);
+                }
+                mOpenHelper = DatabaseHelper.createDatabaseHelper(
+                        getContext(), arg, false /* forMigration */);
+                helper.close();
+                LauncherAppState app = LauncherAppState.getInstanceNoCreate();
+                if (app == null) return null;
+                app.getModel().forceReload();
+                return null;
             }
         }
         return null;
@@ -492,7 +513,8 @@
     }
 
     private void clearFlagEmptyDbCreated() {
-        Utilities.getPrefs(getContext()).edit().remove(EMPTY_DATABASE_CREATED).commit();
+        Utilities.getPrefs(getContext()).edit()
+                .remove(mOpenHelper.getKey(EMPTY_DATABASE_CREATED)).commit();
     }
 
     /**
@@ -505,7 +527,7 @@
     synchronized private void loadDefaultFavoritesIfNecessary() {
         SharedPreferences sp = Utilities.getPrefs(getContext());
 
-        if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
+        if (sp.getBoolean(mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false)) {
             Log.d(TAG, "loading default workspace");
 
             AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
@@ -553,8 +575,13 @@
      */
     private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
         Context ctx = getContext();
-        String authority = Settings.Secure.getString(ctx.getContentResolver(),
-                "launcher3.layout.provider");
+        final String authority;
+        if (!TextUtils.isEmpty(mProviderAuthority)) {
+            authority = mProviderAuthority;
+        } else {
+            authority = Settings.Secure.getString(ctx.getContentResolver(),
+                    "launcher3.layout.provider");
+        }
         if (TextUtils.isEmpty(authority)) {
             return null;
         }
@@ -694,11 +721,25 @@
         }
 
         /**
+         * Re-composite given key in respect to database. If the current db is
+         * {@link LauncherFiles#LAUNCHER_DB}, return the key as-is. Otherwise append the db name to
+         * given key. e.g. consider key="EMPTY_DATABASE_CREATED", dbName="minimal.db", the returning
+         * string will be "EMPTY_DATABASE_CREATED@minimal.db".
+         */
+        String getKey(final String key) {
+            if (TextUtils.equals(getDatabaseName(), LauncherFiles.LAUNCHER_DB)) {
+                return key;
+            }
+            return key + "@" + getDatabaseName();
+        }
+
+        /**
          * Overriden in tests.
          */
         protected void onEmptyDbCreated() {
             // Set the flag for empty DB
-            Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
+            Utilities.getPrefs(mContext).edit().putBoolean(getKey(EMPTY_DATABASE_CREATED), true)
+                    .commit();
         }
 
         public long getSerialNumberForUser(UserHandle user) {
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 5512654..58a418e 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -354,14 +354,20 @@
 
         public static final String METHOD_PREP_FOR_PREVIEW = "prep_for_preview";
 
+        public static final String METHOD_SWITCH_DATABASE = "switch_database";
+
         public static final String EXTRA_VALUE = "value";
 
         public static Bundle call(ContentResolver cr, String method) {
-            return call(cr, method, null);
+            return call(cr, method, null /* arg */);
         }
 
         public static Bundle call(ContentResolver cr, String method, String arg) {
-            return cr.call(CONTENT_URI, method, arg, null);
+            return call(cr, method, arg, null /* extras */);
+        }
+
+        public static Bundle call(ContentResolver cr, String method, String arg, Bundle extras) {
+            return cr.call(CONTENT_URI, method, arg, extras);
         }
     }
 }
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index c78df62..39b0f2f 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -250,7 +250,8 @@
     }
 
     public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
-        if (this != NORMAL || !launcher.getDeviceProfile().shouldFadeAdjacentWorkspaceScreens()) {
+        if ((this != NORMAL && this != HINT_STATE)
+                || !launcher.getDeviceProfile().shouldFadeAdjacentWorkspaceScreens()) {
             return DEFAULT_ALPHA_PROVIDER;
         }
         final int centerPage = launcher.getWorkspace().getNextPage();
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index e29faac..e13d89f 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -1448,11 +1448,8 @@
         int minDistanceFromScreenCenterIndex = -1;
         final int childCount = getChildCount();
         for (int i = 0; i < childCount; ++i) {
-            View layout = getPageAt(i);
-            int childSize = mOrientationHandler.getMeasuredSize(layout);
-            int halfChildSize = (childSize / 2);
-            int childCenter = getChildOffset(i) + halfChildSize;
-            int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
+            int distanceFromScreenCenter = Math.abs(
+                    getDisplacementFromScreenCenter(i, screenCenter));
             if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
                 minDistanceFromScreenCenter = distanceFromScreenCenter;
                 minDistanceFromScreenCenterIndex = i;
@@ -1461,6 +1458,20 @@
         return minDistanceFromScreenCenterIndex;
     }
 
+    private int getDisplacementFromScreenCenter(int childIndex, int screenCenter) {
+        View layout = getPageAt(childIndex);
+        int childSize = mOrientationHandler.getMeasuredSize(layout);
+        int halfChildSize = (childSize / 2);
+        int childCenter = getChildOffset(childIndex) + halfChildSize;
+        return childCenter - screenCenter;
+    }
+
+    protected int getDisplacementFromScreenCenter(int childIndex) {
+        int pageOrientationSize = mOrientationHandler.getMeasuredSize(this);
+        int screenCenter = mOrientationHandler.getPrimaryScroll(this) + (pageOrientationSize / 2);
+        return getDisplacementFromScreenCenter(childIndex, screenCenter);
+    }
+
     protected void snapToDestination() {
         snapToPage(getPageNearestToCenterOfScreen(), getPageSnapDuration());
     }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 1441e0b..a6283ff 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -309,7 +309,9 @@
             // In portrait, we want the pages spaced such that there is no
             // overhang of the previous / next page into the current page viewport.
             // We assume symmetrical padding in portrait mode.
-            setPageSpacing(Math.max(grid.edgeMarginPx, padding.left + 1));
+            int maxInsets = Math.max(insets.left, insets.right);
+            int maxPadding = Math.max(grid.edgeMarginPx, padding.left + 1);
+            setPageSpacing(Math.max(maxInsets, maxPadding));
         }
 
 
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index 860cceb..6ad43ea 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -198,6 +198,7 @@
         public OvershootParams(float startProgress, float overshootPastProgress,
                 float endProgress, float velocityPxPerMs, int totalDistancePx, Context context) {
             velocityPxPerMs = Math.abs(velocityPxPerMs);
+            overshootPastProgress = Math.max(overshootPastProgress, startProgress);
             start = startProgress;
             int startPx = (int) (start * totalDistancePx);
             // Overshoot by about half a frame.
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 5c4a492..88a9aba 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -92,7 +92,7 @@
 
     // Keep as DeviceFlag to allow remote disable in emergency.
     public static final BooleanFlag ENABLE_SUGGESTED_ACTIONS_OVERVIEW = new DeviceFlag(
-            "ENABLE_SUGGESTED_ACTIONS_OVERVIEW", false, "Show chip hints on the overview screen");
+            "ENABLE_SUGGESTED_ACTIONS_OVERVIEW", true, "Show chip hints on the overview screen");
 
     public static final BooleanFlag FOLDER_NAME_SUGGEST = new DeviceFlag(
             "FOLDER_NAME_SUGGEST", true,
@@ -177,6 +177,10 @@
     public static final BooleanFlag USER_EVENT_DISPATCHER = new DeviceFlag(
             "USER_EVENT_DISPATCHER", true, "User event dispatcher collects logs.");
 
+    public static final BooleanFlag ENABLE_MINIMAL_DEVICE = new DeviceFlag(
+            "ENABLE_MINIMAL_DEVICE", false,
+            "Allow user to toggle minimal device mode in launcher.");
+
     public static void initialize(Context context) {
         synchronized (sDebugFlags) {
             for (DebugFlag flag : sDebugFlags) {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 1c18402..b91d1c3 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -741,7 +741,8 @@
 
     @Override
     protected View getAccessibilityInitialFocusView() {
-        return mContent.getFirstItem();
+        View firstItem = mContent.getFirstItem();
+        return firstItem != null ? firstItem : super.getAccessibilityInitialFocusView();
     }
 
     private void closeComplete(boolean wasAnimated) {
@@ -1134,6 +1135,9 @@
      * Rearranges the children based on their rank.
      */
     public void rearrangeChildren() {
+        if (!mContent.areViewsBound()) {
+            return;
+        }
         mContent.arrangeChildren(getIconsInReadingOrder());
         mItemsInvalidated = true;
     }
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 75275b2..32d061c 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -129,6 +129,8 @@
     private float mDotScale;
     private Animator mDotScaleAnim;
 
+    private Rect mTouchArea = new Rect();
+
     private final PointF mTranslationForReorderBounce = new PointF(0, 0);
     private final PointF mTranslationForReorderPreview = new PointF(0, 0);
     private float mScaleForReorderBounce = 1f;
@@ -711,6 +713,11 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
+        if (event.getAction() == MotionEvent.ACTION_DOWN
+                && shouldIgnoreTouchDown(event.getX(), event.getY())) {
+            return false;
+        }
+
         // Call the superclass onTouchEvent first, because sometimes it changes the state to
         // isPressed() on an ACTION_UP
         super.onTouchEvent(event);
@@ -719,6 +726,15 @@
         return true;
     }
 
+    /**
+     * Returns true if the touch down at the provided position be ignored
+     */
+    protected boolean shouldIgnoreTouchDown(float x, float y) {
+        mTouchArea.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
+                getHeight() - getPaddingBottom());
+        return !mTouchArea.contains((int) x, (int) y);
+    }
+
     @Override
     public void cancelLongPress() {
         super.cancelLongPress();
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 32531c0..a08dd30 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -500,6 +500,9 @@
      * Reorders the items such that the {@param empty} spot moves to {@param target}
      */
     public void realTimeReorder(int empty, int target) {
+        if (!mViewsBound) {
+            return;
+        }
         completePendingPageChanges();
         int delay = 0;
         float delayAmount = START_VIEW_REORDER_DELAY;
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 458ffa3..a424f84 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -126,7 +126,7 @@
      */
     public static class PreviewContext extends ContextWrapper {
 
-        private static final Set<MainThreadInitializedObject> WHITELIST = new HashSet<>(
+        private final Set<MainThreadInitializedObject> mAllowedObjects = new HashSet<>(
                 Arrays.asList(UserCache.INSTANCE, InstallSessionHelper.INSTANCE,
                         LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE,
                         CustomWidgetManager.INSTANCE, PluginManagerWrapper.INSTANCE));
@@ -160,7 +160,7 @@
          */
         public <T> T getObject(MainThreadInitializedObject<T> mainThreadInitializedObject,
                 MainThreadInitializedObject.ObjectProvider<T> provider) {
-            if (!WHITELIST.contains(mainThreadInitializedObject)) {
+            if (!mAllowedObjects.contains(mainThreadInitializedObject)) {
                 throw new IllegalStateException("Leaking unknown objects");
             }
             if (mainThreadInitializedObject == LauncherAppState.INSTANCE) {
diff --git a/src/com/android/launcher3/graphics/OverviewScrim.java b/src/com/android/launcher3/graphics/OverviewScrim.java
index 94acbfd..c0c3e5e 100644
--- a/src/com/android/launcher3/graphics/OverviewScrim.java
+++ b/src/com/android/launcher3/graphics/OverviewScrim.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 
 import android.graphics.Rect;
+import android.util.FloatProperty;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -33,10 +34,25 @@
  */
 public class OverviewScrim extends Scrim {
 
+    public static final FloatProperty<OverviewScrim> SCRIM_MULTIPLIER =
+            new FloatProperty<OverviewScrim>("scrimMultiplier") {
+                @Override
+                public Float get(OverviewScrim scrim) {
+                    return scrim.mScrimMultiplier;
+                }
+
+                @Override
+                public void setValue(OverviewScrim scrim, float v) {
+                    scrim.setScrimMultiplier(v);
+                }
+            };
+
     private @NonNull View mStableScrimmedView;
     // Might be higher up if mStableScrimmedView is invisible.
     private @Nullable View mCurrentScrimmedView;
 
+    private float mScrimMultiplier = 1f;
+
     public OverviewScrim(View view) {
         super(view);
         mStableScrimmedView = mCurrentScrimmedView = mLauncher.getOverviewPanel();
@@ -68,4 +84,16 @@
     public @Nullable View getScrimmedView() {
         return mCurrentScrimmedView;
     }
+
+    private void setScrimMultiplier(float scrimMultiplier) {
+        if (Float.compare(mScrimMultiplier, scrimMultiplier) != 0) {
+            mScrimMultiplier = scrimMultiplier;
+            invalidate();
+        }
+    }
+
+    @Override
+    protected int getScrimAlpha() {
+        return Math.round(super.getScrimAlpha() * mScrimMultiplier);
+    }
 }
diff --git a/src/com/android/launcher3/graphics/Scrim.java b/src/com/android/launcher3/graphics/Scrim.java
index f90962d..a151cba 100644
--- a/src/com/android/launcher3/graphics/Scrim.java
+++ b/src/com/android/launcher3/graphics/Scrim.java
@@ -61,7 +61,11 @@
     }
 
     public void draw(Canvas canvas) {
-        canvas.drawColor(setColorAlphaBound(mScrimColor, mScrimAlpha));
+        canvas.drawColor(setColorAlphaBound(mScrimColor, getScrimAlpha()));
+    }
+
+    protected int getScrimAlpha() {
+        return mScrimAlpha;
     }
 
     private void setScrimProgress(float progress) {
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 8e23b65..e754618 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -33,11 +33,16 @@
 import com.android.launcher3.userevent.LauncherLogProto;
 import com.android.launcher3.util.ResourceBasedOverride;
 
+import java.util.List;
+
 /**
  * Handles the user event logging in R+.
+ *
+ * <pre>
  * All of the event ids are defined here.
  * Most of the methods are dummy methods for Launcher3
  * Actual call happens only for Launcher variant that implements QuickStep.
+ * </pre>
  */
 public class StatsLogManager implements ResourceBasedOverride {
 
@@ -49,8 +54,8 @@
     public static final int LAUNCHER_STATE_UNCHANGED = 5;
 
     /**
-     * Returns proper launcher state enum for {@link StatsLogManager}
-     * (to be removed during UserEventDispatcher cleanup)
+     * Returns proper launcher state enum for {@link StatsLogManager}(to be removed during
+     * UserEventDispatcher cleanup)
      */
     public static int containerTypeToAtomState(int containerType) {
         switch (containerType) {
@@ -67,9 +72,8 @@
     }
 
     /**
-     * Returns event enum based on the two {@link ContainerType} transition information when
-     * swipe gesture happens.
-     * (to be removed during UserEventDispatcher cleanup)
+     * Returns event enum based on the two {@link ContainerType} transition information when swipe
+     * gesture happens(to be removed during UserEventDispatcher cleanup).
      */
     public static EventEnum getLauncherAtomEvent(int startContainerType,
             int targetContainerType, EventEnum fallbackEvent) {
@@ -273,7 +277,49 @@
         LAUNCHER_SELECT_MODE_CLOSE(583),
 
         @UiEvent(doc = "User tapped on the highlight items in select mode")
-        LAUNCHER_SELECT_MODE_ITEM(584);
+        LAUNCHER_SELECT_MODE_ITEM(584),
+
+        @UiEvent(doc = "Notification dot on app icon enabled.")
+        LAUNCHER_NOTIFICATION_DOT_ENABLED(611),
+
+        @UiEvent(doc = "Notification dot on app icon disabled.")
+        LAUNCHER_NOTIFICATION_DOT_DISABLED(612),
+
+        @UiEvent(doc = "For new apps, add app icons to home screen enabled.")
+        LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED(613),
+
+        @UiEvent(doc = "For new apps, add app icons to home screen disabled.")
+        LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_DISABLED(614),
+
+        @UiEvent(doc = "Home screen rotation is enabled when phone is rotated.")
+        LAUNCHER_HOME_SCREEN_ROTATION_ENABLED(615),
+
+        @UiEvent(doc = "Home screen rotation is disabled when phone is rotated.")
+        LAUNCHER_HOME_SCREEN_ROTATION_DISABLED(616),
+
+        @UiEvent(doc = "Suggestions in all apps list enabled.")
+        LAUNCHER_ALL_APPS_SUGGESTIONS_ENABLED(619),
+
+        @UiEvent(doc = "Suggestions in all apps list disabled.")
+        LAUNCHER_ALL_APPS_SUGGESTIONS_DISABLED(620),
+
+        @UiEvent(doc = "Suggestions on home screen is enabled.")
+        LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED(621),
+
+        @UiEvent(doc = "Suggestions on home screen is disabled.")
+        LAUNCHER_HOME_SCREEN_SUGGESTIONS_DISABLED(622),
+
+        @UiEvent(doc = "System navigation is 3 button mode.")
+        LAUNCHER_NAVIGATION_MODE_3_BUTTON(623),
+
+        @UiEvent(doc = "System navigation mode is 2 button mode.")
+        LAUNCHER_NAVIGATION_MODE_2_BUTTON(624),
+
+        @UiEvent(doc = "System navigation mode is 0 button mode/gesture navigation mode .")
+        LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON(625),
+
+        @UiEvent(doc = "User tapped on image content in Overview Select mode.")
+        LAUNCHER_SELECT_MODE_IMAGE(627);
 
         // ADD MORE
 
@@ -417,8 +463,8 @@
     }
 
     /**
-     * Logs snapshot, or impression of the current workspace.
+     * Logs impression of the current workspace with additional launcher events.
      */
-    public void logSnapshot() {
+    public void logSnapshot(List<EventEnum> additionalEvents) {
     }
 }
diff --git a/src/com/android/launcher3/model/PredictionModel.java b/src/com/android/launcher3/model/PredictionModel.java
index 1429843..cb3903d 100644
--- a/src/com/android/launcher3/model/PredictionModel.java
+++ b/src/com/android/launcher3/model/PredictionModel.java
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 package com.android.launcher3.model;
+
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.content.ComponentName;
@@ -24,6 +25,7 @@
 import androidx.annotation.AnyThread;
 import androidx.annotation.WorkerThread;
 
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.pm.UserCache;
@@ -64,18 +66,28 @@
         mUserCache = UserCache.INSTANCE.get(mContext);
 
     }
+
     /**
      * Formats and stores a list of component key in device preferences.
      */
     @AnyThread
     public void cachePredictionComponentKeys(List<ComponentKey> componentKeys) {
         MODEL_EXECUTOR.execute(() -> {
+            LauncherAppState appState = LauncherAppState.getInstance(mContext);
             StringBuilder builder = new StringBuilder();
             int count = Math.min(componentKeys.size(), MAX_CACHE_ITEMS);
             for (int i = 0; i < count; i++) {
                 builder.append(serializeComponentKeyToString(componentKeys.get(i)));
                 builder.append("\n");
             }
+            if (componentKeys.isEmpty() /* should invalidate loader items */) {
+                appState.getModel().enqueueModelUpdateTask(new BaseModelUpdateTask() {
+                    @Override
+                    public void execute(LauncherAppState app, BgDataModel model, AllAppsList apps) {
+                        model.cachedPredictedItems.clear();
+                    }
+                });
+            }
             mDevicePrefs.edit().putString(CACHED_ITEMS_KEY, builder.toString()).apply();
         });
     }
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 5aab41a..2d7d6b0 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -21,8 +21,10 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArrayMap;
+import android.util.Log;
 import android.util.LongSparseArray;
 
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
@@ -102,6 +104,9 @@
                 mUsers = null;
                 mUserToSerialMap = null;
             }
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.WORK_PROFILE_REMOVED, "Work profile removed", new Exception());
+            }
         }
     }
 
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java b/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
index 2daa2fe..eb68592 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
@@ -23,7 +23,6 @@
 import android.graphics.drawable.Drawable;
 import android.text.TextUtils;
 import android.util.AttributeSet;
-import android.view.MotionEvent;
 import android.widget.Toast;
 
 import com.android.launcher3.BubbleTextView;
@@ -106,12 +105,12 @@
     }
 
     @Override
-    public boolean onTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            // Show toast if user touches the drag handle (long clicks still start the drag).
-            mShowInstructionToast = mDragHandleBounds.contains((int) ev.getX(), (int) ev.getY());
-        }
-        return super.onTouchEvent(ev);
+    protected boolean shouldIgnoreTouchDown(float x, float y) {
+        // Show toast if user touches the drag handle (long clicks still start the drag).
+        mShowInstructionToast = mDragHandleBounds.contains((int) x, (int) y);
+
+        // assume the whole view as clickable
+        return false;
     }
 
     @Override
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index 60b87d9..2792308 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -311,7 +311,13 @@
                 handler.setStateWithAnimation(state, mConfig, builder);
             }
         }
-        builder.addListener(new AnimationSuccessListener() {
+        builder.addListener(createStateAnimationListener(state));
+        mConfig.setAnimation(builder.buildAnim(), state);
+        return builder;
+    }
+
+    private AnimatorListener createStateAnimationListener(STATE_TYPE state) {
+        return new AnimationSuccessListener() {
 
             @Override
             public void onAnimationStart(Animator animation) {
@@ -326,9 +332,7 @@
                 }
                 onStateTransitionEnd(state);
             }
-        });
-        mConfig.setAnimation(builder.buildAnim(), state);
-        return builder;
+        };
     }
 
     private void onStateTransitionStart(STATE_TYPE state) {
@@ -396,6 +400,19 @@
     }
 
     /**
+     * @see #setCurrentAnimation(AnimatorSet, Animator...). Using this method tells the StateManager
+     * that this is a custom animation to the given state, and thus the StateManager will add an
+     * animation listener to call {@link #onStateTransitionStart} and {@link #onStateTransitionEnd}.
+     * @param anim The custom animation to the given state.
+     * @param toState The state we are animating towards.
+     */
+    public void setCurrentAnimation(AnimatorSet anim, STATE_TYPE toState) {
+        cancelAnimation();
+        setCurrentAnimation(anim);
+        anim.addListener(createStateAnimationListener(toState));
+    }
+
+    /**
      * Sets the animation as the current state animation, i.e., canceled when
      * starting another animation and may block some launcher interactions while running.
      *
diff --git a/src/com/android/launcher3/states/StateAnimationConfig.java b/src/com/android/launcher3/states/StateAnimationConfig.java
index f90ad3c..8b72177 100644
--- a/src/com/android/launcher3/states/StateAnimationConfig.java
+++ b/src/com/android/launcher3/states/StateAnimationConfig.java
@@ -71,6 +71,7 @@
             ANIM_ALL_APPS_HEADER_FADE,
             ANIM_OVERVIEW_MODAL,
             ANIM_DEPTH,
+            ANIM_OVERVIEW_ACTIONS_FADE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AnimType {}
@@ -89,10 +90,11 @@
     public static final int ANIM_ALL_APPS_HEADER_FADE = 12; // e.g. predictions
     public static final int ANIM_OVERVIEW_MODAL = 13;
     public static final int ANIM_DEPTH = 14;
+    public static final int ANIM_OVERVIEW_ACTIONS_FADE = 15;
 
-    private static final int ANIM_TYPES_COUNT = 15;
+    private static final int ANIM_TYPES_COUNT = 16;
 
-    private final Interpolator[] mInterpolators = new Interpolator[ANIM_TYPES_COUNT];
+    protected final Interpolator[] mInterpolators = new Interpolator[ANIM_TYPES_COUNT];
 
     public StateAnimationConfig() { }
 
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 3ca08fd..58bff09 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -98,6 +98,7 @@
     public static final String REQUEST_DISABLE_DEBUG_TRACING = "disable-debug-tracing";
 
     public static final String REQUEST_OVERVIEW_ACTIONS_ENABLED = "overview-actions-enabled";
+    public static final String REQUEST_OVERVIEW_SHARE_ENABLED = "overview-share-enabled";
 
     public static boolean sDisableSensorRotation;
     public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation";
@@ -106,4 +107,5 @@
     public static final String PAUSE_NOT_DETECTED = "b/139891609";
     public static final String OVERIEW_NOT_ALLAPPS = "b/156095088";
     public static final String NO_SWIPE_TO_HOME = "b/158017601";
+    public static final String WORK_PROFILE_REMOVED = "b/159671700";
 }
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index 48c7734..4fd1633 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -17,6 +17,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;
@@ -33,6 +34,7 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.widget.LinearLayout;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.OverScroller;
@@ -70,6 +72,7 @@
         out.halfPageSize = view.getNormalChildHeight() / 2;
         out.halfScreenSize = view.getMeasuredHeight() / 2;
         out.screenCenter = insets.top + view.getPaddingTop() + out.scroll + out.halfPageSize;
+        out.pageParentScale = view.getScaleY();
     }
 
     @Override
@@ -105,6 +108,11 @@
     }
 
     @Override
+    public <T> void setSecondary(T target, Float2DAction<T> action, float param) {
+        action.call(target, param, 0);
+    }
+
+    @Override
     public float getPrimaryDirection(MotionEvent event, int pointerIndex) {
         return event.getY(pointerIndex);
     }
@@ -212,7 +220,11 @@
     }
 
     @Override
-    public int getTaskDismissDirectionFactor() {
+    public int getPrimaryTranslationDirectionFactor() {
+        return -1;
+    }
+
+    public int getSecondaryTranslationDirectionFactor() {
         return 1;
     }
 
@@ -237,7 +249,8 @@
     }
 
     @Override
-    public int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout) {
+    public int getTaskMenuLayoutOrientation(boolean canRecentsActivityRotate,
+        LinearLayout taskMenuLayout) {
         return LinearLayout.HORIZONTAL;
     }
 
@@ -260,4 +273,10 @@
         }
         return new ChildBounds(childHeight, childWidth, childBottom, childLeft);
     }
+
+    @SuppressWarnings("SuspiciousNameCombination")
+    @Override
+    public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
+        return rect.left;
+    }
 }
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index 65b1a7a..c0d488b 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -57,6 +57,7 @@
 
     <T> void set(T target, Int2DAction<T> action, int param);
     <T> void set(T target, Float2DAction<T> action, float param);
+    <T> void setSecondary(T target, Float2DAction<T> action, float param);
     float getPrimaryDirection(MotionEvent event, int pointerIndex);
     float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId);
     int getMeasuredSize(View view);
@@ -74,7 +75,8 @@
     int getScrollOffsetStart(View view, Rect insets);
     int getScrollOffsetEnd(View view, Rect insets);
     SingleAxisSwipeDetector.Direction getOppositeSwipeDirection();
-    int getTaskDismissDirectionFactor();
+    int getPrimaryTranslationDirectionFactor();
+    int getSecondaryTranslationDirectionFactor();
     int getTaskDragDisplacementFactor(boolean isRtl);
     ChildBounds getChildBounds(View child, int childStart, int pageCenter, boolean layoutChild);
     void setMaxScroll(AccessibilityEvent event, int maxScroll);
@@ -94,8 +96,9 @@
     float getTaskMenuX(float x, View thumbnailView);
     float getTaskMenuY(float y, View thumbnailView);
     int getTaskMenuWidth(View view);
-    int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout);
+    int getTaskMenuLayoutOrientation(boolean canRecentsActivityRotate, LinearLayout taskMenuLayout);
     void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp);
+    int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect);
 
     /**
      * Maps the velocity from the coordinate plane of the foreground app to that
@@ -108,6 +111,7 @@
         public int halfPageSize;
         public int screenCenter;
         public int halfScreenSize;
+        public float pageParentScale;
     }
 
     class ChildBounds {
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index 79e5c87..9e53e5f 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -32,6 +32,7 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.widget.LinearLayout;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.OverScroller;
@@ -69,6 +70,7 @@
         out.halfPageSize = view.getNormalChildWidth() / 2;
         out.halfScreenSize = view.getMeasuredWidth() / 2;
         out.screenCenter = insets.left + view.getPaddingLeft() + out.scroll + out.halfPageSize;
+        out.pageParentScale = view.getScaleX();
     }
 
     @Override
@@ -103,6 +105,11 @@
     }
 
     @Override
+    public <T> void setSecondary(T target, Float2DAction<T> action, float param) {
+        action.call(target, 0, param);
+    }
+
+    @Override
     public float getPrimaryDirection(MotionEvent event, int pointerIndex) {
         return event.getX(pointerIndex);
     }
@@ -210,7 +217,11 @@
     }
 
     @Override
-    public int getTaskDismissDirectionFactor() {
+    public int getPrimaryTranslationDirectionFactor() {
+        return 1;
+    }
+
+    public int getSecondaryTranslationDirectionFactor() {
         return -1;
     }
 
@@ -236,8 +247,9 @@
     }
 
     @Override
-    public int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout) {
-        return taskMenuLayout.getOrientation();
+    public int getTaskMenuLayoutOrientation(boolean canRecentsActivityRotate,
+        LinearLayout taskMenuLayout) {
+        return canRecentsActivityRotate ? taskMenuLayout.getOrientation() : LinearLayout.VERTICAL;
     }
 
     @Override
@@ -257,4 +269,9 @@
         }
         return new ChildBounds(childWidth, childHeight, childRight, childTop);
     }
+
+    @Override
+    public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
+        return dp.heightPx - rect.bottom;
+    }
 }
diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
index d5ae2dc..7153452 100644
--- a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
@@ -18,15 +18,17 @@
 
 import android.content.res.Resources;
 import android.graphics.PointF;
+import android.graphics.Rect;
 import android.view.Surface;
 import android.view.View;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Utilities;
 
 public class SeascapePagedViewHandler extends LandscapePagedViewHandler {
 
     @Override
-    public int getTaskDismissDirectionFactor() {
+    public int getSecondaryTranslationDirectionFactor() {
         return -1;
     }
 
@@ -77,4 +79,9 @@
         view.setTranslationX(0);
         view.setTranslationY(translation);
     }
+
+    @Override
+    public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
+        return dp.widthPx - rect.right;
+    }
 }
diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java
index fc9f8f7..f6003dd 100644
--- a/src/com/android/launcher3/util/MainThreadInitializedObject.java
+++ b/src/com/android/launcher3/util/MainThreadInitializedObject.java
@@ -46,7 +46,7 @@
 
         if (mValue == null) {
             if (Looper.myLooper() == Looper.getMainLooper()) {
-                mValue = TraceHelper.whitelistIpcs("main.thread.object",
+                mValue = TraceHelper.allowIpcs("main.thread.object",
                         () -> mProvider.get(context.getApplicationContext()));
             } else {
                 try {
diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java
index a8642b0..5be9529 100644
--- a/src/com/android/launcher3/util/MultiValueAlpha.java
+++ b/src/com/android/launcher3/util/MultiValueAlpha.java
@@ -19,6 +19,8 @@
 import android.util.FloatProperty;
 import android.view.View;
 
+import com.android.launcher3.anim.AlphaUpdateListener;
+
 import java.util.Arrays;
 
 /**
@@ -44,6 +46,8 @@
     private final AlphaProperty[] mMyProperties;
 
     private int mValidMask;
+    // Whether we should change from INVISIBLE to VISIBLE and vice versa at low alpha values.
+    private boolean mUpdateVisibility;
 
     public MultiValueAlpha(View view, int size) {
         mView = view;
@@ -66,6 +70,11 @@
         return mMyProperties[index];
     }
 
+    /** Sets whether we should update between INVISIBLE and VISIBLE based on alpha. */
+    public void setUpdateVisibility(boolean updateVisibility) {
+        mUpdateVisibility = updateVisibility;
+    }
+
     public class AlphaProperty {
 
         private final int mMyMask;
@@ -99,6 +108,9 @@
             mValue = value;
 
             mView.setAlpha(mOthers * mValue);
+            if (mUpdateVisibility) {
+                AlphaUpdateListener.updateVisibility(mView);
+            }
         }
 
         public float getValue() {
diff --git a/src/com/android/launcher3/util/TraceHelper.java b/src/com/android/launcher3/util/TraceHelper.java
index 168227d..c23df77 100644
--- a/src/com/android/launcher3/util/TraceHelper.java
+++ b/src/com/android/launcher3/util/TraceHelper.java
@@ -78,7 +78,7 @@
      * Temporarily ignore blocking binder calls for the duration of this {@link Supplier}.
      */
     @MainThread
-    public static <T> T whitelistIpcs(String rpcName, Supplier<T> supplier) {
+    public static <T> T allowIpcs(String rpcName, Supplier<T> supplier) {
         Object traceToken = INSTANCE.beginSection(rpcName, FLAG_IGNORE_BINDERS);
         try {
             return supplier.get();
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index b010b4b..357eeb8 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -602,13 +602,13 @@
      */
     private boolean computeAllowSysuiScrims(@Nullable WallpaperInfo newWallpaperInfo) {
         if (newWallpaperInfo == null) {
-            // New wallpaper is static, not live. Thus, blacklist isn't applicable.
+            // Static wallpapers need scrim unless determined otherwise by wallpaperColors.
             return true;
         }
         ComponentName newWallpaper = newWallpaperInfo.getComponent();
         for (String wallpaperWithoutScrim : mWallpapersWithoutSysuiScrims) {
             if (newWallpaper.equals(ComponentName.unflattenFromString(wallpaperWithoutScrim))) {
-                // New wallpaper is blacklisted from showing a scrim.
+                // New wallpaper does not need a scrim.
                 return false;
             }
         }
diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
index 1a8e11b..fab0bd4 100644
--- a/src/com/android/launcher3/views/ClipIconView.java
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -36,6 +36,7 @@
 import android.os.Build;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.ViewGroup.MarginLayoutParams;
 import android.view.ViewOutlineProvider;
 
 import androidx.annotation.Nullable;
@@ -44,8 +45,6 @@
 import androidx.dynamicanimation.animation.SpringForce;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.InsettableFrameLayout.LayoutParams;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
@@ -94,7 +93,6 @@
                 }
             };
 
-    private final Launcher mLauncher;
     private final int mBlurSizeOutline;
     private final boolean mIsRtl;
 
@@ -128,7 +126,6 @@
 
     public ClipIconView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mLauncher = Launcher.getLauncher(context);
         mBlurSizeOutline = getResources().getDimensionPixelSize(
                 R.dimen.blur_size_medium_outline);
         mIsRtl = Utilities.isRtl(getResources());
@@ -143,10 +140,41 @@
                         .setStiffness(SpringForce.STIFFNESS_LOW));
     }
 
-    void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
-            boolean isOpening, float scale, float minSize, LayoutParams parentLp,
-            boolean isVerticalBarLayout) {
-        DeviceProfile dp = mLauncher.getDeviceProfile();
+    /**
+     * Update the icon UI to match the provided parameters during an animation frame
+     */
+    public void update(RectF rect, float progress, float shapeProgressStart,
+            float cornerRadius, boolean isOpening, View container,
+            DeviceProfile dp, boolean isVerticalBarLayout) {
+
+        MarginLayoutParams lp = (MarginLayoutParams) container.getLayoutParams();
+
+        float dX = mIsRtl
+                ? rect.left - (dp.widthPx - lp.getMarginStart() - lp.width)
+                : rect.left - lp.getMarginStart();
+        float dY = rect.top - lp.topMargin;
+        container.setTranslationX(dX);
+        container.setTranslationY(dY);
+
+        float minSize = Math.min(lp.width, lp.height);
+        float scaleX = rect.width() / minSize;
+        float scaleY = rect.height() / minSize;
+        float scale = Math.max(1f, Math.min(scaleX, scaleY));
+
+        update(rect, progress, shapeProgressStart, cornerRadius, isOpening, scale,
+                minSize, lp, isVerticalBarLayout, dp);
+
+        container.setPivotX(0);
+        container.setPivotY(0);
+        container.setScaleX(scale);
+        container.setScaleY(scale);
+
+        container.invalidate();
+    }
+
+    private void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
+            boolean isOpening, float scale, float minSize, MarginLayoutParams parentLp,
+            boolean isVerticalBarLayout, DeviceProfile dp) {
         float dX = mIsRtl
                 ? rect.left - (dp.widthPx - parentLp.getMarginStart() - parentLp.width)
                 : rect.left - parentLp.getMarginStart();
@@ -228,8 +256,11 @@
         }
     }
 
-    void setIcon(@Nullable Drawable drawable, int iconOffset, LayoutParams lp, boolean isOpening,
-            boolean isVerticalBarLayout) {
+    /**
+     * Sets the icon for this view as part of initial setup
+     */
+    public void setIcon(@Nullable Drawable drawable, int iconOffset, MarginLayoutParams lp,
+            boolean isOpening, boolean isVerticalBarLayout, DeviceProfile dp) {
         mIsAdaptiveIcon = drawable instanceof AdaptiveIconDrawable;
         if (mIsAdaptiveIcon) {
             boolean isFolderIcon = drawable instanceof FolderAdaptiveIcon;
@@ -264,15 +295,14 @@
                 Utilities.scaleRectAboutCenter(mStartRevealRect, IconShape.getNormalizationScale());
             }
 
-            float aspectRatio = mLauncher.getDeviceProfile().aspectRatio;
             if (isVerticalBarLayout) {
-                lp.width = (int) Math.max(lp.width, lp.height * aspectRatio);
+                lp.width = (int) Math.max(lp.width, lp.height * dp.aspectRatio);
             } else {
-                lp.height = (int) Math.max(lp.height, lp.width * aspectRatio);
+                lp.height = (int) Math.max(lp.height, lp.width * dp.aspectRatio);
             }
 
             int left = mIsRtl
-                    ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width
+                    ? dp.widthPx - lp.getMarginStart() - lp.width
                     : lp.leftMargin;
             layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
 
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 177aff4..8186dfa 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.Utilities.getFullDrawable;
 import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -47,7 +48,6 @@
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
@@ -144,32 +144,8 @@
     public void update(RectF rect, float alpha, float progress, float shapeProgressStart,
             float cornerRadius, boolean isOpening) {
         setAlpha(alpha);
-
-        InsettableFrameLayout.LayoutParams lp =
-                (InsettableFrameLayout.LayoutParams) getLayoutParams();
-
-        DeviceProfile dp = mLauncher.getDeviceProfile();
-        float dX = mIsRtl
-                ? rect.left - (dp.widthPx - lp.getMarginStart() - lp.width)
-                : rect.left - lp.getMarginStart();
-        float dY = rect.top - lp.topMargin;
-        setTranslationX(dX);
-        setTranslationY(dY);
-
-        float minSize = Math.min(lp.width, lp.height);
-        float scaleX = rect.width() / minSize;
-        float scaleY = rect.height() / minSize;
-        float scale = Math.max(1f, Math.min(scaleX, scaleY));
-
-        mClipIconView.update(rect, progress, shapeProgressStart, cornerRadius, isOpening, scale,
-                minSize, lp, mIsVerticalBarLayout);
-
-        setPivotX(0);
-        setPivotY(0);
-        setScaleX(scale);
-        setScaleY(scale);
-
-        invalidate();
+        mClipIconView.update(rect, progress, shapeProgressStart, cornerRadius, isOpening,
+                this, mLauncher.getDeviceProfile(), mIsVerticalBarLayout);
     }
 
     @Override
@@ -220,13 +196,18 @@
         layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
     }
 
+    private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
+            RectF outRect) {
+        getLocationBoundsForView(launcher, v, isOpening, outRect, new Rect());
+    }
+
     /**
      * Gets the location bounds of a view and returns the overall rotation.
      * - For DeepShortcutView, we return the bounds of the icon view.
      * - For BubbleTextView, we return the icon bounds.
      */
-    private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
-            RectF outRect) {
+    public static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
+            RectF outRect, Rect outViewBounds) {
         boolean ignoreTransform = !isOpening;
         if (v instanceof DeepShortcutView) {
             v = ((DeepShortcutView) v).getBubbleText();
@@ -239,17 +220,16 @@
             return;
         }
 
-        Rect iconBounds = new Rect();
         if (v instanceof BubbleTextView) {
-            ((BubbleTextView) v).getIconBounds(iconBounds);
+            ((BubbleTextView) v).getIconBounds(outViewBounds);
         } else if (v instanceof FolderIcon) {
-            ((FolderIcon) v).getPreviewBounds(iconBounds);
+            ((FolderIcon) v).getPreviewBounds(outViewBounds);
         } else {
-            iconBounds.set(0, 0, v.getWidth(), v.getHeight());
+            outViewBounds.set(0, 0, v.getWidth(), v.getHeight());
         }
 
-        float[] points = new float[] {iconBounds.left, iconBounds.top, iconBounds.right,
-                iconBounds.bottom};
+        float[] points = new float[] {outViewBounds.left, outViewBounds.top, outViewBounds.right,
+                outViewBounds.bottom};
         Utilities.getDescendantCoordRelativeToAncestor(v, launcher.getDragLayer(), points,
                 false, ignoreTransform);
         outRect.set(
@@ -336,7 +316,8 @@
         final InsettableFrameLayout.LayoutParams lp =
                 (InsettableFrameLayout.LayoutParams) getLayoutParams();
         mBadge = badge;
-        mClipIconView.setIcon(drawable, iconOffset, lp, mIsOpening, mIsVerticalBarLayout);
+        mClipIconView.setIcon(drawable, iconOffset, lp, mIsOpening, mIsVerticalBarLayout,
+                mLauncher.getDeviceProfile());
         if (drawable instanceof AdaptiveIconDrawable) {
             final int originalHeight = lp.height;
             final int originalWidth = lp.width;
@@ -381,7 +362,7 @@
             if (mIconLoadResult.isIconLoaded) {
                 setIcon(mIconLoadResult.drawable, mIconLoadResult.badge,
                         mIconLoadResult.iconOffset);
-                hideOriginalView(originalView);
+                setIconAndDotVisible(originalView, false);
             } else {
                 mIconLoadResult.onIconLoaded = () -> {
                     if (cancellationSignal.isCanceled()) {
@@ -392,22 +373,13 @@
                             mIconLoadResult.iconOffset);
 
                     setVisibility(VISIBLE);
-                    hideOriginalView(originalView);
+                    setIconAndDotVisible(originalView, false);
                 };
                 mLoadIconSignal = cancellationSignal;
             }
         }
     }
 
-    private void hideOriginalView(View originalView) {
-        if (originalView instanceof IconLabelDotView) {
-            ((IconLabelDotView) originalView).setIconVisible(false);
-            ((IconLabelDotView) originalView).setForceHideDot(true);
-        } else {
-            originalView.setVisibility(INVISIBLE);
-        }
-    }
-
     @WorkerThread
     @SuppressWarnings("WrongThread")
     private static int getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position) {
@@ -477,7 +449,7 @@
         }
         if (!mIsOpening) {
             // When closing an app, we want the item on the workspace to be invisible immediately
-            hideOriginalView(mOriginalIcon);
+            setIconAndDotVisible(mOriginalIcon, false);
         }
     }
 
@@ -573,12 +545,7 @@
 
             if (hideOriginal) {
                 if (isOpening) {
-                    if (originalView instanceof BubbleTextView) {
-                        ((BubbleTextView) originalView).setIconVisible(true);
-                        ((BubbleTextView) originalView).setForceHideDot(false);
-                    } else {
-                        originalView.setVisibility(VISIBLE);
-                    }
+                    setIconAndDotVisible(originalView, true);
                     view.finish(dragLayer);
                 } else {
                     view.mFadeAnimatorSet = view.createFadeAnimation(originalView, dragLayer);
@@ -615,12 +582,10 @@
         });
 
         if (originalView instanceof IconLabelDotView) {
-            IconLabelDotView view = (IconLabelDotView) originalView;
             fade.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
-                    view.setIconVisible(true);
-                    view.setForceHideDot(false);
+                    setIconAndDotVisible(originalView, true);
                 }
             });
         }
diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java
new file mode 100644
index 0000000..040619e
--- /dev/null
+++ b/src/com/android/launcher3/views/FloatingSurfaceView.java
@@ -0,0 +1,241 @@
+/*
+ * 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.views;
+
+import static com.android.launcher3.views.FloatingIconView.getLocationBoundsForView;
+import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Picture;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.GestureNavContract;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.Executors;
+
+/**
+ * Similar to {@link FloatingIconView} but displays a surface with the targetIcon. It then passes
+ * the surfaceHandle to the {@link GestureNavContract}.
+ */
+@TargetApi(Build.VERSION_CODES.R)
+public class FloatingSurfaceView extends AbstractFloatingView implements
+        OnGlobalLayoutListener, Insettable, SurfaceHolder.Callback2 {
+
+    private final RectF mTmpPosition = new RectF();
+
+    private final Launcher mLauncher;
+    private final RectF mIconPosition = new RectF();
+
+    private final Rect mIconBounds = new Rect();
+    private final Picture mPicture = new Picture();
+    private final Runnable mRemoveViewRunnable = this::removeViewFromParent;
+
+    private final SurfaceView mSurfaceView;
+
+
+    private View mIcon;
+    private GestureNavContract mContract;
+
+    public FloatingSurfaceView(Context context) {
+        this(context, null);
+    }
+
+    public FloatingSurfaceView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public FloatingSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mLauncher = Launcher.getLauncher(context);
+
+        mSurfaceView = new SurfaceView(context);
+        mSurfaceView.setZOrderOnTop(true);
+
+        mSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
+        mSurfaceView.getHolder().addCallback(this);
+        mIsOpen = true;
+        addView(mSurfaceView);
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        setCurrentIconVisible(true);
+        mLauncher.getViewCache().recycleView(R.layout.floating_surface_view, this);
+        mContract = null;
+        mIcon = null;
+        mIsOpen = false;
+
+        // Remove after some time, to avoid flickering
+        Executors.MAIN_EXECUTOR.getHandler().postDelayed(mRemoveViewRunnable,
+                DefaultDisplay.INSTANCE.get(mLauncher).getInfo().singleFrameMs);
+    }
+
+    private void removeViewFromParent() {
+        mPicture.beginRecording(1, 1);
+        mPicture.endRecording();
+        mLauncher.getDragLayer().removeView(this);
+    }
+
+    /**
+     * Shows the surfaceView for the provided contract
+     */
+    public static void show(Launcher launcher, GestureNavContract contract) {
+        FloatingSurfaceView view = launcher.getViewCache().getView(R.layout.floating_surface_view,
+                launcher, launcher.getDragLayer());
+        view.mContract = contract;
+        view.mIsOpen = true;
+
+        // Cancel any pending remove
+        Executors.MAIN_EXECUTOR.getHandler().removeCallbacks(view.mRemoveViewRunnable);
+        view.removeViewFromParent();
+        launcher.getDragLayer().addView(view);
+    }
+
+    @Override
+    public void logActionCommand(int command) { }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_ICON_SURFACE) != 0;
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        close(false);
+        return false;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        getViewTreeObserver().addOnGlobalLayoutListener(this);
+        updateIconLocation();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        getViewTreeObserver().removeOnGlobalLayoutListener(this);
+        setCurrentIconVisible(true);
+    }
+
+    @Override
+    public void onGlobalLayout() {
+        updateIconLocation();
+    }
+
+    @Override
+    public void setInsets(Rect insets) { }
+
+    private void updateIconLocation() {
+        if (mContract == null) {
+            return;
+        }
+        View icon = mLauncher.getWorkspace().getFirstMatchForAppClose(
+                mContract.componentName.getPackageName(), mContract.user);
+
+        boolean iconChanged = mIcon != icon;
+        if (iconChanged) {
+            setCurrentIconVisible(true);
+            mIcon = icon;
+            setCurrentIconVisible(false);
+        }
+
+        if (icon != null && icon.isAttachedToWindow()) {
+            getLocationBoundsForView(mLauncher, icon, false, mTmpPosition, mIconBounds);
+
+            if (!mTmpPosition.equals(mIconPosition)) {
+                mIconPosition.set(mTmpPosition);
+                sendIconInfo();
+
+                LayoutParams lp = (LayoutParams) mSurfaceView.getLayoutParams();
+                lp.width = Math.round(mIconPosition.width());
+                lp.height = Math.round(mIconPosition.height());
+                lp.leftMargin = Math.round(mIconPosition.left);
+                lp.topMargin = Math.round(mIconPosition.top);
+            }
+        }
+        if (iconChanged && !mIconBounds.isEmpty()) {
+            // Record the icon display
+            setCurrentIconVisible(true);
+            Canvas c = mPicture.beginRecording(mIconBounds.width(), mIconBounds.height());
+            c.translate(-mIconBounds.left, -mIconBounds.top);
+            mIcon.draw(c);
+            mPicture.endRecording();
+            setCurrentIconVisible(false);
+            drawOnSurface();
+        }
+    }
+
+    private void sendIconInfo() {
+        if (mContract != null && !mIconPosition.isEmpty()) {
+            mContract.sendEndPosition(mIconPosition, mSurfaceView.getSurfaceControl());
+        }
+    }
+
+    @Override
+    public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
+        drawOnSurface();
+        sendIconInfo();
+    }
+
+    @Override
+    public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder,
+            int format, int width, int height) {
+        drawOnSurface();
+    }
+
+    @Override
+    public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {}
+
+    @Override
+    public void surfaceRedrawNeeded(@NonNull SurfaceHolder surfaceHolder) {
+        drawOnSurface();
+    }
+
+    private void drawOnSurface() {
+        SurfaceHolder surfaceHolder = mSurfaceView.getHolder();
+
+        Canvas c = surfaceHolder.lockHardwareCanvas();
+        if (c != null) {
+            mPicture.draw(c);
+            surfaceHolder.unlockCanvasAndPost(c);
+        }
+    }
+
+    private void setCurrentIconVisible(boolean isVisible) {
+        if (mIcon != null) {
+            setIconAndDotVisible(mIcon, isVisible);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/views/IconLabelDotView.java b/src/com/android/launcher3/views/IconLabelDotView.java
index 057caaf..e9113cf 100644
--- a/src/com/android/launcher3/views/IconLabelDotView.java
+++ b/src/com/android/launcher3/views/IconLabelDotView.java
@@ -15,10 +15,24 @@
  */
 package com.android.launcher3.views;
 
+import android.view.View;
+
 /**
  * A view that has an icon, label, and notification dot.
  */
 public interface IconLabelDotView {
     void setIconVisible(boolean visible);
     void setForceHideDot(boolean hide);
+
+    /**
+     * Sets the visibility of icon and dot of the view
+     */
+    static void setIconAndDotVisible(View view, boolean visible) {
+        if (view instanceof IconLabelDotView) {
+            ((IconLabelDotView) view).setIconVisible(visible);
+            ((IconLabelDotView) view).setForceHideDot(!visible);
+        } else {
+            view.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index 9021d9e..ca47728 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -52,7 +52,6 @@
     private static final float MIN_SATUNATION = 0.7f;
 
     private final Rect mRect = new Rect();
-    private View mDefaultView;
     private OnClickListener mClickListener;
     private final LauncherAppWidgetInfo mInfo;
     private final int mStartState;
@@ -111,12 +110,11 @@
 
     @Override
     protected View getDefaultView() {
-        if (mDefaultView == null) {
-            mDefaultView = mInflater.inflate(R.layout.appwidget_not_ready, this, false);
-            mDefaultView.setOnClickListener(this);
-            applyState();
-        }
-        return mDefaultView;
+        View defaultView = mInflater.inflate(R.layout.appwidget_not_ready, this, false);
+        defaultView.setOnClickListener(this);
+        applyState();
+        invalidate();
+        return defaultView;
     }
 
     @Override
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 1c8f095..33c8fbd 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -92,7 +92,7 @@
         <activity
             android:name="com.android.launcher3.testcomponent.TestLauncherActivity"
             android:clearTaskOnLaunch="true"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
             android:enabled="false"
             android:label="Test launcher"
             android:launchMode="singleTask"
diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java
index 8d571ff..f5f93c4 100644
--- a/tests/src/com/android/launcher3/ui/WorkTabTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkTabTest.java
@@ -24,6 +24,7 @@
 import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.util.Log;
 import android.widget.TextView;
 
 import androidx.test.filters.LargeTest;
@@ -34,6 +35,7 @@
 import com.android.launcher3.allapps.AllAppsPagedView;
 import com.android.launcher3.allapps.WorkModeSwitch;
 import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.views.WorkEduView;
 
 import org.junit.After;
@@ -132,6 +134,10 @@
                     l.getResources().getString(R.string.work_profile_edu_personal_apps));
             workEduView.findViewById(R.id.proceed).callOnClick();
         });
+
+        executeOnLauncher(launcher -> Log.d(TestProtocol.WORK_PROFILE_REMOVED,
+                "Work profile status: " + launcher.getAppsView().isPersonalTabVisible()));
+
         // verify work edu is seen next
         waitForLauncherCondition("Launcher did not show the next edu screen", l ->
                 ((AllAppsPagedView) l.getAppsView().getContentView()).getCurrentPage() == WORK_PAGE
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index ce94a3e..0c4e5a9 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -85,7 +85,7 @@
                 final LauncherInstrumentation.GestureScope gestureScope =
                         zeroButtonToOverviewGestureStartsInLauncher()
                                 ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
-                                : LauncherInstrumentation.GestureScope.OUTSIDE;
+                                : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER;
 
                 // b/156044202
                 mLauncher.log("Hierarchy before swiping up to overview:");
@@ -130,7 +130,7 @@
                 }
 
                 mLauncher.swipeToState(startX, startY, endX, endY, 10, OVERVIEW_STATE_ORDINAL,
-                        LauncherInstrumentation.GestureScope.OUTSIDE);
+                        LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER);
                 break;
             }
 
@@ -194,7 +194,7 @@
                 mLauncher.swipeToState(startX, startY, endX, endY, 20, expectedState,
                         launcherWasVisible && isZeroButton
                                 ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
-                                : LauncherInstrumentation.GestureScope.OUTSIDE);
+                                : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER);
                 break;
             }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 223ae29..588b6b8 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -27,7 +27,7 @@
 import java.util.List;
 
 /**
- * Common overview pane for both Launcher and fallback recents
+ * Common overview panel for both Launcher and fallback recents
  */
 public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
     private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
@@ -135,4 +135,19 @@
     public boolean hasTasks() {
         return getTasks().size() > 0;
     }
+
+    /**
+     * Gets Overview Actions.
+     *
+     * @return The Overview Actions
+     */
+    @NonNull
+    public OverviewActions getOverviewActions() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to get overview actions")) {
+            verifyActiveContainer();
+            UiObject2 overviewActions = mLauncher.waitForLauncherObject("action_buttons");
+            return new OverviewActions(overviewActions, mLauncher);
+        }
+    }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 5cf5d0f..85f3234 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -112,9 +112,10 @@
 
     public enum NavigationModel {ZERO_BUTTON, TWO_BUTTON, THREE_BUTTON}
 
-    // Where the gesture happens: outside of Launcher, inside or from inside to outside.
+    // Where the gesture happens: outside of Launcher, inside or from inside to outside and
+    // whether the gesture recognition triggers pilfer.
     public enum GestureScope {
-        OUTSIDE, INSIDE, INSIDE_TO_OUTSIDE
+        OUTSIDE_WITHOUT_PILFER, OUTSIDE_WITH_PILFER, INSIDE, INSIDE_TO_OUTSIDE
     }
 
     ;
@@ -153,6 +154,7 @@
     private static final String CONTEXT_MENU_RES_ID = "deep_shortcuts_container";
     public static final int WAIT_TIME_MS = 10000;
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+    private static final String ANDROID_PACKAGE = "android";
 
     private static WeakReference<VisibleContainer> sActiveContainer = new WeakReference<>(null);
 
@@ -693,7 +695,7 @@
                                 ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL,
                                 launcherWasVisible
                                         ? GestureScope.INSIDE_TO_OUTSIDE
-                                        : GestureScope.OUTSIDE);
+                                        : GestureScope.OUTSIDE_WITH_PILFER);
                     }
                 }
             } else {
@@ -925,6 +927,14 @@
         return waitForObjectBySelector(getOverviewObjectSelector(resName));
     }
 
+    @NonNull
+    UiObject2 waitForAndroidObject(String resId) {
+        final UiObject2 object = mDevice.wait(
+                Until.findObject(By.res(ANDROID_PACKAGE, resId)), WAIT_TIME_MS);
+        assertNotNull("Can't find a android object with id: " + resId, object);
+        return object;
+    }
+
     private UiObject2 waitForObjectBySelector(BySelector selector) {
         final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
         assertNotNull("Can't find a view in Launcher, selector: " + selector, object);
@@ -1159,7 +1169,8 @@
         final boolean notLauncher3 = !isLauncher3();
         switch (action) {
             case MotionEvent.ACTION_DOWN:
-                if (gestureScope != GestureScope.OUTSIDE) {
+                if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
+                        && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER) {
                     expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
                 }
                 if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
@@ -1167,12 +1178,17 @@
                 }
                 break;
             case MotionEvent.ACTION_UP:
-                if (notLauncher3 && gestureScope != GestureScope.INSIDE) {
+                if (notLauncher3 && gestureScope != GestureScope.INSIDE
+                        && (gestureScope == GestureScope.OUTSIDE_WITH_PILFER
+                        || gestureScope == GestureScope.INSIDE_TO_OUTSIDE)) {
                     expectEvent(TestProtocol.SEQUENCE_PILFER, EVENT_PILFER_POINTERS);
                 }
-                if (gestureScope != GestureScope.OUTSIDE) {
-                    expectEvent(TestProtocol.SEQUENCE_MAIN, gestureScope == GestureScope.INSIDE
-                            ? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL);
+                if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
+                        && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER) {
+                    expectEvent(TestProtocol.SEQUENCE_MAIN,
+                            gestureScope == GestureScope.INSIDE
+                                    || gestureScope == GestureScope.OUTSIDE_WITHOUT_PILFER
+                                    ? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL);
                 }
                 if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
                     expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
@@ -1295,6 +1311,11 @@
                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    boolean overviewShareEnabled() {
+        return getTestInfo(TestProtocol.REQUEST_OVERVIEW_SHARE_ENABLED).getBoolean(
+                TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
     private void disableSensorRotation() {
         getTestInfo(TestProtocol.REQUEST_MOCK_SENSOR_ROTATION);
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
new file mode 100644
index 0000000..a30a404
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
@@ -0,0 +1,130 @@
+/*
+ * 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.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.UiObject2;
+
+import com.android.launcher3.testing.TestProtocol;
+
+/**
+ * View containing overview actions
+ */
+public class OverviewActions {
+    private final UiObject2 mOverviewActions;
+    private final LauncherInstrumentation mLauncher;
+
+    OverviewActions(UiObject2 overviewActions, LauncherInstrumentation launcherInstrumentation) {
+        this.mOverviewActions = overviewActions;
+        this.mLauncher = launcherInstrumentation;
+    }
+
+    /**
+     * Clicks screenshot button and closes screenshot ui.
+     */
+    @NonNull
+    public Overview clickAndDismissScreenshot() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to click screenshot button and exit screenshot ui")) {
+            UiObject2 screenshot = mLauncher.waitForObjectInContainer(mOverviewActions,
+                    "action_screenshot");
+            mLauncher.clickLauncherObject(screenshot);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked screenshot button")) {
+                UiObject2 closeScreenshot = mLauncher.waitForSystemUiObject(
+                        "global_screenshot_dismiss_image");
+                if (mLauncher.getNavigationModel()
+                        != LauncherInstrumentation.NavigationModel.THREE_BUTTON) {
+                    mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS,
+                            LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS);
+                    mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS,
+                            LauncherInstrumentation.EVENT_TOUCH_UP_TIS);
+                }
+                closeScreenshot.click();
+                try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                        "dismissed screenshot")) {
+                    return new Overview(mLauncher);
+                }
+            }
+        }
+    }
+
+    /**
+     * Click share button, then drags sharesheet down to remove it.
+     *
+     * Share is currently hidden behind flag, test is kept in case share becomes a default feature.
+     * If share is completely removed then remove this test as well.
+     */
+    @NonNull
+    public Overview clickAndDismissShare() {
+        if (mLauncher.overviewShareEnabled()) {
+            try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+                 LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                         "want to click share button and dismiss sharesheet")) {
+                UiObject2 share = mLauncher.waitForObjectInContainer(mOverviewActions,
+                        "action_share");
+                mLauncher.clickLauncherObject(share);
+                try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                        "clicked share button")) {
+                    mLauncher.waitForAndroidObject("contentPanel");
+                    mLauncher.getDevice().pressBack();
+                    try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                            "dismissed sharesheet")) {
+                        return new Overview(mLauncher);
+                    }
+                }
+            }
+        }
+        return new Overview(mLauncher);
+    }
+
+    /**
+     * Click select button
+     *
+     * @return The select mode buttons that are now shown instead of action buttons.
+     */
+    @NonNull
+    public SelectModeButtons clickSelect() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
+                     mLauncher.addContextLayer("want to click select button")) {
+            UiObject2 select = mLauncher.waitForObjectInContainer(mOverviewActions,
+                    "action_select");
+            mLauncher.clickLauncherObject(select);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked select button")) {
+                return getSelectModeButtons();
+            }
+
+        }
+    }
+
+    /**
+     * Gets the Select Mode Buttons.
+     *
+     * @return The Select Mode Buttons.
+     */
+    @NonNull
+    private SelectModeButtons getSelectModeButtons() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to get select mode buttons")) {
+            UiObject2 selectModeButtons = mLauncher.waitForLauncherObject("select_mode_buttons");
+            return new SelectModeButtons(selectModeButtons, mLauncher);
+        }
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java
new file mode 100644
index 0000000..3507418
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java
@@ -0,0 +1,68 @@
+/*
+ * 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.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.UiObject2;
+
+/**
+ * View containing select mode buttons
+ */
+public class SelectModeButtons {
+    private final UiObject2 mSelectModeButtons;
+    private final LauncherInstrumentation mLauncher;
+
+    SelectModeButtons(UiObject2 selectModeButtons,
+            LauncherInstrumentation launcherInstrumentation) {
+        mSelectModeButtons = selectModeButtons;
+        mLauncher = launcherInstrumentation;
+    }
+
+    /**
+     * Click close button.
+     */
+    @NonNull
+    public Overview clickClose() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
+                     mLauncher.addContextLayer("want to click close button")) {
+            UiObject2 close = mLauncher.waitForObjectInContainer(mSelectModeButtons, "close");
+            mLauncher.clickLauncherObject(close);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked close button")) {
+                return new Overview(mLauncher);
+            }
+        }
+    }
+
+    /**
+     * Click feedback button.
+     */
+    @NonNull
+    public Background clickFeedback() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
+                     mLauncher.addContextLayer("want to click feedback button")) {
+            UiObject2 feedback = mLauncher.waitForObjectInContainer(mSelectModeButtons, "feedback");
+            mLauncher.clickLauncherObject(feedback);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked feedback button")) {
+                return new Background(mLauncher);
+            }
+        }
+    }
+}