Merge "Refactors ButtonDropTarget to add extension data" into ub-launcher3-edmonton
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index bab2cd7..c76ad83 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -21,6 +21,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.launcher3" >
 
+    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="28"/>
     <uses-permission android:name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS" />
     <application
         android:backupAgent="com.android.launcher3.LauncherBackupAgent"
diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar
index 70e545f..120d6f9 100644
--- a/quickstep/libs/sysui_shared.jar
+++ b/quickstep/libs/sysui_shared.jar
Binary files differ
diff --git a/quickstep/res/layout/overview_clear_all_button.xml b/quickstep/res/layout/overview_clear_all_button.xml
index 1ada914..9c4b618 100644
--- a/quickstep/res/layout/overview_clear_all_button.xml
+++ b/quickstep/res/layout/overview_clear_all_button.xml
@@ -1,14 +1,14 @@
 <?xml version="1.0" encoding="utf-8"?>
 
-<TextView
+<com.android.quickstep.views.ClearAllButton
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/clear_all_button"
     style="@android:style/Widget.DeviceDefault.Button.Borderless"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="start|top"
-    android:fontFamily="sans-serif-medium"
     android:text="@string/recents_clear_all"
     android:textColor="?attr/workspaceTextColor"
+    android:visibility="invisible"
     android:textSize="14sp"
 />
\ No newline at end of file
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 37929b6..7673f69 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -17,4 +17,6 @@
     <string name="task_overlay_factory_class" translatable="false"></string>
 
     <string name="overview_callbacks_class" translatable="false"></string>
+
+    <string name="user_event_dispatcher_class" translatable="false">com.google.quickstep.logging.UserEventDispatcherExtension</string>
 </resources>
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/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
index 7da50c8..5a216f6 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
@@ -16,7 +16,6 @@
 package com.android.launcher3.uioverrides;
 
 import static com.android.launcher3.LauncherAnimUtils.ALL_APPS_TRANSITION_MS;
-import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_SEEN;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
 
 import android.view.View;
@@ -47,10 +46,6 @@
 
     @Override
     public void onStateEnabled(Launcher launcher) {
-        if (!launcher.getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)) {
-            launcher.getSharedPrefs().edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply();
-        }
-
         AbstractFloatingView.closeAllOpenViews(launcher);
         dispatchWindowStateChanged(launcher);
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
index 61422e0..f87f006 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
@@ -16,7 +16,6 @@
 package com.android.launcher3.uioverrides;
 
 import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
-import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
 import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
 
@@ -59,10 +58,6 @@
 
     @Override
     public void onStateEnabled(Launcher launcher) {
-        if (!launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) {
-            launcher.getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
-        }
-
         RecentsView rv = launcher.getOverviewPanel();
         rv.setOverviewStateEnabled(true);
         AbstractFloatingView.closeAllOpenViews(launcher);
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..06099b9 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -21,6 +21,8 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN;
+import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_SEEN;
 
 import android.content.Context;
 
@@ -28,7 +30,9 @@
 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.anim.AnimatorPlaybackController;
 import com.android.launcher3.util.TouchController;
 import com.android.quickstep.OverviewInteractionState;
 import com.android.quickstep.RecentsModel;
@@ -44,19 +48,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)};
         }
     }
 
@@ -88,6 +92,55 @@
         recents.reset();
     }
 
