Merge "Reconcile difference btw master and ub-launcher3-master" into ub-launcher3-master
diff --git a/SecondaryDisplayLauncher/src/com/android/launcher3/SecondaryDisplayLauncher.java b/SecondaryDisplayLauncher/src/com/android/launcher3/SecondaryDisplayLauncher.java
index 12fa46b..0a2f18f 100644
--- a/SecondaryDisplayLauncher/src/com/android/launcher3/SecondaryDisplayLauncher.java
+++ b/SecondaryDisplayLauncher/src/com/android/launcher3/SecondaryDisplayLauncher.java
@@ -26,12 +26,11 @@
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.os.Bundle;
-import com.google.android.material.circularreveal.cardview.CircularRevealCardView;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewAnimationUtils;
+import android.view.inputmethod.InputMethodManager;
 import android.widget.GridView;
 import android.widget.ImageButton;
 import android.widget.PopupMenu;
@@ -41,6 +40,9 @@
 import androidx.lifecycle.ViewModelProvider;
 import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory;
 
+import com.google.android.material.circularreveal.cardview.CircularRevealCardView;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
 import java.util.HashSet;
 import java.util.Set;
 
@@ -141,6 +143,16 @@
 
     public void onNewIntent(Intent intent) {
         super.onNewIntent(intent);
+
+        if (Intent.ACTION_MAIN.equals(intent.getAction())) {
+            // Hide keyboard.
+            final View v = getWindow().peekDecorView();
+            if (v != null && v.getWindowToken() != null) {
+                getSystemService(InputMethodManager.class).hideSoftInputFromWindow(
+                        v.getWindowToken(), 0);
+            }
+        }
+
         // A new intent will bring the launcher to top. Hide the app drawer to reset the state.
         showAppDrawer(false);
     }
diff --git a/go/quickstep/src/com/android/launcher3/GoLauncherAppTransitionManagerImpl.java b/go/quickstep/src/com/android/launcher3/GoLauncherAppTransitionManagerImpl.java
index 96b4ae5..d189c50 100644
--- a/go/quickstep/src/com/android/launcher3/GoLauncherAppTransitionManagerImpl.java
+++ b/go/quickstep/src/com/android/launcher3/GoLauncherAppTransitionManagerImpl.java
@@ -31,7 +31,8 @@
     @Override
     protected void composeRecentsLaunchAnimator(AnimatorSet anim, View v,
             RemoteAnimationTargetCompat[] targets, boolean launcherClosing) {
-        //TODO: Implement this based off IconRecentsView
+        // Stubbed. Recents launch animation will come from the recents view itself and will not
+        // use remote animations.
     }
 
     @Override
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java b/go/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
index 5382607..cec12a8 100644
--- a/go/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
+++ b/go/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
@@ -43,7 +43,7 @@
     }
 
     @Override
-    public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) {
+    public float[] getOverviewScaleAndTranslationY(Launcher launcher) {
         return new float[] {1f, 0f};
     }
 
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/go/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 784af7d..0b12ab0 100644
--- a/go/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/go/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -16,16 +16,15 @@
 package com.android.launcher3.uioverrides;
 
 import static com.android.quickstep.views.IconRecentsView.CONTENT_ALPHA;
-import static com.android.quickstep.views.IconRecentsView.TRANSLATION_Y_FACTOR;
 
 import android.util.FloatProperty;
 
-import androidx.annotation.NonNull;
-
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherRecentsToActivityHelper;
 import com.android.quickstep.views.IconRecentsView;
 
+import androidx.annotation.NonNull;
+
 /**
  * State handler for Go's {@link IconRecentsView}.
  */
@@ -39,11 +38,6 @@
     }
 
     @Override
-    FloatProperty<IconRecentsView> getTranslationYFactorProperty() {
-        return TRANSLATION_Y_FACTOR;
-    }
-
-    @Override
     FloatProperty<IconRecentsView> getContentAlphaProperty() {
         return CONTENT_ALPHA;
     }
diff --git a/go/quickstep/src/com/android/quickstep/RecentsActivity.java b/go/quickstep/src/com/android/quickstep/RecentsActivity.java
index c814a71..447e7e7 100644
--- a/go/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/go/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -61,7 +61,7 @@
 
     @Override
     public ActivityOptions getActivityLaunchOptions(View v) {
-        //TODO: Hook into recents launch animation
+        // Stubbed. Recents launch animation will come from the recents view itself.
         return null;
     }
 
diff --git a/go/quickstep/src/com/android/quickstep/TaskInputController.java b/go/quickstep/src/com/android/quickstep/TaskInputController.java
index d97ac8d..8433007 100644
--- a/go/quickstep/src/com/android/quickstep/TaskInputController.java
+++ b/go/quickstep/src/com/android/quickstep/TaskInputController.java
@@ -15,6 +15,10 @@
  */
 package com.android.quickstep;
 
+import android.app.ActivityOptions;
+import android.view.View;
+
+import com.android.quickstep.views.TaskItemView;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 
@@ -37,9 +41,16 @@
      * @param viewHolder the task view holder that has been tapped
      */
     public void onTaskClicked(TaskHolder viewHolder) {
-        // TODO: Add app launch animation as part of the launch options here.
+        TaskItemView itemView = (TaskItemView) (viewHolder.itemView);
+        View v = itemView.getThumbnailView();
+        int left = 0;
+        int top = 0;
+        int width = v.getMeasuredWidth();
+        int height = v.getMeasuredHeight();
+
+        ActivityOptions opts = ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
         ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(viewHolder.getTask().key,
-                null /* options */, null /* resultCallback */, null /* resultCallbackHandler */);
+                opts, null /* resultCallback */, null /* resultCallbackHandler */);
     }
 
     public void onTaskSwiped(TaskHolder viewHolder) {
diff --git a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
index 3fdaefe..504f640 100644
--- a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
+++ b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
@@ -46,20 +46,6 @@
  */
 public final class IconRecentsView extends FrameLayout {
 
-    public static final FloatProperty<IconRecentsView> TRANSLATION_Y_FACTOR =
-            new FloatProperty<IconRecentsView>("translationYFactor") {
-
-                @Override
-                public void setValue(IconRecentsView view, float v) {
-                    view.setTranslationYFactor(v);
-                }
-
-                @Override
-                public Float get(IconRecentsView view) {
-                    return view.mTranslationYFactor;
-                }
-            };
-
     public static final FloatProperty<IconRecentsView> CONTENT_ALPHA =
             new FloatProperty<IconRecentsView>("contentAlpha") {
                 @Override
@@ -91,7 +77,6 @@
     private final TaskInputController mTaskInputController;
 
     private RecentsToActivityHelper mActivityHelper;
-    private float mTranslationYFactor;
     private RecyclerView mTaskRecyclerView;
     private View mEmptyView;
 
@@ -170,15 +155,6 @@
         return view.getThumbnailView();
     }
 
-    public void setTranslationYFactor(float translationFactor) {
-        mTranslationYFactor = translationFactor;
-        setTranslationY(computeTranslationYForFactor(mTranslationYFactor));
-    }
-
-    private float computeTranslationYForFactor(float translationYFactor) {
-        return translationYFactor * (getPaddingBottom() - getPaddingTop());
-    }
-
     /**
      * Update the content view so that the appropriate view is shown based off the current list
      * of tasks.
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/BackgroundAppState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/BackgroundAppState.java
index fdb80da..f712753 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/BackgroundAppState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/BackgroundAppState.java
@@ -52,7 +52,7 @@
     }
 
     @Override
-    public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) {
+    public float[] getOverviewScaleAndTranslationY(Launcher launcher) {
         // Initialize the recents view scale to what it would be when starting swipe up
         RecentsView recentsView = launcher.getOverviewPanel();
         recentsView.getTaskSize(sTempRect);
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/OverviewState.java
index 79e127a..2360eeb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/OverviewState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/OverviewState.java
@@ -69,7 +69,7 @@
     }
 
     @Override
-    public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) {
+    public float[] getOverviewScaleAndTranslationY(Launcher launcher) {
         return new float[] {1f, 0f};
     }
 
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 0b3bd6c..0d5574f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.uioverrides;
 
-import static com.android.quickstep.views.LauncherRecentsView.TRANSLATION_Y_FACTOR;
 import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
 
 import android.animation.ValueAnimator;
@@ -23,8 +22,6 @@
 import android.os.Build;
 import android.util.FloatProperty;
 
-import androidx.annotation.NonNull;
-
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager.AnimationConfig;
@@ -32,6 +29,8 @@
 import com.android.quickstep.views.LauncherRecentsView;
 import com.android.quickstep.views.RecentsView;
 
+import androidx.annotation.NonNull;
+
 /**
  * State handler for handling UI changes for {@link LauncherRecentsView}. In addition to managing
  * the basic view properties, this class also manages changes in the task visuals.
@@ -80,11 +79,6 @@
     }
 
     @Override
-    FloatProperty<LauncherRecentsView> getTranslationYFactorProperty() {
-        return TRANSLATION_Y_FACTOR;
-    }
-
-    @Override
     FloatProperty<RecentsView> getContentAlphaProperty() {
         return CONTENT_ALPHA;
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java
index 5494052..c00b4dc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java
@@ -160,6 +160,7 @@
                                 MotionEvent event = MotionEvent.obtain(ev);
                                 event.setAction(MotionEvent.ACTION_CANCEL);
                                 mConsumerDelegate.onMotionEvent(event);
+                                event.recycle();
                             }
                         } else {
                             mState = STATE_DELEGATE_ACTIVE;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
index d61ed72..ef46b3b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
@@ -21,7 +21,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
-import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -90,7 +89,7 @@
 
             @NonNull
             @Override
-            public Animator createActivityAnimationToHome() {
+            public AnimatorPlaybackController createActivityAnimationToHome() {
                 Animator anim = ObjectAnimator.ofFloat(recentsView, CONTENT_ALPHA, 0);
                 anim.addListener(new AnimationSuccessListener() {
                     @Override
@@ -98,7 +97,10 @@
                         recentsView.startHome();
                     }
                 });
-                return anim;
+                AnimatorSet animatorSet = new AnimatorSet();
+                animatorSet.play(anim);
+                long accuracy = 2 * Math.max(recentsView.getWidth(), recentsView.getHeight());
+                return AnimatorPlaybackController.wrap(animatorSet, accuracy);
             }
         };
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
index e95e2a0..279b83c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
@@ -56,9 +56,7 @@
 import com.android.launcher3.anim.SpringObjectAnimator;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.quickstep.util.ClipAnimationHelper;
 import com.android.quickstep.util.LayoutUtils;
@@ -144,10 +142,9 @@
 
             @NonNull
             @Override
-            public Animator createActivityAnimationToHome() {
+            public AnimatorPlaybackController createActivityAnimationToHome() {
                 long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx);
-                return activity.getStateManager().createAnimationToNewWorkspace(
-                        NORMAL, accuracy).getTarget();
+                return activity.getStateManager().createAnimationToNewWorkspace(NORMAL, accuracy);
             }
         };
     }
@@ -308,7 +305,7 @@
         // starting to line up the side pages during swipe up)
         float prevRvScale = recentsView.getScaleX();
         float prevRvTransY = recentsView.getTranslationY();
-        float targetRvScale = endState.getOverviewScaleAndTranslationYFactor(launcher)[0];
+        float targetRvScale = endState.getOverviewScaleAndTranslationY(launcher)[0];
         SCALE_PROPERTY.set(recentsView, targetRvScale);
         recentsView.setTranslationY(0);
         ClipAnimationHelper clipHelper = new ClipAnimationHelper(launcher);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java
index 84097a1..c8dcf80 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java
@@ -21,7 +21,6 @@
 import static android.view.MotionEvent.ACTION_POINTER_UP;
 import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.INVALID_POINTER_ID;
-
 import static com.android.launcher3.util.RaceConditionTracker.ENTER;
 import static com.android.launcher3.util.RaceConditionTracker.EXIT;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
@@ -45,8 +44,6 @@
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
 
-import androidx.annotation.UiThread;
-
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RaceConditionTracker;
@@ -63,6 +60,8 @@
 
 import java.util.function.Consumer;
 
+import androidx.annotation.UiThread;
+
 /**
  * Input consumer for handling events originating from an activity other than Launcher
  */
@@ -131,7 +130,8 @@
         mVelocityTracker = VelocityTracker.obtain();
 
         mActivityControlHelper = activityControl;
-        mIsDeferredDownTarget = isDeferredDownTarget;
+        boolean continuingPreviousGesture = swipeSharedState.getActiveListener() != null;
+        mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget;
         mOverviewCallbacks = overviewCallbacks;
         mTaskOverlayFactory = taskOverlayFactory;
         mInputConsumer = inputConsumer;
@@ -143,8 +143,7 @@
 
         mDragSlop = NavigationBarCompat.getQuickStepDragSlopPx();
         mTouchSlop = NavigationBarCompat.getQuickStepTouchSlopPx();
-        // If active listener isn't null, we are continuing the previous gesture.
-        mPassedTouchSlop = mPassedDragSlop = mSwipeSharedState.getActiveListener() != null;
+        mPassedTouchSlop = mPassedDragSlop = continuingPreviousGesture;
     }
 
     @Override
@@ -331,12 +330,13 @@
             mVelocityTracker.computeCurrentVelocity(1000,
                     ViewConfiguration.get(this).getScaledMaximumFlingVelocity());
             float velocityX = mVelocityTracker.getXVelocity(mActivePointerId);
+            float velocityY = mVelocityTracker.getYVelocity(mActivePointerId);
             float velocity = isNavBarOnRight() ? velocityX
                     : isNavBarOnLeft() ? -velocityX
-                            : mVelocityTracker.getYVelocity(mActivePointerId);
+                            : velocityY;
 
             mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement);
