Merge "Do not reset the previous state animation, if it is a part of the new state animaiton" into ub-launcher3-master
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 74d455f..1620352 100644
--- a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -19,6 +19,13 @@
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
+import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE;
+import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
+import static com.android.launcher3.anim.Interpolators.APP_CLOSE_ALPHA;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.quickstep.TaskUtils.findTaskViewToLaunch;
+import static com.android.quickstep.TaskUtils.getRecentsWindowAnimator;
+import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
 import static com.android.systemui.shared.recents.utilities.Utilities.getNextFrameNumber;
 import static com.android.systemui.shared.recents.utilities.Utilities.getSurface;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
@@ -31,7 +38,6 @@
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityOptions;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
@@ -45,7 +51,6 @@
 import android.view.Surface;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.animation.Interpolator;
 
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.InsettableFrameLayout.LayoutParams;
@@ -55,12 +60,10 @@
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.shortcuts.DeepShortcutView;
-import com.android.quickstep.RecentsAnimationInterpolator;
-import com.android.quickstep.RecentsAnimationInterpolator.TaskWindowBounds;
+import com.android.quickstep.util.MultiValueUpdateListener;
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityCompat;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
 import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
@@ -79,7 +82,7 @@
         implements OnDeviceProfileChangeListener {
 
     private static final String TAG = "LauncherTransition";
-    private static final int STATUS_BAR_TRANSITION_DURATION = 120;
+    public static final int STATUS_BAR_TRANSITION_DURATION = 120;
 
     private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
             "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS";
@@ -87,7 +90,7 @@
     private static final int APP_LAUNCH_DURATION = 500;
     // Use a shorter duration for x or y translation to create a curve effect
     private static final int APP_LAUNCH_CURVED_DURATION = 233;
-    private static final int RECENTS_LAUNCH_DURATION = 336;
+    public static final int RECENTS_LAUNCH_DURATION = 336;
     private static final int LAUNCHER_RESUME_START_DELAY = 100;
     private static final int CLOSING_TRANSITION_DURATION_MS = 350;
 
@@ -185,64 +188,6 @@
     }
 
     /**
-     * Try to find a TaskView that corresponds with the component of the launched view.
-     *
-     * If this method returns a non-null TaskView, it will be used in composeRecentsLaunchAnimation.
-     * Otherwise, we will assume we are using a normal app transition, but it's possible that the
-     * opening remote target (which we don't get until onAnimationStart) will resolve to a TaskView.
-     */
-    private TaskView findTaskViewToLaunch(
-            BaseDraggingActivity activity, View v, RemoteAnimationTargetCompat[] targets) {
-        if (v instanceof TaskView) {
-            return (TaskView) v;
-        }
-        RecentsView recentsView = activity.getOverviewPanel();
-
-        // It's possible that the launched view can still be resolved to a visible task view, check
-        // the task id of the opening task and see if we can find a match.
-        if (v.getTag() instanceof ItemInfo) {
-            ItemInfo itemInfo = (ItemInfo) v.getTag();
-            ComponentName componentName = itemInfo.getTargetComponent();
-            if (componentName != null) {
-                for (int i = 0; i < recentsView.getChildCount(); i++) {
-                    TaskView taskView = (TaskView) recentsView.getPageAt(i);
-                    if (recentsView.isTaskViewVisible(taskView)) {
-                        Task task = taskView.getTask();
-                        if (componentName.equals(task.key.getComponent())) {
-                            return taskView;
-                        }
-                    }
-                }
-            }
-        }
-
-        if (targets == null) {
-            return null;
-        }
-        // Resolve the opening task id
-        int openingTaskId = -1;
-        for (RemoteAnimationTargetCompat target : targets) {
-            if (target.mode == 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
-        TaskView taskView = recentsView.getTaskView(openingTaskId);
-        if (taskView == null || !recentsView.isTaskViewVisible(taskView)) {
-            return null;
-        }
-        return taskView;
-    }
-
-    /**
      * Composes the animations for a launch from the recents list if possible.
      */
     private boolean composeRecentsLaunchAnimator(View v,
@@ -287,7 +232,8 @@
             };
         }
 
-        target.play(getRecentsWindowAnimator(taskView, skipLauncherChanges, targets));
+        target.play(getRecentsWindowAnimator(taskView, skipLauncherChanges, targets)
+                .setDuration(RECENTS_LAUNCH_DURATION));
         target.play(launcherAnim);
 
         // Set the current animation first, before adding windowAnimEndListener. Setting current
@@ -299,83 +245,6 @@
     }
 
     /**
-     * @return Animator that controls the window of the opening targets for the recents launch
-     * animation.
-     */
-    private ValueAnimator getRecentsWindowAnimator(TaskView v, boolean skipLauncherChanges,
-            RemoteAnimationTargetCompat[] targets) {
-        final RecentsAnimationInterpolator recentsInterpolator = v.getRecentsInterpolator();
-
-        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);
-
-                float alphaDuration = 75;
-                if (!skipLauncherChanges) {
-                    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, alphaDuration, alphaDuration,
-                            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 alpha = getValue(0f, 1f, 0, 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);
-
-                        if (!skipLauncherChanges) {
-                            t.deferTransactionUntil(target.leash, surface, frameNumber);
-                        }
-                    }
-                    if (isFirstFrame) {
-                        t.show(target.leash);
-                    }
-                }
-                t.setEarlyWakeup();
-                t.apply();
-
-                matrix.reset();
-                isFirstFrame = false;
-            }
-        });
-        return appAnimator;
-    }
-
-    /**
      * Content is everything on screen except the background and the floating view (if any).
      *
      * @param show If true: Animate the content so that it moves upwards and fades in.
@@ -401,9 +270,9 @@
 
             ObjectAnimator alpha = ObjectAnimator.ofFloat(appsView, View.ALPHA, alphas);
             alpha.setDuration(217);
-            alpha.setInterpolator(Interpolators.LINEAR);
+            alpha.setInterpolator(LINEAR);
             ObjectAnimator transY = ObjectAnimator.ofFloat(appsView, View.TRANSLATION_Y, trans);
-            transY.setInterpolator(Interpolators.AGGRESSIVE_EASE);
+            transY.setInterpolator(AGGRESSIVE_EASE);
             transY.setDuration(350);
 
             launcherAnimator.play(alpha);
@@ -422,10 +291,10 @@
 
             ObjectAnimator dragLayerAlpha = ObjectAnimator.ofFloat(mDragLayer, View.ALPHA, alphas);
             dragLayerAlpha.setDuration(217);
-            dragLayerAlpha.setInterpolator(Interpolators.LINEAR);
+            dragLayerAlpha.setInterpolator(LINEAR);
             ObjectAnimator dragLayerTransY = ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y,
                     trans);
-            dragLayerTransY.setInterpolator(Interpolators.AGGRESSIVE_EASE);
+            dragLayerTransY.setInterpolator(AGGRESSIVE_EASE);
             dragLayerTransY.setDuration(350);
 
             launcherAnimator.play(dragLayerAlpha);
@@ -517,8 +386,8 @@
         boolean isBelowCenterY = lp.topMargin < centerY;
         x.setDuration(isBelowCenterY ? APP_LAUNCH_DURATION : APP_LAUNCH_CURVED_DURATION);
         y.setDuration(isBelowCenterY ? APP_LAUNCH_CURVED_DURATION : APP_LAUNCH_DURATION);
-        x.setInterpolator(Interpolators.AGGRESSIVE_EASE);
-        y.setInterpolator(Interpolators.AGGRESSIVE_EASE);
+        x.setInterpolator(AGGRESSIVE_EASE);
+        y.setInterpolator(AGGRESSIVE_EASE);
         appIconAnimatorSet.play(x);
         appIconAnimatorSet.play(y);
 
@@ -536,7 +405,7 @@
         ObjectAnimator alpha = ObjectAnimator.ofFloat(mFloatingView, View.ALPHA, 1f, 0f);
         alpha.setStartDelay(32);
         alpha.setDuration(50);
-        alpha.setInterpolator(Interpolators.LINEAR);
+        alpha.setInterpolator(LINEAR);
         appIconAnimatorSet.play(alpha);
 
         appIconAnimatorSet.addListener(new AnimatorListenerAdapter() {
@@ -571,11 +440,14 @@
 
         ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
         appAnimator.setDuration(APP_LAUNCH_DURATION);
-        appAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+        appAnimator.addUpdateListener(new MultiValueUpdateListener() {
+            // Fade alpha for the app window.
+            FloatProp mAlpha = new FloatProp(0f, 1f, 0, 60, LINEAR);
+
             boolean isFirstFrame = true;
 
             @Override
-            public void onAnimationUpdate(ValueAnimator animation) {
+            public void onUpdate(float percent) {
                 final Surface surface = getSurface(mFloatingView);
                 final long frameNumber = surface != null ? getNextFrameNumber(surface) : -1;
                 if (frameNumber == -1) {
@@ -583,8 +455,7 @@
                     Log.w(TAG, "Failed to animate, surface got destroyed.");
                     return;
                 }
-                final float percent = animation.getAnimatedFraction();
-                final float easePercent = Interpolators.AGGRESSIVE_EASE.getInterpolation(percent);
+                final float easePercent = AGGRESSIVE_EASE.getInterpolation(percent);
 
                 // Calculate app icon size.
                 float iconWidth = bounds.width() * mFloatingView.getScaleX();
@@ -609,11 +480,6 @@
                 float transY0 = floatingViewBounds[1] - offsetY;
                 matrix.postTranslate(transX0, transY0);
 
-                // Fade in the app window.
-                float alphaDuration = 60;
-                float alpha = getValue(0f, 1f, 0, alphaDuration,
-                        appAnimator.getDuration() * percent, Interpolators.LINEAR);
-
                 // Animate the window crop so that it starts off as a square, and then reveals
                 // horizontally.
                 float cropHeight = deviceHeight * easePercent + deviceWidth * (1 - easePercent);
@@ -626,7 +492,7 @@
                 TransactionCompat t = new TransactionCompat();
                 for (RemoteAnimationTargetCompat target : targets) {
                     if (target.mode == MODE_OPENING) {
-                        t.setAlpha(target.leash, alpha);
+                        t.setAlpha(target.leash, mAlpha.value);
 
                         // TODO: This isn't correct at the beginning of the animation, but better
                         // than nothing.
@@ -672,13 +538,7 @@
     }
 
     private boolean launcherIsATargetWithMode(RemoteAnimationTargetCompat[] targets, int mode) {
-        int launcherTaskId = mLauncher.getTaskId();
-        for (RemoteAnimationTargetCompat target : targets) {
-            if (target.mode == mode && target.taskId == launcherTaskId) {
-                return true;
-            }
-        }
-        return false;
+        return taskIsATargetWithMode(targets, mLauncher.getTaskId(), mode);
     }
 
     /**
@@ -732,30 +592,23 @@
 
         ValueAnimator closingAnimator = ValueAnimator.ofFloat(0, 1);
         closingAnimator.setDuration(CLOSING_TRANSITION_DURATION_MS);
+        closingAnimator.addUpdateListener(new MultiValueUpdateListener() {
+            FloatProp mDx = new FloatProp(0, endX, 0, 350, AGGRESSIVE_EASE_IN_OUT);
+            FloatProp mScale = new FloatProp(1f, 0.8f, 0, 267, AGGRESSIVE_EASE);
+            FloatProp mAlpha = new FloatProp(1f, 0f, 0, 350, APP_CLOSE_ALPHA);
 
-        closingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             boolean isFirstFrame = true;
 
             @Override
-            public void onAnimationUpdate(ValueAnimator animation) {
-                final float percent = animation.getAnimatedFraction();
-                float currentPlayTime = percent * closingAnimator.getDuration();
-
-                float scale = getValue(1f, 0.8f, 0, 267, currentPlayTime,
-                        Interpolators.AGGRESSIVE_EASE);
-
-                float dX = getValue(0, endX, 0, 350, currentPlayTime,
-                        Interpolators.AGGRESSIVE_EASE_IN_OUT);
-
+            public void onUpdate(float percent) {
                 TransactionCompat t = new TransactionCompat();
                 for (RemoteAnimationTargetCompat app : targets) {
                     if (app.mode == RemoteAnimationTargetCompat.MODE_CLOSING) {
-                        t.setAlpha(app.leash, getValue(1f, 0f, 0, 350, currentPlayTime,
-                                Interpolators.APP_CLOSE_ALPHA));
-                        matrix.setScale(scale, scale,
+                        t.setAlpha(app.leash, mAlpha.value);
+                        matrix.setScale(mScale.value, mScale.value,
                                 app.sourceContainerBounds.centerX(),
                                 app.sourceContainerBounds.centerY());
-                        matrix.postTranslate(dX, 0);
+                        matrix.postTranslate(mDx.value, 0);
                         matrix.postTranslate(app.position.x, app.position.y);
                         t.setMatrix(app.leash, matrix);
                     }
@@ -774,6 +627,7 @@
                 isFirstFrame = false;
             }
         });
+
         return closingAnimator;
     }
 
@@ -834,16 +688,4 @@
         return mLauncher.checkSelfPermission(CONTROL_REMOTE_APP_TRANSITION_PERMISSION)
                 == PackageManager.PERMISSION_GRANTED;
     }
-
-    /**
-     * Helper method that allows us to get interpolated values for embedded
-     * animations with a delay and/or different duration.
-     */
-    private static float getValue(float start, float end, float delay, float duration,
-            float currentPlayTime, Interpolator i) {
-        float time = Math.max(0, currentPlayTime - delay);
-        float newPercent = Math.min(1f, time / duration);
-        newPercent = i.getInterpolation(newPercent);
-        return end * newPercent + start * (1 - newPercent);
-    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
index 84a60bd..4c9fd5a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
@@ -22,7 +22,6 @@
 import android.animation.ValueAnimator;
 import android.util.Log;
 import android.view.MotionEvent;
-import android.view.View;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
@@ -106,7 +105,7 @@
 
             // Now figure out which direction scroll events the controller will start
             // calling the callbacks.
-            final int directionsToDetectScroll;
+            int directionsToDetectScroll = 0;
             boolean ignoreSlopWhenSettling = false;
             if (mCurrentAnimation != null) {
                 directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
@@ -114,12 +113,19 @@
             } else {
                 mTaskBeingDragged = null;
 
-                View view = mRecentsView.getChildAt(mRecentsView.getCurrentPage());
-                if (view instanceof TaskView && mActivity.getDragLayer().isEventOverView(view, ev)) {
-                    // The tile can be dragged down to open the task.
-                    mTaskBeingDragged = (TaskView) view;
-                    directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
-                } else {
+                for (int i = 0; i < mRecentsView.getChildCount(); i++) {
+                    TaskView view = mRecentsView.getPageAt(i);
+                    if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer()
+                            .isEventOverView(view, ev)) {
+                        // The task can be dragged up to dismiss it,
+                        // and down to open if it's the current page.
+                        mTaskBeingDragged = view;
+                        directionsToDetectScroll = i == mRecentsView.getCurrentPage()
+                                ? SwipeDetector.DIRECTION_BOTH : SwipeDetector.DIRECTION_POSITIVE;
+                        break;
+                    }
+                }
+                if (mTaskBeingDragged == null) {
                     mNoIntercept = true;
                     return false;
                 }
@@ -142,10 +148,16 @@
         return mDetector.onTouchEvent(ev);
     }
 
-    private void reInitAnimationController(boolean goingUp) {
+    private boolean reInitAnimationController(boolean goingUp) {
         if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) {
             // No need to init
-            return;
+            return false;
+        }
+        int scrollDirections = mDetector.getScrollDirections();
+        if (goingUp && ((scrollDirections & SwipeDetector.DIRECTION_POSITIVE) == 0)
+                || !goingUp && ((scrollDirections & SwipeDetector.DIRECTION_NEGATIVE) == 0)) {
+            // Trying to re-init in an unsupported direction.
+            return false;
         }
         if (mCurrentAnimation != null) {
             mCurrentAnimation.setPlayFraction(0);
@@ -179,6 +191,7 @@
         mCurrentAnimation.getTarget().addListener(this);
         mCurrentAnimation.dispatchOnStart();
         mProgressMultiplier = 1 / mEndDisplacement;
+        return true;
     }
 
     @Override
@@ -219,8 +232,7 @@
                     // Not allowed
                     goingToEnd = false;
                 } else {
-                    reInitAnimationController(goingUp);
-                    goingToEnd = true;
+                    goingToEnd = reInitAnimationController(goingUp);
                 }
             } else {
                 goingToEnd = true;
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 19ef8ab..eb0be89 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -15,16 +15,29 @@
  */
 package com.android.quickstep;
 
+import static com.android.launcher3.LauncherAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
+import static com.android.launcher3.LauncherAppTransitionManagerImpl.STATUS_BAR_TRANSITION_DURATION;
+import static com.android.quickstep.TaskUtils.getRecentsWindowAnimator;
+import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
 import android.app.ActivityOptions;
 import android.content.Intent;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
 import android.view.View;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAnimationRunner;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
+import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.badge.BadgeInfo;
 import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.util.SystemUiController;
@@ -32,12 +45,18 @@
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.fallback.FallbackRecentsView;
 import com.android.quickstep.fallback.RecentsRootView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
+import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
+import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
 /**
  * A simple activity to show the recently launched tasks
  */
 public class RecentsActivity extends BaseDraggingActivity {
 
+    private Handler mUiHandler = new Handler(Looper.getMainLooper());
     private RecentsRootView mRecentsRootView;
     private FallbackRecentsView mFallbackRecentsView;
 
@@ -84,8 +103,49 @@
     }
 
     @Override
-    public ActivityOptions getActivityLaunchOptions(View v, boolean useDefaultLaunchOptions) {
-        return null;
+    public ActivityOptions getActivityLaunchOptions(final View v, boolean useDefaultLaunchOptions) {
+        if (useDefaultLaunchOptions || !(v instanceof TaskView)) {
+            return null;
+        }
+
+        final TaskView taskView = (TaskView) v;
+        RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mUiHandler) {
+
+            @Override
+            public AnimatorSet getAnimator(RemoteAnimationTargetCompat[] targetCompats) {
+                return composeRecentsLaunchAnimator(taskView, targetCompats);
+            }
+        };
+        return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat(
+                runner, RECENTS_LAUNCH_DURATION,
+                RECENTS_LAUNCH_DURATION - STATUS_BAR_TRANSITION_DURATION));
+    }
+
+    /**
+     * Composes the animations for a launch from the recents list if possible.
+     */
+    private AnimatorSet composeRecentsLaunchAnimator(TaskView taskView,
+            RemoteAnimationTargetCompat[] targets) {
+        AnimatorSet target = new AnimatorSet();
+        boolean activityClosing = taskIsATargetWithMode(targets, getTaskId(), MODE_CLOSING);
+        target.play(getRecentsWindowAnimator(taskView, !activityClosing, targets)
+                .setDuration(RECENTS_LAUNCH_DURATION));
+
+        // Found a visible recents task that matches the opening app, lets launch the app from there
+        if (activityClosing) {
+            Animator adjacentAnimation = mFallbackRecentsView
+                    .createAdjacentPageAnimForTaskLaunch(taskView);
+            adjacentAnimation.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
+            adjacentAnimation.setDuration(RECENTS_LAUNCH_DURATION);
+            adjacentAnimation.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mFallbackRecentsView.resetTaskVisuals();
+                }
+            });
+            target.play(adjacentAnimation);
+        }
+        return target;
     }
 
     @Override