+    public static void onCreate(Launcher launcher) {
+        if (!launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) {
+            launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() {
+                @Override
+                public void onStateSetImmediately(LauncherState state) {
+                }
+
+                @Override
+                public void onStateTransitionStart(LauncherState toState) {
+                }
+
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    boolean swipeUpEnabled = OverviewInteractionState.getInstance(launcher)
+                            .isSwipeUpGestureEnabled();
+                    LauncherState prevState = launcher.getStateManager().getLastState();
+
+                    if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled
+                            && finalState == ALL_APPS && prevState == NORMAL))) {
+                        launcher.getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
+                        launcher.getStateManager().removeStateListener(this);
+                    }
+                }
+            });
+        }
+
+        if (!launcher.getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)) {
+            launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() {
+                @Override
+                public void onStateSetImmediately(LauncherState state) {
+                }
+
+                @Override
+                public void onStateTransitionStart(LauncherState toState) {
+                }
+
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    LauncherState prevState = launcher.getStateManager().getLastState();
+
+                    if (finalState == ALL_APPS && prevState == OVERVIEW) {
+                        launcher.getSharedPrefs().edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply();
+                        launcher.getStateManager().removeStateListener(this);
+                    }
+                }
+            });
+        }
+    }
+
     public static void onStart(Context context) {
         RecentsModel model = RecentsModel.getInstance(context);
         if (model != null) {
@@ -114,9 +167,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 +177,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..529a765 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -59,8 +59,6 @@
 
     LayoutListener createLayoutListener(T activity);
 
-    void onQuickstepGestureStarted(T activity, boolean activityVisible);
-
     /**
      * Updates the UI to indicate quick interaction.
      * @return true if there any any UI change as a result of this
@@ -117,11 +115,6 @@
         }
 
         @Override
-        public void onQuickstepGestureStarted(Launcher activity, boolean activityVisible) {
-            activity.onQuickstepGestureStarted(activityVisible);
-        }
-
-        @Override
         public boolean onQuickInteractionStart(Launcher activity, boolean activityVisible) {
             LauncherState fromState = activity.getStateManager().getState();
             activity.getStateManager().goToState(FAST_OVERVIEW, activityVisible);
@@ -137,10 +130,7 @@
 
         @Override
         public void executeOnWindowAvailable(Launcher activity, Runnable action) {
-            if (activity.getWorkspace().runOnOverlayHidden(action)) {
-                // Notify the activity that qiuckscrub has started
-                onQuickstepGestureStarted(activity, true);
-            }
+            activity.getWorkspace().runOnOverlayHidden(action);
         }
 
         @Override
@@ -263,7 +253,7 @@
         @Override
         public RecentsView getVisibleRecentsView() {
             Launcher launcher = getVisibleLaucher();
-            return launcher != null && launcher.isInState(OVERVIEW)
+            return launcher != null && launcher.getStateManager().getState().overviewUi
                     ? launcher.getOverviewPanel() : null;
         }
 
@@ -310,11 +300,6 @@
     class FallbackActivityControllerHelper implements ActivityControlHelper<RecentsActivity> {
 
         @Override
-        public void onQuickstepGestureStarted(RecentsActivity activity, boolean activityVisible) {
-            // TODO:
-        }
-
-        @Override
         public boolean onQuickInteractionStart(RecentsActivity activity, boolean activityVisible) {
             // Activity does not need any UI change for quickscrub.
             return false;
diff --git a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
index 6ce9372..2d41a5b 100644
--- a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
+++ b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
@@ -22,6 +22,8 @@
 import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.INVALID_POINTER_ID;
 
+import static com.android.systemui.shared.system.ActivityManagerWrapper
+        .CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 
 import android.annotation.TargetApi;
@@ -73,6 +75,7 @@
     private final ActivityControlHelper mActivityControlHelper;
     private final MainThreadExecutor mMainThreadExecutor;
     private final Choreographer mBackgroundThreadChoreographer;
+    private final OverviewCallbacks mOverviewCallbacks;
 
     private final boolean mIsDeferredDownTarget;
     private final PointF mDownPos = new PointF();
@@ -92,8 +95,10 @@
     public OtherActivityTouchConsumer(Context base, RunningTaskInfo runningTaskInfo,
             RecentsModel recentsModel, Intent homeIntent, ActivityControlHelper activityControl,
             MainThreadExecutor mainThreadExecutor, Choreographer backgroundThreadChoreographer,
-            @HitTarget int downHitTarget, VelocityTracker velocityTracker) {
+            @HitTarget int downHitTarget, OverviewCallbacks overviewCallbacks,
+            VelocityTracker velocityTracker) {
         super(base);
+
         mRunningTask = runningTaskInfo;
         mRecentsModel = recentsModel;
         mHomeIntent = homeIntent;
@@ -102,6 +107,7 @@
         mMainThreadExecutor = mainThreadExecutor;
         mBackgroundThreadChoreographer = backgroundThreadChoreographer;
         mIsDeferredDownTarget = activityControl.deferStartingActivity(downHitTarget);
+        mOverviewCallbacks = overviewCallbacks;
     }
 
     @Override
@@ -187,6 +193,11 @@
         if (mInteractionHandler == null) {
             return;
         }
+
+        mOverviewCallbacks.closeAllWindows();
+        ActivityManagerWrapper.getInstance().closeSystemWindows(
+                CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+
         // Notify the handler that the gesture has actually started
         mInteractionHandler.onGestureStarted();
     }
diff --git a/quickstep/src/com/android/quickstep/OverviewCallbacks.java b/quickstep/src/com/android/quickstep/OverviewCallbacks.java
index 62938a7..ac4a40b 100644
--- a/quickstep/src/com/android/quickstep/OverviewCallbacks.java
+++ b/quickstep/src/com/android/quickstep/OverviewCallbacks.java
@@ -40,4 +40,6 @@
     public void onInitOverviewTransition() { }
 
     public void onResetOverview() { }
+
+    public void closeAllWindows() { }
 }
diff --git a/quickstep/src/com/android/quickstep/QuickScrubController.java b/quickstep/src/com/android/quickstep/QuickScrubController.java
index ea9d009..adc245b 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;
@@ -42,9 +43,10 @@
      * Snap to a new page when crossing these thresholds. The first and last auto-advance.
      */
     private static final float[] QUICK_SCRUB_THRESHOLDS = new float[] {
-            0.05f, 0.35f, 0.65f, 0.95f
+            0.04f, 0.27f, 0.50f, 0.73f, 0.96f
     };
 
+    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/TaskSystemShortcut.java b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java
index 6fece31..228af8e 100644
--- a/quickstep/src/com/android/quickstep/TaskSystemShortcut.java
+++ b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java
@@ -27,7 +27,6 @@
 import android.os.UserHandle;
 import android.util.Log;
 import android.view.View;
-import android.view.ViewTreeObserver.OnPreDrawListener;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
@@ -239,7 +238,7 @@
                             Log.w(TAG, "Failed to start screen pinning: ", e);
                         }
                     } else {
-                        Log.w(TAG, taskView.getLaunchTaskFailedMsg());
+                        taskView.notifyTaskLaunchFailed(TAG);
                     }
                 };
                 taskView.launchTask(true, resultCallback, mHandler);
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 25649fa..3babd1f 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;
@@ -166,6 +164,7 @@
     private ISystemUiProxy mISystemUiProxy;
     private OverviewCommandHelper mOverviewCommandHelper;
     private OverviewInteractionState mOverviewInteractionState;