-            mInteractionHandler.onGestureEnded(velocity, velocityX);
+            mInteractionHandler.onGestureEnded(velocity, new PointF(velocityX, velocityY));
         } else {
             // Since we start touch tracking on DOWN, we may reach this state without actually
             // starting the gesture. In that case, just cleanup immediately.
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
index f0bc223..0924f38 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
@@ -173,6 +173,12 @@
         return true;
     }
 
+    public void setCancelWithDeferredScreenshot(boolean deferredWithScreenshot) {
+        if (targetSet != null) {
+            targetSet.controller.setCancelWithDeferredScreenshot(deferredWithScreenshot);
+        }
+    }
+
     public SwipeAnimationTargetSet getController() {
         return targetSet;
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
index fd53f9c..e8d4c19 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -43,12 +43,12 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.Build;
@@ -85,6 +85,7 @@
 import com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState;
 import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory;
 import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.RemoteAnimationTargetSet;
 import com.android.quickstep.util.SwipeAnimationTargetSet;
 import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
@@ -92,7 +93,6 @@
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.recents.utilities.RectFEvaluator;
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.LatencyTrackerCompat;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -110,7 +110,7 @@
         implements SwipeAnimationListener, OnApplyWindowInsetsListener {
     private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName();
 
-    private static final String[] STATE_NAMES = DEBUG_STATES ? new String[17] : null;
+    private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null;
 
     private static int getFlagForIndex(int index, String name) {
         if (DEBUG_STATES) {
@@ -133,37 +133,33 @@
             getFlagForIndex(4, "STATE_SCALED_CONTROLLER_HOME");
     private static final int STATE_SCALED_CONTROLLER_RECENTS =
             getFlagForIndex(5, "STATE_SCALED_CONTROLLER_RECENTS");
-    private static final int STATE_SCALED_CONTROLLER_LAST_TASK =
-            getFlagForIndex(6, "STATE_SCALED_CONTROLLER_LAST_TASK");
 
     private static final int STATE_HANDLER_INVALIDATED =
-            getFlagForIndex(7, "STATE_HANDLER_INVALIDATED");
+            getFlagForIndex(6, "STATE_HANDLER_INVALIDATED");
     private static final int STATE_GESTURE_STARTED =
-            getFlagForIndex(8, "STATE_GESTURE_STARTED");
+            getFlagForIndex(7, "STATE_GESTURE_STARTED");
     private static final int STATE_GESTURE_CANCELLED =
-            getFlagForIndex(9, "STATE_GESTURE_CANCELLED");
+            getFlagForIndex(8, "STATE_GESTURE_CANCELLED");
     private static final int STATE_GESTURE_COMPLETED =
-            getFlagForIndex(10, "STATE_GESTURE_COMPLETED");
+            getFlagForIndex(9, "STATE_GESTURE_COMPLETED");
 
     private static final int STATE_CAPTURE_SCREENSHOT =
-            getFlagForIndex(11, "STATE_CAPTURE_SCREENSHOT");
+            getFlagForIndex(10, "STATE_CAPTURE_SCREENSHOT");
     private static final int STATE_SCREENSHOT_CAPTURED =
-            getFlagForIndex(12, "STATE_SCREENSHOT_CAPTURED");
+            getFlagForIndex(11, "STATE_SCREENSHOT_CAPTURED");
     private static final int STATE_SCREENSHOT_VIEW_SHOWN =
-            getFlagForIndex(13, "STATE_SCREENSHOT_VIEW_SHOWN");
+            getFlagForIndex(12, "STATE_SCREENSHOT_VIEW_SHOWN");
 
     private static final int STATE_RESUME_LAST_TASK =
-            getFlagForIndex(14, "STATE_RESUME_LAST_TASK");
+            getFlagForIndex(13, "STATE_RESUME_LAST_TASK");
     private static final int STATE_START_NEW_TASK =
-            getFlagForIndex(15, "STATE_START_NEW_TASK");
+            getFlagForIndex(14, "STATE_START_NEW_TASK");
     private static final int STATE_CURRENT_TASK_FINISHED =
-            getFlagForIndex(16, "STATE_CURRENT_TASK_FINISHED");
+            getFlagForIndex(15, "STATE_CURRENT_TASK_FINISHED");
 
     private static final int LAUNCHER_UI_STATES =
             STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED;
 
-    // For debugging, keep in sync with above states
-
     enum GestureEndTarget {
         HOME(1, STATE_SCALED_CONTROLLER_HOME, true, false, ContainerType.WORKSPACE),
 
@@ -172,7 +168,7 @@
 
         NEW_TASK(0, STATE_START_NEW_TASK, false, true, ContainerType.APP),
 
-        LAST_TASK(0, STATE_SCALED_CONTROLLER_LAST_TASK, false, false, ContainerType.APP);
+        LAST_TASK(0, STATE_RESUME_LAST_TASK, false, true, ContainerType.APP);
 
         GestureEndTarget(float endShift, int endState, boolean isLauncher, boolean canBeContinued,
                 int containerType) {
@@ -234,6 +230,7 @@
     private ThumbnailData mTaskSnapshot;
 
     private MultiStateCallback mStateCallback;
+    // Used to control launcher components throughout the swipe gesture.
     private AnimatorPlaybackController mLauncherTransitionController;
 
     private T mActivity;
@@ -274,10 +271,8 @@
     private void initStateCallbacks() {
         mStateCallback = new MultiStateCallback(STATE_NAMES);
 
-        // Re-setup the recents UI when gesture starts, as the state could have been changed during
-        // that time by a previous window transition.
-        mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_GESTURE_STARTED,
-                this::setupRecentsViewUi);
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED,
+                this::onLauncherPresentAndGestureStarted);
 
         mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED,
                 this::initializeLauncherAnimationController);
@@ -285,9 +280,6 @@
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN,
                 this::launcherFrameDrawn);
 
-        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED,
-                this::notifyGestureStartedAsync);
-
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED
                         | STATE_GESTURE_CANCELLED,
                 this::resetStateForAnimationCancel);
@@ -295,8 +287,6 @@
         mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED,
                 this::sendRemoteAnimationsToAnimationFactory);
 
-        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_LAST_TASK,
-                this::resumeLastTaskForQuickstep);
         mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED,
                 this::resumeLastTask);
         mStateCallback.addCallback(STATE_START_NEW_TASK | STATE_APP_CONTROLLER_RECEIVED,
@@ -326,8 +316,7 @@
         mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
                 this::invalidateHandlerWithLauncher);
