Merge "Various swipe up animation optimizations" into ub-launcher3-master
diff --git a/quickstep/res/layout/overview_panel.xml b/quickstep/res/layout/overview_panel.xml
index a8b91c5..22f014a 100644
--- a/quickstep/res/layout/overview_panel.xml
+++ b/quickstep/res/layout/overview_panel.xml
@@ -19,7 +19,6 @@
     android:theme="@style/HomeScreenElementTheme"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:layout_gravity="center"
     android:clipChildren="false"
     android:clipToPadding="false"
     android:alpha="0.0"
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index fdf1adc..839d934 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -23,14 +23,14 @@
         android:id="@+id/snapshot"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_marginTop="24dp"
+        android:layout_marginTop="@dimen/task_thumbnail_top_margin"
         android:scaleType="matrix"
         android:background="@drawable/task_thumbnail_background"
         android:elevation="4dp" />
     <ImageView
         android:id="@+id/icon"
-        android:layout_width="48dp"
-        android:layout_height="48dp"
+        android:layout_width="@dimen/task_thumbnail_icon_size"
+        android:layout_height="@dimen/task_thumbnail_icon_size"
         android:layout_gravity="top|center_horizontal"
         android:elevation="5dp"/>
 </com.android.quickstep.TaskView>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
new file mode 100644
index 0000000..4f85957
--- /dev/null
+++ b/quickstep/res/values/dimens.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+
+<resources>
+
+    <dimen name="task_thumbnail_top_margin">24dp</dimen>
+    <dimen name="task_thumbnail_icon_size">48dp</dimen>
+
+    <dimen name="quickstep_fling_threshold_velocity">500dp</dimen>
+    <dimen name="quickstep_fling_min_velocity">250dp</dimen>
+
+</resources>
diff --git a/quickstep/src/com/android/quickstep/AnimatedFloat.java b/quickstep/src/com/android/quickstep/AnimatedFloat.java
new file mode 100644
index 0000000..1f6781e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/AnimatedFloat.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2017 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.util.FloatProperty;
+
+/**
+ * A mutable float which allows animating the value
+ */
+public class AnimatedFloat {
+
+    public static FloatProperty<AnimatedFloat> VALUE = new FloatProperty<AnimatedFloat>("value") {
+        @Override
+        public void setValue(AnimatedFloat obj, float v) {
+            obj.updateValue(v);
+        }
+
+        @Override
+        public Float get(AnimatedFloat obj) {
+            return obj.value;
+        }
+    };
+
+    private final Runnable mUpdateCallback;
+    private ObjectAnimator mValueAnimator;
+
+    public float value;
+
+    public AnimatedFloat(Runnable updateCallback) {
+        mUpdateCallback = updateCallback;
+    }
+
+    public ObjectAnimator animateToValue(float v) {
+        if (mValueAnimator != null) {
+            mValueAnimator.cancel();
+        }
+        mValueAnimator = ObjectAnimator.ofFloat(this, VALUE, v);
+        mValueAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                if (mValueAnimator == animator) {
+                    mValueAnimator = null;
+                }
+            }
+        });
+        return mValueAnimator;
+    }
+
+    /**
+     * Changes the value and calls the callback.
+     * Note that the value can be directly accessed as well to avoid notifying the callback.
+     */
+    public void updateValue(float v) {
+        if (Float.compare(v, value) != 0) {
+            value = v;
+            mUpdateCallback.run();
+        }
+    }
+
+    public ObjectAnimator getCurrentAnimation() {
+        return mValueAnimator;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/MultiStateCallback.java b/quickstep/src/com/android/quickstep/MultiStateCallback.java
new file mode 100644
index 0000000..cca2729
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/MultiStateCallback.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017 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.util.SparseArray;
+
+/**
+ * Utility class to help manage multiple callbacks based on different states.
+ */
+public class MultiStateCallback {
+
+    private final SparseArray<Runnable> mCallbacks = new SparseArray<>();
+
+    private int mState = 0;
+
+    /**
+     * Adds the provided state flags to the global state and executes any callbacks as a result.
+     * @param stateFlag
+     */
+    public void setState(int stateFlag) {
+        mState = mState | stateFlag;
+
+        int count = mCallbacks.size();
+        for (int i = 0; i < count; i++) {
+            int state = mCallbacks.keyAt(i);
+
+            if ((mState & state) == state) {
+                Runnable callback = mCallbacks.valueAt(i);
+                if (callback != null) {
+                    // Set the callback to null, so that it does not run again.
+                    mCallbacks.setValueAt(i, null);
+                    callback.run();
+                }
+            }
+        }
+    }
+
+    /**
+     * Sets the callbacks to be run when the provided states are enabled.
+     * The callback is only run once.
+     */
+    public void addCallback(int stateMask, Runnable callback) {
+        mCallbacks.put(stateMask, callback);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
index 34e3c4e..dc7d648 100644
--- a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
+++ b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
@@ -20,52 +20,57 @@
 import android.animation.RectEvaluator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Rect;
 import android.os.Build;
+import android.os.Handler;
+import android.os.UserHandle;
 import android.support.annotation.BinderThread;
 import android.support.annotation.UiThread;
-import android.util.FloatProperty;
+import android.util.DisplayMetrics;
 import android.view.Choreographer;
 import android.view.Choreographer.FrameCallback;
 import android.view.View;
-import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnPreDrawListener;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.states.InternalStateHandler;
 import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 @TargetApi(Build.VERSION_CODES.O)
 public class NavBarSwipeInteractionHandler extends InternalStateHandler implements FrameCallback {
 
-    private static FloatProperty<NavBarSwipeInteractionHandler> SHIFT =
-            new FloatProperty<NavBarSwipeInteractionHandler>("currentShift") {
-        @Override
-        public void setValue(NavBarSwipeInteractionHandler handler, float v) {
-            handler.setShift(v);
-        }
+    private static final int STATE_LAUNCHER_READY = 1 << 0;
+    private static final int STATE_RECENTS_DELAY_COMPLETE = 1 << 1;
+    private static final int STATE_LOAD_PLAN_READY = 1 << 2;
+    private static final int STATE_RECENTS_FULLY_VISIBLE = 1 << 3;
+    private static final int STATE_ACTIVITY_MULTIPLIER_COMPLETE = 1 << 4;
+    private static final int STATE_SCALED_SNAPSHOT_RECENTS = 1 << 5;
+    private static final int STATE_SCALED_SNAPSHOT_APP = 1 << 6;
 
-        @Override
-        public Float get(NavBarSwipeInteractionHandler handler) {
-            return handler.mCurrentShift;
-        }
-    };
+    private static final long RECENTS_VIEW_VISIBILITY_DELAY = 120;
+    private static final long RECENTS_VIEW_VISIBILITY_DURATION = 150;
+    private static final long DEFAULT_SWIPE_DURATION = 200;
 
-    // The following constants need to be scaled based on density. The scaled versions will be
-    // assigned to the corresponding member variables below.
-    private static final int FLING_THRESHOLD_VELOCITY = 500;
-    private static final int MIN_FLING_VELOCITY = 250;
+    // Ideal velocity for a smooth transition
+    private static final float PIXEL_PER_MS = 2f;
 
     private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
 
@@ -75,43 +80,102 @@
     private final Rect mCurrentRect = new Rect();
     private final RectEvaluator mRectEvaluator = new RectEvaluator(mCurrentRect);
 
-    private final Bitmap mTaskSnapshot;
-    private final int mRunningTaskId;
-    private Future<RecentsTaskLoadPlan> mFutureLoadPlan;
-
-    private Launcher mLauncher;
-    private Choreographer mChoreographer;
-    private SnapshotDragView mDragView;
-    private RecentsView mRecentsView;
-    private Hotseat mHotseat;
-
-    private float mStartDelta;
-    private float mLastDelta;
-
     // Shift in the range of [0, 1].
     // 0 => preview snapShot is completely visible, and hotseat is completely translated down
     // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
     // visible.
-    private float mCurrentShift;
+    private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
+
+    // Activity multiplier in the range of [0, 1]. When the activity becomes visible, this is
+    // animated to 1, so allow for a smooth transition.
+    private final AnimatedFloat mActivityMultiplier = new AnimatedFloat(this::updateFinalShift);
+
+    private final Choreographer mChoreographer;
+    private final AtomicBoolean mFrameScheduled = new AtomicBoolean(false);
+
+    private final int mRunningTaskId;
+    private final Context mContext;
+
+    private final MultiStateCallback mStateCallback;
+
+    private Launcher mLauncher;
+    private SnapshotDragView mDragView;
+    private RecentsView mRecentsView;
+    private Hotseat mHotseat;
+    private RecentsTaskLoadPlan mLoadPlan;
+
+    private boolean mLauncherReady;
+    private boolean mTouchEndHandled;
+
+    private Bitmap mTaskSnapshot;
 
     // These are updated on the binder thread, and eventually picked up on doFrame
     private volatile float mCurrentDisplacement;
     private volatile float mEndVelocity;
     private volatile boolean mTouchEnded = false;
 
-    NavBarSwipeInteractionHandler(Bitmap taskSnapShot, RunningTaskInfo runningTaskInfo) {
-        mTaskSnapshot = taskSnapShot;
+    NavBarSwipeInteractionHandler(
+            RunningTaskInfo runningTaskInfo, Choreographer choreographer, Context context) {
         mRunningTaskId = runningTaskInfo.id;
+        mChoreographer = choreographer;
+        mContext = context;
         WindowManagerWrapper.getInstance().getStableInsets(mStableInsets);
+
+        // Build the state callback
+        mStateCallback = new MultiStateCallback();
+        mStateCallback.addCallback(STATE_LAUNCHER_READY, this::onLauncherReady);
+        mStateCallback.addCallback(STATE_LOAD_PLAN_READY | STATE_RECENTS_DELAY_COMPLETE,
+                this::setTaskPlanToUi);
+        mStateCallback.addCallback(STATE_SCALED_SNAPSHOT_APP, this::resumeLastTask);
+        mStateCallback.addCallback(STATE_RECENTS_FULLY_VISIBLE | STATE_SCALED_SNAPSHOT_RECENTS
+                | STATE_ACTIVITY_MULTIPLIER_COMPLETE,
+                this::onAnimationToLauncherComplete);
+        mStateCallback.addCallback(STATE_LAUNCHER_READY | STATE_SCALED_SNAPSHOT_APP,
+                this::cleanupLauncher);
+    }
+
+    private void onLauncherReady() {
+        mLauncherReady = true;
+        executeFrameUpdate();
+
+        // Wait for some time before loading recents so that the first frame is fast
+        new Handler().postDelayed(() -> mStateCallback.setState(STATE_RECENTS_DELAY_COMPLETE),
+                RECENTS_VIEW_VISIBILITY_DELAY);
+
+        long duration = Math.min(DEFAULT_SWIPE_DURATION,
+                Math.max((long) (-mCurrentDisplacement / PIXEL_PER_MS), 0));
+        if (mCurrentShift.getCurrentAnimation() != null) {
+            ObjectAnimator anim = mCurrentShift.getCurrentAnimation();
+            long theirDuration = anim.getDuration() - anim.getCurrentPlayTime();
+
+            // TODO: Find a better heuristic
+            duration = (duration + theirDuration) / 2;
+        }
+        ObjectAnimator anim = mActivityMultiplier.animateToValue(1)
+                .setDuration(duration);
+        anim.addListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE);
+            }
+        });
+        anim.start();
+    }
+
+    public void setTaskSnapshot(Bitmap taskSnapshot) {
+        mTaskSnapshot = taskSnapshot;
     }
 
     @Override
     public void onLauncherResume() {
-        mStartDelta = mCurrentDisplacement;
-        mLastDelta = mStartDelta;
-        mChoreographer = Choreographer.getInstance();
-
-        scheduleNextFrame();
+        mDragView.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
+            @Override
+            public boolean onPreDraw() {
+                mDragView.getViewTreeObserver().removeOnPreDrawListener(this);
+                mStateCallback.setState(STATE_LAUNCHER_READY);
+                return true;
+            }
+        });
     }
 
     @Override