+    private OverviewCallbacks mOverviewCallbacks;
 
     private Choreographer mMainThreadChoreographer;
     private Choreographer mBackgroundThreadChoreographer;
@@ -181,6 +180,7 @@
         mMainThreadChoreographer = Choreographer.getInstance();
         mEventQueue = new MotionEventQueue(mMainThreadChoreographer, mNoOpTouchConsumer);
         mOverviewInteractionState = OverviewInteractionState.getInstance(this);
+        mOverviewCallbacks = OverviewCallbacks.get(this);
 
         sConnected = true;
 
@@ -232,7 +232,8 @@
             return new OtherActivityTouchConsumer(this, runningTaskInfo, mRecentsModel,
                             mOverviewCommandHelper.overviewIntent,
                             mOverviewCommandHelper.getActivityControlHelper(), mMainThreadExecutor,
-                            mBackgroundThreadChoreographer, downHitTarget, tracker);
+                            mBackgroundThreadChoreographer, downHitTarget, mOverviewCallbacks,
+                            tracker);
         }
     }
 
@@ -332,7 +333,7 @@
             if (mInvalidated) {
                 return;
             }
-            mActivityHelper.onQuickstepGestureStarted(mActivity, true);
+            OverviewCallbacks.get(mActivity).closeAllWindows();
             ActivityManagerWrapper.getInstance()
                     .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
         }
