Merge "Fix bug where two items could occupy same grid cell in hotseat." into ub-launcher3-master
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 11bc883..2f0cd78 100644
--- a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -47,6 +47,10 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.graphics.DrawableFactory;
+import com.android.quickstep.RecentsAnimationInterpolator;
+import com.android.quickstep.RecentsAnimationInterpolator.TaskWindowBounds;
+import com.android.quickstep.RecentsView;
+import com.android.quickstep.TaskView;
 import com.android.systemui.shared.system.ActivityCompat;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
 import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
@@ -70,6 +74,7 @@
     private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
             "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS";
 
+    private static final int RECENTS_LAUNCH_DURATION = 336;
     private static final int LAUNCHER_RESUME_START_DELAY = 150;
     private static final int CLOSING_TRANSITION_DURATION_MS = 350;
 
@@ -139,8 +144,18 @@
                         // Post at front of queue ignoring sync barriers to make sure it gets
                         // processed before the next frame.
                         postAtFrontOfQueueAsynchronously(v.getHandler(), () -> {
-                            LauncherTransitionAnimator animator = new LauncherTransitionAnimator(
-                                    getLauncherAnimators(v), getWindowAnimators(v, targets));
+                            final boolean removeTrackingView;
+                            LauncherTransitionAnimator animator =
+                                    composeRecentsLaunchAnimator(v, targets);
+                            if (animator != null) {
+                                // We are animating the task view directly, do not remove it after
+                                removeTrackingView = false;
+                            } else {
+                                animator = composeAppLaunchAnimator(v, targets);
+                                // A new floating view is created for the animation, remove it after
+                                removeTrackingView = true;
+                            }
+
                             setCurrentAnimator(animator);
                             mAnimator = animator.getAnimatorSet();
                             mAnimator.addListener(new AnimatorListenerAdapter() {
@@ -148,7 +163,10 @@
                                 public void onAnimationEnd(Animator animation) {
                                     // Reset launcher to normal state
                                     v.setVisibility(View.VISIBLE);
-                                    ((ViewGroup) mDragLayer.getParent()).removeView(mFloatingView);
+                                    if (removeTrackingView) {
+                                        ((ViewGroup) mDragLayer.getParent()).removeView(
+                                                mFloatingView);
+                                    }
 
                                     mDragLayer.setAlpha(1f);
                                     mDragLayer.setTranslationY(0f);
@@ -179,6 +197,131 @@
     }
 
     /**
+     * Composes the animations for a launch from the recents list if possible.
+     */
+    private LauncherTransitionAnimator composeRecentsLaunchAnimator(View v,
+            RemoteAnimationTargetCompat[] targets) {
+        // Ensure recents is actually visible
+        if (!mLauncher.isInState(LauncherState.OVERVIEW)) {
+            return null;
+        }
+
+        // Resolve the opening task id
+        int openingTaskId = -1;
+        for (RemoteAnimationTargetCompat target : targets) {
+            if (target.mode == RemoteAnimationTargetCompat.MODE_OPENING) {
+                openingTaskId = target.taskId;
+                break;
+            }
+        }
+
+        // If there is no opening task id, fall back to the normal app icon launch animation
+        if (openingTaskId == -1) {
+            return null;
+        }
+
+        // If the opening task id is not currently visible in overview, then fall back to normal app
+        // icon launch animation
+        RecentsView recentsView = mLauncher.getOverviewPanel();
+        TaskView taskView = recentsView.getTaskView(openingTaskId);
+        if (taskView == null || !recentsView.isTaskViewVisible(taskView)) {
+            return null;
+        }
+
+        // Found a visible recents task that matches the opening app, lets launch the app from there
+        return new LauncherTransitionAnimator(null, getRecentsWindowAnimator(taskView, targets));
+    }
+
+    /**
+     * @return Animator that controls the window of the opening targets for the recents launch
+     * animation.
+     */
+    private ValueAnimator getRecentsWindowAnimator(TaskView v,
+            RemoteAnimationTargetCompat[] targets) {
+        Rect taskViewBounds = new Rect();
+        mDragLayer.getDescendantRectRelativeToSelf(v, taskViewBounds);
+
+        // TODO: Use the actual target insets instead of the current thumbnail insets in case the
+        // device state has changed
+        RecentsAnimationInterpolator recentsInterpolator = new RecentsAnimationInterpolator(
+                new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx),
+                v.getThumbnail().getInsets(),
+                taskViewBounds, new Rect(0, v.getThumbnail().getTop(), 0, 0));
+
+        Rect crop = new Rect();
+        Matrix matrix = new Matrix();
+
+        ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
+        appAnimator.setDuration(RECENTS_LAUNCH_DURATION);
+        appAnimator.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
+        appAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            boolean isFirstFrame = true;
+
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                final Surface surface = getSurface(v);
+                final long frameNumber = surface != null ? getNextFrameNumber(surface) : -1;
+                if (frameNumber == -1) {
+                    // Booo, not cool! Our surface got destroyed, so no reason to animate anything.
+                    Log.w(TAG, "Failed to animate, surface got destroyed.");
+                    return;
+                }
+                final float percent = animation.getAnimatedFraction();
+                TaskWindowBounds tw = recentsInterpolator.interpolate(percent);
+
+                v.setScaleX(tw.taskScale);
+                v.setScaleY(tw.taskScale);
+                v.setTranslationX(tw.taskX);
+                v.setTranslationY(tw.taskY);
+                // Defer fading out the view until after the app window gets faded in
+                v.setAlpha(getValue(1f, 0f, 75, 75,
+                        appAnimator.getDuration() * percent, Interpolators.LINEAR));
+
+                matrix.setScale(tw.winScale, tw.winScale);
+                matrix.postTranslate(tw.winX, tw.winY);
+                crop.set(tw.winCrop);
+
+                // Fade in the app window.
+                float alphaDelay = 0;
+                float alphaDuration = 75;
+                float alpha = getValue(0f, 1f, alphaDelay, alphaDuration,
+                        appAnimator.getDuration() * percent, Interpolators.LINEAR);
+
+                TransactionCompat t = new TransactionCompat();
+                for (RemoteAnimationTargetCompat target : targets) {
+                    if (target.mode == RemoteAnimationTargetCompat.MODE_OPENING) {
+                        t.setAlpha(target.leash, alpha);
+
+                        // TODO: This isn't correct at the beginning of the animation, but better
+                        // than nothing.
+                        matrix.postTranslate(target.position.x, target.position.y);
+                        t.setMatrix(target.leash, matrix);
+                        t.setWindowCrop(target.leash, crop);
+                        t.deferTransactionUntil(target.leash, surface, getNextFrameNumber(surface));
+                    }
+                    if (isFirstFrame) {
+                        t.show(target.leash);
+                    }
+                }
+                t.apply();
+
+                matrix.reset();
+                isFirstFrame = false;
+            }
+        });
+        return appAnimator;
+    }
+
+    /**
+     * Composes the animations for a launch from an app icon.
+     */
+    private LauncherTransitionAnimator composeAppLaunchAnimator(View v,
+            RemoteAnimationTargetCompat[] targets) {
+        return new LauncherTransitionAnimator(getLauncherAnimators(v),
+                getWindowAnimators(v, targets));
+    }
+
+    /**
      * @return Animators that control the movements of the Launcher and icon of the opening target.
      */
     private AnimatorSet getLauncherAnimators(View v) {
diff --git a/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java b/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java
index 80eaef7..aec2869 100644
--- a/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java
+++ b/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java
@@ -32,11 +32,15 @@
     private Animator mWindowAnimator;
 
     LauncherTransitionAnimator(Animator launcherAnimator, Animator windowAnimator) {
-        mLauncherAnimator = launcherAnimator;
+        if (launcherAnimator != null) {
+            mLauncherAnimator = launcherAnimator;
+        }
         mWindowAnimator = windowAnimator;
 
         mAnimatorSet = new AnimatorSet();
-        mAnimatorSet.play(launcherAnimator);
+        if (launcherAnimator != null) {
+            mAnimatorSet.play(launcherAnimator);
+        }
         mAnimatorSet.play(windowAnimator);
     }
 
@@ -53,6 +57,8 @@
     }
 
     public void finishLauncherAnimation() {
-        mLauncherAnimator.end();
+        if (mLauncherAnimator != null) {
+            mLauncherAnimator.end();
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/BaseSwipeInteractionHandler.java b/quickstep/src/com/android/quickstep/BaseSwipeInteractionHandler.java
index 21b032b..b3ebd77 100644
--- a/quickstep/src/com/android/quickstep/BaseSwipeInteractionHandler.java
+++ b/quickstep/src/com/android/quickstep/BaseSwipeInteractionHandler.java
@@ -38,8 +38,10 @@
 
     public abstract void updateInteractionType(@InteractionType int interactionType);
 
+    @WorkerThread
     public abstract void onQuickScrubEnd();
 
+    @WorkerThread
     public abstract void onQuickScrubProgress(float progress);
 
     @WorkerThread
diff --git a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
index 61d4790..d8f7aaf 100644
--- a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
+++ b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
@@ -157,13 +157,7 @@
                             startTouchTrackingForScreenshotAnimation();
                         }
 
-                        // Notify the handler that the gesture has actually started
-                        mInteractionHandler.onGestureStarted();
-
-                        // Notify the system that we have started tracking the event
-                        if (mISystemUiProxy != null) {
-                            executeSafely(mISystemUiProxy::onRecentsAnimationStarted);
-                        }
+                        notifyGestureStarted();
                     }
                 } else {
                     // Move
@@ -182,6 +176,16 @@
         }
     }
 