@@ -129,9 +193,7 @@
 
         // Optimization
         mLauncher.getAppsView().setVisibility(View.GONE);
-
-        // Launch overview, animate if already on home
-        mRecentsView.update(consumeLastLoadPlan());
+        mRecentsView.setVisibility(View.GONE);
     }
 
     /**
@@ -143,121 +205,131 @@
     @BinderThread
     public void updateDisplacement(float displacement) {
         mCurrentDisplacement = displacement;
+        scheduleFrameIfNeeded();
     }
 
     @BinderThread
     public void endTouch(float endVelocity) {
         mEndVelocity = endVelocity;
         mTouchEnded = true;
+        scheduleFrameIfNeeded();
     }
 
-    @UiThread
-    private void scheduleNextFrame() {
-        if (!mTouchEnded) {
+    private void scheduleFrameIfNeeded() {
+        boolean alreadyScheduled = mFrameScheduled.getAndSet(true);
+        if (!alreadyScheduled) {
+            // TODO: Here we might end up scheduling one additional frame in some race conditions.
+            // This can be avoided by synchronising postFrameCallback as well
             mChoreographer.postFrameCallback(this);
-        } else {
-            animateToFinalShift();
         }
     }
 
     @Override
     public void doFrame(long l) {
-        mLastDelta = mCurrentDisplacement;
+        mFrameScheduled.set(false);
+        executeFrameUpdate();
+    }
 
-        float translation = Utilities.boundToRange(mStartDelta - mLastDelta, 0,
-                mHotseat.getHeight());
-        int hotseatHeight = mHotseat.getHeight();
-        float shift = hotseatHeight == 0 ? 0 : translation / hotseatHeight;
-        setShift(shift);
-        scheduleNextFrame();
+    private void executeFrameUpdate() {
+        if (mLauncherReady) {
+            final float displacement = -mCurrentDisplacement;
+            int hotseatHeight = mHotseat.getHeight();
+            float translation = Utilities.boundToRange(displacement, 0, hotseatHeight);
+            float shift = hotseatHeight == 0 ? 0 : translation / hotseatHeight;
+            mCurrentShift.updateValue(shift);
+        }
+
+        if (mTouchEnded) {
+            if (mTouchEndHandled) {
+                return;
+            }
+            mTouchEndHandled = true;
+            animateToFinalShift();
+        }
     }
 
     @UiThread
-    private void setShift(float shift) {
+    private void updateFinalShift() {
+        if (!mLauncherReady) {
+            return;
+        }
+
         if (mTargetRect.isEmpty()) {
             DragLayer dl = mLauncher.getDragLayer();
-
-            // Init target rect.
-            View targetView = ((ViewGroup) mRecentsView.getChildAt(0)).getChildAt(0);
-            dl.getViewRectRelativeToSelf(targetView, mTargetRect);
-            mTargetRect.right = mTargetRect.left + mTargetRect.width();
-            mTargetRect.bottom = mTargetRect.top + mTargetRect.height();
             mSourceRect.set(0, 0, dl.getWidth(), dl.getHeight());
+            Rect targetPadding = RecentsView.getPadding(mLauncher);
+            Rect insets = dl.getInsets();
+            mTargetRect.set(
+                    targetPadding.left + insets.left,
+                    targetPadding.top + insets.top,
+                    mSourceRect.right - targetPadding.right - insets.right,
+                    mSourceRect.bottom - targetPadding.bottom - insets.bottom);
+            mTargetRect.top += mLauncher.getResources()
+                    .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
         }
 
-        if (!mSourceRect.isEmpty()) {
-            mCurrentShift = shift;
-            int hotseatHeight = mHotseat.getHeight();
-            mHotseat.setTranslationY((1 - shift) * hotseatHeight);
+        float shift = mCurrentShift.value * mActivityMultiplier.value;
+        int hotseatHeight = mHotseat.getHeight();
 
-            mRectEvaluator.evaluate(shift, mSourceRect, mTargetRect);
+        mHotseat.setTranslationY((1 - shift) * hotseatHeight);
 
-            float scale = (float) mCurrentRect.width() / mSourceRect.width();
-            mDragView.setTranslationX(mCurrentRect.left - mStableInsets.left * scale * shift);
-            mDragView.setTranslationY(mCurrentRect.top - mStableInsets.top * scale * shift);
-            mDragView.setScaleX(scale);
-            mDragView.setScaleY(scale);
-            mDragView.getViewBounds().setClipTop((int) (mStableInsets.top * shift));
-            mDragView.getViewBounds().setClipBottom((int) (mStableInsets.bottom * shift));
-        }
+        mRectEvaluator.evaluate(shift, mSourceRect, mTargetRect);
+
+        float scale = (float) mCurrentRect.width() / mSourceRect.width();
+        mDragView.setTranslationX(mCurrentRect.left - mStableInsets.left * scale * shift);
+        mDragView.setTranslationY(mCurrentRect.top - mStableInsets.top * scale * shift);
+        mDragView.setScaleX(scale);
+        mDragView.setScaleY(scale);
+        mDragView.getViewBounds().setClipTop((int) (mStableInsets.top * shift));
+        mDragView.getViewBounds().setClipBottom((int) (mStableInsets.bottom * shift));
     }
 
-    void setLastLoadPlan(Future<RecentsTaskLoadPlan> futureLoadPlan) {
-        if (mFutureLoadPlan != null) {
-            mFutureLoadPlan.cancel(true);
-        }
-        mFutureLoadPlan = futureLoadPlan;
+    @UiThread
+    public void setRecentsTaskLoadPlan(RecentsTaskLoadPlan loadPlan) {
+        mLoadPlan = loadPlan;
+        mStateCallback.setState(STATE_LOAD_PLAN_READY);
     }
 
-    private RecentsTaskLoadPlan consumeLastLoadPlan() {
-        try {
-            if (mFutureLoadPlan != null) {
-                return mFutureLoadPlan.get();
-            }
-        } catch (InterruptedException | ExecutionException e) {
-            e.printStackTrace();
-        } finally {
-            mFutureLoadPlan = null;
-        }
-        return null;
+    private void setTaskPlanToUi() {
+        mRecentsView.update(mLoadPlan);
+        mRecentsView.setVisibility(View.VISIBLE);
+
+        // Animate alpha
+        mRecentsView.setAlpha(0);
+        mRecentsView.animate().alpha(1).setDuration(RECENTS_VIEW_VISIBILITY_DURATION)
+                .withEndAction(() -> mStateCallback.setState(STATE_RECENTS_FULLY_VISIBLE));
     }
 
     @UiThread
     private void animateToFinalShift() {
-        float flingThreshold = Utilities.pxFromDp(FLING_THRESHOLD_VELOCITY,
-                    mLauncher.getResources().getDisplayMetrics());
+        Resources res = mContext.getResources();
+        float flingThreshold = res.getDimension(R.dimen.quickstep_fling_threshold_velocity);
         boolean isFling = Math.abs(mEndVelocity) > flingThreshold;
 
-        long duration = 200;
+        long duration = DEFAULT_SWIPE_DURATION;
         final float endShift;
         if (!isFling) {
-            endShift = mCurrentShift >= MIN_PROGRESS_FOR_OVERVIEW ? 1 : 0;
+            endShift = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW ? 1 : 0;
         } else {
             endShift = mEndVelocity < 0 ? 1 : 0;
-            float minFlingVelocity = Utilities.pxFromDp(MIN_FLING_VELOCITY,
-                    mLauncher.getResources().getDisplayMetrics());
-            if (Math.abs(mEndVelocity) > minFlingVelocity) {
-                float distanceToTravel = (endShift - mCurrentShift) * mHotseat.getHeight();
+            float minFlingVelocity = res.getDimension(R.dimen.quickstep_fling_min_velocity);
+            if (Math.abs(mEndVelocity) > minFlingVelocity && mLauncherReady) {
+                float distanceToTravel = (endShift - mCurrentShift.value) * mHotseat.getHeight();
 
                 // we want the page's snap velocity to approximately match the velocity at
                 // which the user flings, so we scale the duration by a value near to the
-                // derivative of the scroll interpolator at zero, ie. 5. We use 4 to make
-                // it a little slower.
-                duration = 4 * Math.round(1000 * Math.abs(distanceToTravel / mEndVelocity));
+                // derivative of the scroll interpolator at zero, ie. 5.
+                duration = 5 * Math.round(1000 * Math.abs(distanceToTravel / mEndVelocity));
             }
         }
 
-        ObjectAnimator anim = ObjectAnimator.ofFloat(this, SHIFT, endShift)
-                .setDuration(duration);
+        ObjectAnimator anim = mCurrentShift.animateToValue(endShift).setDuration(duration);
         anim.setInterpolator(Interpolators.SCROLL);
         anim.addListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationSuccess(Animator animator) {
-                if (Float.compare(mCurrentShift, 0) == 0) {
-                    resumeLastTask();
-                } else {
-                    mDragView.close(false);
-                }
+                mStateCallback.setState((Float.compare(mCurrentShift.value, 0) == 0)
+                        ? STATE_SCALED_SNAPSHOT_APP : STATE_SCALED_SNAPSHOT_RECENTS);
             }
         });
         anim.start();
@@ -265,12 +337,30 @@
 
     @UiThread
     private void resumeLastTask() {
+        TaskKey key = null;
+        if (mLoadPlan != null) {
+            Task task = mLoadPlan.getTaskStack().findTaskWithId(mRunningTaskId);
+            if (task != null) {
+                key = task.key;
+            }
+        }
+
+        if (key == null) {
+            // TODO: We need a better way for this
+            key = new TaskKey(mRunningTaskId, 0, null, UserHandle.myUserId(), 0);
+        }
+
+        ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
+        ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(key, opts, null, null);
+    }
+
+    private void cleanupLauncher() {
         // TODO: These should be done as part of ActivityOptions#OnAnimationStarted
         mHotseat.setTranslationY(0);
         mLauncher.setOnResumeCallback(() -> mDragView.close(false));
+    }
 
-        // TODO: For now, assume that the task stack will have loaded in the bg, will update
-        // the lib api later for direct call
-        mRecentsView.launchTaskWithId(mRunningTaskId);
+    private void onAnimationToLauncherComplete() {
+        mDragView.close(false);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java
index 528b11d..675f456 100644
--- a/quickstep/src/com/android/quickstep/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/RecentsView.java
@@ -75,17 +75,7 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
 
-        // TODO: These are rough calculations which currently use the stable insets
-        DeviceProfile profile = Launcher.getLauncher(getContext()).getDeviceProfile();
-        Rect stableInsets = new Rect();
-        WindowManagerWrapper.getInstance().getStableInsets(stableInsets);
-        Rect padding = profile.getWorkspacePadding(null);
-        float taskWidth = profile.getCurrentWidth() - stableInsets.left - stableInsets.right;
-        float taskHeight = profile.getCurrentHeight() - stableInsets.top - stableInsets.bottom;
-        float overviewHeight = profile.availableHeightPx - padding.top - padding.bottom
-                - stableInsets.top;
-        float overviewWidth = taskWidth * overviewHeight / taskHeight;
-        padding.left = padding.right = (int) ((profile.availableWidthPx - overviewWidth) / 2);
+        Rect padding = getPadding(Launcher.getLauncher(getContext()));
         setPadding(padding.left, padding.top, padding.right, padding.bottom);
     }
 
@@ -166,4 +156,18 @@
             mTaskStackListenerRegistered = registerStackListener;
         }
     }
+
+    public static Rect getPadding(Launcher launcher) {
+        DeviceProfile profile = launcher.getDeviceProfile();
+        Rect stableInsets = new Rect();
+        WindowManagerWrapper.getInstance().getStableInsets(stableInsets);
+        Rect padding = profile.getWorkspacePadding(null);
+        float taskWidth = profile.getCurrentWidth() - stableInsets.left - stableInsets.right;
+        float taskHeight = profile.getCurrentHeight() - stableInsets.top - stableInsets.bottom;
+        float overviewHeight = profile.availableHeightPx - padding.top - padding.bottom
+                - stableInsets.top;
+        float overviewWidth = taskWidth * overviewHeight / taskHeight;
+        padding.left = padding.right = (int) ((profile.availableWidthPx - overviewWidth) / 2);
+        return padding;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 1ff29cf..50f5528 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -33,12 +33,14 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.Log;
+import android.view.Choreographer;
 import android.view.Display;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
 
+import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.R;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
@@ -47,8 +49,6 @@
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.BackgroundExecutor;
 
-import java.util.concurrent.Future;
-
 /**
  * Service connected by system-UI for handling touch interaction.
  */