@@ -343,13 +344,14 @@
                 return;
             }
             if (interactionType == INTERACTION_QUICK_SCRUB) {
+                OverviewCallbacks.get(mActivity).closeAllWindows();
                 ActivityManagerWrapper.getInstance()
                         .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
-                mStartPending = true;
 
+                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..4e17b4b 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -24,7 +24,6 @@
 import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_START_DURATION;
 import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL;
 import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.animation.Animator;
 import android.animation.ObjectAnimator;
@@ -64,13 +63,11 @@
 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;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.LatencyTrackerCompat;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -565,8 +562,6 @@
         mGestureStarted = true;
         mRecentsAnimationWrapper.hideCurrentInputMethod();
         mRecentsAnimationWrapper.enableInputConsumer();
-        ActivityManagerWrapper.getInstance().closeSystemWindows(
-                CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
     }
 
     /**
@@ -580,8 +575,6 @@
             // Once the gesture starts, we can no longer transition home through the button, so
             // reset the force override of the activity visibility
             mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER);
-            mActivityControlHelper.onQuickstepGestureStarted(
-                    curActivity, mWasLauncherAlreadyVisible);
         }
     }
 
@@ -767,7 +760,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/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java
new file mode 100644
index 0000000..d4cdd35
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 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.logging;
+
+import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.systemui.shared.system.MetricsLoggerCompat;
+
+/**
+ * This class handles AOSP MetricsLogger function calls.
+ */
+public class UserEventDispatcherExtension extends UserEventDispatcher {
+
+    public void logStateChangeAction(int action, int dir, int srcChildTargetType,
+                                     int srcParentContainerType, int dstContainerType,
+                                     int pageIndex) {
+        new MetricsLoggerCompat().visibility(MetricsLoggerCompat.OVERVIEW_ACTIVITY,
+                dstContainerType == LauncherLogProto.ContainerType.TASKSWITCHER);
+        super.logStateChangeAction(action, dir, srcChildTargetType, srcParentContainerType,
+                dstContainerType, pageIndex);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
new file mode 100644
index 0000000..f3a0e4f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 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.views;
+
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Button;
+
+public class ClearAllButton extends Button {
+    RecentsView mRecentsView;
+
+    public ClearAllButton(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void setRecentsView(RecentsView recentsView) {
+        mRecentsView = recentsView;
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        // Should be visible to accessibility even when completely covered by the task.
+        // Otherwise, we won't be able to scroll to it.
+        info.setVisibleToUser(true);
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle arguments) {
+        final boolean res = super.performAccessibilityAction(action, arguments);
+        if (action == ACTION_ACCESSIBILITY_FOCUS) {
+            mRecentsView.revealClearAllButton();
+        }
+        return res;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 8d6b890..7a04dcd 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -41,7 +41,6 @@
 import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
-import android.util.Log;
 import android.util.SparseBooleanArray;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
@@ -272,6 +271,7 @@
             loader.unloadTaskData(task);
             loader.getHighResThumbnailLoader().onTaskInvisible(task);
         }
+        onChildViewsChanged();
     }
 
     public boolean isTaskViewVisible(TaskView tv) {
@@ -314,59 +314,31 @@
         final int childCount = getChildCount();
         if (mShowEmptyMessage || childCount == 0) return 0;
 
-        // Current visible coordinate of the end of the oldest task.
         final View lastChild = getChildAt(childCount - 1);
+
+        // Current visible coordinate of the end of the oldest task.
         final int carouselCurrentEnd =
                 (mIsRtl ? lastChild.getLeft() : lastChild.getRight()) - getScrollX();
 
-        // As the end (let's call it E aka carouselCurrentEnd) of the carousel moves over Clear
-        // all button, the button changes trasparency.
-        // fullAlphaX and zeroAlphaX are the points of the 100% and 0% alpha correspondingly.
-        // Alpha changes linearly between 100% and 0% as E moves through this range. It doesn't
-        // change outside of the range.
+        // Visible button-facing end of a centered task.
+        final int centeredTaskEnd = mIsRtl ?
+                getPaddingLeft() + mInsets.left :
+                getWidth() - getPaddingRight() - mInsets.right;
 
-        // Once E hits the border of the Clear-All button that looks towards the most recent
-        // task, the whole button is uncovered, and it should have alpha 100%.
-        final float fullAlphaX = mIsRtl ?
-                mClearAllButton.getX() + mClearAllButton.getWidth() :
-                mClearAllButton.getX();
-
-        // X coordinate of the carousel scrolled as far as possible in the direction towards the
-        // button. Logically, the button is "behind" the least recent task. This is the
-        // coordinate of the end of the least recent task in the carousel just after opening,
-        // with the most recent task in the center, and the rest of tasks go from that point
-        // towards and potentially behind the button.
-        final int carouselMotionLimit = getScrollForPage(childCount - 1) - getScrollForPage(0) +
-                (mIsRtl ?
-                        getPaddingLeft() + mInsets.left :
-                        getWidth() - getPaddingRight() - mInsets.right);
-
-        // The carousel might not be able to ever cover a part of the Clear-all button. Then
-        // always show the button as 100%. Technically, this check also prevents dividing by zero
-        // or getting a negative transparency ratio.
-        if (mIsRtl ? carouselMotionLimit >= fullAlphaX : carouselMotionLimit <= fullAlphaX) {
-            return 1;
-        }
-
-        // If the carousel is able to cover the button completely, we make the button completely
-        // transparent when E hits the border of the button farthest from the most recent task.
-        // Or, the carousel may not be able to move that far towards the button so it completely
-        // covers the it. Then we set the motion limit position of the carousel as the point
-        // where the button reaches 0 alpha.
-        final float zeroAlphaX = mIsRtl ?
-                Math.max(mClearAllButton.getX(), carouselMotionLimit) :
-                Math.min(mClearAllButton.getX() + mClearAllButton.getWidth(), carouselMotionLimit);
+        // The distance of the carousel travel during which the alpha changes from 0 to 1. This
+        // is the motion between the oldest task in its centered position and the oldest task
+        // scrolled to the end.
+        final int alphaChangeRange = (mIsRtl ? 0 : mMaxScrollX) - getScrollForPage(childCount - 1);
 
         return Utilities.boundToRange(
-                (zeroAlphaX - carouselCurrentEnd) /
-                        (zeroAlphaX - fullAlphaX), 0, 1);
+                ((float) (centeredTaskEnd - carouselCurrentEnd)) /
+                        alphaChangeRange, 0, 1);
     }
 
     private void updateClearAllButtonAlpha() {
         if (mClearAllButton != null) {
             final float alpha = calculateClearAllButtonAlpha();
             mClearAllButton.setAlpha(alpha * mContentAlpha);
-            mClearAllButton.setVisibility(alpha == 0 ? INVISIBLE : VISIBLE);
         }
     }
 
@@ -379,7 +351,7 @@
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN && mTouchState == TOUCH_STATE_REST
-                && mScroller.isFinished() && mClearAllButton.getVisibility() == View.VISIBLE) {
+                && mScroller.isFinished() && mClearAllButton.getAlpha() > 0) {
             mClearAllButton.getHitRect(mTempRect);
             mTempRect.offset(-getLeft(), -getTop());
             if (mTempRect.contains((int) ev.getX(), (int) ev.getY())) {
@@ -1019,6 +991,7 @@
         super.onViewAdded(child);
         child.setAlpha(mContentAlpha);
         setAdjacentScale(mAdjacentScale);
+        onChildViewsChanged();
     }
 
     @Override
@@ -1185,7 +1158,7 @@
             tv.setVisibility(VISIBLE);
             getOverlay().remove(drawable);
             if (!result) {
-                Log.w(TAG, tv.getLaunchTaskFailedMsg());
+                tv.notifyTaskLaunchFailed(TAG);
             }
         };
 
@@ -1251,4 +1224,15 @@
         mClearAllButton = clearAllButton;
         updateClearAllButtonAlpha();
     }
+
+    private void onChildViewsChanged() {
+        final int childCount = getChildCount();
+        mClearAllButton.setAccessibilityTraversalAfter(
+                childCount == 0 ? NO_ID : getChildAt(childCount - 1).getId());
+        mClearAllButton.setVisibility(childCount == 0 ? INVISIBLE : VISIBLE);
+    }
+
+    public void revealClearAllButton() {
+        scrollTo(mIsRtl ? 0 : computeMaxScrollX(), 0);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
index 15925b5..a951de9 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2018 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.views;
 
 import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
@@ -9,7 +25,6 @@
 import android.util.FloatProperty;
 import android.view.Gravity;
 import android.view.MotionEvent;
-import android.view.View;
 
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.R;
@@ -31,7 +46,7 @@
     private final Rect mTempRect = new Rect();
 
     private RecentsView mRecentsView;
-    private View mClearAllButton;
+    private ClearAllButton mClearAllButton;
 
     public RecentsViewContainer(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -48,13 +63,15 @@
             mRecentsView.dismissAllTasks();
         });
 
-        mRecentsView = (RecentsView) findViewById(R.id.overview_panel);
+        mRecentsView = findViewById(R.id.overview_panel);
         final InsettableFrameLayout.LayoutParams params =
                 (InsettableFrameLayout.LayoutParams) mClearAllButton.getLayoutParams();
         params.gravity = Gravity.TOP | (RecentsView.FLIP_RECENTS ? Gravity.START : Gravity.END);
         mClearAllButton.setLayoutParams(params);
         mClearAllButton.forceHasOverlappingRendering(false);
+
         mRecentsView.setClearAllButton(mClearAllButton);
+        mClearAllButton.setRecentsView(mRecentsView);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 93bdab2..a7527a6 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.views;
 
+import static android.widget.Toast.LENGTH_SHORT;
 import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA;
 
 import android.animation.Animator;
@@ -36,6 +37,7 @@
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
+import android.widget.Toast;
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
@@ -138,7 +140,7 @@
     public void launchTask(boolean animate) {
         launchTask(animate, (result) -> {
             if (!result) {
-                Log.w(TAG, getLaunchTaskFailedMsg());
+                notifyTaskLaunchFailed(TAG);
             }
         }, getHandler());
     }
@@ -312,11 +314,12 @@
         return super.performAccessibilityAction(action, arguments);
     }
 