-        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED
-                | STATE_SCALED_CONTROLLER_LAST_TASK,
+        mStateCallback.addCallback(STATE_HANDLER_INVALIDATED | STATE_RESUME_LAST_TASK,
                 this::notifyTransitionCancelled);
 
         mStateCallback.addCallback(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED,
@@ -414,6 +403,8 @@
         } else {
             activity.setOnStartCallback(this::onLauncherStart);
         }
+
+        setupRecentsViewUi();
         return true;
     }
 
@@ -425,8 +416,12 @@
             return;
         }
 
-        mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity,
-                mWasLauncherAlreadyVisible, true, this::onAnimatorPlaybackControllerCreated);
+        // 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.
+        if (mGestureEndTarget != HOME) {
+            mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity,
+                    mWasLauncherAlreadyVisible, true, this::onAnimatorPlaybackControllerCreated);
+        }
         AbstractFloatingView.closeAllOpenViews(activity, mWasLauncherAlreadyVisible);
 
         if (mWasLauncherAlreadyVisible) {
@@ -450,11 +445,18 @@
             });
         }
 
-        setupRecentsViewUi();
         activity.getRootView().setOnApplyWindowInsetsListener(this);
         mStateCallback.setState(STATE_LAUNCHER_STARTED);
     }
 
+    private void onLauncherPresentAndGestureStarted() {
+        // Re-setup the recents UI when gesture starts, as the state could have been changed during
+        // that time by a previous window transition.
+        setupRecentsViewUi();
+
+        notifyGestureStartedAsync();
+    }
+
     private void setupRecentsViewUi() {
         if (mContinuingLastGesture) {
             return;
@@ -677,15 +679,19 @@
         }
     }
 
+    /**
+     * @param endVelocity The velocity in the direction of the nav bar to the middle of the screen.
+     * @param velocity The x and y components of the velocity when the gesture ends.
+     */
     @UiThread
-    public void onGestureEnded(float endVelocity, float velocityX) {
+    public void onGestureEnded(float endVelocity, PointF velocity) {
         float flingThreshold = mContext.getResources()
                 .getDimension(R.dimen.quickstep_fling_threshold_velocity);
         boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold;
         setStateOnUiThread(STATE_GESTURE_COMPLETED);
 
         mLogAction = isFling ? Touch.FLING : Touch.SWIPE;
-        handleNormalGestureEnd(endVelocity, isFling, velocityX);
+        handleNormalGestureEnd(endVelocity, isFling, velocity);
     }
 
     @UiThread
@@ -703,9 +709,8 @@
     }
 
     @UiThread
-    private void handleNormalGestureEnd(float endVelocity, boolean isFling, float velocityX) {
-        float velocityPxPerMs = endVelocity / 1000;
-        float velocityXPxPerMs = velocityX / 1000;
+    private void handleNormalGestureEnd(float endVelocity, boolean isFling, PointF velocity) {
+        PointF velocityPxPerMs = new PointF(velocity.x / 1000, velocity.y / 1000);
         long duration = MAX_SWIPE_DURATION;
         float currentShift = mCurrentShift.value;
         final GestureEndTarget endTarget;
@@ -720,7 +725,7 @@
             final int lastTaskIndex = mRecentsView.getTaskViewCount() - 1;
             final int runningTaskIndex = mRecentsView.getRunningTaskIndex();
             taskToLaunch = nextPage <= lastTaskIndex ? nextPage : lastTaskIndex;
-            goingToNewTask = mRecentsView != null && taskToLaunch != runningTaskIndex;
+            goingToNewTask = runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex;
         } else {
             goingToNewTask = false;
         }
@@ -750,7 +755,7 @@
         } else {
             if (SWIPE_HOME.get() && endVelocity < 0 && !mIsShelfPeeking) {
                 // If swiping at a diagonal, base end target on the faster velocity.
-                endTarget = goingToNewTask && Math.abs(velocityX) > Math.abs(endVelocity)
+                endTarget = goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity)
                         ? NEW_TASK : HOME;
             } else if (endVelocity < 0 && (!goingToNewTask || reachedOverviewThreshold)) {
                 // If user scrolled to a new task, only go to recents if they already passed
@@ -760,14 +765,15 @@
                 endTarget = goingToNewTask ? NEW_TASK : LAST_TASK;
             }
             endShift = endTarget.endShift;
-            startShift = Utilities.boundToRange(currentShift - velocityPxPerMs
+            startShift = Utilities.boundToRange(currentShift - velocityPxPerMs.y
                     * SINGLE_FRAME_MS / mTransitionDragLength, 0, 1);
             float minFlingVelocity = mContext.getResources()
                     .getDimension(R.dimen.quickstep_fling_min_velocity);
             if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
                 if (endTarget == RECENTS) {
                     Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams(
-                            startShift, endShift, endShift, velocityPxPerMs, mTransitionDragLength);
+                            startShift, endShift, endShift, velocityPxPerMs.y,
+                            mTransitionDragLength);
                     endShift = overshoot.end;
                     interpolator = overshoot.interpolator;
                     duration = Utilities.boundToRange(overshoot.duration, MIN_OVERSHOOT_DURATION,
@@ -778,7 +784,7 @@
                     // we want the page's snap velocity to approximately match the velocity at
                     // which the user flings, so we scale the duration by a value near to the
                     // derivative of the scroll interpolator at zero, ie. 2.
-                    long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs));
+                    long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y));
                     duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
                 }
             }
@@ -803,7 +809,7 @@
             }
         } else if (endTarget == NEW_TASK || endTarget == LAST_TASK) {
             // Let RecentsView handle the scrolling to the task, which we launch in startNewTask()
-            // or resumeLastTaskForQuickstep().
+            // or resumeLastTask().
             if (mRecentsView != null) {
                 duration = Math.max(duration, mRecentsView.getScroller().getDuration());
             }
@@ -835,14 +841,14 @@
     /** Animates to the given progress, where 0 is the current app and 1 is overview. */
     @UiThread
     private void animateToProgress(float start, float end, long duration, Interpolator interpolator,
-            GestureEndTarget target, float velocityPxPerMs) {
+            GestureEndTarget target, PointF velocityPxPerMs) {
         mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration,
                 interpolator, target, velocityPxPerMs));
     }
 
     @UiThread
     private void animateToProgressInternal(float start, float end, long duration,
-            Interpolator interpolator, GestureEndTarget target, float velocityPxPerMs) {
+            Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) {
         mGestureEndTarget = target;
 
         if (mGestureEndTarget.canBeContinued) {
@@ -855,9 +861,8 @@
             RecentsModel.INSTANCE.get(mContext).endStabilizationSession();
         }
 
-        HomeAnimationFactory homeAnimFactory;
-        Animator windowAnim;
         if (mGestureEndTarget == HOME) {
+            HomeAnimationFactory homeAnimFactory;
             if (mActivity != null) {
                 homeAnimFactory = mActivityControlHelper.prepareHomeUI(mActivity);
             } else {
@@ -872,27 +877,33 @@
 
                     @NonNull
                     @Override
-                    public Animator createActivityAnimationToHome() {
-                        return new AnimatorSet();
+                    public AnimatorPlaybackController createActivityAnimationToHome() {
+                        return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
                     }
                 };
                 mStateCallback.addChangeHandler(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
                         isPresent -> mRecentsView.startHome());
             }
-            windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
+            RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
+            windowAnim.addAnimatorListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    setStateOnUiThread(target.endState);
+                }
+            });
+            windowAnim.start(velocityPxPerMs);
             mLauncherTransitionController = null;
         } else {
-            windowAnim = mCurrentShift.animateToValue(start, end);
-            homeAnimFactory = null;
+            Animator windowAnim = mCurrentShift.animateToValue(start, end);
+            windowAnim.setDuration(duration).setInterpolator(interpolator);
+            windowAnim.addListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    setStateOnUiThread(target.endState);
+                }
+            });
+            windowAnim.start();
         }
-        windowAnim.setDuration(duration).setInterpolator(interpolator);
-        windowAnim.addListener(new AnimationSuccessListener() {
-            @Override
-            public void onAnimationSuccess(Animator animator) {
-                setStateOnUiThread(target.endState);
-            }
-        });
-        windowAnim.start();
         // Always play the entire launcher animation when going home, since it is separate from
         // the animation that has been controlled thus far.
         if (mGestureEndTarget == HOME) {
@@ -903,12 +914,6 @@
         // interpolate over the remaining progress (end - start).
         TimeInterpolator adjustedInterpolator = Interpolators.mapToProgress(
                 interpolator, start, end);
-        if (homeAnimFactory != null) {
-            Animator homeAnim = homeAnimFactory.createActivityAnimationToHome();
-            homeAnim.setDuration(duration).setInterpolator(adjustedInterpolator);
-            homeAnim.start();
-            mLauncherTransitionController = null;
-        }
         if (mLauncherTransitionController == null) {
             return;
         }
@@ -920,50 +925,41 @@
             mLauncherTransitionController.getAnimationPlayer().setDuration(duration);
 
             if (QUICKSTEP_SPRINGS.get()) {
-                mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs);
+                mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs.y);
             }
             mLauncherTransitionController.getAnimationPlayer().start();
         }
     }
 
     /**
-     * Creates an Animator that transforms the current app window into the home app.
+     * Creates an animation that transforms the current app window into the home app.
      * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
      * @param homeAnimationFactory The home animation factory.
      */