+    private void notifyGestureStarted() {
+        // Notify the handler that the gesture has actually started
+        mInteractionHandler.onGestureStarted();
+
+        // Notify the system that we have started tracking the event
+        if (mISystemUiProxy != null) {
+            executeSafely(mISystemUiProxy::onRecentsAnimationStarted);
+        }
+    }
+
     private boolean isNavBarOnRight() {
         return mDisplayRotation == Surface.ROTATION_90 && mStableInsets.right > 0;
     }
@@ -263,7 +267,7 @@
         handler.setLauncherOnDrawCallback(() -> {
             drawWaitLock.countDown();
             if (handler == mInteractionHandler) {
-                switchToMainConsumer();
+                switchToMainChoreographer();
             }
         });
         handler.initWhenReady(mMainThreadExecutor);
@@ -346,6 +350,8 @@
 
     @Override
     public void updateTouchTracking(int interactionType) {
+        notifyGestureStarted();
+
         mMainThreadExecutor.execute(() -> {
             if (mInteractionHandler != null) {
                 mInteractionHandler.updateInteractionType(interactionType);
@@ -378,7 +384,7 @@
 
     public void onTouchTrackingComplete() { }
 
-    public void switchToMainConsumer() { }
+    public void switchToMainChoreographer() { }
 
     @Override
     public void preProcessMotionEvent(MotionEvent ev) {
diff --git a/quickstep/src/com/android/quickstep/QuickScrubController.java b/quickstep/src/com/android/quickstep/QuickScrubController.java
index 3e65ffe..7f9d3a1 100644
--- a/quickstep/src/com/android/quickstep/QuickScrubController.java
+++ b/quickstep/src/com/android/quickstep/QuickScrubController.java
@@ -32,6 +32,7 @@
 
     private static final int NUM_QUICK_SCRUB_SECTIONS = 5;
     private static final long AUTO_ADVANCE_DELAY = 500;
+    private static final int QUICKSCRUB_SNAP_DURATION_PER_PAGE = 325;
     private static final int QUICKSCRUB_END_SNAP_DURATION_PER_PAGE = 60;
 
     private Launcher mLauncher;
@@ -58,17 +59,22 @@
         if (mRecentsView == null) {
         } else {
             int page = mRecentsView.getNextPage();
-            // Settle on the page then launch it.
-            int snapDuration = Math.abs(page - mRecentsView.getPageNearestToCenterOfScreen())
-                    * QUICKSCRUB_END_SNAP_DURATION_PER_PAGE;
-            mRecentsView.snapToPage(page, snapDuration);
-            mRecentsView.postDelayed(() -> {
+            Runnable launchTaskRunnable = () -> {
                 if (page < mRecentsView.getFirstTaskIndex()) {
                     mRecentsView.getPageAt(page).performClick();
                 } else {
                     ((TaskView) mRecentsView.getPageAt(page)).launchTask(true);
                 }
-            }, snapDuration);
+            };
+            int snapDuration = Math.abs(page - mRecentsView.getPageNearestToCenterOfScreen())
+                    * QUICKSCRUB_END_SNAP_DURATION_PER_PAGE;
+            if (mRecentsView.snapToPage(page, snapDuration)) {
+                // Settle on the page then launch it
+                mRecentsView.setNextPageSwitchRunnable(launchTaskRunnable);
+            } else {
+                // No page move needed, just launch it
+                launchTaskRunnable.run();
+            }
         }
     }
 
@@ -92,7 +98,9 @@
 
     private void goToPageWithHaptic(int pageToGoTo) {
         if (pageToGoTo != mRecentsView.getNextPage()) {
-            mRecentsView.snapToPage(pageToGoTo);
+            int duration = Math.abs(pageToGoTo - mRecentsView.getNextPage())
+                    * QUICKSCRUB_SNAP_DURATION_PER_PAGE;
+            mRecentsView.snapToPage(pageToGoTo, duration);
             mRecentsView.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP,
                     HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
         }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java b/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java
new file mode 100644
index 0000000..9cc038f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java
@@ -0,0 +1,111 @@
+/*
+ * 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;
+
+import android.graphics.Rect;
+
+import com.android.launcher3.Utilities;
+
+/**
+ * Helper class to interpolate the animation between a task view representation and an actual
+ * window.
+ */
+public class RecentsAnimationInterpolator {
+
+    public static class TaskWindowBounds {
+        public float taskScale = 1f;
+        public float taskX = 0f;
+        public float taskY = 0f;
+
+        public float winScale = 1f;
+        public float winX = 0f;
+        public float winY = 0f;
+        public Rect winCrop = new Rect();
+
+        @Override
+        public String toString() {
+            return "taskScale=" + taskScale + " taskX=" + taskX + " taskY=" + taskY
+                    + " winScale=" + winScale + " winX=" + winX + " winY=" + winY
+                    + " winCrop=" + winCrop;
+        }
+    }
+
+    private TaskWindowBounds mTmpTaskWindowBounds = new TaskWindowBounds();
+    private Rect mTmpInsets = new Rect();
+
+    private Rect mWindow;
+    private Rect mInsetWindow;
+    private Rect mInsets;
+    private Rect mTask;
+    private Rect mTaskInsets;
+    private Rect mThumbnail;
+
+    private float mTaskScale;
+    private Rect mScaledTask;
+    private Rect mTargetTask;
+    private Rect mSrcWindow;
+
+    public RecentsAnimationInterpolator(Rect window, Rect insets, Rect task, Rect taskInsets) {
+        mWindow = window;
+        mInsets = insets;
+        mTask = task;
+        mTaskInsets = taskInsets;
+        mInsetWindow = new Rect(window);
+        Utilities.insetRect(mInsetWindow, insets);
+
+        mThumbnail = new Rect(task);
+        Utilities.insetRect(mThumbnail, taskInsets);
+        mTaskScale = (float) mInsetWindow.width() / mThumbnail.width();
+        mScaledTask = new Rect(task);
+        Utilities.scaleRectAboutCenter(mScaledTask, mTaskScale);
+        Rect finalScaledTaskInsets = new Rect(taskInsets);
+        Utilities.scaleRect(finalScaledTaskInsets, mTaskScale);
+        mTargetTask = new Rect(mInsetWindow);
+        mTargetTask.offsetTo(window.top + insets.top - finalScaledTaskInsets.top,
+                window.left + insets.left - finalScaledTaskInsets.left);
+
+        float initialWinScale = 1f / mTaskScale;
+        Rect scaledWindow = new Rect(mInsetWindow);
+        Utilities.scaleRectAboutCenter(scaledWindow, initialWinScale);
+        Rect scaledInsets = new Rect(insets);
+        Utilities.scaleRect(scaledInsets, initialWinScale);
+        mSrcWindow = new Rect(scaledWindow);
+        mSrcWindow.offsetTo(mThumbnail.left - scaledInsets.left,
+                mThumbnail.top - scaledInsets.top);
+    }
+
+    public TaskWindowBounds interpolate(float t) {
+        mTmpTaskWindowBounds.taskScale = Utilities.mapRange(t,
+                1, (float) mInsetWindow.width() / mThumbnail.width());
+        mTmpTaskWindowBounds.taskX = Utilities.mapRange(t,
+                0, mTargetTask.left - mScaledTask.left);
+        mTmpTaskWindowBounds.taskY = Utilities.mapRange(t,
+                0, mTargetTask.top - mScaledTask.top);
+
+        mTmpTaskWindowBounds.winScale = mTmpTaskWindowBounds.taskScale / mTaskScale;
+        mTmpTaskWindowBounds.winX = Utilities.mapRange(t,
+                mSrcWindow.left, 0);
+        mTmpTaskWindowBounds.winY = Utilities.mapRange(t,
+                mSrcWindow.top, 0);
+
+        mTmpInsets.set(mInsets);
+        Utilities.scaleRect(mTmpInsets, (1f - t));
+        mTmpTaskWindowBounds.winCrop.set(mWindow);
+        Utilities.insetRect(mTmpTaskWindowBounds.winCrop, mTmpInsets);
+
+        return mTmpTaskWindowBounds;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java
index 8e03f37..6b1f3d3 100644
--- a/quickstep/src/com/android/quickstep/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/RecentsView.java
@@ -75,6 +75,7 @@
     private boolean mOverviewStateEnabled;
     private boolean mTaskStackListenerRegistered;
     private LayoutTransition mLayoutTransition;
+    private Runnable mNextPageSwitchRunnable;
 
     /**
      * TODO: Call reloadIdNeeded in onTaskStackChanged.
@@ -215,6 +216,21 @@
         return mFirstTaskIndex;
     }
 
+    public boolean isTaskViewVisible(TaskView tv) {
+        // For now, just check if it's the active task
+        return indexOfChild(tv) == getNextPage();
+    }
+
+    public TaskView getTaskView(int taskId) {
+        for (int i = getFirstTaskIndex(); i < getChildCount(); i++) {
+            TaskView tv = (TaskView) getChildAt(i);
+            if (tv.getTask().key.id == taskId) {
+                return tv;
+            }
+        }
+        return null;
+    }
+
     public void setStateController(RecentsViewStateController stateController) {
         mStateController = stateController;
     }
@@ -228,6 +244,19 @@
         updateTaskStackListenerState();
     }
 
+    public void setNextPageSwitchRunnable(Runnable r) {
+        mNextPageSwitchRunnable = r;
+    }
+
+    @Override
+    protected void onPageEndTransition() {
+        super.onPageEndTransition();
+        if (mNextPageSwitchRunnable != null) {
+            mNextPageSwitchRunnable.run();
+            mNextPageSwitchRunnable = null;
+        }
+    }
+
     private void applyLoadPlan(RecentsTaskLoadPlan loadPlan) {
         final RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
         TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null;
@@ -254,11 +283,16 @@
         }
         setLayoutTransition(mLayoutTransition);
 
-        // Rebind all task views
+        // Rebind and reset all task views
         for (int i = tasks.size() - 1; i >= 0; i--) {
             final Task task = tasks.get(i);
             final TaskView taskView = (TaskView) getChildAt(tasks.size() - i - 1 + mFirstTaskIndex);
             taskView.bind(task);
+            taskView.setScaleX(1f);
+            taskView.setScaleY(1f);
+            taskView.setTranslationX(0f);
+            taskView.setTranslationY(0f);
+            taskView.setAlpha(1f);
             loader.loadTaskData(task);
         }
     }
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
index 36a0601..4f93b1c 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
@@ -28,6 +28,7 @@
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.PorterDuff.Mode;
+import android.graphics.Rect;
 import android.graphics.Shader;
 import android.util.AttributeSet;
 import android.view.View;
@@ -108,6 +109,13 @@
         updateThumbnailPaintFilter();
     }
 
+    public Rect getInsets() {
+        if (mThumbnailData != null) {
+            return mThumbnailData.insets;
+        }
+        return new Rect();
+    }
+
     @Override
     protected void onDraw(Canvas canvas) {
         canvas.drawRoundRect(0, 0, getMeasuredWidth(), getMeasuredHeight(),
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 6f3a8ff..5e89644 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -190,7 +190,7 @@
                 mHomeIntent, mISystemUiProxy, mMainThreadExecutor) {
 
             @Override
-            public void switchToMainConsumer() {
+            public void switchToMainChoreographer() {
                 if (mCurrentConsumer == this) {
                     mEventQueue.setInterimChoreographer(null);
                 }
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index 6082aea..dd0892b 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -41,6 +41,7 @@
 import android.os.Looper;
 import android.support.annotation.UiThread;
 import android.support.annotation.WorkerThread;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewTreeObserver.OnDrawListener;
 
@@ -65,8 +66,12 @@
 import com.android.systemui.shared.system.TransactionCompat;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 
+import java.util.StringJoiner;
+
 @TargetApi(Build.VERSION_CODES.O)
 public class WindowTransformSwipeHandler extends BaseSwipeInteractionHandler {
+    private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName();
+    private static final boolean DEBUG_STATES = false;
 
     // Launcher UI related states
     private static final int STATE_LAUNCHER_PRESENT = 1 << 0;
@@ -86,8 +91,21 @@
     private static final int LAUNCHER_UI_STATES =
             STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_ACTIVITY_MULTIPLIER_COMPLETE;
 
+    // For debugging, keep in sync with above states
+    private static final String[] STATES = new String[] {
+            "STATE_LAUNCHER_PRESENT",
+            "STATE_LAUNCHER_DRAWN",
+            "STATE_ACTIVITY_MULTIPLIER_COMPLETE",
+            "STATE_APP_CONTROLLER_RECEIVED",
+            "STATE_SCALED_CONTROLLER_RECENTS",
+            "STATE_SCALED_CONTROLLER_APP",
+            "STATE_HANDLER_INVALIDATED",
+            "STATE_GESTURE_STARTED"
+    };
+
     private static final long MAX_SWIPE_DURATION = 200;
     private static final long MIN_SWIPE_DURATION = 80;
+    private static final int QUICK_SWITCH_START_DURATION = 133;
     private static final int QUICK_SWITCH_SNAP_DURATION = 120;
 
     private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
@@ -142,6 +160,7 @@
 
     private @InteractionType int mInteractionType = INTERACTION_NORMAL;
     private boolean mStartedQuickScrubFromHome;
+    private boolean mDeferredQuickScrubEnd;
 
     private final RecentsAnimationWrapper mRecentsAnimationWrapper = new RecentsAnimationWrapper();
     private Matrix mTmpMatrix = new Matrix();
@@ -153,7 +172,13 @@
     }
 
     private void initStateCallbacks() {
-        mStateCallback = new MultiStateCallback();
+        mStateCallback = new MultiStateCallback() {
+            @Override
+            public void setState(int stateFlag) {
+                debugNewState(stateFlag);
+                super.setState(stateFlag);
+            }
+        };
         mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED,
                 this::initializeLauncherAnimationController);
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN,
@@ -352,9 +377,10 @@
 
     private void updateUiForQuickScrub() {
         mStartedQuickScrubFromHome = mWasLauncherAlreadyVisible;
+        mDeferredQuickScrubEnd = false;
         mQuickScrubController = mRecentsView.getQuickScrubController();
         mQuickScrubController.onQuickScrubStart(mStartedQuickScrubFromHome);
-        animateToProgress(1f, MAX_SWIPE_DURATION);
+        animateToProgress(1f, QUICK_SWITCH_START_DURATION);
         if (mStartedQuickScrubFromHome) {
             mLauncherLayoutListener.setVisibility(View.INVISIBLE);
         }
@@ -547,7 +573,11 @@
     }
 
     public void reset() {
-        setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+        if (mInteractionType != INTERACTION_QUICK_SCRUB) {
+            // Only invalidate the handler if we are not quick scrubbing, otherwise, it will be
+            // invalidated after the quick scrub ends
+            setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+        }
     }
 
     private void invalidateHandler() {
@@ -573,36 +603,45 @@
     }
 
     private void switchToScreenshot() {
+        synchronized (mRecentsAnimationWrapper) {
+            if (mRecentsAnimationWrapper.controller != null) {
+                TransactionCompat transaction = new TransactionCompat();
+                for (RemoteAnimationTargetCompat app : mRecentsAnimationWrapper.targets) {
+                    if (app.mode == MODE_CLOSING) {
+                        // Update the screenshot of the task
+                        final ThumbnailData thumbnail =
+                                mRecentsAnimationWrapper.controller.screenshotTask(app.taskId);
+                        mRecentsView.updateThumbnail(app.taskId, thumbnail);
+                    }
+                }
+                transaction.apply();
+            }
+        }
+        mRecentsAnimationWrapper.finish(true /* toHome */);
+
         if (mInteractionType == INTERACTION_QUICK_SWITCH) {
             for (int i = mRecentsView.getFirstTaskIndex(); i < mRecentsView.getPageCount(); i++) {
                 TaskView taskView = (TaskView) mRecentsView.getPageAt(i);
                 if (taskView.getTask().key.id != mRunningTaskId) {
-                    mRecentsView.snapToPage(i, QUICK_SWITCH_SNAP_DURATION);
-                    taskView.postDelayed(() -> {taskView.launchTask(true);},
-                            QUICK_SWITCH_SNAP_DURATION);
+                    Runnable launchTaskRunnable = () -> taskView.launchTask(true);
+                    if (mRecentsView.snapToPage(i, QUICK_SWITCH_SNAP_DURATION)) {
+                        // Snap to the new page then launch it
+                        mRecentsView.setNextPageSwitchRunnable(launchTaskRunnable);
+                    } else {
+                        // No need to move page, just launch task directly
+                        launchTaskRunnable.run();
+                    }
                     break;
                 }
             }
         } else if (mInteractionType == INTERACTION_QUICK_SCRUB) {
             if (mQuickScrubController != null) {
-                mQuickScrubController.snapToPageForCurrentQuickScrubSection();
-            }
-        } else {
-            synchronized (mRecentsAnimationWrapper) {
-                if (mRecentsAnimationWrapper.controller != null) {
-                    TransactionCompat transaction = new TransactionCompat();
-                    for (RemoteAnimationTargetCompat app : mRecentsAnimationWrapper.targets) {
-                        if (app.mode == MODE_CLOSING) {
-                            // Update the screenshot of the task
-                            final ThumbnailData thumbnail =
-                                    mRecentsAnimationWrapper.controller.screenshotTask(app.taskId);
-                            mRecentsView.updateThumbnail(app.taskId, thumbnail);
-                        }
-                    }
-                    transaction.apply();
+                if (mDeferredQuickScrubEnd) {
+                    onQuickScrubEnd();
+                } else {
+                    mQuickScrubController.snapToPageForCurrentQuickScrubSection();
                 }
             }
-            mRecentsAnimationWrapper.finish(true /* toHome */);
         }
     }
 
@@ -610,7 +649,6 @@
         // Re apply state in case we did something funky during the transition.
         mLauncher.getStateManager().reapplyState();
 
-
         // Animate ui the first icon.
         View currentRecentsPage = mRecentsView.getPageAt(mRecentsView.getCurrentPage());
         if (currentRecentsPage instanceof TaskView) {
@@ -619,11 +657,24 @@
     }
 
     public void onQuickScrubEnd() {
+        if ((mStateCallback.getState() & STATE_SCALED_CONTROLLER_RECENTS) == 0) {
+            // If we are still animating into recents, then defer until that has run to end
+            // quick scrub since we need to finish the window animation before launching the next
+            // task
+            mDeferredQuickScrubEnd = true;
+            return;
+        }
+
         if (mQuickScrubController != null) {
             mQuickScrubController.onQuickScrubEnd();
         } else {
             // TODO:
         }
+
+        // Normally this is handled in reset(), but since we are still scrubbing after the
+        // transition into recents, we need to defer the handler invalidation for quick scrub until
+        // after the gesture ends
+        setStateOnUiThread(STATE_HANDLER_INVALIDATED);
     }
 
     public void onQuickScrubProgress(float progress) {
@@ -633,4 +684,24 @@
             // TODO:
         }
     }
+
+    private synchronized void debugNewState(int stateFlag) {
+        if (!DEBUG_STATES) {
+            return;
+        }
+
+        int state = mStateCallback.getState();
+        StringJoiner currentStateStr = new StringJoiner(", ", "[", "]");
+        String stateFlagStr = "Unknown-" + stateFlag;
+        for (int i = 0; i < STATES.length; i++) {
+            if ((state & (i << i)) != 0) {
+                currentStateStr.add(STATES[i]);
+            }
+            if (stateFlag == (1 << i)) {
+                stateFlagStr = STATES[i] + " (" + stateFlag + ")";
+            }
+        }
+        Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding " + stateFlagStr + " to "
+                + currentStateStr);
+    }
 }
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 0ebae81..bb137b0 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -1622,7 +1622,7 @@
         return (float) Math.sin(f);
     }
 
-    protected void snapToPageWithVelocity(int whichPage, int velocity) {
+    protected boolean snapToPageWithVelocity(int whichPage, int velocity) {
         whichPage = validateNewPage(whichPage);
         int halfScreenSize = getMeasuredWidth() / 2;
 
@@ -1633,8 +1633,7 @@
         if (Math.abs(velocity) < mMinFlingVelocity) {
             // If the velocity is low enough, then treat this more as an automatic page advance
             // as opposed to an apparent physical response to flinging
-            snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
-            return;
+            return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
         }
 
         // Here we compute a "distance" that will be used in the computation of the overall
@@ -1653,39 +1652,39 @@
         // interpolator at zero, ie. 5. We use 4 to make it a little slower.
         duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
 
-        snapToPage(whichPage, delta, duration);
+        return snapToPage(whichPage, delta, duration);
     }
 
-    public void snapToPage(int whichPage) {
-        snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
+    public boolean snapToPage(int whichPage) {
+        return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
     }
 
-    public void snapToPageImmediately(int whichPage) {
-        snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION, true, null);
+    public boolean snapToPageImmediately(int whichPage) {
+        return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION, true, null);
     }
 
-    public void snapToPage(int whichPage, int duration) {
-        snapToPage(whichPage, duration, false, null);
+    public boolean snapToPage(int whichPage, int duration) {
+        return snapToPage(whichPage, duration, false, null);
     }
 
-    protected void snapToPage(int whichPage, int duration, TimeInterpolator interpolator) {
-        snapToPage(whichPage, duration, false, interpolator);
+    protected boolean snapToPage(int whichPage, int duration, TimeInterpolator interpolator) {
+        return snapToPage(whichPage, duration, false, interpolator);
     }
 
-    protected void snapToPage(int whichPage, int duration, boolean immediate,
+    protected boolean snapToPage(int whichPage, int duration, boolean immediate,
             TimeInterpolator interpolator) {
         whichPage = validateNewPage(whichPage);
 
         int newX = getScrollForPage(whichPage);
         final int delta = newX - getUnboundedScrollX();
-        snapToPage(whichPage, delta, duration, immediate, interpolator);
+        return snapToPage(whichPage, delta, duration, immediate, interpolator);
     }
 
-    protected void snapToPage(int whichPage, int delta, int duration) {
-        snapToPage(whichPage, delta, duration, false, null);
+    protected boolean snapToPage(int whichPage, int delta, int duration) {
+        return snapToPage(whichPage, delta, duration, false, null);
     }
 
-    protected void snapToPage(int whichPage, int delta, int duration, boolean immediate,
+    protected boolean snapToPage(int whichPage, int delta, int duration, boolean immediate,
             TimeInterpolator interpolator) {
         whichPage = validateNewPage(whichPage);
 
@@ -1723,6 +1722,7 @@
         }
 
         invalidate();
+        return Math.abs(delta) > 0;
     }
 
     public void scrollLeft() {
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 158c540..d559b44 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -237,16 +237,27 @@
             int cx = r.centerX();
             int cy = r.centerY();
             r.offset(-cx, -cy);
+            scaleRect(r, scale);
+            r.offset(cx, cy);
+        }
+    }
 
+    public static void scaleRect(Rect r, float scale) {
+        if (scale != 1.0f) {
             r.left = (int) (r.left * scale + 0.5f);
             r.top = (int) (r.top * scale + 0.5f);
             r.right = (int) (r.right * scale + 0.5f);
             r.bottom = (int) (r.bottom * scale + 0.5f);
-
-            r.offset(cx, cy);
         }
     }
 
+    public static void insetRect(Rect r, Rect insets) {
+        r.left = Math.min(r.right, r.left + insets.left);
+        r.top = Math.min(r.bottom, r.top + insets.top);
+        r.right = Math.max(r.left, r.right - insets.right);
+        r.bottom = Math.max(r.top, r.bottom - insets.bottom);
+    }
+
     public static float shrinkRect(Rect r, float scaleX, float scaleY) {
         float scale = Math.min(Math.min(scaleX, scaleY), 1.0f);
         if (scale < 1.0f) {
@@ -261,6 +272,10 @@
         return scale;
     }
 
+    public static float mapRange(float value, float min, float max) {
+        return min + (value * (max - min));
+    }
+
     public static boolean isSystemApp(Context context, Intent intent) {
         PackageManager pm = context.getPackageManager();
         ComponentName cn = intent.getComponent();
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index 2343654..0dcebe3 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -50,6 +50,9 @@
 
     public static final Interpolator OVERSHOOT_0 = new OvershootInterpolator(0);
 
+    public static final Interpolator TOUCH_RESPONSE_INTERPOLATOR =
+            new PathInterpolator(0.3f, 0f, 0.1f, 1f);
+
     /**
      * Inversion of zInterpolate, compounded with an ease-out.
      */