-    public String getLaunchTaskFailedMsg() {
+    public void notifyTaskLaunchFailed(String tag) {
         String msg = "Failed to launch task";
         if (mTask != null) {
             msg += " (task=" + mTask.key.baseIntent + " userId=" + mTask.key.userId + ")";
         }
-        return msg;
+        Log.w(tag, msg);
+        Toast.makeText(getContext(), R.string.activity_not_available, LENGTH_SHORT).show();
     }
 }
diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java
index ab73074..168bd08 100644
--- a/src/com/android/launcher3/IconCache.java
+++ b/src/com/android/launcher3/IconCache.java
@@ -780,7 +780,7 @@
     }
 
     private static final class IconDB extends SQLiteCacheHelper {
-        private final static int RELEASE_VERSION = 21;
+        private final static int RELEASE_VERSION = 22;
 
         private final static String TABLE_NAME = "icons";
         private final static String COLUMN_ROWID = "rowid";
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 2c08169..9a9e001 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -283,6 +283,7 @@
         mDragController = new DragController(this);
         mAllAppsController = new AllAppsTransitionController(this);
         mStateManager = new LauncherStateManager(this);
+        UiFactory.onCreate(this);
 
         mAppWidgetManager = AppWidgetManagerCompat.getInstance(this);
 
@@ -318,7 +319,7 @@
             if (!internalStateHandled) {
                 // If we are not binding synchronously, show a fade in animation when
                 // the first page bind completes.
-                mDragLayer.setAlpha(0);
+                mLauncherView.setAlpha(0);
             }
         } else {
             // Pages bound synchronously.
@@ -1154,12 +1155,6 @@
         }
     }
 