-    private Animator createWindowAnimationToHome(float startProgress,
+    private RectFSpringAnim createWindowAnimationToHome(float startProgress,
             HomeAnimationFactory homeAnimationFactory) {
         final RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet;
-        RectF startRect = new RectF(mClipAnimationHelper.applyTransform(targetSet,
+        final RectF startRect = new RectF(mClipAnimationHelper.applyTransform(targetSet,
                 mTransformParams.setProgress(startProgress)));
-        RectF originalTarget = new RectF(mClipAnimationHelper.getTargetRect());
-        final RectF finalTarget = homeAnimationFactory.getWindowTargetRect();
-
-        final RectFEvaluator rectFEvaluator = new RectFEvaluator();
-        final RectF targetRect = new RectF();
-        final RectF currentRect = new RectF();
+        final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
 
         final View floatingView = homeAnimationFactory.getFloatingView();
         final boolean isFloatingIconView = floatingView instanceof FloatingIconView;
 
-        ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+        RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect);
         if (isFloatingIconView) {
-            anim.addListener((FloatingIconView) floatingView);
+            anim.addAnimatorListener((FloatingIconView) floatingView);
         }
 
+        AnimatorPlaybackController homeAnim = homeAnimationFactory.createActivityAnimationToHome();
+
         // 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.
         final float windowAlphaThreshold = isFloatingIconView ? 0.75f : 1f;
-        anim.addUpdateListener(animation -> {
-            float progress = animation.getAnimatedFraction();
+        anim.addOnUpdateListener((currentRect, progress) -> {
             float interpolatedProgress = Interpolators.ACCEL_1_5.getInterpolation(progress);
-            // Initially go towards original target (task view in recents),
-            // but accelerate towards the final target.
-            // TODO: This is technically not correct. Instead, motion should continue at
-            // the released velocity but accelerate towards the target.
-            targetRect.set(rectFEvaluator.evaluate(interpolatedProgress,
-                    originalTarget, finalTarget));
-            currentRect.set(rectFEvaluator.evaluate(interpolatedProgress, startRect, targetRect));
+
+            homeAnim.setPlayFraction(progress);
 
             float iconAlpha = Utilities.mapToRange(interpolatedProgress, 0,
                     windowAlphaThreshold, 0f, 1f, Interpolators.LINEAR);
@@ -975,10 +971,17 @@
                 ((FloatingIconView) floatingView).update(currentRect, iconAlpha, progress,
                         windowAlphaThreshold);
             }
+
         });
-        anim.addListener(new AnimationSuccessListener() {
+        anim.addAnimatorListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                homeAnim.dispatchOnStart();
+            }
+
             @Override
             public void onAnimationSuccess(Animator animator) {
+                homeAnim.getAnimationPlayer().end();
                 if (mRecentsView != null) {
                     mRecentsView.post(mRecentsView::resetTaskVisuals);
                 }
@@ -988,16 +991,11 @@
     }
 
     @UiThread
-    private void resumeLastTaskForQuickstep() {
-        setStateOnUiThread(STATE_RESUME_LAST_TASK);
-        doLogGesture(LAST_TASK);
-        reset();
-    }
-
-    @UiThread
     private void resumeLastTask() {
         mRecentsAnimationWrapper.finish(false /* toRecents */, null);
         TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", false);
+        doLogGesture(LAST_TASK);
+        reset();
     }
 
     @UiThread
@@ -1005,18 +1003,15 @@
         // Launch the task user scrolled to (mRecentsView.getNextPage()).
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             // We finish recents animation inside launchTask() when live tile is enabled.
-            mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).launchTask(false,
-                    result -> setStateOnUiThread(STATE_HANDLER_INVALIDATED),
-                    mMainThreadHandler);
+            mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).launchTask(false);
         } else {
             mRecentsAnimationWrapper.finish(true /* toRecents */, () -> {
-                mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).launchTask(false,
-                        result -> setStateOnUiThread(STATE_HANDLER_INVALIDATED),
-                        mMainThreadHandler);
+                mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).launchTask(false);
             });
         }
         TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", false);
         doLogGesture(NEW_TASK);
+        reset();
     }
 
     public void reset() {
@@ -1141,6 +1136,7 @@
             mLauncherTransitionController = null;
         }
         mActivityControlHelper.onSwipeUpComplete(mActivity);
+        mRecentsAnimationWrapper.setCancelWithDeferredScreenshot(true);
 
         // Animate the first icon.
         mRecentsView.animateUpRunningTaskIconScale(mLiveTileOverlay.cancelIconAnimation());
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java
index 62f2183..94e704a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java
@@ -38,9 +38,16 @@
  */
 public class RecentsAnimationListenerSet implements RecentsAnimationListener {
 
+    // The actual app surface is replaced by a screenshot upon recents animation cancelation when
+    // deferredWithScreenshot is true. Launcher takes the responsibility to clean up this screenshot
+    // after app transition is finished. This delay is introduced to cover the app transition
+    // period of time.
+    private final int TRANSITION_DELAY = 100;
+
     private final Set<SwipeAnimationListener> mListeners = new ArraySet<>();
     private final boolean mShouldMinimizeSplitScreen;
     private final Consumer<SwipeAnimationTargetSet> mOnFinishListener;
+    private RecentsAnimationControllerCompat mController;
 
     public RecentsAnimationListenerSet(boolean shouldMinimizeSplitScreen,
             Consumer<SwipeAnimationTargetSet> onFinishListener) {
@@ -64,6 +71,7 @@
     public final void onAnimationStart(RecentsAnimationControllerCompat controller,
             RemoteAnimationTargetCompat[] targets, Rect homeContentInsets,
             Rect minimizedHomeBounds) {
+        mController = controller;
         SwipeAnimationTargetSet targetSet = new SwipeAnimationTargetSet(controller, targets,
                 homeContentInsets, minimizedHomeBounds, mShouldMinimizeSplitScreen,
                 mOnFinishListener);
@@ -75,12 +83,17 @@
     }
 
     @Override
-    public final void onAnimationCanceled() {
+    public final void onAnimationCanceled(boolean deferredWithScreenshot) {
         Utilities.postAsyncCallback(MAIN_THREAD_EXECUTOR.getHandler(), () -> {
             for (SwipeAnimationListener listener : getListeners()) {
                 listener.onRecentsAnimationCanceled();
             }
         });
+        // TODO: handle the transition better instead of simply using a transition delay.
+        if (deferredWithScreenshot) {
+            MAIN_THREAD_EXECUTOR.getHandler().postDelayed(() -> mController.cleanupScreenshot(),
+                    TRANSITION_DELAY);
+        }
     }
 
     private SwipeAnimationListener[] getListeners() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
new file mode 100644
index 0000000..2edeb3a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2019 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 android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.FloatProperty;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.FlingSpringAnim;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+
+/**
+ * Applies spring forces to animate from a starting rect to a target rect,
+ * while providing update callbacks to the caller.
+ */
+public class RectFSpringAnim {
+
+    /**
+     * Although the rect position animation takes an indefinite amount of time since it depends on
+     * the initial velocity and applied forces, scaling from the starting rect to the target rect
+     * can be done in parallel at a fixed duration. Update callbacks are sent based on the progress
+     * of this animation, while the end callback is sent after all animations finish.
+     */
+    private static final long RECT_SCALE_DURATION = 180;
+
+    private static final FloatPropertyCompat<RectFSpringAnim> RECT_CENTER_X =
+            new FloatPropertyCompat<RectFSpringAnim>("rectCenterXSpring") {
+                @Override
+                public float getValue(RectFSpringAnim anim) {
+                    return anim.mCurrentCenterX;
+                }
+
+                @Override
+                public void setValue(RectFSpringAnim anim, float currentCenterX) {
+                    anim.mCurrentCenterX = currentCenterX;
+                    anim.onUpdate();
+                }
+            };
+
+    private static final FloatPropertyCompat<RectFSpringAnim> RECT_CENTER_Y =
+            new FloatPropertyCompat<RectFSpringAnim>("rectCenterYSpring") {
+                @Override
+                public float getValue(RectFSpringAnim anim) {
+                    return anim.mCurrentCenterY;
+                }
+
+                @Override
+                public void setValue(RectFSpringAnim anim, float currentCenterY) {
+                    anim.mCurrentCenterY = currentCenterY;
+                    anim.onUpdate();
+                }
+            };
+
+    private static final FloatProperty<RectFSpringAnim> RECT_SCALE_PROGRESS =
+            new FloatProperty<RectFSpringAnim>("rectScaleProgress") {
+                @Override
+                public Float get(RectFSpringAnim anim) {
+                    return anim.mCurrentScaleProgress;
+                }
+
+                @Override
+                public void setValue(RectFSpringAnim anim, float currentScaleProgress) {
+                    anim.mCurrentScaleProgress = currentScaleProgress;
+                    anim.onUpdate();
+                }
+            };
+
+    private final RectF mStartRect;
+    private final RectF mTargetRect;
+    private final RectF mCurrentRect = new RectF();
+    private final List<OnUpdateListener> mOnUpdateListeners = new ArrayList<>();
+    private final List<Animator.AnimatorListener> mAnimatorListeners = new ArrayList<>();
+
+    private float mCurrentCenterX;
+    private float mCurrentCenterY;
+    private float mCurrentScaleProgress;
+    private boolean mRectXAnimEnded;
+    private boolean mRectYAnimEnded;
+    private boolean mRectScaleAnimEnded;
+
+    public RectFSpringAnim(RectF startRect, RectF targetRect) {
+        mStartRect = startRect;
+        mTargetRect = targetRect;
+        mCurrentCenterX = mStartRect.centerX();
+        mCurrentCenterY = mStartRect.centerY();
+    }
+
+    public void addOnUpdateListener(OnUpdateListener onUpdateListener) {
+        mOnUpdateListeners.add(onUpdateListener);
+    }
+
+    public void addAnimatorListener(Animator.AnimatorListener animatorListener) {
+        mAnimatorListeners.add(animatorListener);
+    }
+
+    public void start(PointF velocityPxPerMs) {
+        // Only tell caller that we ended if both x and y animations have ended.
+        OnAnimationEndListener onXEndListener = ((animation, canceled, centerX, velocityX) -> {
+            mRectXAnimEnded = true;
+            maybeOnEnd();
+        });
+        OnAnimationEndListener onYEndListener = ((animation, canceled, centerY, velocityY) -> {
+            mRectYAnimEnded = true;
+            maybeOnEnd();
+        });
+        FlingSpringAnim rectXAnim = new FlingSpringAnim(this, RECT_CENTER_X, mCurrentCenterX,
+                mTargetRect.centerX(), velocityPxPerMs.x * 1000, onXEndListener);
+        FlingSpringAnim rectYAnim = new FlingSpringAnim(this, RECT_CENTER_Y, mCurrentCenterY,
+                mTargetRect.centerY(), velocityPxPerMs.y * 1000, onYEndListener);
+
+        ValueAnimator rectScaleAnim = ObjectAnimator.ofPropertyValuesHolder(this,
+                PropertyValuesHolder.ofFloat(RECT_SCALE_PROGRESS, 1))
+                .setDuration(RECT_SCALE_DURATION);
+        rectScaleAnim.addListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                mRectScaleAnimEnded = true;
+                maybeOnEnd();
+            }
+        });
+
+        rectXAnim.start();
+        rectYAnim.start();
+        rectScaleAnim.start();
+        for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
+            animatorListener.onAnimationStart(null);
+        }
+    }
+
+    private void onUpdate() {
+        if (!mOnUpdateListeners.isEmpty()) {
+            float currentWidth = Utilities.mapRange(mCurrentScaleProgress, mStartRect.width(),
+                    mTargetRect.width());
+            float currentHeight = Utilities.mapRange(mCurrentScaleProgress, mStartRect.height(),
+                    mTargetRect.height());
+            mCurrentRect.set(mCurrentCenterX - currentWidth / 2, mCurrentCenterY - currentHeight / 2,
+                    mCurrentCenterX + currentWidth / 2, mCurrentCenterY + currentHeight / 2);
+            for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
+                onUpdateListener.onUpdate(mCurrentRect, mCurrentScaleProgress);
+            }
+        }
+    }
+
+    private void maybeOnEnd() {
+        if (mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded) {
+            for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
+                animatorListener.onAnimationEnd(null);
+            }
+        }
+    }
+
+    public interface OnUpdateListener {
+        void onUpdate(RectF currentRect, float progress);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
index cf42a36..19e9cb4 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -198,7 +198,7 @@
             final ActivityOptions options = ActivityOptions.makeScaleUpAnimation(
                     this, 0, 0,
                     getWidth(), getHeight());
-            launcher.startActivityForResult(intent, 0, options.toBundle());
+            launcher.startActivity(intent, options.toBundle());
             launcher.getUserEventDispatcher().logActionOnControl(LauncherLogProto.Action.Touch.TAP,
                     LauncherLogProto.ControlType.APP_USAGE_SETTINGS, this);
         } catch (ActivityNotFoundException e) {
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 97bce5e..8da1d2b 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
@@ -56,27 +56,6 @@
 @TargetApi(Build.VERSION_CODES.O)
 public class LauncherRecentsView extends RecentsView<Launcher> {
 
-    public static final FloatProperty<LauncherRecentsView> TRANSLATION_Y_FACTOR =
-            new FloatProperty<LauncherRecentsView>("translationYFactor") {
-
-                @Override
-                public void setValue(LauncherRecentsView view, float v) {
-                    view.setTranslationYFactor(v);
-                }
-
-                @Override
-                public Float get(LauncherRecentsView view) {
-                    return view.mTranslationYFactor;
-                }
-            };
-
-    /**
-     * A ratio representing the view's relative placement within its padded space. For example, 0
-     * is top aligned and 0.5 is centered vertically.
-     */
-    @ViewDebug.ExportedProperty(category = "launcher")
-    private float mTranslationYFactor;
-
     private final TransformParams mTransformParams = new TransformParams();
     private ChipsContainer mChipsContainer;
 
@@ -95,18 +74,7 @@
 
     @Override
     public void startHome() {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            takeScreenshotAndFinishRecentsAnimation(true,
-                    () -> mActivity.getStateManager().goToState(NORMAL));
-        } else {
-            mActivity.getStateManager().goToState(NORMAL);
-        }
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        super.onLayout(changed, left, top, right, bottom);
-        setTranslationYFactor(mTranslationYFactor);
+        mActivity.getStateManager().goToState(NORMAL);
     }
 
     @Override
@@ -117,9 +85,9 @@
         params.bottomMargin = mActivity.getDeviceProfile().chipHintBottomMarginPx;
     }
 