@@ -95,6 +155,7 @@
     protected void onStart() {
         super.onStart();
         UiFactory.onStart(this);
+        mFallbackRecentsView.resetTaskVisuals();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java
index 5bf1d07..c66f00f 100644
--- a/quickstep/src/com/android/quickstep/TaskUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskUtils.java
@@ -16,25 +16,47 @@
 
 package com.android.quickstep;
 
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
+import static com.android.systemui.shared.recents.utilities.Utilities.getNextFrameNumber;
+import static com.android.systemui.shared.recents.utilities.Utilities.getSurface;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
+
+import android.animation.ValueAnimator;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.graphics.Matrix;
+import android.graphics.Rect;
 import android.os.UserHandle;
 import android.util.Log;
+import android.view.Surface;
+import android.view.View;
 
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.ItemInfo;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.util.ComponentKey;
+import com.android.quickstep.RecentsAnimationInterpolator.TaskWindowBounds;
+import com.android.quickstep.util.MultiValueUpdateListener;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.TransactionCompat;
 
 /**
  * Contains helpful methods for retrieving data from {@link Task}s.
- * TODO: remove this once we switch to getting the icon and label from IconCache.
  */
 public class TaskUtils {
 
     private static final String TAG = "TaskUtils";
 
+    /**
+     * TODO: remove this once we switch to getting the icon and label from IconCache.
+     */
     public static CharSequence getTitle(Context context, Task task) {
         LauncherAppsCompat launcherAppsCompat = LauncherAppsCompat.getInstance(context);
         UserManagerCompat userManagerCompat = UserManagerCompat.getInstance(context);
@@ -53,4 +75,148 @@
     public static ComponentKey getComponentKeyForTask(Task.TaskKey taskKey) {
         return new ComponentKey(taskKey.getComponent(), UserHandle.of(taskKey.userId));
     }
+
+
+    /**
+     * Try to find a TaskView that corresponds with the component of the launched view.
+     *
+     * If this method returns a non-null TaskView, it will be used in composeRecentsLaunchAnimation.
+     * Otherwise, we will assume we are using a normal app transition, but it's possible that the
+     * opening remote target (which we don't get until onAnimationStart) will resolve to a TaskView.
+     */
+    public static TaskView findTaskViewToLaunch(
+            BaseDraggingActivity activity, View v, RemoteAnimationTargetCompat[] targets) {
+        if (v instanceof TaskView) {
+            return (TaskView) v;
+        }
+        RecentsView recentsView = activity.getOverviewPanel();
+
+        // It's possible that the launched view can still be resolved to a visible task view, check
+        // the task id of the opening task and see if we can find a match.
+        if (v.getTag() instanceof ItemInfo) {
+            ItemInfo itemInfo = (ItemInfo) v.getTag();
+            ComponentName componentName = itemInfo.getTargetComponent();
+            if (componentName != null) {
+                for (int i = 0; i < recentsView.getChildCount(); i++) {
+                    TaskView taskView = recentsView.getPageAt(i);
+                    if (recentsView.isTaskViewVisible(taskView)) {
+                        Task task = taskView.getTask();
+                        if (componentName.equals(task.key.getComponent())) {
+                            return taskView;
+                        }
+                    }
+                }
+            }
+        }
+
+        if (targets == null) {
+            return null;
+        }
+        // Resolve the opening task id
+        int openingTaskId = -1;
+        for (RemoteAnimationTargetCompat target : targets) {
+            if (target.mode == 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
+        TaskView taskView = recentsView.getTaskView(openingTaskId);
+        if (taskView == null || !recentsView.isTaskViewVisible(taskView)) {
+            return null;
+        }
+        return taskView;
+    }
+
+
+    /**
+     * @return Animator that controls the window of the opening targets for the recents launch
+     * animation.
+     */
+    public static ValueAnimator getRecentsWindowAnimator(TaskView v, boolean skipViewChanges,
+            RemoteAnimationTargetCompat[] targets) {
+        final RecentsAnimationInterpolator recentsInterpolator = v.getRecentsInterpolator();
+
+        Rect crop = new Rect();
+        Matrix matrix = new Matrix();
+
+        ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
+        appAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR);
+        appAnimator.addUpdateListener(new MultiValueUpdateListener() {
+
+            // Defer fading out the view until after the app window gets faded in
+            FloatProp mViewAlpha = new FloatProp(1f, 0f, 75, 75, LINEAR);
+            FloatProp mTaskAlpha = new FloatProp(0f, 1f, 0, 75, LINEAR);
+
+            boolean isFirstFrame = true;
+
+            @Override
+            public void onUpdate(float percent) {
+
+                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;
+                }
+                TaskWindowBounds tw = recentsInterpolator.interpolate(percent);
+
+                if (!skipViewChanges) {
+                    v.setScaleX(tw.taskScale);
+                    v.setScaleY(tw.taskScale);
+                    v.setTranslationX(tw.taskX);
+                    v.setTranslationY(tw.taskY);
+                    v.setAlpha(mViewAlpha.value);
+                }
+
+                matrix.setScale(tw.winScale, tw.winScale);
+                matrix.postTranslate(tw.winX, tw.winY);
+                crop.set(tw.winCrop);
+
+                TransactionCompat t = new TransactionCompat();
+                for (RemoteAnimationTargetCompat target : targets) {
+                    if (target.mode == RemoteAnimationTargetCompat.MODE_OPENING) {
+                        t.setAlpha(target.leash, mTaskAlpha.value);
+
+                        // 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);
+
+                        if (!skipViewChanges) {
+                            t.deferTransactionUntil(target.leash, surface, frameNumber);
+                        }
+                    }
+                    if (isFirstFrame) {
+                        t.show(target.leash);
+                    }
+                }
+                t.setEarlyWakeup();
+                t.apply();
+
+                matrix.reset();
+                isFirstFrame = false;
+            }
+        });
+        return appAnimator;
+    }
+
+    public static boolean taskIsATargetWithMode(RemoteAnimationTargetCompat[] targets,
+            int taskId, int mode) {
+        for (RemoteAnimationTargetCompat target : targets) {
+            if (target.mode == mode && target.taskId == taskId) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java b/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java
new file mode 100644
index 0000000..e798d5c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java
@@ -0,0 +1,68 @@
+/*
+ * 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.util;
+
+import android.animation.ValueAnimator;
+import android.view.animation.Interpolator;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class to update multiple values with different interpolators and durations during
+ * the same animation.
+ */
+public abstract class MultiValueUpdateListener implements ValueAnimator.AnimatorUpdateListener {
+
+    private final ArrayList<FloatProp> mAllProperties = new ArrayList<>();
+
+    @Override
+    public final void onAnimationUpdate(ValueAnimator animator) {
+        final float percent = animator.getAnimatedFraction();
+        final float currentPlayTime = percent * animator.getDuration();
+
+        for (int i = mAllProperties.size() - 1; i >= 0; i--) {
+            FloatProp prop = mAllProperties.get(i);
+            float time = Math.max(0, currentPlayTime - prop.mDelay);
+            float newPercent = Math.min(1f, time / prop.mDuration);
+            newPercent = prop.mInterpolator.getInterpolation(newPercent);
+            prop.value = prop.mEnd * newPercent + prop.mStart * (1 - newPercent);
+        }
+        onUpdate(percent);
+    }
+
+    public abstract void onUpdate(float percent);
+
+    public final class FloatProp {
+
+        public float value;
+
+        private final float mStart;
+        private final float mEnd;
+        private final float mDelay;
+        private final float mDuration;
+        private final Interpolator mInterpolator;
+
+        public FloatProp(float start, float end, float delay, float duration, Interpolator i) {
+            value = mStart = start;
+            mEnd = end;
+            mDelay = delay;
+            mDuration = duration;
+            mInterpolator = i;
+
+            mAllProperties.add(this);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 7e81ba9..faaa40d 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -659,14 +659,14 @@
         int[] newScroll = new int[count];
         getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView);
 
-        int maxScrollDiff = 0;
-        int lastPage = mIsRtl ? 0 : count - 1;
-        if (getChildAt(lastPage) == taskView) {
-            if (count > 1) {
-                int secondLastPage = mIsRtl ? 1 : count - 2;
-                maxScrollDiff = oldScroll[lastPage] - newScroll[secondLastPage];
-            }
+        int scrollDiffPerPage = 0;
+        int leftmostPage = mIsRtl ? count -1 : 0;
+        int rightmostPage = mIsRtl ? 0 : count - 1;
+        if (count > 1) {
+            int secondRightmostPage = mIsRtl ? 1 : count - 2;
+            scrollDiffPerPage = oldScroll[rightmostPage] - oldScroll[secondRightmostPage];
         }
+        int draggedIndex = indexOfChild(taskView);
 
         boolean needsCurveUpdates = false;
         for (int i = 0; i < count; i++) {
@@ -678,7 +678,26 @@
                             duration, LINEAR, anim);
                 }
             } else {
-                int scrollDiff = newScroll[i] - oldScroll[i] + maxScrollDiff;
+                // If we just take newScroll - oldScroll, everything to the right of dragged task
+                // translates to the left. We need to offset this in some cases:
+                // - In RTL, add page offset to all pages, since we want pages to move to the right
+                // Additionally, add a page offset if:
+                // - Current page is rightmost page (leftmost for RTL)
+                // - Dragging an adjacent page on the left side (right side for RTL)
+                int offset = mIsRtl ? scrollDiffPerPage : 0;
+                if (mCurrentPage == draggedIndex) {
+                    int lastPage = mIsRtl ? leftmostPage : rightmostPage;
+                    if (mCurrentPage == lastPage) {
+                        offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
+                    }
+                } else {
+                    // Dragging an adjacent page.
+                    int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR)
+                    if (draggedIndex == negativeAdjacent) {
+                        offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
+                    }
+                }
+                int scrollDiff = newScroll[i] - oldScroll[i] + offset;
                 if (scrollDiff != 0) {
                     addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff),
                             duration, ACCEL, anim);
@@ -710,9 +729,15 @@
                                TaskUtils.getComponentKeyForTask(task.key));
                    }
                }
+               int pageToSnapTo = mCurrentPage;
+               if (draggedIndex < pageToSnapTo) {
+                   pageToSnapTo -= 1;
+               }
                removeView(taskView);
                if (getChildCount() == 0) {
                    onAllTasksRemoved();
+               } else {
+                   snapToPageImmediately(pageToSnapTo);
                }
            }
            resetTaskVisuals();
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index cabccbf..ba96d4a 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -105,7 +105,8 @@
      * Indicates if the device has a debug build. Should only be used to store additional info or
      * add extra logging and not for changing the app behavior.
      */
-    public static final boolean IS_DEBUG_DEVICE = Build.TYPE.toLowerCase().contains("debug");
+    public static final boolean IS_DEBUG_DEVICE = Build.TYPE.toLowerCase().contains("debug")
+            || Build.TYPE.toLowerCase().equals("eng");
 
     // An intent extra to indicate the horizontal scroll of the wallpaper.
     public static final String EXTRA_WALLPAPER_OFFSET = "com.android.launcher3.WALLPAPER_OFFSET";
diff --git a/src/com/android/launcher3/touch/SwipeDetector.java b/src/com/android/launcher3/touch/SwipeDetector.java
index 4b36ad9..703e4fd 100644
--- a/src/com/android/launcher3/touch/SwipeDetector.java
+++ b/src/com/android/launcher3/touch/SwipeDetector.java
@@ -194,6 +194,10 @@
         mIgnoreSlopWhenSettling = ignoreSlop;
     }
 
+    public int getScrollDirections() {
+        return mScrollConditions;
+    }
+
     private boolean shouldScrollStart(MotionEvent ev, int pointerIndex) {
         // reject cases where the angle or slop condition is not met.
         if (Math.max(mDir.getActiveTouchSlop(ev, pointerIndex, mDownPos), mTouchSlop)