Merge "Fixing flakiness of ShortcutsToHomeTest" into ub-launcher3-master
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/RecentsUiFactory.java b/go/quickstep/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
index d0c255c..7381574 100644
--- a/go/quickstep/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
+++ b/go/quickstep/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
@@ -24,27 +24,39 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherStateManager.StateHandler;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.TouchController;
+import com.android.quickstep.OverviewInteractionState;
+
+import java.util.ArrayList;
 
 /**
  * Provides recents-related {@link UiFactory} logic and classes.
  */
-public final class RecentsUiFactory {
+public abstract class RecentsUiFactory {
 
     // Scale recents takes before animating in
     private static final float RECENTS_PREPARE_SCALE = 1.33f;
 
-    private RecentsUiFactory() {}
+    public static TouchController[] createTouchControllers(Launcher launcher) {
+        ArrayList<TouchController> list = new ArrayList<>();
+        list.add(launcher.getDragController());
 
-    /**
-     * Creates and returns a touch controller for swiping recents tasks.
-     *
-     * @param launcher the launcher activity
-     * @return the touch controller for recents tasks
-     */
-    public static TouchController createTaskSwipeController(Launcher launcher) {
-        // We leave all input handling to the view itself.
-        return null;
+        if (launcher.getDeviceProfile().isVerticalBarLayout()) {
+            list.add(new OverviewToAllAppsTouchController(launcher));
+            list.add(new LandscapeEdgeSwipeController(launcher));
+        } else {
+            boolean allowDragToOverview = OverviewInteractionState.INSTANCE.get(launcher)
+                    .isSwipeUpGestureEnabled();
+            list.add(new PortraitStatesTouchController(launcher, allowDragToOverview));
+        }
+        if (FeatureFlags.PULL_DOWN_STATUS_BAR && Utilities.IS_DEBUG_DEVICE
+                && !launcher.getDeviceProfile().isMultiWindowMode
+                && !launcher.getDeviceProfile().isVerticalBarLayout()) {
+            list.add(new StatusBarTouchController(launcher));
+        }
+        return list.toArray(new TouchController[list.size()]);
     }
 
     /**
@@ -62,7 +74,7 @@
      *
      * @param launcher the launcher activity
      */
-    public static void prepareToShowRecents(Launcher launcher) {
+    public static void prepareToShowOverview(Launcher launcher) {
         View overview = launcher.getOverviewPanel();
         if (overview.getVisibility() != VISIBLE) {
             SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
@@ -74,7 +86,7 @@
      *
      * @param launcher the launcher activity
      */
-    public static void resetRecents(Launcher launcher) {}
+    public static void resetOverview(Launcher launcher) {}
 
     /**
      * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/FlingAndHoldTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/FlingAndHoldTouchController.java
new file mode 100644
index 0000000..fb83cd3
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/FlingAndHoldTouchController.java
@@ -0,0 +1,95 @@
+/*
+ * 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.uioverrides;
+
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherStateManager.NON_ATOMIC_COMPONENT;
+
+import android.animation.ValueAnimator;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.quickstep.util.MotionPauseDetector;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Touch controller which handles swipe and hold to go to Overview
+ */
+public class FlingAndHoldTouchController extends PortraitStatesTouchController {
+
+    private final MotionPauseDetector mMotionPauseDetector;
+
+    public FlingAndHoldTouchController(Launcher l) {
+        super(l, false /* allowDragToOverview */);
+        mMotionPauseDetector = new MotionPauseDetector(l);
+    }
+
+    @Override
+    public void onDragStart(boolean start) {
+        mMotionPauseDetector.clear();
+
+        super.onDragStart(start);
+
+        if (mStartState == NORMAL) {
+            mMotionPauseDetector.setOnMotionPauseListener(isPaused -> {
+                RecentsView recentsView = mLauncher.getOverviewPanel();
+                recentsView.setOverviewStateEnabled(isPaused);
+                maybeUpdateAtomicAnim(NORMAL, OVERVIEW, isPaused ? 1 : 0);
+            });
+        }
+    }
+
+    @Override
+    public boolean onDrag(float displacement) {
+        mMotionPauseDetector.addPosition(displacement);
+        return super.onDrag(displacement);
+    }
+
+    @Override
+    public void onDragEnd(float velocity, boolean fling) {
+        if (mMotionPauseDetector.isPaused() && mStartState == NORMAL) {
+            float range = getShiftRange();
+            long maxAccuracy = (long) (2 * range);
+
+            // Let the state manager know that the animation didn't go to the target state,
+            // but don't cancel ourselves (we already clean up when the animation completes).
+            Runnable onCancel = mCurrentAnimation.getOnCancelRunnable();
+            mCurrentAnimation.setOnCancelRunnable(null);
+            mCurrentAnimation.dispatchOnCancel();
+            mCurrentAnimation = mLauncher.getStateManager()
+                    .createAnimationToNewWorkspace(OVERVIEW, new AnimatorSetBuilder(), maxAccuracy,
+                            onCancel, NON_ATOMIC_COMPONENT);
+
+            final int logAction = fling ? Touch.FLING : Touch.SWIPE;
+            mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(OVERVIEW, logAction));
+
+
+            ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
+            maybeUpdateAtomicAnim(NORMAL, OVERVIEW, 1f);
+            mCurrentAnimation.dispatchOnStartWithVelocity(1, velocity);
+
+            // TODO: Find a better duration
+            anim.setDuration(100);
+            anim.start();
+            settleAtomicAnimation(1f, anim.getDuration());
+        } else {
+            super.onDragEnd(velocity, fling);
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
index f18f43c..51e9495 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
@@ -21,32 +21,65 @@
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.config.FeatureFlags.SWIPE_HOME;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager.StateHandler;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.TouchController;
+import com.android.launcher3.util.UiThreadHelper;
+import com.android.launcher3.util.UiThreadHelper.AsyncCommand;
+import com.android.quickstep.OverviewInteractionState;
 import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+
+import java.util.ArrayList;
 
 /**
  * Provides recents-related {@link UiFactory} logic and classes.
  */
-public final class RecentsUiFactory {
+public abstract class RecentsUiFactory {
+
+    private static final AsyncCommand SET_SHELF_HEIGHT_CMD = (visible, height) ->
+            WindowManagerWrapper.getInstance().setShelfHeight(visible != 0, height);
 
     // Scale recents takes before animating in
     private static final float RECENTS_PREPARE_SCALE = 1.33f;
 
-    private RecentsUiFactory() {}
+    public static TouchController[] createTouchControllers(Launcher launcher) {
+        boolean swipeUpEnabled = OverviewInteractionState.INSTANCE.get(launcher)
+                .isSwipeUpGestureEnabled();
+        boolean swipeUpToHome = swipeUpEnabled && SWIPE_HOME.get();
 
-    /**
-     * Creates and returns a touch controller for swiping recents tasks.
-     *
-     * @param launcher the launcher activity
-     * @return the touch controller for recents tasks
-     */
-    public static TouchController createTaskSwipeController(Launcher launcher) {
-        return new LauncherTaskViewController(launcher);
+
+        ArrayList<TouchController> list = new ArrayList<>();
+        list.add(launcher.getDragController());
+
+        if (swipeUpToHome) {
+            list.add(new FlingAndHoldTouchController(launcher));
+            list.add(new OverviewToAllAppsTouchController(launcher));
+        } else {
+            if (launcher.getDeviceProfile().isVerticalBarLayout()) {
+                list.add(new OverviewToAllAppsTouchController(launcher));
+                list.add(new LandscapeEdgeSwipeController(launcher));
+            } else {
+                list.add(new PortraitStatesTouchController(launcher,
+                        swipeUpEnabled /* allowDragToOverview */));
+            }
+        }
+
+        if (FeatureFlags.PULL_DOWN_STATUS_BAR && Utilities.IS_DEBUG_DEVICE
+                && !launcher.getDeviceProfile().isMultiWindowMode
+                && !launcher.getDeviceProfile().isVerticalBarLayout()) {
+            list.add(new StatusBarTouchController(launcher));
+        }
+
+        list.add(new LauncherTaskViewController(launcher));
+        return list.toArray(new TouchController[list.size()]);
     }
 
     /**
@@ -64,7 +97,7 @@
      *
      * @param launcher the launcher activity
      */
-    public static void prepareToShowRecents(Launcher launcher) {
+    public static void prepareToShowOverview(Launcher launcher) {
         RecentsView overview = launcher.getOverviewPanel();
         if (overview.getVisibility() != VISIBLE || overview.getContentAlpha() == 0) {
             SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
@@ -76,9 +109,8 @@
      *
      * @param launcher the launcher activity
      */
-    public static void resetRecents(Launcher launcher) {
-        RecentsView recents = launcher.getOverviewPanel();
-        recents.reset();
+    public static void resetOverview(Launcher launcher) {
+        launcher.<RecentsView>getOverviewPanel().reset();
     }
 
     /**
@@ -88,6 +120,14 @@
      */
     public static void onLauncherStateOrResumeChanged(Launcher launcher) {
         LauncherState state = launcher.getStateManager().getState();
+        if (!OverviewInteractionState.INSTANCE.get(launcher).swipeGestureInitializing()) {
+            DeviceProfile profile = launcher.getDeviceProfile();
+            boolean visible = (state == NORMAL || state == OVERVIEW) && launcher.isUserActive()
+                    && !profile.isVerticalBarLayout();
+            UiThreadHelper.runAsyncCommand(launcher, SET_SHELF_HEIGHT_CMD,
+                    visible ? 1 : 0, profile.hotseatBarSizePx);
+        }
+
         if (state == NORMAL) {
             launcher.<RecentsView>getOverviewPanel().setSwipeDownShouldLaunchApp(false);
         }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewToAllAppsTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewToAllAppsTouchController.java
index 0f9b57f..a069ed5 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewToAllAppsTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewToAllAppsTouchController.java
@@ -36,7 +36,7 @@
 public class OverviewToAllAppsTouchController extends PortraitStatesTouchController {
 
     public OverviewToAllAppsTouchController(Launcher l) {
-        super(l);
+        super(l, true /* allowDragToOverview */);
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
index ea0e552..d20ffbb 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
@@ -25,6 +25,7 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
 
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
@@ -67,14 +68,17 @@
 
     private final PortraitOverviewStateTouchHelper mOverviewPortraitStateTouchHelper;
 
-    private InterpolatorWrapper mAllAppsInterpolatorWrapper = new InterpolatorWrapper();
+    private final InterpolatorWrapper mAllAppsInterpolatorWrapper = new InterpolatorWrapper();
+
+    private final boolean mAllowDragToOverview;
 
     // If true, we will finish the current animation instantly on second touch.
     private boolean mFinishFastOnSecondTouch;
 
-    public PortraitStatesTouchController(Launcher l) {
+    public PortraitStatesTouchController(Launcher l, boolean allowDragToOverview) {
         super(l, SwipeDetector.VERTICAL);
         mOverviewPortraitStateTouchHelper = new PortraitOverviewStateTouchHelper(l);
+        mAllowDragToOverview = allowDragToOverview;
     }
 
     @Override
@@ -82,7 +86,7 @@
         if (mCurrentAnimation != null) {
             if (mFinishFastOnSecondTouch) {
                 // TODO: Animate to finish instead.
-                mCurrentAnimation.getAnimationPlayer().end();
+                mCurrentAnimation.skipToEnd();
             }
 
             AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
@@ -128,7 +132,8 @@
         } else if (fromState == OVERVIEW) {
             return isDragTowardPositive ? ALL_APPS : NORMAL;
         } else if (fromState == NORMAL && isDragTowardPositive) {
-            return TouchInteractionService.isConnected() ? OVERVIEW : ALL_APPS;
+            return mAllowDragToOverview && TouchInteractionService.isConnected()
+                    ? OVERVIEW : ALL_APPS;
         }
         return fromState;
     }
@@ -241,7 +246,10 @@
     private void handleFirstSwipeToOverview(final ValueAnimator animator,
             final long expectedDuration, final LauncherState targetState, final float velocity,
             final boolean isFling) {
-        if (mFromState == NORMAL && mToState == OVERVIEW && targetState == OVERVIEW) {
+        if (QUICKSTEP_SPRINGS.get() && mFromState == OVERVIEW && mToState == ALL_APPS
+                && targetState == OVERVIEW) {
+            mFinishFastOnSecondTouch = true;
+        } else  if (mFromState == NORMAL && mToState == OVERVIEW && targetState == OVERVIEW) {
             mFinishFastOnSecondTouch = true;
             if (isFling && expectedDuration != 0) {
                 // Update all apps interpolator to add a bit of overshoot starting from currFraction
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index d0a9e3c..70aae13 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -35,68 +35,32 @@
 import android.util.Base64;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.QuickstepAppTransitionManagerImpl;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.util.TouchController;
-import com.android.launcher3.util.UiThreadHelper;
-import com.android.launcher3.util.UiThreadHelper.AsyncCommand;
 import com.android.quickstep.OverviewInteractionState;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.util.RemoteFadeOutAnimationListener;
 import com.android.systemui.shared.system.ActivityCompat;
-import com.android.systemui.shared.system.WindowManagerWrapper;
 
 import java.io.ByteArrayOutputStream;
 import java.io.PrintWriter;
-import java.util.ArrayList;
 import java.util.zip.Deflater;
 
-public class UiFactory {
-
-    private static final AsyncCommand SET_SHELF_HEIGHT_CMD = (visible, height) ->
-            WindowManagerWrapper.getInstance().setShelfHeight(visible != 0, height);
-
-    public static TouchController[] createTouchControllers(Launcher launcher) {
-        boolean swipeUpEnabled = OverviewInteractionState.INSTANCE.get(launcher)
-                .isSwipeUpGestureEnabled();
-        ArrayList<TouchController> list = new ArrayList<>();
-        list.add(launcher.getDragController());
-
-        if (!swipeUpEnabled || launcher.getDeviceProfile().isVerticalBarLayout()) {
-            list.add(new OverviewToAllAppsTouchController(launcher));
-        }
-
-        if (launcher.getDeviceProfile().isVerticalBarLayout()) {
-            list.add(new LandscapeEdgeSwipeController(launcher));
-        } else {
-            list.add(new PortraitStatesTouchController(launcher));
-        }
-        if (FeatureFlags.PULL_DOWN_STATUS_BAR && Utilities.IS_DEBUG_DEVICE
-                && !launcher.getDeviceProfile().isMultiWindowMode
-                && !launcher.getDeviceProfile().isVerticalBarLayout()) {
-            list.add(new StatusBarTouchController(launcher));
-        }
-        TouchController taskSwipeController =
-                RecentsUiFactory.createTaskSwipeController(launcher);
-        if (taskSwipeController != null) {
-            list.add(taskSwipeController);
-        }
-        return list.toArray(new TouchController[list.size()]);
-    }
+public class UiFactory extends RecentsUiFactory {
 
     public static void setOnTouchControllersChangedListener(Context context, Runnable listener) {
         OverviewInteractionState.INSTANCE.get(context).setOnSwipeUpSettingChangedListener(listener);
     }
 
     public static StateHandler[] getStateHandler(Launcher launcher) {
-        return new StateHandler[] {launcher.getAllAppsController(), launcher.getWorkspace(),
-                RecentsUiFactory.createRecentsViewStateController(launcher),
+        return new StateHandler[] {
+                launcher.getAllAppsController(),
+                launcher.getWorkspace(),
+                createRecentsViewStateController(launcher),
                 new BackButtonAlphaHandler(launcher)};
     }
 
@@ -116,10 +80,6 @@
                 .setBackButtonAlpha(shouldBackButtonBeHidden ? 0 : 1, true /* animate */);
     }
 
-    public static void resetOverview(Launcher launcher) {
-        RecentsUiFactory.resetRecents(launcher);
-    }
-
     public static void onCreate(Launcher launcher) {
         if (!launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) {
             launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() {
@@ -171,19 +131,6 @@
                 .getHighResLoadingState().setVisible(true);
     }
 
-    public static void onLauncherStateOrResumeChanged(Launcher launcher) {
-        LauncherState state = launcher.getStateManager().getState();
-        if (!OverviewInteractionState.INSTANCE.get(launcher).swipeGestureInitializing()) {
-            DeviceProfile profile = launcher.getDeviceProfile();
-            boolean visible = (state == NORMAL || state == OVERVIEW) && launcher.isUserActive()
-                    && !profile.isVerticalBarLayout();
-            UiThreadHelper.runAsyncCommand(launcher, SET_SHELF_HEIGHT_CMD,
-                    visible ? 1 : 0, profile.hotseatBarSizePx);
-        }
-
-        RecentsUiFactory.onLauncherStateOrResumeChanged(launcher);
-    }
-
     public static void onTrimMemory(Context context, int level) {
         RecentsModel model = RecentsModel.INSTANCE.get(context);
         if (model != null) {
@@ -233,8 +180,4 @@
                 out.toByteArray(), Base64.NO_WRAP | Base64.NO_PADDING));
         return true;
     }
-
-    public static void prepareToShowOverview(Launcher launcher) {
-        RecentsUiFactory.prepareToShowRecents(launcher);
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
index b152bb9..5755205 100644
--- a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
+++ b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
@@ -67,6 +67,8 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
+import androidx.annotation.Nullable;
+
 /**
  * Touch consumer for handling events originating from an activity other than Launcher
  */
@@ -196,7 +198,7 @@
 
                 if (mPassedInitialSlop && mInteractionHandler != null) {
                     // Move
-                    dispatchMotion(ev, displacement - mStartDisplacement);
+                    dispatchMotion(ev, displacement - mStartDisplacement, null);
 
                     if (FeatureFlags.SWIPE_HOME.get()) {
                         mMotionPauseDetector.addPosition(displacement);
@@ -217,11 +219,11 @@
         }
     }
 
-    private void dispatchMotion(MotionEvent ev, float displacement) {
+    private void dispatchMotion(MotionEvent ev, float displacement, @Nullable Float velocityX) {
         mInteractionHandler.updateDisplacement(displacement);
         boolean isLandscape = isNavBarOnLeft() || isNavBarOnRight();
         if (!isLandscape) {
-            mInteractionHandler.dispatchMotionEventToRecentsView(ev);
+            mInteractionHandler.dispatchMotionEventToRecentsView(ev, velocityX);
         }
     }
 
@@ -316,15 +318,16 @@
      */
     private void finishTouchTracking(MotionEvent ev) {
         if (mPassedInitialSlop && mInteractionHandler != null) {
-            dispatchMotion(ev, getDisplacement(ev) - mStartDisplacement);
 
             mVelocityTracker.computeCurrentVelocity(1000,
                     ViewConfiguration.get(this).getScaledMaximumFlingVelocity());
-
             float velocityX = mVelocityTracker.getXVelocity(mActivePointerId);
             float velocity = isNavBarOnRight() ? velocityX
                     : isNavBarOnLeft() ? -velocityX
                             : mVelocityTracker.getYVelocity(mActivePointerId);
+
+            dispatchMotion(ev, getDisplacement(ev) - mStartDisplacement, velocityX);
+
             mInteractionHandler.onGestureEnded(velocity, velocityX);
         } else {
             // Since we start touch tracking on DOWN, we may reach this state without actually
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index d118e99..86a8081 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -70,6 +70,7 @@
 
 import androidx.annotation.AnyThread;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
 
@@ -624,23 +625,37 @@
     }
 
     @WorkerThread
-    public void dispatchMotionEventToRecentsView(MotionEvent event) {
+    @SuppressWarnings("WrongThread")
+    public void dispatchMotionEventToRecentsView(MotionEvent event, @Nullable Float velocityX) {
         if (mRecentsView == null) {
             return;
         }
-        // Pass the motion events to RecentsView to allow scrolling during swipe up.
-        if (mDispatchedDownEvent) {
-            mRecentsView.dispatchTouchEvent(event);
+        if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
+            dispatchMotionEventToRecentsViewUi(event, velocityX);
         } else {
+            MotionEvent ev = MotionEvent.obtain(event);
+            postAsyncCallback(mMainThreadHandler, () -> {
+                dispatchMotionEventToRecentsViewUi(ev, velocityX);
+                ev.recycle();
+            });
+        }
+    }
+
+    @UiThread
+    private void dispatchMotionEventToRecentsViewUi(MotionEvent event, @Nullable Float velocityX) {
+        // Pass the motion events to RecentsView to allow scrolling during swipe up.
+        if (!mDispatchedDownEvent) {
             // The first event we dispatch should be ACTION_DOWN.
             mDispatchedDownEvent = true;
             MotionEvent downEvent = MotionEvent.obtain(event);
             downEvent.setAction(MotionEvent.ACTION_DOWN);
             int flags = downEvent.getEdgeFlags();
             downEvent.setEdgeFlags(flags | TouchInteractionService.EDGE_NAV_BAR);
-            mRecentsView.dispatchTouchEvent(downEvent);
+            mRecentsView.simulateTouchEvent(downEvent, velocityX);
             downEvent.recycle();
         }
+
+        mRecentsView.simulateTouchEvent(event, velocityX);
     }
 
     @WorkerThread
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index 258e922..7969eec 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -135,6 +135,10 @@
         mIsPaused = mHasEverBeenPaused = false;
     }
 
+    public boolean isPaused() {
+        return mIsPaused;
+    }
+
     public interface OnMotionPauseListener {
         void onMotionPauseChanged(boolean isPaused);
     }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 840d2bd..923da7b 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -159,6 +159,8 @@
 
     private final ViewPool<TaskView> mTaskViewPool;
 
+    @Nullable Float mSimulatedVelocityX = null;
+
     /**
      * TODO: Call reloadIdNeeded in onTaskStackChanged.
      */
@@ -1610,4 +1612,18 @@
             }
         }
     }
+
+    public void simulateTouchEvent(MotionEvent event, @Nullable Float velocityX) {
+        mSimulatedVelocityX = velocityX;
+        dispatchTouchEvent(event);
+        mSimulatedVelocityX = null;
+    }
+
+    @Override
+    protected int computeXVelocity() {
+        if (mSimulatedVelocityX != null) {
+            return mSimulatedVelocityX.intValue();
+        }
+        return super.computeXVelocity();
+    }
 }
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index a6b3a19..8f9e7c8 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -1134,9 +1134,7 @@
                 final int activePointerId = mActivePointerId;
                 final int pointerIndex = ev.findPointerIndex(activePointerId);
                 final float x = ev.getX(pointerIndex);
-                final VelocityTracker velocityTracker = mVelocityTracker;
-                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
-                int velocityX = (int) velocityTracker.getXVelocity(activePointerId);
+                int velocityX = computeXVelocity();
                 final int deltaX = (int) (x - mDownMotionX);
                 final int pageWidth = getPageAt(mCurrentPage).getMeasuredWidth();
                 boolean isSignificantMove = Math.abs(deltaX) > pageWidth *
@@ -1240,6 +1238,12 @@
         return true;
     }
 
+    protected int computeXVelocity() {
+        final VelocityTracker velocityTracker = mVelocityTracker;
+        velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+        return (int) velocityTracker.getXVelocity(mActivePointerId);
+    }
+
     protected boolean shouldFlingForVelocity(int velocityX) {
         return Math.abs(velocityX) > mFlingThresholdVelocity;
     }
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 62f59e4..cf070c5 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -80,6 +80,9 @@
 
     private OnAnimationEndDispatcher mEndListener;
     private DynamicAnimation.OnAnimationEndListener mSpringEndListener;
+    // We need this variable to ensure the end listener is called immediately, otherwise we run into
+    // issues where the callback interferes with the states of the swipe detector.
+    private boolean mSkipToEnd = false;
 
     protected AnimatorPlaybackController(AnimatorSet anim, long duration,
             Runnable onCancelRunnable) {
@@ -232,7 +235,11 @@
     }
 
     private void dispatchOnStartRecursively(Animator animator) {
-        for (AnimatorListener l : nonNullList(animator.getListeners())) {
+        List<AnimatorListener> listeners = animator instanceof SpringObjectAnimator
+                ? nonNullList(((SpringObjectAnimator) animator).getSuperListeners())
+                : nonNullList(animator.getListeners());
+
+        for (AnimatorListener l : listeners) {
             l.onAnimationStart(animator);
         }
 
@@ -280,6 +287,17 @@
         return mOnCancelRunnable;
     }
 
+    public void skipToEnd() {
+        mSkipToEnd = true;
+        for (SpringAnimation spring : mSprings) {
+            if (spring.canSkipToEnd()) {
+                spring.skipToEnd();
+            }
+        }
+        mAnimationPlayer.end();
+        mSkipToEnd = false;
+    }
+
     public static class AnimatorPlaybackControllerVL extends AnimatorPlaybackController {
 
         private final ValueAnimator[] mChildAnimations;
@@ -343,19 +361,34 @@
      */
     private class OnAnimationEndDispatcher extends AnimationSuccessListener {
 
+        boolean mAnimatorDone = false;
+        boolean mSpringsDone = false;
+        boolean mDispatched = false;
+
         @Override
         public void onAnimationStart(Animator animation) {
             mCancelled = false;
+            mDispatched = false;
         }
 
         @Override
         public void onAnimationSuccess(Animator animator) {
+            if (mSprings.isEmpty()) {
+                mSpringsDone = mAnimatorDone = true;
+            }
+            if (isAnySpringRunning()) {
+                mAnimatorDone = true;
+            } else {
+                mSpringsDone = true;
+            }
+
             // We wait for the spring (if any) to finish running before completing the end callback.
-            if (mSprings.isEmpty() || !isAnySpringRunning()) {
+            if (!mDispatched && (mSkipToEnd || (mAnimatorDone && mSpringsDone))) {
                 dispatchOnEndRecursively(mAnim);
                 if (mEndAction != null) {
                     mEndAction.run();
                 }
+                mDispatched = true;
             }
         }
 
diff --git a/src/com/android/launcher3/anim/SpringObjectAnimator.java b/src/com/android/launcher3/anim/SpringObjectAnimator.java
index 4ece909..e4aec10 100644
--- a/src/com/android/launcher3/anim/SpringObjectAnimator.java
+++ b/src/com/android/launcher3/anim/SpringObjectAnimator.java
@@ -73,7 +73,9 @@
         mListeners = new ArrayList<>();
         setFloatValues(values);
 
-        mObjectAnimator.addListener(new AnimatorListenerAdapter() {
+        // We use this listener and track mListeners so that we can sync the animator and spring
+        // listeners.
+        super.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
                 mAnimatorEnded = false;
@@ -94,7 +96,7 @@
                 for (AnimatorListener l : mListeners) {
                     l.onAnimationCancel(animation);
                 }
-                mSpring.animateToFinalPosition(mObject.getProgress());
+                mSpring.cancel();
             }
         });
 
@@ -145,6 +147,10 @@
         mListeners.add(listener);
     }
 
+    public ArrayList<AnimatorListener> getSuperListeners() {
+        return super.getListeners();
+    }
+
     @Override
     public ArrayList<AnimatorListener> getListeners() {
         return mListeners;
@@ -167,8 +173,8 @@
 
     @Override
     public void cancel() {
-        mSpring.animateToFinalPosition(mObject.getProgress());
         mObjectAnimator.cancel();
+        mSpring.cancel();
     }
 
     @Override
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index 6ad69d7..fa4ebaf 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -17,13 +17,16 @@
 package com.android.launcher3.config;
 
 import static androidx.core.util.Preconditions.checkNotNull;
+
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.provider.Settings;
+
 import androidx.annotation.GuardedBy;
 import androidx.annotation.Keep;
 import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.Utilities;
 
 import java.util.ArrayList;
@@ -95,8 +98,9 @@
     public static final TogglableFlag APPLY_CONFIG_AT_RUNTIME = new TogglableFlag(
             "APPLY_CONFIG_AT_RUNTIME", true, "Apply display changes dynamically");
 
-    public static final TogglableFlag ENABLE_TASK_STABILIZER = new TogglableFlag(
-            "ENABLE_TASK_STABILIZER", false, "Stable task list across fast task switches");
+    public static final ToggleableGlobalSettingsFlag ENABLE_TASK_STABILIZER
+            = new ToggleableGlobalSettingsFlag("ENABLE_TASK_STABILIZER", false,
+            "Stable task list across fast task switches");
 
     public static final TogglableFlag QUICKSTEP_SPRINGS = new TogglableFlag("QUICKSTEP_SPRINGS",
             false, "Enable springs for quickstep animations");
@@ -249,11 +253,17 @@
 
         @Override
         void updateStorage(Context context, boolean value) {
+            if (contentResolver == null) {
+                return;
+            }
             Settings.Global.putInt(contentResolver, getKey(), value ? 1 : 0);
         }
 
         @Override
         boolean getFromStorage(Context context, boolean defaultValue) {
+            if (contentResolver == null) {
+                return defaultValue;
+            }
             return Settings.Global.getInt(contentResolver, getKey(), defaultValue ? 1 : 0) == 1;
         }
 
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index bb14328..0e2ed6c 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -46,7 +46,6 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
@@ -298,7 +297,7 @@
      * When going between normal and overview states, see if we passed the overview threshold and
      * play the appropriate atomic animation if so.
      */
-    private void maybeUpdateAtomicAnim(LauncherState fromState, LauncherState toState,
+    protected void maybeUpdateAtomicAnim(LauncherState fromState, LauncherState toState,
             float progress) {
         if (!goingBetweenNormalAndOverview(fromState, toState)) {
             return;
@@ -435,7 +434,11 @@
             mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity);
         }
         anim.start();
-        mAtomicAnimAutoPlayInfo = new AutoPlayAtomicAnimationInfo(endProgress, anim.getDuration());
+        settleAtomicAnimation(endProgress, anim.getDuration());
+    }
+
+    protected void settleAtomicAnimation(float endProgress, long duration) {
+        mAtomicAnimAutoPlayInfo = new AutoPlayAtomicAnimationInfo(endProgress, duration);
         maybeAutoPlayAtomicComponentsAnim();
     }