-    public void setTranslationYFactor(float translationFactor) {
-        mTranslationYFactor = translationFactor;
-        setTranslationY(computeTranslationYForFactor(mTranslationYFactor));
+    @Override
+    public void setTranslationY(float translationY) {
+        super.setTranslationY(translationY);
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             LauncherState state = mActivity.getStateManager().getState();
             if (state == OVERVIEW || state == ALL_APPS) {
@@ -128,10 +96,6 @@
         }
     }
 
-    public float computeTranslationYForFactor(float translationYFactor) {
-        return translationYFactor * (getPaddingBottom() - getPaddingTop());
-    }
-
     public void setHintVisibility(float v) {
         if (mChipsContainer != null && ENABLE_HINTS_IN_OVERVIEW.get()) {
             mChipsContainer.setHintVisibility(v);
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 37febf9a..a02df62 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
@@ -96,7 +96,6 @@
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.util.ClipAnimationHelper;
-import com.android.quickstep.util.SwipeAnimationTargetSet;
 import com.android.quickstep.util.TaskViewDrawable;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -106,7 +105,6 @@
 import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
 import com.android.systemui.shared.system.TaskStackChangeListener;
-import com.android.systemui.shared.system.WindowCallbacksCompat;
 
 import java.util.ArrayList;
 import java.util.function.Consumer;
@@ -1608,50 +1606,4 @@
 
         mRecentsAnimationWrapper.finish(toRecents, onFinishComplete);
     }
-
-    public void takeScreenshotAndFinishRecentsAnimation(boolean toRecents,
-            Runnable onFinishComplete) {
-        if (mRecentsAnimationWrapper == null || getRunningTaskView() == null) {
-            if (onFinishComplete != null) {
-                onFinishComplete.run();
-            }
-            return;
-        }
-
-        SwipeAnimationTargetSet controller = mRecentsAnimationWrapper.getController();
-        if (controller != null) {
-            // Update the screenshot of the task
-            ThumbnailData taskSnapshot = controller.screenshotTask(mRunningTaskId);
-            TaskView taskView = updateThumbnail(mRunningTaskId, taskSnapshot);
-            if (taskView != null) {
-                taskView.setShowScreenshot(true);
-                // Defer finishing the animation until the next launcher frame with the
-                // new thumbnail
-                new WindowCallbacksCompat(taskView) {
-
-                    // The number of frames to defer until we actually finish the animation
-                    private int mDeferFrameCount = 2;
-
-                    @Override
-                    public void onPostDraw(Canvas canvas) {
-                        if (mDeferFrameCount > 0) {
-                            mDeferFrameCount--;
-                            // Workaround, detach and reattach to invalidate the root node for
-                            // another draw
-                            detach();
-                            attach();
-                            taskView.invalidate();
-                            return;
-                        }
-
-                        detach();
-                        mRecentsAnimationWrapper.finish(toRecents, () -> {
-                            onFinishComplete.run();
-                            mRunningTaskId = -1;
-                        });
-                    }
-                }.attach();
-            }
-        }
-    }
 }
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 682152e..d15a392 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
@@ -208,13 +208,7 @@
                 R.layout.task_view_menu_option, this, false);
         menuOption.setIconAndLabelFor(
                 menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            menuOptionView.setOnClickListener(
-                    view -> mTaskView.getRecentsView().takeScreenshotAndFinishRecentsAnimation(true,
-                            () -> onClickListener.onClick(view)));
-        } else {
-            menuOptionView.setOnClickListener(onClickListener);
-        }
+        menuOptionView.setOnClickListener(onClickListener);
         mOptionLayout.addView(menuOptionView);
     }
 
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 98495db..38aaac5 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
@@ -261,11 +261,10 @@
             Handler resultCallbackHandler) {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             if (isRunningTask()) {
-                getRecentsView().finishRecentsAnimation(false,
+                getRecentsView().finishRecentsAnimation(false /* toRecents */,
                         () -> resultCallbackHandler.post(() -> resultCallback.accept(true)));
             } else {
-                getRecentsView().takeScreenshotAndFinishRecentsAnimation(true,
-                        () -> launchTaskInternal(animate, resultCallback, resultCallbackHandler));
+                launchTaskInternal(animate, resultCallback, resultCallbackHandler);
             }
         } else {
             launchTaskInternal(animate, resultCallback, resultCallbackHandler);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
index 1eaa8bc..5ae562e 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
@@ -78,8 +78,9 @@
     }
 
     @Override
-    public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) {
-        return new float[] {0.9f, -0.2f};
+    public float[] getOverviewScaleAndTranslationY(Launcher launcher) {
+        float slightParallax = -launcher.getDeviceProfile().allAppsCellHeightPx * 0.3f;
+        return new float[] {0.9f, slightParallax};
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index df9dbe4..e74d84d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -26,8 +26,6 @@
 import android.view.View;
 import android.view.animation.Interpolator;
 
-import androidx.annotation.NonNull;
-
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager.AnimationConfig;
@@ -35,6 +33,8 @@
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.PropertySetter;
 
+import androidx.annotation.NonNull;
+
 /**
  * State handler for recents view. Manages UI changes and animations for recents view based off the
  * current {@link LauncherState}.
@@ -53,9 +53,9 @@
 
     @Override
     public void setState(@NonNull LauncherState state) {
-        float[] scaleTranslationYFactor = state.getOverviewScaleAndTranslationYFactor(mLauncher);
-        SCALE_PROPERTY.set(mRecentsView, scaleTranslationYFactor[0]);
-        getTranslationYFactorProperty().set(mRecentsView, scaleTranslationYFactor[1]);
+        float[] scaleTranslationY = state.getOverviewScaleAndTranslationY(mLauncher);
+        SCALE_PROPERTY.set(mRecentsView, scaleTranslationY[0]);
+        mRecentsView.setTranslationY(scaleTranslationY[1]);
         getContentAlphaProperty().set(mRecentsView, state.overviewUi ? 1f : 0);
     }
 
@@ -79,11 +79,11 @@
     void setStateWithAnimationInternal(@NonNull final LauncherState toState,
             @NonNull AnimatorSetBuilder builder, @NonNull AnimationConfig config) {
         PropertySetter setter = config.getPropertySetter(builder);
-        float[] scaleTranslationYFactor = toState.getOverviewScaleAndTranslationYFactor(mLauncher);
+        float[] scaleTranslationY = toState.getOverviewScaleAndTranslationY(mLauncher);
         Interpolator scaleAndTransYInterpolator = getScaleAndTransYInterpolator(toState, builder);
-        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleTranslationYFactor[0],
+        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleTranslationY[0],
                 scaleAndTransYInterpolator);
-        setter.setFloat(mRecentsView, getTranslationYFactorProperty(), scaleTranslationYFactor[1],
+        setter.setFloat(mRecentsView, View.TRANSLATION_Y, scaleTranslationY[1],
                 scaleAndTransYInterpolator);
         setter.setFloat(mRecentsView, getContentAlphaProperty(), toState.overviewUi ? 1 : 0,
                 builder.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
@@ -102,13 +102,6 @@
     }
 
     /**
-     * Get property for translation Y factor for the recents view.
-     *
-     * @return the float property for the recents view
-     */
-    abstract FloatProperty getTranslationYFactorProperty();
-
-    /**
      * Get property for content alpha for the recents view.
      *
      * @return the float property for the view's content alpha
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index 75be2e4..418f7f4 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -15,7 +15,6 @@
  */
 package com.android.quickstep;
 
-import android.animation.Animator;
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.Intent;
@@ -132,6 +131,6 @@
 
         @NonNull RectF getWindowTargetRect();
 
-        @NonNull Animator createActivityAnimationToHome();
+        @NonNull AnimatorPlaybackController createActivityAnimationToHome();
     }
 }