-    public void onQuickstepGestureStarted(boolean isVisible) {
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onQuickstepGestureStarted(isVisible);
-        }
-    }
-
     public AllAppsTransitionController getAllAppsController() {
         return mAllAppsController;
     }
@@ -2131,8 +2126,8 @@
 
     @Override
     public void finishFirstPageBind(final ViewOnDrawExecutor executor) {
-        if (mDragLayer.getAlpha() < 1) {
-            mDragLayer.animate().alpha(1).withEndAction(
+        if (mLauncherView.getAlpha() < 1) {
+            mLauncherView.animate().alpha(1).withEndAction(
                     executor == null ? null : executor::onLoadAnimationCompleted).start();
         } else if (executor != null) {
             executor.onLoadAnimationCompleted();
diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java
index 6aef658..34bdb3c 100644
--- a/src/com/android/launcher3/LauncherCallbacks.java
+++ b/src/com/android/launcher3/LauncherCallbacks.java
@@ -18,7 +18,6 @@
 
 import android.content.Intent;
 import android.os.Bundle;
-import android.view.Menu;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -70,12 +69,4 @@
      * Extensions points for adding / replacing some other aspects of the Launcher experience.
      */
     boolean hasSettings();
-
-    /**
-     * Called when launcher integrated quickstep and some quickstep gesture started. It can be
-     * called multiple times for a single gesture an UI or background thread.
-     *
-     * @param isVisible if Launcher was visible when the gesture started.
-     */
-    void onQuickstepGestureStarted(boolean isVisible);
 }
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/PagedView.java b/src/com/android/launcher3/PagedView.java
index 8311ab9..87ee076 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -1171,11 +1171,19 @@
                     mNextPage = getPageNearestToCenterOfScreen(unscaledScrollX);
                     int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1);
                     int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0);
-                    if (mSettleOnPageInFreeScroll && unscaledScrollX > firstPageScroll
-                            && unscaledScrollX < lastPageScroll) {
-                        // Make sure we land directly on a page. If flinging past one of the ends,
-                        // don't change the velocity as it will get stopped at the end anyway.
-                        mScroller.setFinalX((int) (getScrollForPage(mNextPage) * getScaleX()));
+                    if (mSettleOnPageInFreeScroll && unscaledScrollX > 0
+                            && unscaledScrollX < mMaxScrollX) {
+                        // If scrolling ends in the half of the added space that is closer to the
+                        // end, settle to the end. Otherwise snap to the nearest page.
+                        // If flinging past one of the ends, don't change the velocity as it will
+                        // get stopped at the end anyway.
+                        final int finalX = unscaledScrollX < firstPageScroll / 2 ?
+                                0 :
+                                unscaledScrollX > (lastPageScroll + mMaxScrollX) / 2 ?
+                                        mMaxScrollX :
+                                        getScrollForPage(mNextPage);
+
+                        mScroller.setFinalX((int) (finalX * getScaleX()));
                         // Ensure the scroll/snap doesn't happen too fast;
                         int extraScrollDuration = OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION
                                 - mScroller.getDuration();
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/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 37405db..211d98f 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -45,6 +45,7 @@
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.uioverrides.AllAppsScrim;
 import com.android.launcher3.keyboard.FocusedItemDecorator;
@@ -122,6 +123,13 @@
     }
 
     @Override
+    protected void setDampedScrollShift(float shift) {
+        // Bound the shift amount to avoid content from drawing on top (Y-val) of the QSB.
+        float maxShift = getSearchView().getHeight() / 2f;
+        super.setDampedScrollShift(Utilities.boundToRange(shift, -maxShift, maxShift));
+    }
+
+    @Override
     public void onDeviceProfileChanged(DeviceProfile dp) {
         for (AdapterHolder holder : mAH) {
             if (holder.recyclerView != null) {
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/graphics/IconNormalizer.java b/src/com/android/launcher3/graphics/IconNormalizer.java
index 81f3f90..a2a0801 100644
--- a/src/com/android/launcher3/graphics/IconNormalizer.java
+++ b/src/com/android/launcher3/graphics/IconNormalizer.java
@@ -24,6 +24,7 @@
 import android.graphics.Paint;
 import android.graphics.Path;
 import android.graphics.PorterDuff;
+import android.graphics.PorterDuff.Mode;
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -66,12 +67,10 @@
 
     private final int mMaxSize;
     private final Bitmap mBitmap;
-    private final Bitmap mBitmapARGB;
     private final Canvas mCanvas;
     private final Paint mPaintMaskShape;
     private final Paint mPaintMaskShapeOutline;
     private final byte[] mPixels;
-    private final int[] mPixelsARGB;
 
     private final Rect mAdaptiveIconBounds;
     private float mAdaptiveIconScale;
@@ -83,9 +82,6 @@
     private final Path mShapePath;
     private final Matrix mMatrix;
 
-    private final Paint mPaintIcon;
-    private final Canvas mCanvasARGB;
-
     /** package private **/
     IconNormalizer(Context context) {
         // Use twice the icon size as maximum size to avoid scaling down twice.
@@ -93,19 +89,11 @@
         mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8);
         mCanvas = new Canvas(mBitmap);
         mPixels = new byte[mMaxSize * mMaxSize];
-        mPixelsARGB = new int[mMaxSize * mMaxSize];
         mLeftBorder = new float[mMaxSize];
         mRightBorder = new float[mMaxSize];
         mBounds = new Rect();
         mAdaptiveIconBounds = new Rect();
 
-        // Needed for isShape() method
-        mBitmapARGB = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ARGB_8888);
-        mCanvasARGB = new Canvas(mBitmapARGB);
-
-        mPaintIcon = new Paint();
-        mPaintIcon.setColor(Color.WHITE);
-
         mPaintMaskShape = new Paint();
         mPaintMaskShape.setColor(Color.RED);
         mPaintMaskShape.setStyle(Paint.Style.FILL);
@@ -115,7 +103,7 @@
         mPaintMaskShapeOutline.setStrokeWidth(2 * context.getResources().getDisplayMetrics().density);
         mPaintMaskShapeOutline.setStyle(Paint.Style.STROKE);
         mPaintMaskShapeOutline.setColor(Color.BLACK);
-        mPaintMaskShapeOutline.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
+        mPaintMaskShapeOutline.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
 
         mShapePath = new Path();
         mMatrix = new Matrix();
@@ -141,8 +129,6 @@
         // Condition 2:
         // Actual icon (white) and the fitted shape (e.g., circle)(red) XOR operation
         // should generate transparent image, if the actual icon is equivalent to the shape.
-        mBitmapARGB.eraseColor(Color.TRANSPARENT);
-        mCanvasARGB.drawBitmap(mBitmap, 0, 0, mPaintIcon);
 
         // Fit the shape within the icon's bounding box
         mMatrix.reset();
@@ -151,31 +137,41 @@
         maskPath.transform(mMatrix, mShapePath);
 
         // XOR operation
-        mCanvasARGB.drawPath(mShapePath, mPaintMaskShape);
+        mCanvas.drawPath(mShapePath, mPaintMaskShape);
 
         // DST_OUT operation around the mask path outline
-        mCanvasARGB.drawPath(mShapePath, mPaintMaskShapeOutline);
+        mCanvas.drawPath(mShapePath, mPaintMaskShapeOutline);
 
         // Check if the result is almost transparent
-        return isTransparentBitmap(mBitmapARGB);
+        return isTransparentBitmap();
     }
 
     /**
      * Used to determine if certain the bitmap is transparent.
      */
-    private boolean isTransparentBitmap(Bitmap bitmap) {
-        int w = mBounds.width();
-        int h = mBounds.height();
-        bitmap.getPixels(mPixelsARGB, 0 /* the first index to write into the array */,
-                w /* stride */,
-                mBounds.left, mBounds.top,
-                w, h);
+    private boolean isTransparentBitmap() {
+        ByteBuffer buffer = ByteBuffer.wrap(mPixels);
+        buffer.rewind();
+        mBitmap.copyPixelsToBuffer(buffer);
+
+        int y = mBounds.top;
+        // buffer position
+        int index = y * mMaxSize;
+        // buffer shift after every row, width of buffer = mMaxSize
+        int rowSizeDiff = mMaxSize - mBounds.right;
+
         int sum = 0;
-        for (int i = w * h - 1; i >= 0; i--) {
-            if(Color.alpha(mPixelsARGB[i]) > MIN_VISIBLE_ALPHA) {
+        for (; y < mBounds.bottom; y++) {
+            index += mBounds.left;
+            for (int x = mBounds.left; x < mBounds.right; x++) {
+                if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
                     sum++;
+                }
+                index++;
             }
+            index += rowSizeDiff;
         }
+
         float percentageDiffPixels = ((float) sum) / (mBounds.width() * mBounds.height());
         return percentageDiffPixels < PIXEL_DIFF_PERCENTAGE_THRESHOLD;
     }
@@ -306,7 +302,7 @@
         mBounds.bottom = bottomY;
 
         if (outBounds != null) {
-            outBounds.set(((float) mBounds.left) / width, ((float) mBounds.top),
+            outBounds.set(((float) mBounds.left) / width, ((float) mBounds.top) / height,
                     1 - ((float) mBounds.right) / width,
                     1 - ((float) mBounds.bottom) / height);
         }
diff --git a/src/com/android/launcher3/logging/UserEventDispatcher.java b/src/com/android/launcher3/logging/UserEventDispatcher.java
index bf870cc..2c1eb32 100644
--- a/src/com/android/launcher3/logging/UserEventDispatcher.java
+++ b/src/com/android/launcher3/logging/UserEventDispatcher.java
@@ -285,7 +285,7 @@
      * Used primarily for swipe up and down when state changes when swipe up happens from the
      * navbar bezel, the {@param srcChildContainerType} is NAVBAR and
      * {@param srcParentContainerType} is either one of the two
-     * (1) WORKSPACE: if the launcher the foreground activity
+     * (1) WORKSPACE: if the launcher is the foreground activity
      * (2) APP: if another app was the foreground activity
      */
     public void logStateChangeAction(int action, int dir, int srcChildTargetType,
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);
     }
 }
diff --git a/src/com/android/launcher3/views/SpringRelativeLayout.java b/src/com/android/launcher3/views/SpringRelativeLayout.java
index a508191..598738b 100644
--- a/src/com/android/launcher3/views/SpringRelativeLayout.java
+++ b/src/com/android/launcher3/views/SpringRelativeLayout.java
@@ -97,7 +97,7 @@
         mActiveEdge = edge;
     }
 
-    private void setDampedScrollShift(float shift) {
+    protected void setDampedScrollShift(float shift) {
         if (shift != mDampedScrollShift) {
             mDampedScrollShift = shift;
             invalidate();
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java b/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
index bd1a96e..b8cd035 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
@@ -40,6 +40,8 @@
 
     public static void onLauncherStateOrFocusChanged(Launcher launcher) { }
 
+    public static void onCreate(Launcher launcher) { }
+
     public static void onStart(Launcher launcher) { }
 
     public static void onLauncherStateOrResumeChanged(Launcher launcher) { }