Merge "Setting correct font for clear-all button" into ub-launcher3-edmonton
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 85ccf29..94aaf15 100644
--- a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -335,10 +335,14 @@
             launcherAnimator.play(dragLayerAlpha);
             launcherAnimator.play(dragLayerTransY);
             mDragLayer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+
+            // Pause page indicator animations as they lead to layer trashing.
+            mLauncher.getWorkspace().getPageIndicator().pauseAnimations();
             endListener = () -> {
                 mDragLayer.setLayerType(View.LAYER_TYPE_NONE, null);
                 mDragLayer.setAlpha(1);
                 mDragLayer.setTranslationY(0);
+                mLauncher.getWorkspace().getPageIndicator().skipAnimationsToEnd();
             };
         }
         return new Pair<>(launcherAnimator, endListener);
@@ -694,6 +698,13 @@
             workspaceAnimator.setStartDelay(LAUNCHER_RESUME_START_DELAY);
             workspaceAnimator.setDuration(333);
             workspaceAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+            currentPage.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+            workspaceAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    currentPage.setLayerType(View.LAYER_TYPE_NONE, null);
+                }
+            });
 
             // Animate the shelf in two parts: slide in, and overeshoot.
             AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
@@ -715,7 +726,6 @@
             allAppsOvershoot.setDuration(153);
             allAppsOvershoot.setInterpolator(Interpolators.OVERSHOOT_0);
 
-
             anim.play(workspaceAnimator);
             anim.playSequentially(allAppsSlideIn, allAppsOvershoot);
             anim.addListener(mReapplyStateListener);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
index 2e95c04..2a603d7 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
@@ -187,10 +187,7 @@
             builder = new AnimatorSetBuilder();
         }
 
-        if (mPendingAnimation != null) {
-            mPendingAnimation.finish(false, Touch.SWIPE);
-            mPendingAnimation = null;
-        }
+        cancelPendingAnim();
 
         RecentsView recentsView = mLauncher.getOverviewPanel();
         TaskView taskView = (TaskView) recentsView.getChildAt(recentsView.getNextPage());
@@ -199,10 +196,16 @@
             mPendingAnimation = recentsView.createTaskLauncherAnimation(taskView, maxAccuracy);
             mPendingAnimation.anim.setInterpolator(Interpolators.ZOOM_IN);
 
-            mCurrentAnimation = AnimatorPlaybackController.wrap(mPendingAnimation.anim, maxAccuracy);
+            Runnable onCancelRunnable = () -> {
+                cancelPendingAnim();
+                clearState();
+            };
+            mCurrentAnimation = AnimatorPlaybackController.wrap(mPendingAnimation.anim, maxAccuracy,
+                    onCancelRunnable);
+            mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation);
         } else {
             mCurrentAnimation = mLauncher.getStateManager()
-                    .createAnimationToNewWorkspace(mToState, builder, maxAccuracy);
+                    .createAnimationToNewWorkspace(mToState, builder, maxAccuracy, this::clearState);
         }
 
         if (totalShift == 0) {
@@ -212,6 +215,13 @@
         return 1 / totalShift;
     }
 
+    private void cancelPendingAnim() {
+        if (mPendingAnimation != null) {
+            mPendingAnimation.finish(false, Touch.SWIPE);
+            mPendingAnimation = null;
+        }
+    }
+
     @Override
     protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
             LauncherState targetState, float velocity, boolean isFling) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 49d4931..2579bc2 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -63,7 +63,7 @@
     @Override
     public void setStateWithAnimation(final LauncherState toState,
             AnimatorSetBuilder builder, AnimationConfig config) {
-        PropertySetter setter = config.getProperSetter(builder);
+        PropertySetter setter = config.getPropertySetter(builder);
         float[] scaleTranslationYFactor = toState.getOverviewScaleAndTranslationYFactor(mLauncher);
         setter.setFloat(mRecentsView, ADJACENT_SCALE, scaleTranslationYFactor[0],
                 builder.getInterpolator(ANIM_OVERVIEW_TRANSLATION, LINEAR));
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
index 63a7984..dc3f79c 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
@@ -87,12 +87,14 @@
 
     protected abstract boolean isRecentsInteractive();
 
+    protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
+    }
+
     @Override
     public void onAnimationCancel(Animator animation) {
         if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) {
             Log.e(TAG, "Who dare cancel the animation when I am in control", new Exception());
-            mDetector.finishedScrolling();
-            mCurrentAnimation = null;
+            clearState();
         }
     }
 