diff --git a/quickstep/src/com/android/quickstep/TestInformationProvider.java b/quickstep/src/com/android/quickstep/TestInformationProvider.java
index 0c478d2..e57d3ec 100644
--- a/quickstep/src/com/android/quickstep/TestInformationProvider.java
+++ b/quickstep/src/com/android/quickstep/TestInformationProvider.java
@@ -25,6 +25,9 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherState;
 import com.android.launcher3.TestProtocol;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.uioverrides.OverviewState;
@@ -68,6 +71,9 @@
             final Context context = getContext();
             final DeviceProfile deviceProfile = InvariantDeviceProfile.INSTANCE.
                     get(context).getDeviceProfile(context);
+            final LauncherAppState launcherAppState = LauncherAppState.getInstanceNoCreate();
+            final Launcher launcher = launcherAppState != null ?
+                    (Launcher) launcherAppState.getModel().getCallback() : null;
 
             switch (method) {
                 case TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT: {
@@ -76,12 +82,35 @@
                     response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) swipeHeight);
                     break;
                 }
+
                 case TestProtocol.REQUEST_BACKGROUND_TO_OVERVIEW_SWIPE_HEIGHT: {
                     final float swipeHeight =
                             LayoutUtils.getShelfTrackingDistance(context, deviceProfile);
                     response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) swipeHeight);
                     break;
                 }
+
+                case TestProtocol.REQUEST_ALL_APPS_TO_OVERVIEW_SWIPE_HEIGHT: {
+                    if (launcher == null) return null;
+
+                    final float progress = LauncherState.OVERVIEW.getVerticalProgress(launcher)
+                            - LauncherState.ALL_APPS.getVerticalProgress(launcher);
+                    final float distance =
+                            launcher.getAllAppsController().getShiftRange() * progress;
+                    response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) distance);
+                    break;
+                }
+
+                case TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT: {
+                    if (launcher == null) return null;
+
+                    final float progress = LauncherState.NORMAL.getVerticalProgress(launcher)
+                            - LauncherState.ALL_APPS.getVerticalProgress(launcher);
+                    final float distance =
+                            launcher.getAllAppsController().getShiftRange() * progress;
+                    response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) distance);
+                    break;
+                }
             }
             return response;
         }
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 6582df2..f081303 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -20,7 +20,10 @@
 import static com.android.launcher3.Utilities.getDevicePrefs;
 
 import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -114,6 +117,7 @@
 
     private final ArrayList<OnIDPChangeListener> mChangeListeners = new ArrayList<>();
     private ConfigMonitor mConfigMonitor;
+    private OverlayMonitor mOverlayMonitor;
 
     @VisibleForTesting
     public InvariantDeviceProfile() {}
@@ -131,6 +135,7 @@
         defaultLayoutId = p.defaultLayoutId;
         demoModeLayoutId = p.demoModeLayoutId;
         mExtraAttrs = p.mExtraAttrs;
+        mOverlayMonitor = p.mOverlayMonitor;
     }
 
     @TargetApi(23)
@@ -138,8 +143,12 @@
         initGrid(context, Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null));
         mConfigMonitor = new ConfigMonitor(context,
                 APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess);
+        mOverlayMonitor = new OverlayMonitor(context);
     }
 
+    /**
+     * This constructor should NOT have any monitors by design.
+     */
     public InvariantDeviceProfile(Context context, String gridName) {
         String newName = initGrid(context, gridName);
         if (newName == null || !newName.equals(gridName)) {
@@ -555,4 +564,20 @@
             return this;
         }
     }
+
+    private class OverlayMonitor extends BroadcastReceiver {
+
+        private final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
+
+        OverlayMonitor(Context context) {
+            IntentFilter filter = new IntentFilter(ACTION_OVERLAY_CHANGED);
+            filter.addDataScheme("package");
+            context.registerReceiver(this, filter);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            onConfigChanged(context);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index cee1c26..875288a 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -192,13 +192,7 @@
         return getWorkspaceScaleAndTranslation(launcher);
     }
 
-    /**
-     * Returns 2 floats designating how to transition overview:
-     *   scale for the current and adjacent pages
-     *   translationY factor where 0 is top aligned and 0.5 is centered vertically
-     */
-    public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) {
-        // TODO: Simplify to use a constant value instead of a factor.
+    public float[] getOverviewScaleAndTranslationY(Launcher launcher) {
         return new float[] {1.1f, 0f};
     }
 
diff --git a/src/com/android/launcher3/TestProtocol.java b/src/com/android/launcher3/TestProtocol.java
index 5f752cc..4eb3627 100644
--- a/src/com/android/launcher3/TestProtocol.java
+++ b/src/com/android/launcher3/TestProtocol.java
@@ -36,4 +36,8 @@
             "home-to-overview-swipe-height";
     public static final String REQUEST_BACKGROUND_TO_OVERVIEW_SWIPE_HEIGHT =
             "background-to-overview-swipe-height";
+    public static final String REQUEST_ALL_APPS_TO_OVERVIEW_SWIPE_HEIGHT =
+            "all-apps-to-overview-swipe-height";
+    public static final String REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT =
+            "home-to-all-apps-swipe-height";
 }
diff --git a/src/com/android/launcher3/anim/FlingSpringAnim.java b/src/com/android/launcher3/anim/FlingSpringAnim.java
new file mode 100644
index 0000000..3d21d82
--- /dev/null
+++ b/src/com/android/launcher3/anim/FlingSpringAnim.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 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.anim;
+
+import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener;
+import androidx.dynamicanimation.animation.FlingAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+/**
+ * Given a property to animate and a target value and starting velocity, first apply friction to
+ * the fling until we pass the target, then apply a spring force to pull towards the target.
+ */
+public class FlingSpringAnim {
+
+    private static final float FLING_FRICTION = 1.5f;
+    // Have the spring pull towards the target if we've slowed down too much before reaching it.
+    private static final float FLING_END_THRESHOLD_PX = 50f;
+    private static final float SPRING_STIFFNESS = 350f;
+    private static final float SPRING_DAMPING = SpringForce.DAMPING_RATIO_LOW_BOUNCY;
+
+    private final FlingAnimation mFlingAnim;
+
+    public <K> FlingSpringAnim(K object, FloatPropertyCompat<K> property, float startPosition,
+            float targetPosition, float startVelocity, OnAnimationEndListener onEndListener) {
+        mFlingAnim = new FlingAnimation(object, property)
+                .setFriction(FLING_FRICTION)
+                .setMinimumVisibleChange(FLING_END_THRESHOLD_PX)
+                .setStartVelocity(startVelocity)
+                .setMinValue(Math.min(startPosition, targetPosition))
+                .setMaxValue(Math.max(startPosition, targetPosition));
+        mFlingAnim.addEndListener(((animation, canceled, value, velocity) -> {
+            SpringAnimation springAnim = new SpringAnimation(object, property)
+                    .setStartVelocity(velocity)
+                    .setSpring(new SpringForce(targetPosition)
+                            .setStiffness(SPRING_STIFFNESS)
+                            .setDampingRatio(SPRING_DAMPING));
+            springAnim.addEndListener(onEndListener);
+            springAnim.start();
+        }));
+    }
+
+    public void start() {
+        mFlingAnim.start();
+    }
+}
diff --git a/src/com/android/launcher3/anim/SpringObjectAnimator.java b/src/com/android/launcher3/anim/SpringObjectAnimator.java
index cc70e32..f74590b 100644
--- a/src/com/android/launcher3/anim/SpringObjectAnimator.java
+++ b/src/com/android/launcher3/anim/SpringObjectAnimator.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.anim;
 
+import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
@@ -32,8 +34,6 @@
 import androidx.dynamicanimation.animation.SpringAnimation;
 import androidx.dynamicanimation.animation.SpringForce;
 
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
-
 /**
  * This animator allows for an object's property to be be controlled by an {@link ObjectAnimator} or
  * a {@link SpringAnimation}. It extends ValueAnimator so it can be used in an AnimatorSet.
@@ -51,9 +51,9 @@
     private SpringProperty<T> mProperty;
 
     private ArrayList<AnimatorListener> mListeners;
-    private boolean mSpringEnded = false;
-    private boolean mAnimatorEnded = false;
-    private boolean mEnded = false;
+    private boolean mSpringEnded = true;
+    private boolean mAnimatorEnded = true;
+    private boolean mEnded = true;
 
     private static final FloatPropertyCompat<ProgressInterface> sFloatProperty =
             new FloatPropertyCompat<ProgressInterface>("springObjectAnimator") {
diff --git a/src/com/android/launcher3/util/ConfigMonitor.java b/src/com/android/launcher3/util/ConfigMonitor.java
index 12280f8..12d35e9 100644
--- a/src/com/android/launcher3/util/ConfigMonitor.java
+++ b/src/com/android/launcher3/util/ConfigMonitor.java
@@ -41,8 +41,6 @@
 
     private static final String TAG = "ConfigMonitor";
 
-    private final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
-
     private final Point mTmpPoint1 = new Point();
     private final Point mTmpPoint2 = new Point();
 
@@ -78,11 +76,6 @@
         // Listen for configuration change
         mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
 
-        // Listen for {@link OverlayManager} change
-        IntentFilter filter = new IntentFilter(ACTION_OVERLAY_CHANGED);
-        filter.addDataScheme("package");
-        mContext.registerReceiver(this, filter);
-
         // Listen for display manager change
         mContext.getSystemService(DisplayManager.class)
                 .registerDisplayListener(this, new Handler(UiThreadHelper.getBackgroundLooper()));
@@ -91,12 +84,6 @@
     @Override
     public void onReceive(Context context, Intent intent) {
         Configuration config = context.getResources().getConfiguration();
-        // TODO: when overlay manager service encodes more information to the Uri such as category
-        // of the overlay, only listen to the ones that are of interest to launcher.
-        if (intent != null && ACTION_OVERLAY_CHANGED.equals(intent.getAction())) {
-            Log.d(TAG, "Overlay changed.");
-            notifyChange();
-        }
         if (mFontScale != config.fontScale || mDensity != config.densityDpi) {
             Log.d(TAG, "Configuration changed.");
             notifyChange();
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index e5c70da..2a5418d 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.views;
 
+import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
@@ -55,8 +57,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
-import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
-
 /**
  * A view that is created to look like another view with the purpose of creating fluid animations.
  */