@@ -75,6 +75,8 @@
     private RunningTaskInfo mRunningTask;
     private Intent mHomeIntent;
     private ComponentName mLauncher;
+    private Choreographer mChoreographer;
+    private MainThreadExecutor mMainThreadExecutor;
 
     private int mDisplayRotation;
     private final Point mDisplaySize = new Point();
@@ -107,6 +109,9 @@
                     res.getInteger(R.integer.config_recentsMaxIconCacheSize), 0);
             sRecentsTaskLoader.startLoader(this);
         }
+
+        mChoreographer = Choreographer.getInstance();
+        mMainThreadExecutor = new MainThreadExecutor();
     }
 
     @Override
@@ -196,28 +201,34 @@
 
     private void startTouchTracking() {
         // Create the shared handler
-        mInteractionHandler = new NavBarSwipeInteractionHandler(getCurrentTaskSnapshot(),
-                mRunningTask);
+        final NavBarSwipeInteractionHandler handler =
+                new NavBarSwipeInteractionHandler(mRunningTask, mChoreographer, this);
 
         // Preload and start the recents activity on a background thread
         final Context context = this;
         final RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(context);
-        Future<RecentsTaskLoadPlan> loadPlanFuture = BackgroundExecutor.get().submit(() -> {
-            // Preload the plan
-            RecentsTaskLoader loader = TouchInteractionService.getRecentsTaskLoader();
-            loadPlan.preloadPlan(loader, mRunningTask.id, UserHandle.myUserId());
+        final int taskId = mRunningTask.id;
 
-            // Start the activity with our custom handler
-            Intent homeIntent = mInteractionHandler.addToIntent(new Intent(mHomeIntent));
+        BackgroundExecutor.get().submit(() -> {
+            // Get the snap shot before
+            handler.setTaskSnapshot(getCurrentTaskSnapshot());
+
+            // Start the launcher activity with our custom handler
+            Intent homeIntent = handler.addToIntent(new Intent(mHomeIntent));
             startActivity(homeIntent, ActivityOptions.makeCustomAnimation(this, 0, 0).toBundle());
             /*
             ActivityManagerWrapper.getInstance().startRecentsActivity(null, options,
                     ActivityOptions.makeCustomAnimation(this, 0, 0), UserHandle.myUserId(),
                     null, null);
              */
-        }, loadPlan);
 
-        mInteractionHandler.setLastLoadPlan(loadPlanFuture);
+            // Preload the plan
+            RecentsTaskLoader loader = TouchInteractionService.getRecentsTaskLoader();
+            loadPlan.preloadPlan(loader, taskId, UserHandle.myUserId());
+            // Set the load plan on UI thread
+            mMainThreadExecutor.execute(() -> handler.setRecentsTaskLoadPlan(loadPlan));
+        });
+        mInteractionHandler = handler;
     }
 
     private void endInteraction() {