@@ -194,8 +196,12 @@
             mEndDisplacement = dl.getHeight() - mTempCords[1];
         }
 
+        if (mCurrentAnimation != null) {
+            mCurrentAnimation.setOnCancelRunnable(null);
+        }
         mCurrentAnimation = AnimatorPlaybackController
-                .wrap(mPendingAnimation.anim, maxDuration);
+                .wrap(mPendingAnimation.anim, maxDuration, this::clearState);
+        onUserControlledAnimationCreated(mCurrentAnimation);
         mCurrentAnimation.getTarget().addListener(this);
         mCurrentAnimation.dispatchOnStart();
         mProgressMultiplier = 1 / mEndDisplacement;
@@ -271,8 +277,17 @@
             mPendingAnimation.finish(wasSuccess, logAction);
             mPendingAnimation = null;
         }
+        clearState();
+    }
+
+    private void clearState() {
         mDetector.finishedScrolling();
+        mDetector.setDetectableScrollConditions(0, false);
         mTaskBeingDragged = null;
         mCurrentAnimation = null;
+        if (mPendingAnimation != null) {
+            mPendingAnimation.finish(false, Touch.SWIPE);
+            mPendingAnimation = null;
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index 01e2bf3..cb83a0d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -29,6 +29,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager.StateHandler;
+import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.util.TouchController;
 import com.android.quickstep.OverviewInteractionState;
 import com.android.quickstep.RecentsModel;
@@ -44,19 +45,19 @@
             return new TouchController[] {
                     launcher.getDragController(),
                     new OverviewToAllAppsTouchController(launcher),
-                    new LauncherTaskViewcontroller(launcher)};
+                    new LauncherTaskViewController(launcher)};
         }
         if (launcher.getDeviceProfile().isVerticalBarLayout()) {
             return new TouchController[] {
                     launcher.getDragController(),
                     new OverviewToAllAppsTouchController(launcher),
                     new LandscapeEdgeSwipeController(launcher),
-                    new LauncherTaskViewcontroller(launcher)};
+                    new LauncherTaskViewController(launcher)};
         } else {
             return new TouchController[] {
                     launcher.getDragController(),
                     new PortraitStatesTouchController(launcher),
-                    new LauncherTaskViewcontroller(launcher)};
+                    new LauncherTaskViewController(launcher)};
         }
     }
 
@@ -114,9 +115,9 @@
         }
     }
 