@@ -143,9 +143,6 @@
             setBackgroundDrawableBounds(bgScale);
 
             mRevealAnimator.setCurrentFraction(shapeRevealProgress);
-            if (Float.compare(shapeRevealProgress, 1f) >= 0f) {
-                mRevealAnimator.end();
-            }
         }
         invalidate();
         invalidateOutline();
@@ -160,6 +157,9 @@
 
     @Override
     public void onAnimationEnd(Animator animator) {
+        if (mRevealAnimator != null) {
+            mRevealAnimator.end();
+        }
         if (mEndRunnable != null) {
             mEndRunnable.run();
         }
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 48cf9e8..e259cfe 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -18,8 +18,10 @@
 import static com.android.launcher3.Utilities.EXTRA_WALLPAPER_FLAVOR;
 import static com.android.launcher3.Utilities.EXTRA_WALLPAPER_OFFSET;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ResolveInfo;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.text.TextUtils;
@@ -150,7 +152,7 @@
         RectF target = new RectF(x - halfSize, y - halfSize, x + halfSize, y + halfSize);
 
         ArrayList<OptionItem> options = new ArrayList<>();
-        int res = FeatureFlags.STYLE_WALLPAPER.get() ?
+        int res = FeatureFlags.STYLE_WALLPAPER.get() && existsStyleWallpapers(launcher) ?
                 R.string.styles_wallpaper_button_text : R.string.wallpaper_button_text;
         options.add(new OptionItem(res, R.drawable.ic_wallpaper,
                 ControlType.WALLPAPER_BUTTON, OptionsPopupView::startWallpaperPicker));
@@ -164,6 +166,14 @@
         show(launcher, target, options);
     }
 
+    private static boolean existsStyleWallpapers(Launcher launcher) {
+        Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER);
+        intent.setComponent(new ComponentName(launcher.getString(R.string.wallpaper_picker_package),
+                "com.android.customization.picker.CustomizationPickerActivity"));
+        ResolveInfo ri = launcher.getPackageManager().resolveActivity(intent, 0);
+        return ri != null;
+    }
+
     public static boolean onWidgetsClicked(View view) {
         return openWidgets(Launcher.getLauncher(view.getContext()));
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 1353a23..122151e 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -44,6 +44,13 @@
         return LauncherInstrumentation.ContainerType.ALL_APPS;
     }
 
+    private boolean hasClickableIcon(UiObject2 allAppsContainer, BySelector appIconSelector) {
+        final UiObject2 icon = allAppsContainer.findObject(appIconSelector);
+        if (icon == null) return false;
+        final UiObject2 navBar = mLauncher.waitForSystemUiObject("navigation_bar_frame");
+        return icon.getVisibleBounds().bottom < navBar.getVisibleBounds().top;
+    }
+
     /**
      * Finds an icon. Fails if the icon doesn't exist. Scrolls the app list when needed to make
      * sure the icon is visible.
@@ -55,10 +62,10 @@
     public AppIcon getAppIcon(String appName) {
         final UiObject2 allAppsContainer = verifyActiveContainer();
         final BySelector appIconSelector = AppIcon.getAppIconSelector(appName, mLauncher);
-        if (!allAppsContainer.hasObject(appIconSelector)) {
+        if (!hasClickableIcon(allAppsContainer, appIconSelector)) {
             scrollBackToBeginning();
             int attempts = 0;
-            while (!allAppsContainer.hasObject(appIconSelector) &&
+            while (!hasClickableIcon(allAppsContainer, appIconSelector) &&
                     allAppsContainer.scroll(Direction.DOWN, 0.8f)) {
                 LauncherInstrumentation.assertTrue(
                         "Exceeded max scroll attempts: " + MAX_SCROLL_ATTEMPTS,
diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java b/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
index 2642815..dcc51b5 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
@@ -23,6 +23,8 @@
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.UiObject2;
 
+import com.android.launcher3.TestProtocol;
+
 /**
  * Operations on AllApps opened from Overview.
  */
@@ -45,7 +47,11 @@
         final UiObject2 qsb = mLauncher.waitForObjectInContainer(
                 allAppsContainer, "search_container_all_apps");
         final Point start = qsb.getVisibleCenter();
-        final int endY = (int) (mLauncher.getDevice().getDisplayHeight() * 0.6);
+        final int swipeHeight = mLauncher.getTestInfo(
+                TestProtocol.REQUEST_ALL_APPS_TO_OVERVIEW_SWIPE_HEIGHT).
+                getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+
+        final int endY = start.y + swipeHeight + mLauncher.getTouchSlop();
         LauncherInstrumentation.log("AllAppsFromOverview.switchBackToOverview before swipe");
         mLauncher.swipe(start.x, start.y, start.x, endY, OVERVIEW_STATE_ORDINAL);
 
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index d39a38e..fbeb3a2 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.tapl;
 
 import android.graphics.Point;
+import android.os.SystemClock;
 import android.view.MotionEvent;
 import android.widget.TextView;
 
@@ -41,10 +42,12 @@
      */
     public AppIconMenu openMenu() {
         final Point iconCenter = mObject.getVisibleCenter();
-        mLauncher.sendPointer(MotionEvent.ACTION_DOWN, iconCenter);
+        final long downTime = SystemClock.uptimeMillis();
+        mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, iconCenter);
         final UiObject2 deepShortcutsContainer = mLauncher.waitForLauncherObject(
                 "deep_shortcuts_container");
-        mLauncher.sendPointer(MotionEvent.ACTION_UP, iconCenter);
+        mLauncher.sendPointer(
+                downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, iconCenter);
         return new AppIconMenu(mLauncher, deepShortcutsContainer);
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 7031cd3..ef509ed 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -22,7 +22,9 @@
 
 import static org.junit.Assert.assertTrue;
 