-    private static class LauncherTaskViewcontroller extends TaskViewTouchController<Launcher> {
+    private static class LauncherTaskViewController extends TaskViewTouchController<Launcher> {
 
-        public LauncherTaskViewcontroller(Launcher activity) {
+        public LauncherTaskViewController(Launcher activity) {
             super(activity);
         }
 
@@ -124,5 +125,10 @@
         protected boolean isRecentsInteractive() {
             return mActivity.isInState(OVERVIEW);
         }
+
+        @Override
+        protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
+            mActivity.getStateManager().setCurrentUserControlledAnimation(animController);
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index 88cd376..8a6abb2 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -263,7 +263,7 @@
         @Override
         public RecentsView getVisibleRecentsView() {
             Launcher launcher = getVisibleLaucher();
-            return launcher != null && launcher.isInState(OVERVIEW)
+            return launcher != null && launcher.getStateManager().getState().overviewUi
                     ? launcher.getOverviewPanel() : null;
         }
 
diff --git a/quickstep/src/com/android/quickstep/QuickScrubController.java b/quickstep/src/com/android/quickstep/QuickScrubController.java
index ea9d009..7bb9877 100644
--- a/quickstep/src/com/android/quickstep/QuickScrubController.java
+++ b/quickstep/src/com/android/quickstep/QuickScrubController.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep;
 
+import android.util.Log;
 import android.view.HapticFeedbackConstants;
 
 import com.android.launcher3.Alarm;
@@ -45,6 +46,7 @@
             0.05f, 0.35f, 0.65f, 0.95f
     };
 
+    private static final String TAG = "QuickScrubController";
     private static final boolean ENABLE_AUTO_ADVANCE = true;
     private static final long AUTO_ADVANCE_DELAY = 500;
     private static final int QUICKSCRUB_SNAP_DURATION_PER_PAGE = 325;
@@ -58,6 +60,8 @@
     private int mQuickScrubSection;
     private boolean mStartedFromHome;
     private boolean mFinishedTransitionToQuickScrub;
+    private Runnable mOnFinishedTransitionToQuickScrubRunnable;
+    private ActivityControlHelper mActivityControlHelper;
 
     public QuickScrubController(BaseActivity activity, RecentsView recentsView) {
         mActivity = activity;
@@ -68,11 +72,13 @@
         }
     }
 
-    public void onQuickScrubStart(boolean startingFromHome) {
+    public void onQuickScrubStart(boolean startingFromHome, ActivityControlHelper controlHelper) {
         mInQuickScrub = true;
         mStartedFromHome = startingFromHome;
         mQuickScrubSection = 0;
         mFinishedTransitionToQuickScrub = false;
+        mOnFinishedTransitionToQuickScrubRunnable = null;
+        mActivityControlHelper = controlHelper;
 
         snapToNextTaskIfAvailable();
         mActivity.getUserEventDispatcher().resetActionDurationMillis();
@@ -85,13 +91,18 @@
         }
         int page = mRecentsView.getNextPage();
         Runnable launchTaskRunnable = () -> {
-            TaskView taskView = ((TaskView) mRecentsView.getPageAt(page));
+            TaskView taskView = mRecentsView.getPageAt(page);
             if (taskView != null) {
-                taskView.launchTask(true);
+                taskView.launchTask(true, (result) -> {
+                    if (!result) {
+                        taskView.notifyTaskLaunchFailed(TAG);
+                        breakOutOfQuickScrub();
+                    }
+                }, taskView.getHandler());
             } else {
-                // Break out of quick scrub so user can interact with launcher.
-                mActivity.onBackPressed();
+                breakOutOfQuickScrub();
             }
+            mActivityControlHelper = null;
         };
         int snapDuration = Math.abs(page - mRecentsView.getPageNearestToCenterOfScreen())
                 * QUICKSCRUB_END_SNAP_DURATION_PER_PAGE;
@@ -100,13 +111,27 @@
             mRecentsView.setNextPageSwitchRunnable(launchTaskRunnable);
         } else {
             // No page move needed, just launch it
-            launchTaskRunnable.run();
+            if (mFinishedTransitionToQuickScrub) {
+                launchTaskRunnable.run();
+            } else {
+                mOnFinishedTransitionToQuickScrubRunnable = launchTaskRunnable;
+            }
         }
         mActivity.getUserEventDispatcher().logActionOnControl(Touch.DRAGDROP,
                 ControlType.QUICK_SCRUB_BUTTON, null, mStartedFromHome ?
                         ContainerType.WORKSPACE : ContainerType.APP);
     }
 