-import android.view.ViewConfiguration;
+import android.graphics.Point;
+import android.os.SystemClock;
+import android.view.MotionEvent;
 
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.By;
@@ -35,6 +37,8 @@
  * indicate Launcher as long as Launcher is not in Overview state.
  */
 public class Background extends LauncherInstrumentation.VisibleContainer {
+    private static final int ZERO_BUTTON_SWIPE_UP_GESTURE_DURATION = 500;
+    private static final int ZERO_BUTTON_SWIPE_UP_HOLD_DURATION = 400;
 
     Background(LauncherInstrumentation launcher) {
         super(launcher);
@@ -61,17 +65,41 @@
     }
 
     protected void goToOverviewUnchecked(int expectedState) {
-        if (mLauncher.isSwipeUpEnabled()) {
-            final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
-            final int startY = getSwipeStartY();
-            final int swipeHeight = mLauncher.getTestInfo(
-                    getSwipeHeightRequestName()).
-                    getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
-            final int slop = ViewConfiguration.get(mLauncher.getContext()).getScaledTouchSlop();
+        switch (mLauncher.getNavigationModel()) {
+            case ZERO_BUTTON: {
+                final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
+                final int startY = getSwipeStartY();
+                final int swipeHeight = mLauncher.getTestInfo(getSwipeHeightRequestName()).
+                        getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+                final Point start = new Point(centerX, startY);
+                final Point end =
+                        new Point(centerX, startY - swipeHeight - mLauncher.getTouchSlop());
 
-            mLauncher.swipe(centerX, startY, centerX, startY - swipeHeight - slop, expectedState);
-        } else {
-            mLauncher.waitForSystemUiObject("recent_apps").click();
+                final long downTime = SystemClock.uptimeMillis();
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start);
+                mLauncher.movePointer(downTime, ZERO_BUTTON_SWIPE_UP_GESTURE_DURATION, start, end);
+                LauncherInstrumentation.sleep(ZERO_BUTTON_SWIPE_UP_HOLD_DURATION);
+                mLauncher.sendPointer(
+                        downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, end);
+                break;
+            }
+
+            case TWO_BUTTON: {
+                final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
+                final int startY = getSwipeStartY();
+                final int swipeHeight = mLauncher.getTestInfo(getSwipeHeightRequestName()).
+                        getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+
+                mLauncher.swipe(
+                        centerX, startY, centerX,
+                        startY - swipeHeight - mLauncher.getTouchSlop(),
+                        expectedState);
+                break;
+            }
+
+            case THREE_BUTTON:
+                mLauncher.waitForSystemUiObject("recent_apps").click();
+                break;
         }
     }
 
@@ -80,6 +108,6 @@
     }
 
     protected int getSwipeStartY() {
-        return mLauncher.waitForSystemUiObject("home").getVisibleBounds().centerY();
+        return mLauncher.waitForSystemUiObject("navigation_bar_frame").getVisibleBounds().centerY();
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 6e92dad..8cf1262 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -44,8 +44,10 @@
      * Flings forward (left) and waits the fling's end.
      */
     public void flingForward() {
-        final UiObject2 overview = verifyActiveContainer();
         LauncherInstrumentation.log("Overview.flingForward before fling");
+        final UiObject2 overview = verifyActiveContainer();
+        final int margin = (int) (50 * mLauncher.getDisplayDensity()) + 1;
+        overview.setGestureMargins(margin, 0, 0, 0);
         overview.fling(Direction.LEFT, (int) (FLING_SPEED * mLauncher.getDisplayDensity()));
         mLauncher.waitForIdle();
         verifyActiveContainer();
@@ -71,8 +73,10 @@
      * Flings backward (right) and waits the fling's end.
      */
     public void flingBackward() {
-        final UiObject2 overview = verifyActiveContainer();
         LauncherInstrumentation.log("Overview.flingBackward before fling");
+        final UiObject2 overview = verifyActiveContainer();
+        final int margin = (int) (50 * mLauncher.getDisplayDensity()) + 1;
+        overview.setGestureMargins(0, 0, margin, 0);
         overview.fling(Direction.RIGHT, (int) (FLING_SPEED * mLauncher.getDisplayDensity()));
         mLauncher.waitForIdle();
         verifyActiveContainer();
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index ce37348..f44022b 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.tapl;
 
+import static com.android.launcher3.TestProtocol.BACKGROUND_APP_STATE_ORDINAL;
 import static com.android.systemui.shared.system.SettingsCompat.SWIPE_UP_SETTING_NAME;
 
 import android.app.ActivityManager;
@@ -30,13 +31,17 @@
 import android.os.SystemClock;
 import android.provider.Settings;
 import android.util.Log;
+import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.Surface;
+import android.view.ViewConfiguration;
 import android.view.accessibility.AccessibilityEvent;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.Configurator;
 import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
@@ -58,6 +63,7 @@
 public final class LauncherInstrumentation {
 
     private static final String TAG = "Tapl";
+    private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 20;
 
     // Types for launcher containers that the user is interacting with. "Background" is a
     // pseudo-container corresponding to inactive launcher covered by another app.
@@ -65,6 +71,8 @@
         WORKSPACE, ALL_APPS, OVERVIEW, WIDGETS, BACKGROUND, BASE_OVERVIEW
     }
 
+    public enum NavigationModel {ZERO_BUTTON, TWO_BUTTON, THREE_BUTTON}
+
     // Base class for launcher containers.
     static abstract class VisibleContainer {
         protected final LauncherInstrumentation mLauncher;
@@ -149,7 +157,11 @@
         sActiveContainer = new WeakReference<>(container);
     }
 
-    public boolean isSwipeUpEnabled() {
+    public NavigationModel getNavigationModel() {
+        return isSwipeUpEnabled() ? NavigationModel.TWO_BUTTON : NavigationModel.THREE_BUTTON;
+    }
+
+    private boolean isSwipeUpEnabled() {
         final boolean swipeUpEnabledDefaultValue = SwipeUpSetting.isSwipeUpEnabledDefaultValue();
         return SwipeUpSetting.isSwipeUpSettingAvailable() ?
                 Settings.Secure.getInt(
@@ -285,24 +297,49 @@
         // We need waiting for any accessibility event generated after pressing Home because
         // otherwise waitForIdle may return immediately in case when there was a big enough pause in
         // accessibility events prior to pressing Home.
-        executeAndWaitForEvent(
-                () -> {
-                    log("LauncherInstrumentation.pressHome before clicking");
-                    waitForSystemUiObject("home").click();
-                },
-                event -> true,
-                "Pressing Home didn't produce any events");
-        mDevice.waitForIdle();
+        if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
+            if (hasLauncherObject(WORKSPACE_RES_ID)) {
+                log("0-button pressHome: already in workspace");
+            } else if (hasLauncherObject(OVERVIEW_RES_ID)) {
+                log("0-button pressHome: overview");
+                mDevice.pressHome();
+            } else if (hasLauncherObject(WIDGETS_RES_ID)) {
+                log("0-button pressHome: widgets");
+                mDevice.pressHome();
+            } else if (hasLauncherObject(APPS_RES_ID)) {
+                log("0-button pressHome: all apps");
+                mDevice.pressHome();
+            } else {
+                log("0-button pressHome: another app");
+                assertTrue("Launcher is visible, don't know how to go home",
+                        !mDevice.hasObject(By.pkg(getLauncherPackageName())));
+                final UiObject2 navBar = waitForSystemUiObject("navigation_bar_frame");
 
-        // Temporarily press home twice as the first click sometimes gets ignored  (b/124239413)
-        executeAndWaitForEvent(
-                () -> {
-                    log("LauncherInstrumentation.pressHome before clicking");
-                    waitForSystemUiObject("home").click();
-                },
-                event -> true,
-                "Pressing Home didn't produce any events");
-        mDevice.waitForIdle();
+                swipe(
+                        navBar.getVisibleBounds().centerX(), navBar.getVisibleBounds().centerY(),
+                        navBar.getVisibleBounds().centerX(), 0,
+                        BACKGROUND_APP_STATE_ORDINAL, ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME);
+            }
+        } else {
+            executeAndWaitForEvent(
+                    () -> {
+                        log("LauncherInstrumentation.pressHome before clicking");
+                        waitForSystemUiObject("home").click();
+                    },
+                    event -> true,
+                    "Pressing Home didn't produce any events");
+            mDevice.waitForIdle();
+
+            // Temporarily press home twice as the first click sometimes gets ignored  (b/124239413)
+            executeAndWaitForEvent(
+                    () -> {
+                        log("LauncherInstrumentation.pressHome before clicking");
+                        waitForSystemUiObject("home").click();
+                    },
+                    event -> true,
+                    "Pressing Home didn't produce any events");
+            mDevice.waitForIdle();
+        }
         return getWorkspace();
     }
 
@@ -423,6 +460,11 @@
         return object;
     }
 
+    @Nullable
+    private boolean hasLauncherObject(String resId) {
+        return mDevice.hasObject(getLauncherObjectSelector(resId));
+    }
+
     @NonNull
     UiObject2 waitForLauncherObject(String resName) {
         final BySelector selector = getLauncherObjectSelector(resName);
@@ -445,8 +487,12 @@
     }
 
     void swipe(int startX, int startY, int endX, int endY, int expectedState) {
+        swipe(startX, startY, endX, endY, expectedState, 60);
+    }
+
+    void swipe(int startX, int startY, int endX, int endY, int expectedState, int steps) {
         final Bundle parcel = (Bundle) executeAndWaitForEvent(
-                () -> mDevice.swipe(startX, startY, endX, endY, 60),
+                () -> mDevice.swipe(startX, startY, endX, endY, steps),
                 event -> TestProtocol.SWITCHED_TO_STATE_MESSAGE.equals(event.getClassName()),
                 "Swipe failed to receive an event for the swipe end: " + startX + ", " + startY
                         + ", " + endX + ", " + endY);
@@ -458,14 +504,58 @@
         mDevice.waitForIdle();
     }
 
-    void sendPointer(int action, Point point) {
-        final MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(),
-                SystemClock.uptimeMillis(), action, point.x, point.y, 0);
-        mInstrumentation.sendPointerSync(event);
+    float getDisplayDensity() {
+        return mInstrumentation.getTargetContext().getResources().getDisplayMetrics().density;
+    }
+
+    int getTouchSlop() {
+        return ViewConfiguration.get(getContext()).getScaledTouchSlop();
+    }
+
+    private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
+            float x, float y) {
+        MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
+        properties.id = 0;
+        properties.toolType = Configurator.getInstance().getToolType();
+
+        MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+        coords.pressure = 1;
+        coords.size = 1;
+        coords.x = x;
+        coords.y = y;
+
+        return MotionEvent.obtain(downTime, eventTime, action, 1,
+                new MotionEvent.PointerProperties[]{properties},
+                new MotionEvent.PointerCoords[]{coords},
+                0, 0, 1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+    }
+
+    void sendPointer(long downTime, long currentTime, int action, Point point) {
+        final MotionEvent event = getMotionEvent(downTime, currentTime, action, point.x, point.y);
+        mInstrumentation.getUiAutomation().injectInputEvent(event, true);
         event.recycle();
     }
 
-    float getDisplayDensity() {
-        return mInstrumentation.getTargetContext().getResources().getDisplayMetrics().density;
+    void movePointer(long downTime, long duration, Point from, Point to) {
+        final Point point = new Point();
+        for (; ; ) {
+            sleep(16);
+
+            final long currentTime = SystemClock.uptimeMillis();
+            final float progress = (currentTime - downTime) / (float) duration;
+            if (progress > 1) return;
+
+            point.x = from.x + (int) (progress * (to.x - from.x));
+            point.y = from.y + (int) (progress * (to.y - from.y));
+
+            sendPointer(downTime, currentTime, MotionEvent.ACTION_MOVE, point);
+        }
+    }
+
+    static void sleep(int duration) {
+        try {
+            Thread.sleep(duration);
+        } catch (InterruptedException e) {
+        }
     }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 819e10d..7d97acd 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -51,15 +51,16 @@
     @NonNull
     public AllApps switchToAllApps() {
         verifyActiveContainer();
-        // Swipe from the hotseat to near the top, e.g. 10% of the screen.
         final UiObject2 hotseat = mHotseat;
         final Point start = hotseat.getVisibleCenter();
-        final int endY = (int) (mLauncher.getDevice().getDisplayHeight() * 0.1f);
+        final int swipeHeight = mLauncher.getTestInfo(
+                TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT).
+                getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
         mLauncher.swipe(
                 start.x,
                 start.y,
                 start.x,
-                endY,
+                start.y - swipeHeight - mLauncher.getTouchSlop(),
                 ALL_APPS_STATE_ORDINAL
         );
 
@@ -135,6 +136,8 @@
      */
     public void flingForward() {
         final UiObject2 workspace = verifyActiveContainer();
+        final int margin = (int) (50 * mLauncher.getDisplayDensity()) + 1;
+        workspace.setGestureMargins(0, 0, margin, 0);
         workspace.fling(Direction.RIGHT, (int) (FLING_SPEED * mLauncher.getDisplayDensity()));
         mLauncher.waitForIdle();
         verifyActiveContainer();
@@ -146,6 +149,8 @@
      */
     public void flingBackward() {
         final UiObject2 workspace = verifyActiveContainer();
+        final int margin = (int) (50 * mLauncher.getDisplayDensity()) + 1;
+        workspace.setGestureMargins(margin, 0, 0, 0);
         workspace.fling(Direction.LEFT, (int) (FLING_SPEED * mLauncher.getDisplayDensity()));
         mLauncher.waitForIdle();
         verifyActiveContainer();