+    /**
+     * Attempts to go to normal overview or back to home, so UI doesn't prevent user interaction.
+     */
+    private void breakOutOfQuickScrub() {
+        if (mRecentsView.getChildCount() == 0 || mActivityControlHelper == null
+                || !mActivityControlHelper.switchToRecentsIfVisible()) {
+            mActivity.onBackPressed();
+        }
+    }
+
     public void onQuickScrubProgress(float progress) {
         int quickScrubSection = 0;
         for (float threshold : QUICK_SCRUB_THRESHOLDS) {
@@ -135,6 +160,10 @@
 
     public void onFinishedTransitionToQuickScrub() {
         mFinishedTransitionToQuickScrub = true;
+        if (mOnFinishedTransitionToQuickScrubRunnable != null) {
+            mOnFinishedTransitionToQuickScrubRunnable.run();
+            mOnFinishedTransitionToQuickScrubRunnable = null;
+        }
     }
 
     public void snapToNextTaskIfAvailable() {
@@ -166,6 +195,12 @@
     @Override
     public void onAlarm(Alarm alarm) {
         int currPage = mRecentsView.getNextPage();
+        boolean recentsVisible = mActivityControlHelper != null
+                && mActivityControlHelper.getVisibleRecentsView() != null;
+        if (!recentsVisible) {
+            Log.w(TAG, "Failed to auto advance; recents not visible");
+            return;
+        }
         if (mQuickScrubSection == QUICK_SCRUB_THRESHOLDS.length
                 && currPage < mRecentsView.getPageCount() - 1) {
             goToPageWithHaptic(currPage + 1);
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 25649fa..c9afcb5 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -21,9 +21,7 @@
 import static android.view.MotionEvent.ACTION_POINTER_DOWN;
 import static android.view.MotionEvent.ACTION_POINTER_UP;
 import static android.view.MotionEvent.ACTION_UP;
-
-import static com.android.systemui.shared.system.ActivityManagerWrapper
-        .CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE;
 
 import android.annotation.TargetApi;
@@ -348,8 +346,8 @@
                 mStartPending = true;
 
                 Runnable action = () -> {
-                    mQuickScrubController.onQuickScrubStart(
-                            mActivityHelper.onQuickInteractionStart(mActivity, true));
+                    mQuickScrubController.onQuickScrubStart(mActivityHelper.onQuickInteractionStart(
+                            mActivity, true), mActivityHelper);
                     mQuickScrubController.onQuickScrubProgress(mLastProgress);
                     mStartPending = false;
 
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index ec50e67..027f2e9 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -64,7 +64,6 @@
 import com.android.quickstep.ActivityControlHelper.LayoutListener;
 import com.android.quickstep.TouchConsumer.InteractionType;
 import com.android.quickstep.util.ClipAnimationHelper;
-import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.RemoteAnimationTargetSet;
 import com.android.quickstep.util.SysuiEventLogger;
 import com.android.quickstep.views.RecentsView;
@@ -767,7 +766,7 @@
         }
 
         mActivityControlHelper.onQuickInteractionStart(mActivity, false);
-        mQuickScrubController.onQuickScrubStart(false);
+        mQuickScrubController.onQuickScrubStart(false, mActivityControlHelper);
 
         // Inform the last progress in case we skipped before.
         mQuickScrubController.onQuickScrubProgress(mCurrentQuickScrubProgress);
diff --git a/src/com/android/launcher3/LauncherStateManager.java b/src/com/android/launcher3/LauncherStateManager.java
index d196c37..7f25301 100644
--- a/src/com/android/launcher3/LauncherStateManager.java
+++ b/src/com/android/launcher3/LauncherStateManager.java
@@ -238,16 +238,18 @@
      */
     public AnimatorPlaybackController createAnimationToNewWorkspace(
             LauncherState state, long duration) {
-        return createAnimationToNewWorkspace(state, new AnimatorSetBuilder(), duration);
+        return createAnimationToNewWorkspace(state, new AnimatorSetBuilder(), duration, null);
     }
 
-    public AnimatorPlaybackController createAnimationToNewWorkspace(
-            LauncherState state, AnimatorSetBuilder builder, long duration) {
+    public AnimatorPlaybackController createAnimationToNewWorkspace(LauncherState state,
+            AnimatorSetBuilder builder, long duration, Runnable onCancelRunnable) {
         mConfig.reset();
         mConfig.userControlled = true;
         mConfig.duration = duration;
-        return AnimatorPlaybackController.wrap(
-                createAnimationToNewWorkspaceInternal(state, builder, null), duration);
+        mConfig.playbackController = AnimatorPlaybackController.wrap(
+                createAnimationToNewWorkspaceInternal(state, builder, null), duration,
+                onCancelRunnable);
+        return mConfig.playbackController;
     }
 
     protected AnimatorSet createAnimationToNewWorkspaceInternal(final LauncherState state,
@@ -358,6 +360,12 @@
         mConfig.reset();
     }
 
+    public void setCurrentUserControlledAnimation(AnimatorPlaybackController controller) {
+        setCurrentAnimation(controller.getTarget());
+        mConfig.userControlled = true;
+        mConfig.playbackController = controller;
+    }
+
     /**
      * Sets the animation as the current state animation, i.e., canceled when
      * starting another animation and may block some launcher interactions while running.
@@ -405,30 +413,39 @@
     public static class AnimationConfig extends AnimatorListenerAdapter {
         public long duration;
         public boolean userControlled;
-        private PropertySetter mProperSetter;
+        public AnimatorPlaybackController playbackController;
+        private PropertySetter mPropertySetter;
 
         private AnimatorSet mCurrentAnimation;
         private LauncherState mTargetState;
 
+        /**
+         * Cancels the current animation and resets config variables.
+         */
         public void reset() {
             duration = 0;
             userControlled = false;
-            mProperSetter = null;
+            mPropertySetter = null;
             mTargetState = null;
 
-            if (mCurrentAnimation != null) {
+            if (playbackController != null) {
+                playbackController.getAnimationPlayer().cancel();
+                playbackController.dispatchOnCancel();
+            } else if (mCurrentAnimation != null) {
                 mCurrentAnimation.setDuration(0);
                 mCurrentAnimation.cancel();
-                mCurrentAnimation = null;
             }
+
+            mCurrentAnimation = null;
+            playbackController = null;
         }
 
-        public PropertySetter getProperSetter(AnimatorSetBuilder builder) {
-            if (mProperSetter == null) {
-                mProperSetter = duration == 0 ? NO_ANIM_PROPERTY_SETTER
+        public PropertySetter getPropertySetter(AnimatorSetBuilder builder) {
+            if (mPropertySetter == null) {
+                mPropertySetter = duration == 0 ? NO_ANIM_PROPERTY_SETTER
                         : new AnimatedPropertySetter(duration, builder);
             }
-            return mProperSetter;
+            return mPropertySetter;
         }
 
         @Override
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index 420a7c4..77a45bf 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -53,7 +53,7 @@
 
     public void setStateWithAnimation(LauncherState toState, AnimatorSetBuilder builder,
             AnimationConfig config) {
-        setWorkspaceProperty(toState, config.getProperSetter(builder));
+        setWorkspaceProperty(toState, config.getPropertySetter(builder));
     }
 
     public float getFinalScale() {
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 6a0e1cc..53d3da6 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -163,7 +163,7 @@
             AnimatorSetBuilder builder, AnimationConfig config) {
         float targetProgress = toState.getVerticalProgress(mLauncher);
         if (Float.compare(mProgress, targetProgress) == 0) {
-            setAlphas(toState, config.getProperSetter(builder));
+            setAlphas(toState, config.getPropertySetter(builder));
             // Fail fast
             onProgressAnimationEnd();
             return;
@@ -178,7 +178,7 @@
 
         builder.play(anim);
 
-        setAlphas(toState, config.getProperSetter(builder));
+        setAlphas(toState, config.getPropertySetter(builder));
     }
 
     private void setAlphas(LauncherState toState, PropertySetter setter) {
diff --git a/src/com/android/launcher3/allapps/DiscoveryBounce.java b/src/com/android/launcher3/allapps/DiscoveryBounce.java
index f73916c..8f1c8df 100644
--- a/src/com/android/launcher3/allapps/DiscoveryBounce.java
+++ b/src/com/android/launcher3/allapps/DiscoveryBounce.java
@@ -42,7 +42,7 @@
  */
 public class DiscoveryBounce extends AbstractFloatingView {
 
-    private static final long DELAY_MS = 200;
+    private static final long DELAY_MS = 450;
 
     public static final String HOME_BOUNCE_SEEN = "launcher.apps_view_shown";
     public static final String SHELF_BOUNCE_SEEN = "launcher.shelf_bounce_seen";
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 1dba7d6..8e729e8 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -35,18 +35,23 @@
  */
 public abstract class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateListener {
 
+    public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) {
+        return wrap(anim, duration, null);
+    }
+
     /**
      * Creates an animation controller for the provided animation.
      * The actual duration does not matter as the animation is manually controlled. It just
      * needs to be larger than the total number of pixels so that we don't have jittering due
      * to float (animation-fraction * total duration) to int conversion.
      */
-    public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) {
+    public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration,
+            Runnable onCancelRunnable) {
 
         /**
          * TODO: use {@link AnimatorSet#setCurrentPlayTime(long)} once b/68382377 is fixed.
          */
-        return new AnimatorPlaybackControllerVL(anim, duration);
+        return new AnimatorPlaybackControllerVL(anim, duration, onCancelRunnable);
     }
 
     private final ValueAnimator mAnimationPlayer;
@@ -58,10 +63,13 @@
     private Runnable mEndAction;
 
     protected boolean mTargetCancelled = false;
+    protected Runnable mOnCancelRunnable;
 
-    protected AnimatorPlaybackController(AnimatorSet anim, long duration) {
+    protected AnimatorPlaybackController(AnimatorSet anim, long duration,
+            Runnable onCancelRunnable) {
         mAnim = anim;
         mDuration = duration;
+        mOnCancelRunnable = onCancelRunnable;
 
         mAnimationPlayer = ValueAnimator.ofFloat(0, 1);
         mAnimationPlayer.setInterpolator(Interpolators.LINEAR);
@@ -72,6 +80,21 @@
             @Override
             public void onAnimationCancel(Animator animation) {
                 mTargetCancelled = true;
+                if (mOnCancelRunnable != null) {
+                    mOnCancelRunnable.run();
+                    mOnCancelRunnable = null;
+                }
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mTargetCancelled = false;
+                mOnCancelRunnable = null;
+            }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mTargetCancelled = false;
             }
         });
     }
@@ -163,12 +186,33 @@
         }
     }
 
+    public void dispatchOnCancel() {
+        dispatchOnCancelRecursively(mAnim);
+    }
+
+    private void dispatchOnCancelRecursively(Animator animator) {
+        for (AnimatorListener l : nonNullList(animator.getListeners())) {
+            l.onAnimationCancel(animator);
+        }
+
+        if (animator instanceof AnimatorSet) {
+            for (Animator anim : nonNullList(((AnimatorSet) animator).getChildAnimations())) {
+                dispatchOnCancelRecursively(anim);
+            }
+        }
+    }
+
+    public void setOnCancelRunnable(Runnable runnable) {
+        mOnCancelRunnable = runnable;
+    }
+
     public static class AnimatorPlaybackControllerVL extends AnimatorPlaybackController {
 
         private final ValueAnimator[] mChildAnimations;
 
-        private AnimatorPlaybackControllerVL(AnimatorSet anim, long duration) {
-            super(anim, duration);
+        private AnimatorPlaybackControllerVL(AnimatorSet anim, long duration,
+                Runnable onCancelRunnable) {
+            super(anim, duration, onCancelRunnable);
 
             // Build animation list
             ArrayList<ValueAnimator> childAnims = new ArrayList<>();
diff --git a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
index 3c16cde..1383f53 100644
--- a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
+++ b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
@@ -41,8 +41,9 @@
     private static final int LINE_ALPHA_ANIMATOR_INDEX = 0;
     private static final int NUM_PAGES_ANIMATOR_INDEX = 1;
     private static final int TOTAL_SCROLL_ANIMATOR_INDEX = 2;
+    private static final int ANIMATOR_COUNT = 3;
 
-    private ValueAnimator[] mAnimators = new ValueAnimator[3];
+    private ValueAnimator[] mAnimators = new ValueAnimator[ANIMATOR_COUNT];
 
     private final Handler mDelayedLineFadeHandler = new Handler(Looper.getMainLooper());
     private final Launcher mLauncher;
@@ -232,6 +233,28 @@
         mAnimators[animatorIndex].start();
     }
 
+    /**
+     * Pauses all currently running animations.
+     */
+    public void pauseAnimations() {
+        for (int i = 0; i < ANIMATOR_COUNT; i++) {
+            if (mAnimators[i] != null) {
+                mAnimators[i].pause();
+            }
+        }
+    }
+
+    /**
+     * Force-ends all currently running or paused animations.
+     */
+    public void skipAnimationsToEnd() {
+        for (int i = 0; i < ANIMATOR_COUNT; i++) {
+            if (mAnimators[i] != null) {
+                mAnimators[i].end();
+            }
+        }
+    }
+
     @Override
     public void setInsets(Rect insets) {
         DeviceProfile grid = mLauncher.getDeviceProfile();
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index c0ad110..d5c0788 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -18,10 +18,7 @@
 import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
-import android.util.Log;
 import android.view.MotionEvent;
 
 import com.android.launcher3.Launcher;
@@ -30,13 +27,13 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.util.TouchController;
 import com.android.launcher3.util.PendingAnimation;
+import com.android.launcher3.util.TouchController;
 
 /**
  * TouchController for handling state changes
  */
-public abstract class AbstractStateChangeTouchController extends AnimatorListenerAdapter
+public abstract class AbstractStateChangeTouchController
         implements TouchController, SwipeDetector.Listener {
 
     private static final String TAG = "ASCTouchController";
@@ -146,8 +143,10 @@
         mToState = newToState;
 
         mStartProgress = 0;
+        if (mCurrentAnimation != null) {
+            mCurrentAnimation.setOnCancelRunnable(null);
+        }
         mProgressMultiplier = initCurrentAnimation();
-        mCurrentAnimation.getTarget().addListener(this);
         mCurrentAnimation.dispatchOnStart();
         return true;
     }
@@ -203,7 +202,6 @@
             targetState = (progress > SUCCESS_TRANSITION_PROGRESS) ? mToState : mFromState;
         }
 
-
         final float endProgress;
         final float startProgress;
         final long duration;
@@ -220,6 +218,8 @@
                         endProgress - Math.max(progress, 0));
             }
         } else {
+            mCurrentAnimation.setOnCancelRunnable(null);
+            mCurrentAnimation.dispatchOnCancel();
             endProgress = 0;
             if (progress <= 0) {
                 duration = 0;
@@ -236,6 +236,7 @@
         ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
         anim.setFloatValues(startProgress, endProgress);
         updateSwipeCompleteAnimation(anim, duration, targetState, velocity, fling);
+        mCurrentAnimation.dispatchOnStart();
         anim.start();
     }
 
@@ -275,13 +276,6 @@
     protected void clearState() {
         mCurrentAnimation = null;
         mDetector.finishedScrolling();
-    }
-
-    @Override
-    public void onAnimationCancel(Animator animation) {
-        if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) {
-            Log.e(TAG, "Who dare cancel the animation when I am in control", new Exception());
-            clearState();
-        }
+        mDetector.setDetectableScrollConditions(0, false);
     }
 }