Merge "Can now swipe away to dismiss second task" into ub-launcher3-master
diff --git a/proguard.flags b/proguard.flags
index 086337d..e9f6db4 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -107,6 +107,11 @@
     public <init>(...);
 }
 
+# MainProcessInitializer
+-keep class com.android.quickstep.QuickstepProcessInitializer {
+    public <init>(...);
+}
+
 -keep interface com.android.launcher3.userevent.nano.LauncherLogProto.** {
   *;
 }
diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar
index b75a193..69ce74a 100644
--- a/quickstep/libs/sysui_shared.jar
+++ b/quickstep/libs/sysui_shared.jar
Binary files differ
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index ab6d8af..c741913 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -20,7 +20,6 @@
     <dimen name="task_thumbnail_icon_size">48dp</dimen>
     <dimen name="task_menu_background_radius">12dp</dimen>
     <dimen name="task_corner_radius">2dp</dimen>
-    <dimen name="task_fade_length">20dp</dimen>
     <dimen name="recents_page_spacing">10dp</dimen>
 
     <!-- The speed in dp/s at which the user needs to be scrolling in recents such that we start
@@ -37,4 +36,13 @@
     <dimen name="recents_empty_message_text_size">16sp</dimen>
     <dimen name="recents_empty_message_text_padding">16dp</dimen>
 
+    <!-- Total space (start + end) between the task card and the edge of the screen
+         in various configurations -->
+    <dimen name="task_card_vert_space">40dp</dimen>
+    <dimen name="portrait_task_card_horz_space">136dp</dimen>
+    <dimen name="landscape_task_card_horz_space">200dp</dimen>
+    <dimen name="multi_window_task_card_horz_space">100dp</dimen>
+    <!-- Copied from framework resource:
+       docked_stack_divider_thickness - 2 * docked_stack_divider_insets -->
+    <dimen name="multi_window_task_divider_size">10dp</dimen>
 </resources>
diff --git a/quickstep/res/values/override.xml b/quickstep/res/values/override.xml
index 2bd9f8f..d683659 100644
--- a/quickstep/res/values/override.xml
+++ b/quickstep/res/values/override.xml
@@ -18,5 +18,7 @@
   <string name="app_transition_manager_class" translatable="false">com.android.launcher3.LauncherAppTransitionManagerImpl</string>
 
   <string name="instant_app_resolver_class" translatable="false">com.android.quickstep.InstantAppResolverImpl</string>
+
+  <string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
 </resources>
 
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index af81a59..8fbb512 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,11 +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;
@@ -78,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";
@@ -86,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;
 
@@ -106,7 +110,7 @@
     private DeviceProfile mDeviceProfile;
     private View mFloatingView;
 
-    private RemoteAnimationRunnerCompat mRemoteAnimationOverride;
+    private RemoteAnimationProvider mRemoteAnimationProvider;
 
     private final AnimatorListenerAdapter mReapplyStateListener = new AnimatorListenerAdapter() {
         @Override
@@ -179,66 +183,8 @@
         return getDefaultActivityLaunchOptions(launcher, v);
     }
 
-    public void setRemoteAnimationOverride(RemoteAnimationRunnerCompat remoteAnimationOverride) {
-        mRemoteAnimationOverride = remoteAnimationOverride;
-    }
-
-    /**
-     * 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;
+    public void setRemoteAnimationProvider(RemoteAnimationProvider animationProvider) {
+        mRemoteAnimationProvider = animationProvider;
     }
 
     /**
@@ -284,7 +230,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
@@ -296,82 +243,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.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.
@@ -397,9 +268,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);
@@ -418,10 +289,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);
@@ -513,8 +384,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);
 
@@ -532,7 +403,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() {
@@ -567,11 +438,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) {
@@ -579,8 +453,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();
@@ -605,11 +478,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);
@@ -622,7 +490,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.
@@ -635,6 +503,7 @@
                         t.show(target.leash);
                     }
                 }
+                t.setEarlyWakeup();
                 t.apply();
 
                 matrix.reset();
@@ -667,13 +536,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);
     }
 
     /**
@@ -683,52 +546,33 @@
     private RemoteAnimationRunnerCompat getWallpaperOpenRunner() {
         return new LauncherAnimationRunner(mHandler) {
             @Override
-            public void onAnimationStart(RemoteAnimationTargetCompat[] targetCompats,
-                    Runnable runnable) {
-                if (mLauncher.getStateManager().getState().overviewUi
-                        && mRemoteAnimationOverride != null) {
-                    // This transition is only used for the fallback activity and should not be
-                    // managed here (but necessary to implement here since the defined remote
-                    // animation currently takes precendence over the one defined in the activity
-                    // options).
-                    mRemoteAnimationOverride.onAnimationStart(targetCompats, runnable);
-                    return;
-                }
-                super.onAnimationStart(targetCompats, runnable);
-            }
-
-            @Override
-            public void onAnimationCancelled() {
-                if (mLauncher.getStateManager().getState().overviewUi
-                        && mRemoteAnimationOverride != null) {
-                    // This transition is only used for the fallback activity and should not be
-                    // managed here (but necessary to implement here since the defined remote
-                    // animation currently takes precendence over the one defined in the activity
-                    // options).
-                    mRemoteAnimationOverride.onAnimationCancelled();
-                    return;
-                }
-                super.onAnimationCancelled();
-            }
-
-            @Override
             public AnimatorSet getAnimator(RemoteAnimationTargetCompat[] targetCompats) {
-                AnimatorSet anim = new AnimatorSet();
-                anim.play(getClosingWindowAnimators(targetCompats));
-
-                // Normally, we run the launcher content animation when we are transitioning home,
-                // but if home is already visible, then we don't want to animate the contents of
-                // launcher unless we know that we are animating home as a result of the home button
-                // press with quickstep, which will result in launcher being started on touch down,
-                // prior to the animation home (and won't be in the targets list because it is
-                // already visible). In that case, we force invisibility on touch down, and only
-                // reset it after the animation to home is initialized.
-                if (launcherIsATargetWithMode(targetCompats, MODE_OPENING)
-                        || mLauncher.isForceInvisible()) {
-                    // Only register the content animation for cancellation when state changes
-                    mLauncher.getStateManager().setCurrentAnimation(anim);
-                    createLauncherResumeAnimation(anim);
+                AnimatorSet anim = null;
+                RemoteAnimationProvider provider = mRemoteAnimationProvider;
+                if (provider != null) {
+                    anim = provider.createWindowAnimation(targetCompats);
                 }
+
+                if (anim == null) {
+                    anim = new AnimatorSet();
+                    anim.play(getClosingWindowAnimators(targetCompats));
+
+                    // Normally, we run the launcher content animation when we are transitioning
+                    // home, but if home is already visible, then we don't want to animate the
+                    // contents of launcher unless we know that we are animating home as a result
+                    // of the home button press with quickstep, which will result in launcher being
+                    // started on touch down, prior to the animation home (and won't be in the
+                    // targets list because it is already visible). In that case, we force
+                    // invisibility on touch down, and only reset it after the animation to home
+                    // is initialized.
+                    if (launcherIsATargetWithMode(targetCompats, MODE_OPENING)
+                            || mLauncher.isForceInvisible()) {
+                        // Only register the content animation for cancellation when state changes
+                        mLauncher.getStateManager().setCurrentAnimation(anim);
+                        createLauncherResumeAnimation(anim);
+                    }
+                }
+
                 mLauncher.setForceInvisible(false);
                 return anim;
             }
@@ -746,30 +590,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);
                     }
@@ -781,12 +618,14 @@
                         t.show(app.leash);
                     }
                 }
+                t.setEarlyWakeup();
                 t.apply();
 
                 matrix.reset();
                 isFirstFrame = false;
             }
         });
+
         return closingAnimator;
     }
 
@@ -847,16 +686,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/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java
index 0d1038a..27f1698 100644
--- a/quickstep/src/com/android/launcher3/LauncherInitListener.java
+++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java
@@ -15,13 +15,16 @@
  */
 package com.android.launcher3;
 
-import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
-
 import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
 import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
 
 import com.android.launcher3.states.InternalStateHandler;
 import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
+import com.android.quickstep.util.RemoteAnimationProvider;
 
 import java.util.function.BiPredicate;
 
@@ -30,15 +33,33 @@
 
     private final BiPredicate<Launcher, Boolean> mOnInitListener;
 
+    private RemoteAnimationProvider mRemoteAnimationProvider;
+
     public LauncherInitListener(BiPredicate<Launcher, Boolean> onInitListener) {
         mOnInitListener = onInitListener;
     }
 
     @Override
     protected boolean init(Launcher launcher, boolean alreadyOnHome) {
-        // For the duration of the gesture, lock the screen orientation to ensure that we do not
-        // rotate mid-quickscrub
-        launcher.getRotationHelper().setStateHandlerRequest(REQUEST_LOCK);
+        if (mRemoteAnimationProvider != null) {
+            LauncherAppTransitionManagerImpl appTransitionManager =
+                    (LauncherAppTransitionManagerImpl) launcher.getAppTransitionManager();
+
+            // Set a one-time animation provider. After the first call, this will get cleared.
+            // TODO: Probably also check the intended target id.
+            appTransitionManager.setRemoteAnimationProvider((targets) -> {
+
+                // On the first call clear the reference.
+                appTransitionManager.setRemoteAnimationProvider(null);
+                RemoteAnimationProvider provider = mRemoteAnimationProvider;
+                mRemoteAnimationProvider = null;
+
+                if (provider != null && launcher.getStateManager().getState().overviewUi) {
+                    return provider.createWindowAnimation(targets);
+                }
+                return null;
+            });
+        }
         return mOnInitListener.test(launcher, alreadyOnHome);
     }
 
@@ -49,6 +70,18 @@
 
     @Override
     public void unregister() {
+        mRemoteAnimationProvider = null;
         clearReference();
     }
+
+    @Override
+    public void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider,
+            Context context, Handler handler, long duration) {
+        mRemoteAnimationProvider = animProvider;
+
+        register();
+
+        Bundle options = animProvider.toActivityOptions(handler, duration).toBundle();
+        context.startActivity(addToIntent(new Intent((intent))), options);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/LandscapeStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/LandscapeStatesTouchController.java
index 355b88d..30ceb43 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/LandscapeStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/LandscapeStatesTouchController.java
@@ -60,12 +60,13 @@
 
     @Override
     protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
-        if (fromState == ALL_APPS) {
+        if (fromState == ALL_APPS && !isDragTowardPositive) {
             // Should swipe down go to OVERVIEW instead?
             return TouchInteractionService.isConnected() ?
                     mLauncher.getStateManager().getLastState() : NORMAL;
-        } else {
+        } else if (isDragTowardPositive) {
             return ALL_APPS;
         }
+        return fromState;
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
index d97b7b2..9c7db30 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
@@ -19,13 +19,11 @@
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
 import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
 
-import android.graphics.Rect;
 import android.view.View;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.Workspace;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.quickstep.views.RecentsView;
 
@@ -47,14 +45,8 @@
 
     @Override
     public float[] getWorkspaceScaleAndTranslation(Launcher launcher) {
-        Rect pageRect = new Rect();
-        RecentsView.getPageRect(launcher.getDeviceProfile(), launcher, pageRect);
-
-        if (launcher.getWorkspace().getNormalChildWidth() <= 0 || pageRect.isEmpty()) {
-            return super.getWorkspaceScaleAndTranslation(launcher);
-        }
-
-        return getScaleAndTranslationForPageRect(launcher, pageRect);
+        // TODO: provide a valid value
+        return new float[]{1, 0, -launcher.getDeviceProfile().hotseatBarSizePx / 2};
     }
 
     @Override
@@ -93,24 +85,10 @@
         };
     }
 
-    public static float[] getScaleAndTranslationForPageRect(Launcher launcher, Rect pageRect) {
-        Workspace ws = launcher.getWorkspace();
-        float childWidth = ws.getNormalChildWidth();
-
-        float scale = pageRect.width() / childWidth;
-        Rect insets = launcher.getDragLayer().getInsets();
-
-        float halfHeight = ws.getExpectedHeight() / 2;
-        float childTop = halfHeight - scale * (halfHeight - ws.getPaddingTop() - insets.top);
-        float translationY = pageRect.top - childTop;
-
-        return new float[] {scale, 0, translationY};
-    }
-
     @Override
     public int getVisibleElements(Launcher launcher) {
         if (launcher.getDeviceProfile().isVerticalBarLayout()) {
-            return NONE;
+            return DRAG_HANDLE_INDICATOR;
         } else {
             return HOTSEAT_SEARCH_BOX | DRAG_HANDLE_INDICATOR |
                     (launcher.getAppsView().getFloatingHeaderView().hasVisibleContent()
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
index 1b65ca0..012b545 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
 import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.quickstep.TouchInteractionService;
 import com.android.quickstep.util.SysuiEventLogger;
@@ -140,7 +141,7 @@
 
     @Override
     protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
-        if (fromState == ALL_APPS) {
+        if (fromState == ALL_APPS && !isDragTowardPositive) {
             // Should swipe down go to OVERVIEW instead?
             return TouchInteractionService.isConnected() ?
                     mLauncher.getStateManager().getLastState() : NORMAL;
@@ -182,7 +183,7 @@
         }
 
         if (mPendingAnimation != null) {
-            mPendingAnimation.finish(false);
+            mPendingAnimation.finish(false, Touch.SWIPE);
             mPendingAnimation = null;
         }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
index 42f8105..4c9fd5a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
@@ -29,7 +29,6 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.touch.SwipeDetector;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.PendingAnimation;
 import com.android.launcher3.util.TouchController;
@@ -164,7 +163,7 @@
             mCurrentAnimation.setPlayFraction(0);
         }
         if (mPendingAnimation != null) {
-            mPendingAnimation.finish(false);
+            mPendingAnimation.finish(false, Touch.SWIPE);
             mPendingAnimation = null;
         }
 
@@ -261,15 +260,9 @@
 
     private void onCurrentAnimationEnd(boolean wasSuccess, int logAction) {
         if (mPendingAnimation != null) {
-            mPendingAnimation.finish(wasSuccess);
+            mPendingAnimation.finish(wasSuccess, logAction);
             mPendingAnimation = null;
         }
-        if (wasSuccess) {
-            if (!mCurrentAnimationIsGoingUp) {
-                mActivity.getUserEventDispatcher().logTaskLaunch(logAction,
-                        Direction.DOWN, mTaskBeingDragged.getTask().getTopComponent());
-            }
-        }
         mDetector.finishedScrolling();
         mTaskBeingDragged = null;
         mCurrentAnimation = null;
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index 3e96c44..95947d7 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -19,10 +19,10 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_BACK;
 
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
-import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
@@ -36,22 +36,17 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppTransitionManagerImpl;
 import com.android.launcher3.LauncherInitListener;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.util.ViewOnDrawExecutor;
 import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.views.LauncherLayoutListener;
+import com.android.quickstep.views.LauncherRecentsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.ActivityOptionsCompat;
-import com.android.systemui.shared.system.AssistDataReceiver;
-import com.android.systemui.shared.system.RecentsAnimationListener;
-import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
 
 import java.util.function.BiPredicate;
 
@@ -64,7 +59,13 @@
 
     void onQuickstepGestureStarted(T activity, boolean activityVisible);
 
-    void onQuickInteractionStart(T activity, boolean activityVisible);
+    /**
+     * Updates the UI to indicate quick interaction.
+     * @return true if there any any UI change as a result of this
+     */
+    boolean onQuickInteractionStart(T activity, boolean activityVisible);
+
+    void executeOnWindowAvailable(T activity, Runnable action);
 
     void executeOnNextDraw(T activity, TaskView targetView, Runnable action);
 
@@ -82,11 +83,8 @@
 
     ActivityInitListener createActivityInitListener(BiPredicate<T, Boolean> onInitListener);
 
-    void startRecentsFromSwipe(Intent intent, AssistDataReceiver assistDataReceiver,
-            final RecentsAnimationListener remoteAnimationListener);
-
-    void startRecentsFromButton(Context context, Intent intent,
-            RecentsAnimationListener remoteAnimationListener);
+    @Nullable
+    T getCreatedActivity();
 
     @UiThread
     @Nullable
@@ -95,6 +93,12 @@
     @UiThread
     boolean switchToRecentsIfVisible();
 
+    /**
+     * @return {@code true} if recents activity should be started immediately on touchDown,
+     *         {@code false} if it should deferred until some threshold is crossed.
+     */
+    boolean deferStartingActivity(int downHitTarget);
+
     class LauncherActivityControllerHelper implements ActivityControlHelper<Launcher> {
 
         @Override
@@ -108,8 +112,18 @@
         }
 
         @Override
-        public void onQuickInteractionStart(Launcher activity, boolean activityVisible) {
+        public boolean onQuickInteractionStart(Launcher activity, boolean activityVisible) {
+            LauncherState fromState = activity.getStateManager().getState();
             activity.getStateManager().goToState(FAST_OVERVIEW, activityVisible);
+            return !fromState.overviewUi;
+        }
+
+        @Override
+        public void executeOnWindowAvailable(Launcher activity, Runnable action) {
+            if (activity.getWorkspace().runOnOverlayHidden(action)) {
+                // Notify the activity that qiuckscrub has started
+                onQuickstepGestureStarted(activity, true);
+            }
         }
 
         @Override
@@ -128,7 +142,7 @@
 
         @Override
         public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
-            RecentsView.getPageRect(dp, context, outRect);
+            LauncherRecentsView.getPageRect(dp, context, outRect);
             if (dp.isVerticalBarLayout()) {
                 Rect targetInsets = dp.getInsets();
                 int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
@@ -207,34 +221,9 @@
             return new LauncherInitListener(onInitListener);
         }
 
-        @Override
-        public void startRecentsFromSwipe(Intent intent, AssistDataReceiver assistDataReceiver,
-                final RecentsAnimationListener remoteAnimationListener) {
-            ActivityManagerWrapper.getInstance().startRecentsActivity(
-                    intent, assistDataReceiver, remoteAnimationListener, null, null);
-        }
-
-        @Override
-        public void startRecentsFromButton(Context context, Intent intent,
-                RecentsAnimationListener remoteAnimationListener) {
-            // We should use the remove animation for the fallback activity recents button case,
-            // it works better with PiP.  In Launcher, we have already registered the remote
-            // animation definition, which takes priority over explicitly defined remote
-            // animations in the provided activity options when starting the activity, so we
-            // just register a remote animation factory to get a callback to handle this.
-            LauncherAppTransitionManagerImpl appTransitionManager =
-                    (LauncherAppTransitionManagerImpl) getLauncher().getAppTransitionManager();
-            appTransitionManager.setRemoteAnimationOverride(new RecentsAnimationActivityOptions(
-                    remoteAnimationListener, () -> {
-                        // Once the controller is finished, also reset the remote animation override
-                        appTransitionManager.setRemoteAnimationOverride(null);
-                    }));
-            context.startActivity(intent);
-        }
-
         @Nullable
-        @UiThread
-        private Launcher getLauncher() {
+        @Override
+        public Launcher getCreatedActivity() {
             LauncherAppState app = LauncherAppState.getInstanceNoCreate();
             if (app == null) {
                 return null;
@@ -245,7 +234,7 @@
         @Nullable
         @UiThread
         private Launcher getVisibleLaucher() {
-            Launcher launcher = getLauncher();
+            Launcher launcher = getCreatedActivity();
             return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus() ?
                     launcher : null;
         }
@@ -267,6 +256,11 @@
             }
             return false;
         }
+
+        @Override
+        public boolean deferStartingActivity(int downHitTarget) {
+            return downHitTarget == HIT_TARGET_BACK;
+        }
     }
 
     class FallbackActivityControllerHelper implements ActivityControlHelper<RecentsActivity> {
@@ -277,8 +271,14 @@
         }
 
         @Override
-        public void onQuickInteractionStart(RecentsActivity activity, boolean activityVisible) {
-            // TODO:
+        public boolean onQuickInteractionStart(RecentsActivity activity, boolean activityVisible) {
+            // Activity does not need any UI change for quickscrub.
+            return false;
+        }
+
+        @Override
+        public void executeOnWindowAvailable(RecentsActivity activity, Runnable action) {
+            action.run();
         }
 
         @Override
@@ -295,7 +295,7 @@
 
         @Override
         public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
-            FallbackRecentsView.getCenterPageRect(dp, context, outRect);
+            FallbackRecentsView.getPageRect(dp, context, outRect);
             if (dp.isVerticalBarLayout()) {
                 Rect targetInsets = dp.getInsets();
                 int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
@@ -352,31 +352,16 @@
             return new RecentsActivityTracker(onInitListener);
         }
 
+        @Nullable
         @Override
-        public void startRecentsFromSwipe(Intent intent, AssistDataReceiver assistDataReceiver,
-                final RecentsAnimationListener remoteAnimationListener) {
-            // We can use the normal recents animation for swipe up
-            ActivityManagerWrapper.getInstance().startRecentsActivity(
-                    intent, assistDataReceiver, remoteAnimationListener, null, null);
-        }
-
-        @Override
-        public void startRecentsFromButton(Context context, Intent intent,
-                RecentsAnimationListener remoteAnimationListener) {
-            // We should use the remove animation for the fallback activity recents button case,
-            // it works better with PiP. For the fallback activity, we should not have registered
-            // the launcher app transition manager, so we should just start the remote animation here.
-            ActivityOptions options = ActivityOptionsCompat.makeRemoteAnimation(
-                    new RemoteAnimationAdapterCompat(
-                            new RecentsAnimationActivityOptions(remoteAnimationListener, null),
-                            10000, 10000));
-            context.startActivity(intent, options.toBundle());
+        public RecentsActivity getCreatedActivity() {
+            return RecentsActivityTracker.getCurrentActivity();
         }
 
         @Nullable
         @Override
         public RecentsView getVisibleRecentsView() {
-            RecentsActivity activity = RecentsActivityTracker.getCurrentActivity();
+            RecentsActivity activity = getCreatedActivity();
             if (activity != null && activity.hasWindowFocus()) {
                 return activity.getOverviewPanel();
             }
@@ -387,6 +372,12 @@
         public boolean switchToRecentsIfVisible() {
             return false;
         }
+
+        @Override
+        public boolean deferStartingActivity(int downHitTarget) {
+            // Always defer starting the activity when using fallback
+            return true;
+        }
     }
 
     interface LayoutListener {
@@ -403,5 +394,8 @@
         void register();
 
         void unregister();
+
+        void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider,
+                Context context, Handler handler, long duration);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
index 4d695de..28c950b 100644
--- a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
+++ b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
@@ -21,8 +21,7 @@
 import static android.view.MotionEvent.ACTION_POINTER_UP;
 import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.INVALID_POINTER_ID;
-import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_BACK;
-import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_OVERVIEW;
+
 import static com.android.systemui.shared.system.NavigationBarCompat.QUICK_STEP_DRAG_SLOP_PX;
 
 import android.annotation.TargetApi;
@@ -55,7 +54,6 @@
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 
-import java.util.Arrays;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -66,8 +64,6 @@
 public class OtherActivityTouchConsumer extends ContextWrapper implements TouchConsumer {
 
     private static final long LAUNCHER_DRAW_TIMEOUT_MS = 150;
-    private static final int[] DEFERRED_HIT_TARGETS = false
-            ? new int[] {HIT_TARGET_BACK, HIT_TARGET_OVERVIEW} : new int[] {HIT_TARGET_BACK};
 
     private final RunningTaskInfo mRunningTask;
     private final RecentsModel mRecentsModel;
@@ -102,7 +98,7 @@
         mActivityControlHelper = activityControl;
         mMainThreadExecutor = mainThreadExecutor;
         mBackgroundThreadChoreographer = backgroundThreadChoreographer;
-        mIsDeferredDownTarget = Arrays.binarySearch(DEFERRED_HIT_TARGETS, downHitTarget) >= 0;
+        mIsDeferredDownTarget = activityControl.deferStartingActivity(downHitTarget);
     }
 
     @Override
@@ -218,7 +214,8 @@
         handler.initWhenReady();
 
         TraceHelper.beginSection("RecentsController");
-        Runnable startActivity = () -> mActivityControlHelper.startRecentsFromSwipe(mHomeIntent,
+        Runnable startActivity = () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
+                mHomeIntent,
                 new AssistDataReceiver() {
                     @Override
                     public void onHandleAssistData(Bundle bundle) {
@@ -247,7 +244,7 @@
                             handler.onRecentsAnimationCanceled();
                         }
                     }
-                });
+                }, null, null);
 
         if (Looper.myLooper() != Looper.getMainLooper()) {
             startActivity.run();
@@ -305,6 +302,13 @@
 
     @Override
     public void updateTouchTracking(int interactionType) {
+        if (!mPassedInitialSlop && mIsDeferredDownTarget && mInteractionHandler == null) {
+            // If we deferred starting the window animation on touch down, then
+            // start tracking now
+            startTouchTrackingForWindowAnimation(SystemClock.uptimeMillis());
+            mPassedInitialSlop = true;
+        }
+
         notifyGestureStarted();
         if (mInteractionHandler != null) {
             mInteractionHandler.updateInteractionType(interactionType);
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 8e59578..e7ed517 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -15,92 +15,86 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.LauncherState.OVERVIEW;
+import static android.content.Intent.ACTION_PACKAGE_ADDED;
+import static android.content.Intent.ACTION_PACKAGE_CHANGED;
+import static android.content.Intent.ACTION_PACKAGE_REMOVED;
 
-import android.animation.Animator;
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.systemui.shared.system.ActivityManagerWrapper
+        .CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.systemui.shared.system.PackageManagerWrapper
+        .ACTION_PREFERRED_ACTIVITY_CHANGED;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+
+import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
-import android.app.ActivityManager.RecentTaskInfo;
-import android.app.ActivityManager.RunningTaskInfo;
-import android.app.ActivityOptions;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ResolveInfo;
-import android.graphics.Point;
 import android.graphics.Rect;
-import android.graphics.RectF;
 import android.os.Build;
-import android.os.Bundle;
+import android.os.PatternMatcher;
 import android.os.SystemClock;
-import android.os.UserHandle;
-import android.support.annotation.UiThread;
-import android.support.annotation.WorkerThread;
-import android.util.SparseArray;
+import android.util.Log;
+import android.view.View;
 import android.view.ViewConfiguration;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.MainThreadExecutor;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.states.InternalStateHandler;
-import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
 import com.android.quickstep.ActivityControlHelper.FallbackActivityControllerHelper;
 import com.android.quickstep.ActivityControlHelper.LauncherActivityControllerHelper;
+import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.SysuiEventLogger;
 import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
-import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
-import com.android.systemui.shared.recents.view.RecentsTransition;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.AssistDataReceiver;
-import com.android.systemui.shared.system.BackgroundExecutor;
-import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
-import com.android.systemui.shared.system.RecentsAnimationListener;
+import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
-import java.util.Collections;
-import java.util.List;
-import java.util.function.Consumer;
+import java.util.ArrayList;
 
 /**
  * Helper class to handle various atomic commands for switching between Overview.
  */
 @TargetApi(Build.VERSION_CODES.P)
-public class OverviewCommandHelper extends InternalStateHandler {
-
-    private static final int RID_RESET_SWIPE_HANDLER = 0;
-    private static final int RID_CANCEL_CONTROLLER = 1;
-    private static final int RID_CANCEL_ZOOM_OUT_ANIMATION = 2;
+public class OverviewCommandHelper {
 
     private static final long RECENTS_LAUNCH_DURATION = 200;
 
     private static final String TAG = "OverviewCommandHelper";
-    private static final boolean DEBUG_START_FALLBACK_ACTIVITY = false;
 
     private final Context mContext;
     private final ActivityManagerWrapper mAM;
     private final RecentsModel mRecentsModel;
     private final MainThreadExecutor mMainThreadExecutor;
+    private final ComponentName mMyHomeComponent;
 
-    public final Intent homeIntent;
-    public final ComponentName launcher;
+    private final BroadcastReceiver mUserPreferenceChangeReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            initOverviewTargets();
+        }
+    };
+    private final BroadcastReceiver mOtherHomeAppUpdateReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            initOverviewTargets();
+        }
+    };
+    private String mUpdateRegisteredPackage;
 
-    private final SparseArray<Runnable> mCurrentCommandFinishRunnables = new SparseArray<>();
-    // Monotonically increasing command ids.
-    private int mCurrentCommandId = 0;
+    public Intent overviewIntent;
+    public ComponentName overviewComponent;
+    private ActivityControlHelper mActivityControlHelper;
 
     private long mLastToggleTime;
-    private WindowTransformSwipeHandler mWindowTransformSwipeHandler;
-
-    private final Point mWindowSize = new Point();
-    private final Rect mTaskTargetRect = new Rect();
-    private final RectF mTempTaskTargetRect = new RectF();
 
     public OverviewCommandHelper(Context context) {
         mContext = context;
@@ -108,266 +102,226 @@
         mMainThreadExecutor = new MainThreadExecutor();
         mRecentsModel = RecentsModel.getInstance(mContext);
 
-        homeIntent = new Intent(Intent.ACTION_MAIN)
+        Intent myHomeIntent = new Intent(Intent.ACTION_MAIN)
                 .addCategory(Intent.CATEGORY_HOME)
-                .setPackage(context.getPackageName())
+                .setPackage(mContext.getPackageName());
+        ResolveInfo info = context.getPackageManager().resolveActivity(myHomeIntent, 0);
+        mMyHomeComponent = new ComponentName(context.getPackageName(), info.activityInfo.name);
+
+        mContext.registerReceiver(mUserPreferenceChangeReceiver,
+                new IntentFilter(ACTION_PREFERRED_ACTIVITY_CHANGED));
+        initOverviewTargets();
+    }
+
+    private void initOverviewTargets() {
+        ComponentName defaultHome = PackageManagerWrapper.getInstance()
+                .getHomeActivities(new ArrayList<>());
+
+        final String overviewIntentCategory;
+        if (defaultHome == null || mMyHomeComponent.equals(defaultHome)) {
+            // User default home is same as out home app. Use Overview integrated in Launcher.
+            overviewComponent = mMyHomeComponent;
+            mActivityControlHelper = new LauncherActivityControllerHelper();
+            overviewIntentCategory = Intent.CATEGORY_HOME;
+
+            if (mUpdateRegisteredPackage != null) {
+                // Remove any update listener as we don't care about other packages.
+                mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver);
+                mUpdateRegisteredPackage = null;
+            }
+        } else {
+            // The default home app is a different launcher. Use the fallback Overview instead.
+            overviewComponent = new ComponentName(mContext, RecentsActivity.class);
+            mActivityControlHelper = new FallbackActivityControllerHelper();
+            overviewIntentCategory = Intent.CATEGORY_DEFAULT;
+
+            // User's default home app can change as a result of package updates of this app (such
+            // as uninstalling the app or removing the "Launcher" feature in an update).
+            // Listen for package updates of this app (and remove any previously attached
+            // package listener).
+            if (!defaultHome.getPackageName().equals(mUpdateRegisteredPackage)) {
+                if (mUpdateRegisteredPackage != null) {
+                    mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver);
+                }
+
+                mUpdateRegisteredPackage = defaultHome.getPackageName();
+                IntentFilter updateReceiver = new IntentFilter(ACTION_PACKAGE_ADDED);
+                updateReceiver.addAction(ACTION_PACKAGE_CHANGED);
+                updateReceiver.addAction(ACTION_PACKAGE_REMOVED);
+                updateReceiver.addDataScheme("package");
+                updateReceiver.addDataSchemeSpecificPart(mUpdateRegisteredPackage,
+                        PatternMatcher.PATTERN_LITERAL);
+                mContext.registerReceiver(mOtherHomeAppUpdateReceiver, updateReceiver);
+            }
+        }
+
+        overviewIntent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(overviewIntentCategory)
+                .setComponent(overviewComponent)
                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        ResolveInfo info = context.getPackageManager().resolveActivity(homeIntent, 0);
+    }
 
-        if (DEBUG_START_FALLBACK_ACTIVITY) {
-            launcher = new ComponentName(context, RecentsActivity.class);
-            homeIntent.addCategory(Intent.CATEGORY_DEFAULT)
-                    .removeCategory(Intent.CATEGORY_HOME);
-        } else {
-            launcher = new ComponentName(context.getPackageName(), info.activityInfo.name);
+    public void onDestroy() {
+        mContext.unregisterReceiver(mUserPreferenceChangeReceiver);
+
+        if (mUpdateRegisteredPackage != null) {
+            mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver);
+            mUpdateRegisteredPackage = null;
         }
-
-        // Clear the packageName as system can fail to dedupe it b/64108432
-        homeIntent.setComponent(launcher).setPackage(null);
-    }
-
-    private void openRecents() {
-        Intent intent = addToIntent(new Intent(homeIntent));
-        mContext.startActivity(intent);
-        initWhenReady();
-    }
-
-    @UiThread
-    private void addFinishCommand(int requestId, int id, Runnable action) {
-        if (requestId < mCurrentCommandId) {
-            action.run();
-        } else {
-            mCurrentCommandFinishRunnables.put(id, action);
-        }
-    }
-
-    @UiThread
-    private void clearFinishCommand(int requestId, int id) {
-        if (requestId == mCurrentCommandId) {
-            mCurrentCommandFinishRunnables.remove(id);
-        }
-    }
-
-    @UiThread
-    private void initSwipeHandler(ActivityControlHelper helper, long time,
-            Consumer<WindowTransformSwipeHandler> onAnimationInitCallback) {
-        final int commandId = mCurrentCommandId;
-        final RunningTaskInfo runningTask = ActivityManagerWrapper.getInstance().getRunningTask();
-        final int runningTaskId = runningTask.id;
-        final WindowTransformSwipeHandler handler =
-                new WindowTransformSwipeHandler(runningTask, mContext, time, helper);
-
-        // Preload the plan
-        mRecentsModel.loadTasks(runningTaskId, null);
-        mWindowTransformSwipeHandler = handler;
-
-        mTempTaskTargetRect.setEmpty();
-        handler.setGestureEndCallback(() -> {
-            if (mWindowTransformSwipeHandler == handler) {
-                mWindowTransformSwipeHandler = null;
-                mTempTaskTargetRect.setEmpty();
-            }
-            clearFinishCommand(commandId, RID_RESET_SWIPE_HANDLER);
-            clearFinishCommand(commandId, RID_CANCEL_CONTROLLER);
-        });
-        handler.initWhenReady();
-        addFinishCommand(commandId, RID_RESET_SWIPE_HANDLER, handler::reset);
-
-        TraceHelper.beginSection(TAG);
-        Runnable startActivity = () -> helper.startRecentsFromButton(mContext,
-                addToIntent(homeIntent),
-                new RecentsAnimationListener() {
-                    public void onAnimationStart(
-                            RecentsAnimationControllerCompat controller,
-                            RemoteAnimationTargetCompat[] apps, Rect homeContentInsets,
-                            Rect minimizedHomeBounds) {
-                        if (mWindowTransformSwipeHandler == handler) {
-                            TraceHelper.partitionSection(TAG, "Received");
-                            handler.onRecentsAnimationStart(controller, apps, homeContentInsets,
-                                    minimizedHomeBounds);
-                            mTempTaskTargetRect.set(handler.getTargetRect(mWindowSize));
-
-                            ThumbnailData thumbnail = mAM.getTaskThumbnail(runningTaskId,
-                                    true /* reducedResolution */);
-                            mMainThreadExecutor.execute(() -> {
-                                addFinishCommand(commandId,
-                                        RID_CANCEL_CONTROLLER, () -> controller.finish(true));
-                                if (commandId == mCurrentCommandId) {
-                                    onAnimationInitCallback.accept(handler);
-
-                                    // The animation has started, which means the other activity
-                                    // should be paused, lets update the thumbnail
-                                    handler.switchToScreenshotImmediate(thumbnail);
-                                }
-                            });
-                        } else {
-                            TraceHelper.endSection(TAG, "Finishing no handler");
-                            controller.finish(false /* toHome */);
-                        }
-                    }
-
-                    public void onAnimationCanceled() {
-                        TraceHelper.endSection(TAG, "Cancelled: " + handler);
-                        if (mWindowTransformSwipeHandler == handler) {
-                            handler.onRecentsAnimationCanceled();
-                        }
-                    }
-                });
-
-        // We should almost always get touch-town on background thread. This is an edge case
-        // when the background Choreographer has not yet initialized.
-        BackgroundExecutor.get().submit(startActivity);
-    }
-
-    @UiThread
-    private void startZoomOutAnim(final WindowTransformSwipeHandler handler) {
-        final int commandId = mCurrentCommandId;
-        ValueAnimator anim = ValueAnimator.ofInt(0, -handler.getTransitionLength());
-        anim.addUpdateListener((a) -> handler.updateDisplacement((Integer) a.getAnimatedValue()));
-        anim.addListener(new AnimationSuccessListener() {
-            @Override
-            public void onAnimationSuccess(Animator animator) {
-                handler.onGestureEnded(0);
-                clearFinishCommand(commandId, RID_CANCEL_ZOOM_OUT_ANIMATION);
-            }
-        });
-        handler.onGestureStarted();
-        anim.setDuration(RECENTS_LAUNCH_DURATION);
-        anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-        anim.start();
-        addFinishCommand(commandId, RID_CANCEL_ZOOM_OUT_ANIMATION, anim::cancel);
     }
 
     public void onOverviewToggle() {
         // If currently screen pinning, do not enter overview
-        if (ActivityManagerWrapper.getInstance().isScreenPinningActive()) {
+        if (mAM.isScreenPinningActive()) {
             return;
         }
 
-        ActivityManagerWrapper.getInstance().closeSystemWindows("recentapps");
-        long time = SystemClock.elapsedRealtime();
-        mMainThreadExecutor.execute(() -> {
-            long elapsedTime = time - mLastToggleTime;
-            mLastToggleTime = time;
-
-            mCurrentCommandId++;
-            mTempTaskTargetRect.round(mTaskTargetRect);
-            int runnableCount = mCurrentCommandFinishRunnables.size();
-            if (runnableCount > 0) {
-                for (int i = 0; i < runnableCount; i++) {
-                    mCurrentCommandFinishRunnables.valueAt(i).run();
-                }
-                mCurrentCommandFinishRunnables.clear();
-            }
-
-            // TODO: We need to fix this case with PIP, when an activity first enters PIP, it shows
-            //       the menu activity which takes window focus, prevening the right condition from
-            //       being run below
-            ActivityControlHelper helper = getActivityControlHelper();
-            RecentsView recents = helper.getVisibleRecentsView();
-            if (recents != null) {
-                // Launch the next task
-                recents.showNextTask();
-            } else {
-                if (elapsedTime < ViewConfiguration.getDoubleTapTimeout()) {
-                    // The user tried to launch back into overview too quickly, either after
-                    // launching an app, or before overview has actually shown, just ignore for now
-                    return;
-                }
-
-                // Start overview
-                if (helper.switchToRecentsIfVisible()) {
-                    SysuiEventLogger.writeDummyRecentsTransition(0);
-                    // Do nothing
-                } else {
-                    initSwipeHandler(helper, time, this::startZoomOutAnim);
-                }
-            }
-        });
+        mAM.closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+        mMainThreadExecutor.execute(new RecentsActivityCommand<>());
     }
 
     public void onOverviewShown() {
-        getLauncher().runOnUiThread(() -> {
-                    if (isOverviewAlmostVisible()) {
-                        final RecentsView rv = getLauncher().getOverviewPanel();
-                        rv.snapToTaskAfterNext();
-                    } else {
-                        openRecents();
-                    }
-                }
-        );
-    }
-
-    public void onOverviewHidden() {
-        getLauncher().runOnUiThread(() -> {
-                    if (isOverviewAlmostVisible()) {
-                        final RecentsView rv = getLauncher().getOverviewPanel();
-                        rv.launchNextTask();
-                    }
-                }
-        );
-    }
-
-    @WorkerThread
-    private void startLastTask() {
-        // TODO: This should go through recents model.
-        List<RecentTaskInfo> tasks = mAM.getRecentTasks(2, UserHandle.myUserId());
-        if (tasks.size() > 1) {
-            RecentTaskInfo rti = tasks.get(1);
-
-            final ActivityOptions options;
-            if (!mTaskTargetRect.isEmpty()) {
-                final Rect targetRect = new Rect(mTaskTargetRect);
-                targetRect.offset(Utilities.isRtl(mContext.getResources())
-                        ? - mTaskTargetRect.width() : mTaskTargetRect.width(), 0);
-                final AppTransitionAnimationSpecCompat specCompat =
-                        new AppTransitionAnimationSpecCompat(rti.id, null, targetRect);
-                AppTransitionAnimationSpecsFuture specFuture =
-                        new AppTransitionAnimationSpecsFuture(mMainThreadExecutor.getHandler()) {
-
-                    @Override
-                    public List<AppTransitionAnimationSpecCompat> composeSpecs() {
-                        return Collections.singletonList(specCompat);
-                    }
-                };
-                options = RecentsTransition.createAspectScaleAnimation(mContext,
-                        mMainThreadExecutor.getHandler(), true /* scaleUp */,
-                        specFuture, () -> {});
-            } else {
-                options = ActivityOptions.makeBasic();
-            }
-            mAM.startActivityFromRecents(rti.id, options);
-        }
-    }
-
-    private boolean isOverviewAlmostVisible() {
-        if (clearReference()) {
-            return true;
-        }
-        if (!mAM.getRunningTask().topActivity.equals(launcher)) {
-            return false;
-        }
-        Launcher launcher = getLauncher();
-        return launcher != null && launcher.isStarted() && launcher.isInState(OVERVIEW);
-    }
-
-    private Launcher getLauncher() {
-        return (Launcher) LauncherAppState.getInstance(mContext).getModel().getCallback();
-    }
-
-    @Override
-    protected boolean init(Launcher launcher, boolean alreadyOnHome) {
-        AbstractFloatingView.closeAllOpenViews(launcher, alreadyOnHome);
-        launcher.getStateManager().goToState(OVERVIEW, alreadyOnHome);
-        clearReference();
-        return false;
+        mMainThreadExecutor.execute(new ShowRecentsCommand());
     }
 
     public ActivityControlHelper getActivityControlHelper() {
-        if (DEBUG_START_FALLBACK_ACTIVITY) {
-            return new FallbackActivityControllerHelper();
-        } else {
-            return new LauncherActivityControllerHelper();
+        return mActivityControlHelper;
+    }
+
+    private class ShowRecentsCommand extends RecentsActivityCommand {
+
+        @Override
+        protected boolean handleCommand(long elapsedTime) {
+            RecentsView recents = mHelper.getVisibleRecentsView();
+            if (recents != null) {
+                recents.snapToTaskAfterNext();
+                return true;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    private class RecentsActivityCommand<T extends BaseDraggingActivity> implements Runnable {
+
+        protected final ActivityControlHelper<T> mHelper;
+        private final long mCreateTime;
+        private final int mRunningTaskId;
+
+        private ActivityInitListener mListener;
+        private T mActivity;
+
+        public RecentsActivityCommand() {
+            mHelper = getActivityControlHelper();
+            mCreateTime = SystemClock.elapsedRealtime();
+            mRunningTaskId = mAM.getRunningTask().id;
+
+            // Preload the plan
+            mRecentsModel.loadTasks(mRunningTaskId, null);
+        }
+
+        @Override
+        public void run() {
+            long elapsedTime = mCreateTime - mLastToggleTime;
+            mLastToggleTime = mCreateTime;
+
+            if (!handleCommand(elapsedTime)) {
+                // Start overview
+                if (mHelper.switchToRecentsIfVisible()) {
+                    SysuiEventLogger.writeDummyRecentsTransition(0);
+                    // Do nothing
+                } else {
+                    mListener = mHelper.createActivityInitListener(this::onActivityReady);
+                    mListener.registerAndStartActivity(overviewIntent, this::createWindowAnimation,
+                            mContext, mMainThreadExecutor.getHandler(), RECENTS_LAUNCH_DURATION);
+                }
+            }
+        }
+
+        protected boolean handleCommand(long elapsedTime) {
+            // TODO: We need to fix this case with PIP, when an activity first enters PIP, it shows
+            //       the menu activity which takes window focus, preventing the right condition from
+            //       being run below
+            RecentsView recents = mHelper.getVisibleRecentsView();
+            if (recents != null) {
+                // Launch the next task
+                recents.showNextTask();
+                return true;
+            } else if (elapsedTime < ViewConfiguration.getDoubleTapTimeout()) {
+                // The user tried to launch back into overview too quickly, either after
+                // launching an app, or before overview has actually shown, just ignore for now
+                return true;
+            }
+            return false;
+        }
+
+        private boolean onActivityReady(T activity, Boolean wasVisible) {
+            activity.<RecentsView>getOverviewPanel().setCurrentTask(mRunningTaskId);
+            AbstractFloatingView.closeAllOpenViews(activity, wasVisible);
+            mHelper.prepareRecentsUI(activity, wasVisible);
+            if (wasVisible) {
+                AnimatorPlaybackController controller =
+                        mHelper.createControllerForVisibleActivity(activity);
+                controller.dispatchOnStart();
+                ValueAnimator anim =
+                        controller.getAnimationPlayer().setDuration(RECENTS_LAUNCH_DURATION);
+                anim.setInterpolator(FAST_OUT_SLOW_IN);
+                anim.start();
+            }
+            mActivity = activity;
+            return false;
+        }
+
+        private AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targetCompats) {
+            if (mListener != null) {
+                mListener.unregister();
+            }
+            RemoteAnimationProvider.showOpeningTarget(targetCompats);
+            AnimatorSet anim = new AnimatorSet();
+            if (mActivity == null) {
+                Log.e(TAG, "Animation created, before activity");
+                anim.play(ValueAnimator.ofInt(0, 1).setDuration(100));
+                return anim;
+            }
+
+            RemoteAnimationTargetCompat closingTarget = null;
+            // Use the top closing app to determine the insets for the animation
+            for (RemoteAnimationTargetCompat target : targetCompats) {
+                if (target.mode == MODE_CLOSING) {
+                    closingTarget = target;
+                    break;
+                }
+            }
+            if (closingTarget == null) {
+                Log.e(TAG, "No closing app");
+                anim.play(ValueAnimator.ofInt(0, 1).setDuration(100));
+                return anim;
+            }
+
+            final ClipAnimationHelper clipHelper = new ClipAnimationHelper();
+
+            // At this point, the activity is already started and laid-out. Get the home-bounds
+            // relative to the screen using the rootView of the activity.
+            int loc[] = new int[2];
+            View rootView = mActivity.getRootView();
+            rootView.getLocationOnScreen(loc);
+            Rect homeBounds = new Rect(loc[0], loc[1],
+                    loc[0] + rootView.getWidth(), loc[1] + rootView.getHeight());
+            clipHelper.updateSource(homeBounds, closingTarget);
+
+            Rect targetRect = new Rect();
+            mHelper.getSwipeUpDestinationAndLength(
+                    mActivity.getDeviceProfile(), mActivity, targetRect);
+            clipHelper.updateTargetRect(targetRect);
+
+
+            ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
+            valueAnimator.setDuration(RECENTS_LAUNCH_DURATION).setInterpolator(FAST_OUT_SLOW_IN);
+            valueAnimator.addUpdateListener((v) -> {
+                clipHelper.applyTransform(targetCompats, (float) v.getAnimatedValue());
+            });
+            anim.play(valueAnimator);
+            return anim;
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
new file mode 100644
index 0000000..aed9959
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -0,0 +1,34 @@
+/*
+ * 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.content.Context;
+
+import com.android.launcher3.MainProcessInitializer;
+import com.android.systemui.shared.system.ThreadedRendererCompat;
+
+public class QuickstepProcessInitializer extends MainProcessInitializer {
+
+    public QuickstepProcessInitializer(Context context) { }
+
+    @Override
+    protected void init(Context context) {
+        super.init(context);
+
+        // Elevate GPU priority for Quickstep and Remote animations.
+        ThreadedRendererCompat.setContextPriority(ThreadedRendererCompat.EGL_CONTEXT_PRIORITY_HIGH_IMG);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index cf60fdf..eb0be89 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -15,15 +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;
@@ -31,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;
 
@@ -83,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
@@ -94,6 +155,7 @@
     protected void onStart() {
         super.onStart();
         UiFactory.onStart(this);
+        mFallbackRecentsView.resetTaskVisuals();
     }
 
     @Override
@@ -103,8 +165,26 @@
     }
 
     @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+        RecentsActivityTracker.onRecentsActivityNewIntent(this);
+    }
+
+    @Override
     protected void onDestroy() {
         super.onDestroy();
         RecentsActivityTracker.onRecentsActivityDestroy(this);
     }
+
+    @Override
+    public void onBackPressed() {
+        // TODO: Launch the task we came from
+        startHome();
+    }
+
+    public void startHome() {
+        startActivity(new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsActivityTracker.java b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java
index 5bd606e..fb6090e 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivityTracker.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java
@@ -16,9 +16,15 @@
 package com.android.quickstep;
 
 import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
 import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
 
+import com.android.launcher3.MainThreadExecutor;
 import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
+import com.android.quickstep.util.RemoteAnimationProvider;
 
 import java.lang.ref.WeakReference;
 import java.util.function.BiPredicate;
@@ -29,9 +35,8 @@
 @TargetApi(Build.VERSION_CODES.P)
 public class RecentsActivityTracker implements ActivityInitListener {
 
-    private static final Object LOCK = new Object();
-    private static WeakReference<RecentsActivityTracker> sTracker = new WeakReference<>(null);
     private static WeakReference<RecentsActivity> sCurrentActivity = new WeakReference<>(null);
+    private static final Scheduler sScheduler = new Scheduler();
 
     private final BiPredicate<RecentsActivity, Boolean> mOnInitListener;
 
@@ -41,41 +46,86 @@
 
     @Override
     public void register() {
-        synchronized (LOCK) {
-            sTracker = new WeakReference<>(this);
-        }
+        sScheduler.schedule(this);
     }
 
     @Override
     public void unregister() {
-        synchronized (LOCK) {
-            if (sTracker.get() == this) {
-                sTracker.clear();
-            }
-        }
+        sScheduler.clearReference(this);
     }
 
-    public static void onRecentsActivityCreate(RecentsActivity activity) {
-        synchronized (LOCK) {
-            RecentsActivityTracker tracker = sTracker.get();
-            if (tracker != null && tracker.mOnInitListener.test(activity, false)) {
-                sTracker.clear();
-            }
-            sCurrentActivity = new WeakReference<>(activity);
-        }
-    }
-
-    public static void onRecentsActivityDestroy(RecentsActivity activity) {
-        synchronized (LOCK) {
-            if (sCurrentActivity.get() == activity) {
-                sCurrentActivity.clear();
-            }
-        }
+    private boolean init(RecentsActivity activity, boolean visible) {
+        return mOnInitListener.test(activity, visible);
     }
 
     public static RecentsActivity getCurrentActivity() {
-        synchronized (LOCK) {
-            return sCurrentActivity.get();
+        return sCurrentActivity.get();
+    }
+
+    @Override
+    public void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider,
+            Context context, Handler handler, long duration) {
+        register();
+
+        Bundle options = animProvider.toActivityOptions(handler, duration).toBundle();
+        context.startActivity(intent, options);
+    }
+
+    public static void onRecentsActivityCreate(RecentsActivity activity) {
+        sCurrentActivity = new WeakReference<>(activity);
+        sScheduler.initIfPending(activity, false);
+    }
+
+
+    public static void onRecentsActivityNewIntent(RecentsActivity activity) {
+        sScheduler.initIfPending(activity, activity.isStarted());
+    }
+
+    public static void onRecentsActivityDestroy(RecentsActivity activity) {
+        if (sCurrentActivity.get() == activity) {
+            sCurrentActivity.clear();
+        }
+    }
+
+
+    private static class Scheduler implements Runnable {
+
+        private WeakReference<RecentsActivityTracker> mPendingTracker = new WeakReference<>(null);
+        private MainThreadExecutor mMainThreadExecutor;
+
+        public synchronized void schedule(RecentsActivityTracker tracker) {
+            mPendingTracker = new WeakReference<>(tracker);
+            if (mMainThreadExecutor == null) {
+                mMainThreadExecutor = new MainThreadExecutor();
+            }
+            mMainThreadExecutor.execute(this);
+        }
+
+        @Override
+        public void run() {
+            RecentsActivity activity = sCurrentActivity.get();
+            if (activity != null) {
+                initIfPending(activity, activity.isStarted());
+            }
+        }
+
+        public synchronized boolean initIfPending(RecentsActivity activity, boolean alreadyOnHome) {
+            RecentsActivityTracker tracker = mPendingTracker.get();
+            if (tracker != null) {
+                if (!tracker.init(activity, alreadyOnHome)) {
+                    mPendingTracker.clear();
+                }
+                return true;
+            }
+            return false;
+        }
+
+        public synchronized boolean clearReference(RecentsActivityTracker tracker) {
+            if (mPendingTracker.get() == tracker) {
+                mPendingTracker.clear();
+                return true;
+            }
+            return false;
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationActivityOptions.java b/quickstep/src/com/android/quickstep/RecentsAnimationActivityOptions.java
deleted file mode 100644
index a25e192..0000000
--- a/quickstep/src/com/android/quickstep/RecentsAnimationActivityOptions.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
-import com.android.systemui.shared.system.RecentsAnimationListener;
-import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.TransactionCompat;
-import com.android.systemui.shared.system.WindowManagerWrapper;
-import java.util.function.Consumer;
-
-/**
- * Class to create activity options to emulate recents transition.
- */
-public class RecentsAnimationActivityOptions implements RemoteAnimationRunnerCompat {
-
-    private final RecentsAnimationListener mListener;
-    private final Runnable mFinishCallback;
-
-    public RecentsAnimationActivityOptions(RecentsAnimationListener listener,
-            Runnable finishCallback) {
-        mListener = listener;
-        mFinishCallback = finishCallback;
-    }
-
-    @Override
-    public void onAnimationStart(RemoteAnimationTargetCompat[] targetCompats,
-            Runnable runnable) {
-        showOpeningTarget(targetCompats);
-        RemoteRecentsAnimationControllerCompat dummyRecentsAnim =
-                new RemoteRecentsAnimationControllerCompat(() -> {
-                    runnable.run();
-                    if (mFinishCallback != null) {
-                        mFinishCallback.run();
-                    }
-                });
-
-        Rect insets = new Rect();
-        WindowManagerWrapper.getInstance().getStableInsets(insets);
-        mListener.onAnimationStart(dummyRecentsAnim, targetCompats, insets, null);
-    }
-
-    @Override
-    public void onAnimationCancelled() {
-        mListener.onAnimationCanceled();
-    }
-
-    private void showOpeningTarget(RemoteAnimationTargetCompat[] targetCompats) {
-        TransactionCompat t = new TransactionCompat();
-        for (RemoteAnimationTargetCompat target : targetCompats) {
-            int layer = target.mode == RemoteAnimationTargetCompat.MODE_CLOSING
-                    ? Integer.MAX_VALUE
-                    : target.prefixOrderIndex;
-            t.setLayer(target.leash, layer);
-            t.show(target.leash);
-        }
-        t.apply();
-    }
-
-    private class RemoteRecentsAnimationControllerCompat extends RecentsAnimationControllerCompat {
-
-        final Runnable mFinishCallback;
-
-        public RemoteRecentsAnimationControllerCompat(Runnable finishCallback) {
-            mFinishCallback = finishCallback;
-        }
-
-        @Override
-        public ThumbnailData screenshotTask(int taskId) {
-            return new ThumbnailData();
-        }
-
-        @Override
-        public void setInputConsumerEnabled(boolean enabled) { }
-
-        @Override
-        public void setAnimationTargetsBehindSystemBars(boolean behindSystemBars) { }
-
-        @Override
-        public void finish(boolean toHome) {
-            // This should never be called with toHome == false
-            if (mFinishCallback != null) {
-                mFinishCallback.run();
-            }
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java
index 2df951b..c66f00f 100644
--- a/quickstep/src/com/android/quickstep/TaskUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskUtils.java
@@ -16,24 +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);
@@ -48,4 +71,152 @@
         return userManagerCompat.getBadgedLabelForUser(
             applicationInfo.loadLabel(packageManager), user);
     }
+
+    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/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index b610f4d..33b922d 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -21,8 +21,7 @@
 import static android.view.MotionEvent.ACTION_POINTER_DOWN;
 import static android.view.MotionEvent.ACTION_POINTER_UP;
 import static android.view.MotionEvent.ACTION_UP;
-import static com.android.launcher3.LauncherState.FAST_OVERVIEW;
-import static com.android.launcher3.LauncherState.NORMAL;
+
 import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE;
 
 import android.annotation.TargetApi;
@@ -43,9 +42,7 @@
 import android.view.View;
 import android.view.ViewConfiguration;
 
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherState;
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.R;
 import com.android.launcher3.util.TraceHelper;
@@ -194,6 +191,7 @@
 
     @Override
     public void onDestroy() {
+        mOverviewCommandHelper.onDestroy();
         sConnected = false;
         super.onDestroy();
     }
@@ -220,36 +218,38 @@
 
     private TouchConsumer getCurrentTouchConsumer(
             @HitTarget int downHitTarget, boolean forceToLauncher, VelocityTracker tracker) {
-        RunningTaskInfo runningTaskInfo = mAM.getRunningTask();
+        RunningTaskInfo runningTaskInfo = mAM.getRunningTask(0);
 
         if (runningTaskInfo == null && !forceToLauncher) {
             return mNoOpTouchConsumer;
         } else if (forceToLauncher ||
-                runningTaskInfo.topActivity.equals(mOverviewCommandHelper.launcher)) {
-            return getLauncherConsumer();
+                runningTaskInfo.topActivity.equals(mOverviewCommandHelper.overviewComponent)) {
+            return getOverviewConsumer();
         } else {
             if (tracker == null) {
                 tracker = VelocityTracker.obtain();
             }
             return new OtherActivityTouchConsumer(this, runningTaskInfo, mRecentsModel,
-                            mOverviewCommandHelper.homeIntent,
+                            mOverviewCommandHelper.overviewIntent,
                             mOverviewCommandHelper.getActivityControlHelper(), mMainThreadExecutor,
                             mBackgroundThreadChoreographer, downHitTarget, tracker);
         }
     }
 
-    private TouchConsumer getLauncherConsumer() {
-        Launcher launcher = (Launcher) LauncherAppState.getInstance(this).getModel().getCallback();
-        if (launcher == null) {
+    private TouchConsumer getOverviewConsumer() {
+        ActivityControlHelper activityHelper = mOverviewCommandHelper.getActivityControlHelper();
+        BaseDraggingActivity activity = activityHelper.getCreatedActivity();
+        if (activity == null) {
             return mNoOpTouchConsumer;
         }
-        View target = launcher.getDragLayer();
-        return new LauncherTouchConsumer(launcher, target);
+        return new OverviewTouchConsumer(activityHelper, activity);
     }
 
-    private static class LauncherTouchConsumer implements TouchConsumer {
+    private static class OverviewTouchConsumer<T extends BaseDraggingActivity>
+            implements TouchConsumer {
 
-        private final Launcher mLauncher;
+        private final ActivityControlHelper<T> mActivityHelper;
+        private final T mActivity;
         private final View mTarget;
         private final int[] mLocationOnScreen = new int[2];
         private final PointF mDownPos = new PointF();
@@ -260,12 +260,17 @@
         private boolean mInvalidated = false;
         private boolean mHadWindowFocusOnDown;
 
-        LauncherTouchConsumer(Launcher launcher, View target) {
-            mLauncher = launcher;
-            mTarget = target;
+        private float mLastProgress = 0;
+        private boolean mStartPending = false;
+        private boolean mEndPending = false;
+
+        OverviewTouchConsumer(ActivityControlHelper<T> activityHelper, T activity) {
+            mActivityHelper = activityHelper;
+            mActivity = activity;
+            mTarget = activity.getDragLayer();
             mTouchSlop = ViewConfiguration.get(mTarget.getContext()).getScaledTouchSlop();
 
-            mQuickScrubController = mLauncher.<RecentsView>getOverviewPanel()
+            mQuickScrubController = mActivity.<RecentsView>getOverviewPanel()
                     .getQuickScrubController();
         }
 
@@ -327,16 +332,22 @@
                 return;
             }
             if (interactionType == INTERACTION_QUICK_SCRUB) {
+                mStartPending = true;
+
                 Runnable action = () -> {
-                    LauncherState fromState = mLauncher.getStateManager().getState();
-                    mLauncher.getStateManager().goToState(FAST_OVERVIEW, true);
-                    mQuickScrubController.onQuickScrubStart(fromState == NORMAL);
+                    mQuickScrubController.onQuickScrubStart(
+                            mActivityHelper.onQuickInteractionStart(mActivity, true));
+                    mQuickScrubController.onQuickScrubProgress(mLastProgress);
+                    mStartPending = false;
+
+                    if (mEndPending) {
+                        mQuickScrubController.onQuickScrubEnd();
+                        mEndPending = false;
+                    }
+
                 };
 
-                if (mLauncher.getWorkspace().runOnOverlayHidden(action)) {
-                    // Hide the minus one overlay so launcher can get window focus.
-                    mLauncher.onQuickstepGestureStarted(true);
-                }
+                mActivityHelper.executeOnWindowAvailable(mActivity, action);
             }
         }
 
@@ -345,12 +356,17 @@
             if (mInvalidated) {
                 return;
             }
-            mQuickScrubController.onQuickScrubEnd();
+            if (mStartPending) {
+                mEndPending = true;
+            } else {
+                mQuickScrubController.onQuickScrubEnd();
+            }
         }
 
         @Override
         public void onQuickScrubProgress(float progress) {
-            if (mInvalidated) {
+            mLastProgress = progress;
+            if (mInvalidated || mEndPending) {
                 return;
             }
             mQuickScrubController.onQuickScrubProgress(progress);
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index f6cf85a..fb061d0 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -30,11 +30,8 @@
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.content.res.Resources;
-import android.graphics.Matrix;
-import android.graphics.Matrix.ScaleToFit;
 import android.graphics.Point;
 import android.graphics.Rect;
-import android.graphics.RectF;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
@@ -64,11 +61,11 @@
 import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
 import com.android.quickstep.ActivityControlHelper.LayoutListener;
 import com.android.quickstep.TouchConsumer.InteractionType;
+import com.android.quickstep.util.ClipAnimationHelper;
 import com.android.quickstep.util.SysuiEventLogger;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.recents.utilities.RectFEvaluator;
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.LatencyTrackerCompat;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -132,26 +129,8 @@
 
     private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
 
-    // The bounds of the source app in device coordinates
-    private final Rect mSourceStackBounds = new Rect();
-    // The insets of the source app
-    private final Rect mSourceInsets = new Rect();
-    // The source app bounds with the source insets applied, in the source app window coordinates
-    private final RectF mSourceRect = new RectF();
-    // The bounds of the task view in launcher window coordinates
-    private final RectF mTargetRect = new RectF();
-    // Doesn't change after initialized, used as an anchor when changing mTargetRect
-    private final RectF mInitialTargetRect = new RectF();
-    // The insets to be used for clipping the app window, which can be larger than mSourceInsets
-    // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In
-    // app window coordinates.
-    private final RectF mSourceWindowClipInsets = new RectF();
+    private final ClipAnimationHelper mClipAnimationHelper = new ClipAnimationHelper();
 
-    // The bounds of launcher (not including insets) in device coordinates
-    private final Rect mHomeStackBounds = new Rect();
-    // The clip rect in source app window coordinates
-    private final Rect mClipRect = new Rect();
-    private final RectFEvaluator mRectFEvaluator = new RectFEvaluator();
     protected Runnable mGestureEndCallback;
     protected boolean mIsGoingToHome;
     private DeviceProfile mDp;
@@ -185,6 +164,7 @@
     private float mCurrentDisplacement;
     private boolean mGestureStarted;
     private int mLogAction = Touch.SWIPE;
+    private float mCurrentQuickScrubProgress;
 
     private @InteractionType int mInteractionType = INTERACTION_NORMAL;
 
@@ -192,14 +172,9 @@
             InputConsumerController.getRecentsAnimationInputConsumer();
 
     private final RecentsAnimationWrapper mRecentsAnimationWrapper = new RecentsAnimationWrapper();
-    private Matrix mTmpMatrix = new Matrix();
     private final long mTouchTimeMs;
     private long mLauncherFrameDrawnTime;
 
-    // Only used with the recents activity, when the screenshot should be fetched at the beginning
-    // of the animation and not at the end when the activity is already paused
-    private boolean mSkipScreenshotAtEndOfTransition;
-
     WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context, long touchTimeMs,
             ActivityControlHelper<T> controller) {
         mContext = context;
@@ -254,11 +229,11 @@
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
                 this::invalidateHandlerWithLauncher);
 
-        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_QUICK_SCRUB_START,
+        mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START,
                 this::onQuickScrubStart);
-        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_QUICK_SCRUB_START
+        mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START
                 | STATE_SCALED_CONTROLLER_RECENTS, this::onFinishedTransitionToQuickScrub);
-        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SWITCH_TO_SCREENSHOT_COMPLETE
+        mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_SWITCH_TO_SCREENSHOT_COMPLETE
                 | STATE_QUICK_SCRUB_END, this::switchToFinalAppAfterQuickScrub);
     }
 
@@ -273,42 +248,11 @@
 
     private void initTransitionEndpoints(DeviceProfile dp) {
         mDp = dp;
-        mSourceRect.set(mSourceInsets.left, mSourceInsets.top,
-                mSourceStackBounds.width() - mSourceInsets.right,
-                mSourceStackBounds.height() - mSourceInsets.bottom);
 
         Rect tempRect = new Rect();
         mTransitionDragLength = mActivityControlHelper
                 .getSwipeUpDestinationAndLength(dp, mContext, tempRect);
-
-        mTargetRect.set(tempRect);
-        mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left,
-                mHomeStackBounds.top - mSourceStackBounds.top);
-        mInitialTargetRect.set(mTargetRect);
-
-        // Calculate the clip based on the target rect (since the content insets and the
-        // launcher insets may differ, so the aspect ratio of the target rect can differ
-        // from the source rect. The difference between the target rect (scaled to the
-        // source rect) is the amount to clip on each edge.
-        RectF scaledTargetRect = new RectF(mTargetRect);
-        Utilities.scaleRectFAboutCenter(scaledTargetRect,
-                mSourceRect.width() / mTargetRect.width());
-        scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top);
-        mSourceWindowClipInsets.set(
-                Math.max(scaledTargetRect.left, 0),
-                Math.max(scaledTargetRect.top, 0),
-                Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0),
-                Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0));
-        mSourceRect.set(scaledTargetRect);
-    }
-
-    public int getTransitionLength() {
-        return mTransitionDragLength;
-    }
-
-    public RectF getTargetRect(Point outWindowSize) {
-        outWindowSize.set(mDp.widthPx, mDp.heightPx);
-        return mInitialTargetRect;
+        mClipAnimationHelper.updateTargetRect(tempRect);
     }
 
     private long getFadeInDuration() {
@@ -365,7 +309,6 @@
             return;
         }
 
-        mStateCallback.setState(STATE_LAUNCHER_STARTED);
         mActivityControlHelper.prepareRecentsUI(mActivity, mWasLauncherAlreadyVisible);
         AbstractFloatingView.closeAllOpenViews(activity, mWasLauncherAlreadyVisible);
 
@@ -400,6 +343,7 @@
         mRecentsView.showTask(mRunningTaskId);
         mRecentsView.setFirstTaskIconScaledDown(true /* isScaledDown */, false /* animate */);
         mLayoutListener.open();
+        mStateCallback.setState(STATE_LAUNCHER_STARTED);
     }
 
     public void setLauncherOnDrawCallback(Runnable callback) {
@@ -482,40 +426,10 @@
 
         synchronized (mRecentsAnimationWrapper) {
             if (mRecentsAnimationWrapper.controller != null) {
-                RectF currentRect;
-                synchronized (mTargetRect) {
-                    Interpolator interpolator = mInteractionType == INTERACTION_QUICK_SCRUB
-                            ? ACCEL_2 : LINEAR;
-                    float interpolated = interpolator.getInterpolation(shift);
-                    currentRect = mRectFEvaluator.evaluate(interpolated, mSourceRect, mTargetRect);
-                    // Stay lined up with the center of the target, since it moves for quick scrub.
-                    currentRect.offset(mTargetRect.centerX() - currentRect.centerX(), 0);
-                }
-
-                mClipRect.left = (int) (mSourceWindowClipInsets.left * shift);
-                mClipRect.top = (int) (mSourceWindowClipInsets.top * shift);
-                mClipRect.right = (int)
-                        (mSourceStackBounds.width() - (mSourceWindowClipInsets.right * shift));
-                mClipRect.bottom = (int)
-                        (mSourceStackBounds.height() - (mSourceWindowClipInsets.bottom * shift));
-
-                mTmpMatrix.setRectToRect(mSourceRect, currentRect, ScaleToFit.FILL);
-
-                TransactionCompat transaction = new TransactionCompat();
-                for (RemoteAnimationTargetCompat app : mRecentsAnimationWrapper.targets) {
-                    if (app.mode == MODE_CLOSING) {
-                        mTmpMatrix.postTranslate(app.position.x, app.position.y);
-                        transaction.setMatrix(app.leash, mTmpMatrix)
-                                .setWindowCrop(app.leash, mClipRect);
-
-                        if (app.isNotInRecents) {
-                            transaction.setAlpha(app.leash, 1 - shift);
-                        }
-
-                        transaction.show(app.leash);
-                    }
-                }
-                transaction.apply();
+                Interpolator interpolator = mInteractionType == INTERACTION_QUICK_SCRUB
+                        ? ACCEL_2 : LINEAR;
+                float interpolated = interpolator.getInterpolation(shift);
+                mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targets, interpolated);
             }
         }
 
@@ -533,13 +447,9 @@
                 if (firstTask != null) {
                     int scrollForFirstTask = mRecentsView.getScrollForPage(0);
                     int offsetFromFirstTask = (scrollForFirstTask - mRecentsView.getScrollX());
-                    synchronized (mTargetRect) {
-                        mTargetRect.set(mInitialTargetRect);
-                        Utilities.scaleRectFAboutCenter(mTargetRect, firstTask.getScaleX());
-                        float offsetX = offsetFromFirstTask + firstTask.getTranslationX();
-                        float offsetY = mRecentsView.getTranslationY();
-                        mTargetRect.offset(offsetX, offsetY);
-                    }
+                    mClipAnimationHelper.offsetTarget(firstTask.getScaleX(),
+                            offsetFromFirstTask + firstTask.getTranslationX(),
+                            mRecentsView.getTranslationY());
                 }
                 if (mRecentsAnimationWrapper.controller != null) {
                     // TODO: This logic is spartanic!
@@ -564,13 +474,15 @@
             for (RemoteAnimationTargetCompat target : apps) {
                 if (target.mode == MODE_CLOSING) {
                     DeviceProfile dp = LauncherAppState.getIDP(mContext).getDeviceProfile(mContext);
+                    final Rect homeStackBounds;
+
                     if (minimizedHomeBounds != null) {
-                        mHomeStackBounds.set(minimizedHomeBounds);
+                        homeStackBounds = minimizedHomeBounds;
                         dp = dp.getMultiWindowProfile(mContext,
                                 new Point(minimizedHomeBounds.width(), minimizedHomeBounds.height()));
                         dp.updateInsets(homeContentInsets);
                     } else {
-                        mHomeStackBounds.set(new Rect(0, 0, dp.widthPx, dp.heightPx));
+                        homeStackBounds = new Rect(0, 0, dp.widthPx, dp.heightPx);
                         // TODO: Workaround for an existing issue where the home content insets are
                         // not valid immediately after rotation, just use the stable insets for now
                         Rect insets = new Rect();
@@ -578,16 +490,7 @@
                         dp.updateInsets(insets);
                     }
 
-                    // Initialize the start and end animation bounds
-                    // TODO: Remove once platform is updated
-                    try {
-                        mSourceInsets.set(target.getContentInsets());
-                    } catch (Error e) {
-                        // TODO: Remove once platform is updated, use stable insets as fallback
-                        WindowManagerWrapper.getInstance().getStableInsets(mSourceInsets);
-                    }
-                    mSourceStackBounds.set(target.sourceContainerBounds);
-
+                    mClipAnimationHelper.updateSource(homeStackBounds, target);
                     initTransitionEndpoints(dp);
                 }
             }
@@ -736,7 +639,7 @@
         };
 
         synchronized (mRecentsAnimationWrapper) {
-            if (mRecentsAnimationWrapper.controller != null && !mSkipScreenshotAtEndOfTransition) {
+            if (mRecentsAnimationWrapper.controller != null) {
                 TransactionCompat transaction = new TransactionCompat();
                 for (RemoteAnimationTargetCompat app : mRecentsAnimationWrapper.targets) {
                     if (app.mode == MODE_CLOSING) {
@@ -745,6 +648,7 @@
                                 mRecentsAnimationWrapper.controller.screenshotTask(app.taskId);
                         TaskView taskView = mRecentsView.updateThumbnail(app.taskId, thumbnail);
                         if (taskView != null) {
+                            taskView.setAlpha(1);
                             // Defer finishing the animation until the next launcher frame with the
                             // new thumbnail
                             mActivityControlHelper.executeOnNextDraw(mActivity, taskView,
@@ -763,12 +667,6 @@
         doLogGesture(true /* toLauncher */);
     }
 
-    @UiThread
-    public void switchToScreenshotImmediate(ThumbnailData thumbnail) {
-        mRecentsView.updateThumbnail(mRunningTaskId, thumbnail);
-        mSkipScreenshotAtEndOfTransition = true;
-    }
-
     private void setupLauncherUiAfterSwipeUpAnimation() {
         if (mLauncherTransitionController != null) {
             mLauncherTransitionController.getAnimationPlayer().end();
@@ -787,6 +685,9 @@
     private void onQuickScrubStart() {
         mActivityControlHelper.onQuickInteractionStart(mActivity, mWasLauncherAlreadyVisible);
         mQuickScrubController.onQuickScrubStart(false);
+
+        // Inform the last progress in case we skipped before.
+        mQuickScrubController.onQuickScrubProgress(mCurrentQuickScrubProgress);
     }
 
     private void onFinishedTransitionToQuickScrub() {
@@ -794,10 +695,8 @@
     }
 
     public void onQuickScrubProgress(float progress) {
+        mCurrentQuickScrubProgress = progress;
         if (Looper.myLooper() != Looper.getMainLooper() || mQuickScrubController == null) {
-            // TODO: We can still get progress events while launcher is not ready on the worker
-            // thread. Keep track of last received progress and apply that progress when launcher
-            // is ready
             return;
         }
         mQuickScrubController.onQuickScrubProgress(progress);
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 7d7f88f..89422af 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -18,15 +18,16 @@
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Rect;
+import android.support.annotation.AnyThread;
 import android.util.AttributeSet;
 import android.view.View;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Insettable;
 import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.RecentsView;
 
-public class FallbackRecentsView extends RecentsView<RecentsActivity> implements Insettable {
+public class FallbackRecentsView extends RecentsView<RecentsActivity> {
 
     public FallbackRecentsView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
@@ -40,7 +41,7 @@
 
     @Override
     protected void onAllTasksRemoved() {
-        mActivity.finish();
+        mActivity.startHome();
     }
 
     @Override
@@ -56,30 +57,18 @@
     }
 
     @Override
-    public void setInsets(Rect insets) {
-        mInsets.set(insets);
-        DeviceProfile dp = mActivity.getDeviceProfile();
-        Rect padding = getPadding(dp, getContext());
-        verticalCenter(padding, dp);
-        setPadding(padding.left, padding.top, padding.right, padding.bottom);
-    }
-
-    private static void verticalCenter(Rect padding, DeviceProfile dp) {
-        Rect insets = dp.getInsets();
-        int totalSpace = (padding.top + padding.bottom - insets.top - insets.bottom) / 2;
-        padding.top = insets.top + totalSpace;
-        padding.bottom = insets.bottom + totalSpace;
-    }
-
-    public static void getCenterPageRect(DeviceProfile grid, Context context, Rect outRect) {
-        Rect targetPadding = getPadding(grid, context);
-        verticalCenter(targetPadding, grid);
-        getPageRect(grid, context, outRect, targetPadding);
-    }
-
-    @Override
     public void draw(Canvas canvas) {
         maybeDrawEmptyMessage(canvas);
         super.draw(canvas);
     }
+
+    @Override
+    protected void getTaskSize(DeviceProfile dp, Rect outRect) {
+        LayoutUtils.calculateTaskSize(getContext(), dp, 0, outRect);
+    }
+
+    @AnyThread
+    public static void getPageRect(DeviceProfile grid, Context context, Rect outRect) {
+        LayoutUtils.calculateTaskSize(context, grid, 0, outRect);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
new file mode 100644
index 0000000..493e9e2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
@@ -0,0 +1,135 @@
+/*
+ * 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 static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+
+import android.graphics.Matrix;
+import android.graphics.Matrix.ScaleToFit;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.launcher3.Utilities;
+import com.android.systemui.shared.recents.utilities.RectFEvaluator;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.TransactionCompat;
+
+/**
+ * Utility class to handle window clip animation
+ */
+public class ClipAnimationHelper {
+
+    // The bounds of the source app in device coordinates
+    private final Rect mSourceStackBounds = new Rect();
+    // The insets of the source app
+    private final Rect mSourceInsets = new Rect();
+    // The source app bounds with the source insets applied, in the source app window coordinates
+    private final RectF mSourceRect = new RectF();
+    // The bounds of the task view in launcher window coordinates
+    private final RectF mTargetRect = new RectF();
+    // Doesn't change after initialized, used as an anchor when changing mTargetRect
+    private final RectF mInitialTargetRect = new RectF();
+    // The insets to be used for clipping the app window, which can be larger than mSourceInsets
+    // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In
+    // app window coordinates.
+    private final RectF mSourceWindowClipInsets = new RectF();
+
+    // The bounds of launcher (not including insets) in device coordinates
+    public final Rect mHomeStackBounds = new Rect();
+
+    // The clip rect in source app window coordinates
+    private final Rect mClipRect = new Rect();
+    private final RectFEvaluator mRectFEvaluator = new RectFEvaluator();
+    private final Matrix mTmpMatrix = new Matrix();
+
+
+    public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) {
+        mHomeStackBounds.set(homeStackBounds);
+        mSourceInsets.set(target.getContentInsets());
+        mSourceStackBounds.set(target.sourceContainerBounds);
+
+        // TODO: Should sourceContainerBounds already have this offset?
+        mSourceStackBounds.offsetTo(target.position.x, target.position.y);
+    }
+
+    public void updateTargetRect(Rect targetRect) {
+        mSourceRect.set(mSourceInsets.left, mSourceInsets.top,
+                mSourceStackBounds.width() - mSourceInsets.right,
+                mSourceStackBounds.height() - mSourceInsets.bottom);
+        mTargetRect.set(targetRect);
+        mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left,
+                mHomeStackBounds.top - mSourceStackBounds.top);
+
+        mInitialTargetRect.set(mTargetRect);
+
+        // Calculate the clip based on the target rect (since the content insets and the
+        // launcher insets may differ, so the aspect ratio of the target rect can differ
+        // from the source rect. The difference between the target rect (scaled to the
+        // source rect) is the amount to clip on each edge.
+        RectF scaledTargetRect = new RectF(mTargetRect);
+        Utilities.scaleRectFAboutCenter(scaledTargetRect,
+                mSourceRect.width() / mTargetRect.width());
+        scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top);
+        mSourceWindowClipInsets.set(
+                Math.max(scaledTargetRect.left, 0),
+                Math.max(scaledTargetRect.top, 0),
+                Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0),
+                Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0));
+        mSourceRect.set(scaledTargetRect);
+    }
+
+    public void applyTransform(RemoteAnimationTargetCompat[] targets, float progress) {
+        RectF currentRect;
+        synchronized (mTargetRect) {
+            currentRect =  mRectFEvaluator.evaluate(progress, mSourceRect, mTargetRect);
+            // Stay lined up with the center of the target, since it moves for quick scrub.
+            currentRect.offset(mTargetRect.centerX() - currentRect.centerX(), 0);
+        }
+
+        mClipRect.left = (int) (mSourceWindowClipInsets.left * progress);
+        mClipRect.top = (int) (mSourceWindowClipInsets.top * progress);
+        mClipRect.right = (int)
+                (mSourceStackBounds.width() - (mSourceWindowClipInsets.right * progress));
+        mClipRect.bottom = (int)
+                (mSourceStackBounds.height() - (mSourceWindowClipInsets.bottom * progress));
+
+        mTmpMatrix.setRectToRect(mSourceRect, currentRect, ScaleToFit.FILL);
+
+        TransactionCompat transaction = new TransactionCompat();
+        for (RemoteAnimationTargetCompat app : targets) {
+            if (app.mode == MODE_CLOSING) {
+                mTmpMatrix.postTranslate(app.position.x, app.position.y);
+                transaction.setMatrix(app.leash, mTmpMatrix)
+                        .setWindowCrop(app.leash, mClipRect);
+                if (app.isNotInRecents) {
+                    transaction.setAlpha(app.leash, 1 - progress);
+                }
+
+                transaction.show(app.leash);
+            }
+        }
+        transaction.setEarlyWakeup();
+        transaction.apply();
+    }
+
+    public void offsetTarget(float scale, float offsetX, float offsetY) {
+        synchronized (mTargetRect) {
+            mTargetRect.set(mInitialTargetRect);
+            Utilities.scaleRectFAboutCenter(mTargetRect, scale);
+            mTargetRect.offset(offsetX, offsetY);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
new file mode 100644
index 0000000..f29f9e4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -0,0 +1,84 @@
+/*
+ * 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.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+
+public class LayoutUtils {
+
+    public static void calculateLauncherTaskSize(Context context, DeviceProfile dp, Rect outRect) {
+        float extraSpace = dp.isVerticalBarLayout() ? 0 : dp.hotseatBarSizePx;
+        calculateTaskSize(context, dp, extraSpace, outRect);
+    }
+
+    public static void calculateTaskSize(Context context, DeviceProfile dp,
+            float extraVerticalSpace, Rect outRect) {
+        float taskWidth, taskHeight, paddingHorz;
+        Resources res = context.getResources();
+        Rect insets = dp.getInsets();
+
+        if (dp.isMultiWindowMode) {
+            DeviceProfile fullDp = dp.getFullScreenProfile();
+            // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to
+            // account for system insets
+            taskWidth = fullDp.availableWidthPx;
+            taskHeight = fullDp.availableHeightPx;
+            float halfDividerSize = res.getDimension(R.dimen.multi_window_task_divider_size) / 2;
+
+            if (fullDp.isLandscape) {
+                taskWidth = taskWidth / 2 - halfDividerSize;
+            } else {
+                taskHeight = taskHeight / 2 - halfDividerSize;
+            }
+            paddingHorz = res.getDimension(R.dimen.multi_window_task_card_horz_space);
+        } else {
+            taskWidth = dp.availableWidthPx;
+            taskHeight = dp.availableHeightPx;
+            paddingHorz = res.getDimension(dp.isVerticalBarLayout()
+                    ? R.dimen.landscape_task_card_horz_space
+                    : R.dimen.portrait_task_card_horz_space);
+        }
+
+        float topIconMargin = res.getDimension(R.dimen.task_thumbnail_top_margin);
+        float paddingVert = res.getDimension(R.dimen.task_card_vert_space);
+
+        // Note this should be same as dp.availableWidthPx and dp.availableHeightPx unless
+        // we override the insets ourselves.
+        int launcherVisibleWidth = dp.widthPx - insets.left - insets.right;
+        int launcherVisibleHeight = dp.heightPx - insets.top - insets.bottom;
+
+        float availableHeight = launcherVisibleHeight
+                - topIconMargin - extraVerticalSpace - paddingVert;
+        float availableWidth = launcherVisibleWidth - paddingHorz;
+
+        float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight);
+        float outWidth = scale * taskWidth;
+        float outHeight = scale * taskHeight;
+
+        // Center in the visible space
+        float x = insets.left + (taskWidth - outWidth) / 2;
+        float y = insets.top + Math.max(topIconMargin,
+                (launcherVisibleHeight - extraVerticalSpace - outHeight) / 2);
+        outRect.set(Math.round(x), Math.round(y),
+                Math.round(x + outWidth), Math.round(y + outHeight));
+    }
+}
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/util/RemoteAnimationProvider.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
new file mode 100644
index 0000000..2ffcae3
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import android.animation.AnimatorSet;
+import android.app.ActivityOptions;
+import android.os.Handler;
+
+import com.android.launcher3.LauncherAnimationRunner;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
+import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.TransactionCompat;
+
+@FunctionalInterface
+public interface RemoteAnimationProvider {
+
+    AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targets);
+
+    default ActivityOptions toActivityOptions(Handler handler, long duration) {
+        LauncherAnimationRunner runner = new LauncherAnimationRunner(handler) {
+            @Override
+            public AnimatorSet getAnimator(RemoteAnimationTargetCompat[] targetCompats) {
+                return createWindowAnimation(targetCompats);
+            }
+        };
+        return ActivityOptionsCompat.makeRemoteAnimation(
+                new RemoteAnimationAdapterCompat(runner, duration, 0));
+    }
+
+    static void showOpeningTarget(RemoteAnimationTargetCompat[] targetCompats) {
+        TransactionCompat t = new TransactionCompat();
+        for (RemoteAnimationTargetCompat target : targetCompats) {
+            int layer = target.mode == RemoteAnimationTargetCompat.MODE_CLOSING
+                    ? Integer.MAX_VALUE
+                    : target.prefixOrderIndex;
+            t.setLayer(target.leash, layer);
+            t.show(target.leash);
+        }
+        t.apply();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java b/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java
index 6b7143d..ac34d90 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.views;
 
+import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
 import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
 
 import android.graphics.Rect;
@@ -39,6 +40,10 @@
         super(launcher, null);
         mLauncher = launcher;
         setVisibility(INVISIBLE);
+
+        // For the duration of the gesture, lock the screen orientation to ensure that we do not
+        // rotate mid-quickscrub
+        launcher.getRotationHelper().setStateHandlerRequest(REQUEST_LOCK);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 6788827..4b4af3f 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -27,22 +27,22 @@
 import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.os.Build;
+import android.support.annotation.AnyThread;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.view.View;
 import android.view.ViewDebug;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Insettable;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
+import com.android.quickstep.util.LayoutUtils;
 
 /**
  * {@link RecentsView} used in Launcher activity
  */
 @TargetApi(Build.VERSION_CODES.O)
-public class LauncherRecentsView extends RecentsView<Launcher> implements Insettable {
+public class LauncherRecentsView extends RecentsView<Launcher> {
 
     public static final FloatProperty<LauncherRecentsView> TRANSLATION_Y_FACTOR =
             new FloatProperty<LauncherRecentsView>("translationYFactor") {
@@ -61,8 +61,6 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private float mTranslationYFactor;
 
-    private Rect mPagePadding = new Rect();
-
     public LauncherRecentsView(Context context) {
         this(context, null);
     }
@@ -77,16 +75,6 @@
     }
 
     @Override
-    public void setInsets(Rect insets) {
-        mInsets.set(insets);
-        DeviceProfile dp = mActivity.getDeviceProfile();
-        Rect padding = getPadding(dp, getContext());
-        setPadding(padding.left, padding.top, padding.right, padding.bottom);
-        mPagePadding.set(padding);
-        mPagePadding.top += getResources().getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
-    }
-
-    @Override
     protected void onAllTasksRemoved() {
         mActivity.getStateManager().goToState(NORMAL);
     }
@@ -99,7 +87,7 @@
 
     public void setTranslationYFactor(float translationFactor) {
         mTranslationYFactor = translationFactor;
-        setTranslationY(mTranslationYFactor * (mPagePadding.bottom - mPagePadding.top));
+        setTranslationY(mTranslationYFactor * (getPaddingBottom() - getPaddingTop()));
     }
 
     @Override
@@ -138,4 +126,14 @@
                 mActivity.getAllAppsController(), ALL_APPS_PROGRESS, allAppsProgressOffscreen));
         return anim;
     }
+
+    @Override
+    protected void getTaskSize(DeviceProfile dp, Rect outRect) {
+        LayoutUtils.calculateLauncherTaskSize(getContext(), dp, outRect);
+    }
+
+    @AnyThread
+    public static void getPageRect(DeviceProfile grid, Context context, Rect outRect) {
+        LayoutUtils.calculateLauncherTaskSize(context, grid, outRect);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 644b098..faaa40d 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -53,18 +53,22 @@
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Insettable;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.PendingAnimation;
 import com.android.launcher3.util.Themes;
 import com.android.quickstep.QuickScrubController;
 import com.android.quickstep.RecentsAnimationInterpolator;
 import com.android.quickstep.RecentsAnimationInterpolator.TaskWindowBounds;
 import com.android.quickstep.RecentsModel;
+import com.android.quickstep.TaskUtils;
 import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
 import com.android.systemui.shared.recents.model.RecentsTaskLoader;
 import com.android.systemui.shared.recents.model.Task;
@@ -72,7 +76,6 @@
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.TaskStackChangeListener;
-import com.android.systemui.shared.system.WindowManagerWrapper;
 
 import java.util.ArrayList;
 
@@ -81,7 +84,9 @@
  */
 @TargetApi(Build.VERSION_CODES.P)
 public abstract class RecentsView<T extends BaseActivity>
-        extends PagedView implements OnSharedPreferenceChangeListener {
+        extends PagedView implements OnSharedPreferenceChangeListener, Insettable {
+
+    private final Rect mTempRect = new Rect();
 
     public static final FloatProperty<RecentsView> CONTENT_ALPHA =
             new FloatProperty<RecentsView>("contentAlpha") {
@@ -96,8 +101,6 @@
         }
     };
 
-
-
     public static final FloatProperty<RecentsView> ADJACENT_SCALE =
             new FloatProperty<RecentsView>("adjacentScale") {
         @Override
@@ -113,8 +116,6 @@
     private static final String PREF_FLIP_RECENTS = "pref_flip_recents";
     private static final int DISMISS_TASK_DURATION = 300;
 
-    private static final Rect sTempStableInsets = new Rect();
-
     protected final T mActivity;
     private final QuickScrubController mQuickScrubController;
     private final float mFastFlingVelocity;
@@ -130,13 +131,7 @@
     private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
         @Override
         public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) {
-            for (int i = 0; i < getChildCount(); i++) {
-                final TaskView taskView = (TaskView) getChildAt(i);
-                if (taskView.getTask().key.id == taskId) {
-                    taskView.getThumbnail().setThumbnail(taskView.getTask(), snapshot);
-                    return;
-                }
-            }
+            updateThumbnail(taskId, snapshot);
         }
 
         @Override
@@ -238,7 +233,6 @@
             final TaskView taskView = (TaskView) getChildAt(i);
             if (taskView.getTask().key.id == taskId) {
                 taskView.onTaskDataLoaded(taskView.getTask(), thumbnailData);
-                taskView.setAlpha(1);
                 return taskView;
             }
         }
@@ -324,7 +318,7 @@
 
     private void applyLoadPlan(RecentsTaskLoadPlan loadPlan) {
         if (mPendingAnimation != null) {
-            mPendingAnimation.addEndListener((b) -> applyLoadPlan(loadPlan));
+            mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(loadPlan));
             return;
         }
         TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null;
@@ -401,69 +395,20 @@
         }
     }
 
-    protected static Rect getPadding(DeviceProfile profile, Context context) {
-        WindowManagerWrapper.getInstance().getStableInsets(sTempStableInsets);
-        Rect padding = new Rect(profile.workspacePadding);
-
-        float taskWidth = profile.widthPx - sTempStableInsets.left - sTempStableInsets.right;
-        float taskHeight = profile.heightPx - sTempStableInsets.top - sTempStableInsets.bottom;
-
-        float overviewHeight, overviewWidth;
-        if (profile.isVerticalBarLayout()) {
-            float maxPadding = Math.max(padding.left, padding.right);
-
-            // Use the same padding on both sides for symmetry.
-            float availableWidth = taskWidth - 2 * maxPadding;
-            float availableHeight = profile.availableHeightPx - padding.top - padding.bottom
-                    - sTempStableInsets.top;
-            float scaledRatio = Math.min(availableWidth / taskWidth, availableHeight / taskHeight);
-            overviewHeight = taskHeight * scaledRatio;
-            overviewWidth = taskWidth * scaledRatio;
-
-        } else {
-            overviewHeight = profile.availableHeightPx - padding.top - padding.bottom
-                    - sTempStableInsets.top;
-            overviewWidth = taskWidth * overviewHeight / taskHeight;
-        }
-
-        padding.bottom = profile.availableHeightPx - padding.top - sTempStableInsets.top
-                - Math.round(overviewHeight);
-        padding.left = padding.right = (int) ((profile.availableWidthPx - overviewWidth) / 2);
-
-        // If the height ratio is larger than the width ratio, the screenshot will get cropped
-        // at the bottom when swiping up. In this case, increase the top/bottom padding to make it
-        // the same aspect ratio.
-        Rect pageRect = new Rect();
-        getPageRect(profile, context, pageRect, padding);
-        float widthRatio = (float) pageRect.width() / taskWidth;
-        float heightRatio = (float) pageRect.height() / taskHeight;
-        if (heightRatio > widthRatio) {
-            float additionalVerticalPadding = pageRect.height() - widthRatio * taskHeight;
-            additionalVerticalPadding = Math.round(additionalVerticalPadding);
-            padding.top += additionalVerticalPadding / 2;
-            padding.bottom += additionalVerticalPadding / 2;
-        }
-
-        return padding;
-    }
-
-    public static void getPageRect(DeviceProfile grid, Context context, Rect outRect) {
-        Rect targetPadding = getPadding(grid, context);
-        getPageRect(grid, context, outRect, targetPadding);
-    }
-
-    protected static void getPageRect(DeviceProfile grid, Context context, Rect outRect,
-            Rect targetPadding) {
-        Rect insets = grid.getInsets();
-        outRect.set(
-                targetPadding.left + insets.left,
-                targetPadding.top + insets.top,
-                grid.widthPx - targetPadding.right - insets.right,
-                grid.heightPx - targetPadding.bottom - insets.bottom);
-        outRect.top += context.getResources()
+    @Override
+    public void setInsets(Rect insets) {
+        mInsets.set(insets);
+        DeviceProfile dp = mActivity.getDeviceProfile();
+        getTaskSize(dp, mTempRect);
+        mTempRect.top -= getResources()
                 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
+        setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
+                dp.widthPx - mTempRect.right - mInsets.right,
+                dp.heightPx - mTempRect.bottom - mInsets.bottom);
     }
 
+    protected abstract void getTaskSize(DeviceProfile dp, Rect outRect);
+
     @Override
     protected boolean computeScrollHelper() {
         boolean scrolling = super.computeScrollHelper();
@@ -601,12 +546,7 @@
                     new ActivityManager.TaskDescription(), 0, new ComponentName("", ""), false);
             taskView.bind(mTmpRunningTask);
         }
-
-        mRunningTaskId = runningTaskId;
-        setCurrentPage(0);
-
-        // Load the tasks (if the loading is already
-        mLoadPlanId = mModel.loadTasks(runningTaskId, this::applyLoadPlan);
+        setCurrentTask(mRunningTaskId);
 
         // Hide the task that we are animating into, ignore if there is no associated task (ie. the
         // assistant)
@@ -615,6 +555,17 @@
         }
     }
 
+    /**
+     * Similar to {@link #showTask(int)} but does not put any restrictions on the first tile.
+     */
+    public void setCurrentTask(int runningTaskId) {
+        mRunningTaskId = runningTaskId;
+        setCurrentPage(0);
+
+        // Load the tasks (if the loading is already
+        mLoadPlanId = mModel.loadTasks(runningTaskId, this::applyLoadPlan);
+    }
+
     public void showNextTask() {
         TaskView runningTaskView = getTaskView(mRunningTaskId);
         if (runningTaskView == null) {
@@ -767,10 +718,16 @@
         }
 
         mPendingAnimation = pendingAnimation;
-        mPendingAnimation.addEndListener((isSuccess) -> {
-           if (isSuccess) {
+        mPendingAnimation.addEndListener((onEndListener) -> {
+           if (onEndListener.isSuccess) {
                if (removeTask) {
-                   ActivityManagerWrapper.getInstance().removeTask(taskView.getTask().key.id);
+                   Task task = taskView.getTask();
+                   if (task != null) {
+                       ActivityManagerWrapper.getInstance().removeTask(task.key.id);
+                       mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
+                               onEndListener.logAction, Direction.UP,
+                               TaskUtils.getComponentKeyForTask(task.key));
+                   }
                }
                int pageToSnapTo = mCurrentPage;
                if (draggedIndex < pageToSnapTo) {
@@ -818,7 +775,7 @@
         AnimatorPlaybackController controller = AnimatorPlaybackController.wrap(
                 pendingAnim.anim, DISMISS_TASK_DURATION);
         controller.dispatchOnStart();
-        controller.setEndAction(() -> pendingAnim.finish(true));
+        controller.setEndAction(() -> pendingAnim.finish(true, Touch.SWIPE));
         controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN);
         controller.start();
     }
@@ -857,11 +814,6 @@
         snapToPageRelative(1);
     }
 
-    public void launchNextTask() {
-        final TaskView nextTask = (TaskView) getChildAt(getNextPage());
-        nextTask.launchTask(true);
-    }
-
     public void setContentAlpha(float alpha) {
         if (mContentAlpha == alpha) {
             return;
@@ -1075,9 +1027,15 @@
         anim.setDuration(duration);
 
         mPendingAnimation = new PendingAnimation(anim);
-        mPendingAnimation.addEndListener((isSuccess) -> {
-            if (isSuccess) {
+        mPendingAnimation.addEndListener((onEndListener) -> {
+            if (onEndListener.isSuccess) {
                 tv.launchTask(false);
+                Task task = tv.getTask();
+                if (task != null) {
+                    mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
+                            onEndListener.logAction, Direction.DOWN,
+                            TaskUtils.getComponentKeyForTask(task.key));
+                }
             } else {
                 resetTaskVisuals();
             }
@@ -1089,7 +1047,10 @@
     @Override
     protected void notifyPageSwitchListener(int prevPage) {
         super.notifyPageSwitchListener(prevPage);
-        getChildAt(mCurrentPage).sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+        View currChild = getChildAt(mCurrentPage);
+        if (currChild != null) {
+            currChild.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+        }
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index 8b41b58..58b7db7 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -22,12 +22,9 @@
 import android.graphics.BitmapShader;
 import android.graphics.Canvas;
 import android.graphics.Color;
-import android.graphics.ComposeShader;
 import android.graphics.LightingColorFilter;
-import android.graphics.LinearGradient;
 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;
@@ -50,14 +47,16 @@
     private static final LightingColorFilter[] sDimFilterCache = new LightingColorFilter[256];
 
     private final float mCornerRadius;
-    private final float mFadeLength;
 
+    private final BaseActivity mActivity;
     private final TaskOverlay mOverlay;
     private final Paint mPaint = new Paint();
-    private final Paint mLockedPaint = new Paint();
+    private final Paint mBackgroundPaint = new Paint();
 
     private final Matrix mMatrix = new Matrix();
 
+    private float mClipBottom = -1;
+
     private Task mTask;
     private ThumbnailData mThumbnailData;
     protected BitmapShader mBitmapShader;
@@ -75,10 +74,10 @@
     public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         mCornerRadius = getResources().getDimension(R.dimen.task_corner_radius);
-        mFadeLength = getResources().getDimension(R.dimen.task_fade_length);
         mOverlay = TaskOverlayFactory.get(context).createOverlay(this);
         mPaint.setFilterBitmap(true);
-        mLockedPaint.setColor(Color.WHITE);
+        mBackgroundPaint.setColor(Color.WHITE);
+        mActivity = BaseActivity.fromContext(context);
     }
 
     public void bind() {
@@ -90,7 +89,9 @@
      */
     public void setThumbnail(Task task, ThumbnailData thumbnailData) {
         mTask = task;
-        mPaint.setColor(task == null ? Color.BLACK : task.colorBackground | 0xFF000000);
+        int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000;
+        mPaint.setColor(color);
+        mBackgroundPaint.setColor(color);
 
         if (thumbnailData != null && thumbnailData.thumbnail != null) {
             Bitmap bm = thumbnailData.thumbnail;
@@ -128,8 +129,23 @@
         if (mTask == null) {
             return;
         }
-        canvas.drawRoundRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mCornerRadius,
-                mCornerRadius, mTask.isLocked ? mLockedPaint : mPaint);
+        int width = getMeasuredWidth();
+        int height = getMeasuredHeight();
+        if (mClipBottom > 0 && !mTask.isLocked) {
+            canvas.save();
+            canvas.clipRect(0, 0, width, mClipBottom);
+
+            canvas.drawRoundRect(0, 0, width, height, mCornerRadius, mCornerRadius, mPaint);
+            canvas.restore();
+            canvas.save();
+            canvas.clipRect(0, mClipBottom, width, height);
+            canvas.drawRoundRect(0, 0, width, height, mCornerRadius, mCornerRadius,
+                    mBackgroundPaint);
+            canvas.restore();
+        } else {
+            canvas.drawRoundRect(0, 0, width, height, mCornerRadius,
+                    mCornerRadius, mTask.isLocked ? mBackgroundPaint : mPaint);
+        }
     }
 
     private void updateThumbnailPaintFilter() {
@@ -137,7 +153,7 @@
         if (mBitmapShader != null) {
             LightingColorFilter filter = getLightingColorFilter(mul);
             mPaint.setColorFilter(filter);
-            mLockedPaint.setColorFilter(filter);
+            mBackgroundPaint.setColorFilter(filter);
         } else {
             mPaint.setColorFilter(null);
             mPaint.setColor(Color.argb(255, mul, mul, mul));
@@ -147,47 +163,38 @@
 
     private void updateThumbnailMatrix() {
         boolean rotate = false;
+        mClipBottom = -1;
         if (mBitmapShader != null && mThumbnailData != null) {
             float scale = mThumbnailData.scale;
+            Rect thumbnailInsets  = mThumbnailData.insets;
             float thumbnailWidth = mThumbnailData.thumbnail.getWidth() -
-                    (mThumbnailData.insets.left + mThumbnailData.insets.right) * scale;
+                    (thumbnailInsets.left + thumbnailInsets.right) * scale;
             float thumbnailHeight = mThumbnailData.thumbnail.getHeight() -
-                    (mThumbnailData.insets.top + mThumbnailData.insets.bottom) * scale;
+                    (thumbnailInsets.top + thumbnailInsets.bottom) * scale;
+
             final float thumbnailScale;
-            final DeviceProfile profile = BaseActivity.fromContext(getContext())
-                    .getDeviceProfile();
+            final DeviceProfile profile = mActivity.getDeviceProfile();
+
             if (getMeasuredWidth() == 0) {
                 // If we haven't measured , skip the thumbnail drawing and only draw the background
                 // color
                 thumbnailScale = 0f;
             } else {
                 final Configuration configuration =
-                        getContext().getApplicationContext().getResources().getConfiguration();
-                if (configuration.orientation == mThumbnailData.orientation) {
-                    // If we are in the same orientation as the screenshot, just scale it to the
-                    // width of the task view
-                    thumbnailScale = getMeasuredWidth() / thumbnailWidth;
-                } else {
-                    if (FeatureFlags.OVERVIEW_USE_SCREENSHOT_ORIENTATION) {
-                        rotate = true;
-                        // Scale the height (will be width after rotation) to the width of this view
-                        thumbnailScale = getMeasuredWidth() / thumbnailHeight;
-                    } else {
-                        if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
-                            // Scale the landscape thumbnail up to app size, then scale that to the
-                            // task view size to match other portrait screenshots
-                            thumbnailScale = ((float) getMeasuredWidth() / profile.widthPx);
-                        } else {
-                            // Otherwise, scale the screenshot to fit 1:1 in the current orientation
-                            thumbnailScale = 1;
-                        }
-                    }
-                }
+                        getContext().getResources().getConfiguration();
+                // Rotate the screenshot if not in multi-window mode
+                rotate = FeatureFlags.OVERVIEW_USE_SCREENSHOT_ORIENTATION &&
+                        configuration.orientation != mThumbnailData.orientation &&
+                        !mActivity.isInMultiWindowModeCompat();
+                // Scale the screenshot to always fit the width of the card.
+                thumbnailScale = rotate
+                        ? getMeasuredWidth() / thumbnailHeight
+                        : getMeasuredWidth() / thumbnailWidth;
             }
+
             if (rotate) {
                 int rotationDir = profile.isVerticalBarLayout() && !profile.isSeascape() ? -1 : 1;
                 mMatrix.setRotate(90 * rotationDir);
-                Rect thumbnailInsets  = mThumbnailData.insets;
                 int newLeftInset = rotationDir == 1 ? thumbnailInsets.bottom : thumbnailInsets.top;
                 int newTopInset = rotationDir == 1 ? thumbnailInsets.left : thumbnailInsets.right;
                 mMatrix.postTranslate(-newLeftInset * scale, -newTopInset * scale);
@@ -209,27 +216,11 @@
             mMatrix.postScale(thumbnailScale, thumbnailScale);
             mBitmapShader.setLocalMatrix(mMatrix);
 
-            Shader shader = mBitmapShader;
-            if (!FeatureFlags.OVERVIEW_USE_SCREENSHOT_ORIENTATION) {
-                float bitmapHeight = Math.max(thumbnailHeight * thumbnailScale, 0);
-                if (Math.round(bitmapHeight) < getMeasuredHeight()) {
-                    int color = mPaint.getColor();
-                    LinearGradient fade = new LinearGradient(
-                            0, bitmapHeight - mFadeLength, 0, bitmapHeight,
-                            color & 0x00FFFFFF, color, Shader.TileMode.CLAMP);
-                    shader = new ComposeShader(fade, shader, Mode.DST_OVER);
-                }
-
-                float bitmapWidth = Math.max(thumbnailWidth * thumbnailScale, 0);
-                if (Math.round(bitmapWidth) < getMeasuredWidth()) {
-                    int color = mPaint.getColor();
-                    LinearGradient fade = new LinearGradient(
-                            bitmapWidth - mFadeLength, 0, bitmapWidth, 0,
-                            color & 0x00FFFFFF, color, Shader.TileMode.CLAMP);
-                    shader = new ComposeShader(fade, shader, Mode.DST_OVER);
-                }
+            float bitmapHeight = Math.max(thumbnailHeight * thumbnailScale, 0);
+            if (Math.round(bitmapHeight) < getMeasuredHeight()) {
+                mClipBottom = bitmapHeight;
             }
-            mPaint.setShader(shader);
+            mPaint.setShader(mBitmapShader);
         }
 
         if (rotate) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 42da472..f04acaf 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -31,11 +31,15 @@
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
+import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.quickstep.RecentsAnimationInterpolator;
 import com.android.quickstep.TaskSystemShortcut;
+import com.android.quickstep.TaskUtils;
 import com.android.quickstep.views.RecentsView.PageCallbacks;
 import com.android.quickstep.views.RecentsView.ScrollState;
 import com.android.systemui.shared.recents.model.Task;
@@ -82,7 +86,13 @@
 
     public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        setOnClickListener((view) -> launchTask(true /* animate */));
+        setOnClickListener((view) -> {
+            if (mTask != null) {
+                launchTask(true /* animate */);
+                BaseActivity.fromContext(context).getUserEventDispatcher().logTaskLaunchOrDismiss(
+                        Touch.TAP, Direction.NONE, TaskUtils.getComponentKeyForTask(mTask.key));
+            }
+        });
         setOutlineProvider(new TaskOutlineProvider(getResources()));
     }
 
@@ -138,7 +148,10 @@
         mSnapshotView.setThumbnail(task, thumbnailData);
         mIconView.setImageDrawable(task.icon);
         mIconView.setOnClickListener(icon -> TaskMenuView.showForTask(this));
-        mIconView.setOnLongClickListener(icon -> TaskMenuView.showForTask(this));
+        mIconView.setOnLongClickListener(icon -> {
+            requestDisallowInterceptTouchEvent(true);
+            return TaskMenuView.showForTask(this);
+        });
     }
 
     @Override
diff --git a/res/drawable/ic_close.xml b/res/drawable/ic_close.xml
index fc9ed49..8b2f55f 100644
--- a/res/drawable/ic_close.xml
+++ b/res/drawable/ic_close.xml
@@ -19,5 +19,5 @@
     android:viewportWidth="24.0">
   <path
       android:fillColor="@android:color/white"
-      android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+      android:pathData="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41z" />
 </vector>
\ No newline at end of file
diff --git a/res/drawable/ic_uninstall_no_shadow.xml b/res/drawable/ic_uninstall_no_shadow.xml
index 2a86e10..37632d1 100644
--- a/res/drawable/ic_uninstall_no_shadow.xml
+++ b/res/drawable/ic_uninstall_no_shadow.xml
@@ -21,6 +21,11 @@
         android:tint="?android:attr/textColorPrimary" >
     <path
         android:fillColor="#FFFFFFFF"
-        android:pathData="M6,19c0,1.1,0.9,2,2,2h8c1.1,0,2-0.9,2-2V7H6V19z M18,4h-2.5l-0.71-0.71C14.61,3.11,14.35,3,14.09,
-        3H9.9C9.64,3,9.38,3.11,9.2,3.29L8.49,4h-2.5c-0.55,0-1,0.45-1,1s0.45,1,1,1h12c0.55,0,1-0.45,1-1C19,4.45,18.55,4,18,4L18,4z"/>
+        android:pathData="M15,4V3H9v1H4v2h1v13c0,1.1,0.9,2,2,2h10c1.1,0,2-0.9,2-2V6h1V4H15z M17,19H7V6h10V19z" />
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M 9 8 H 11 V 17 H 9 V 8 Z" />
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M 13 8 H 15 V 17 H 13 V 8 Z" />
 </vector>
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 6f35752..4deed73 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -41,7 +41,7 @@
     public final boolean transposeLayoutWithOrientation;
 
     // Device properties in current orientation
-    private final boolean isLandscape;
+    public final boolean isLandscape;
     public final boolean isMultiWindowMode;
 
     public final int widthPx;
diff --git a/src/com/android/launcher3/ItemInfoWithIcon.java b/src/com/android/launcher3/ItemInfoWithIcon.java
index bf985c3..4677d31 100644
--- a/src/com/android/launcher3/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/ItemInfoWithIcon.java
@@ -91,6 +91,11 @@
     public static final int FLAG_ADAPTIVE_ICON = 1 << 8;
 
     /**
+     * Flag indicating that the icon is badged.
+     */
+    public static final int FLAG_ICON_BADGED = 1 << 9;
+
+    /**
      * Status associated with the system state of the underlying item. This is calculated every
      * time a new info is created and not persisted on the disk.
      */
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index b410f4f..90c55c9 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -294,6 +294,7 @@
         mPopupDataProvider = new PopupDataProvider(this);
 
         mRotationHelper = new RotationHelper(this);
+        mAppTransitionManager = LauncherAppTransitionManager.newInstance(this);
 
         boolean internalStateHandled = InternalStateHandler.handleCreate(this, getIntent());
         if (internalStateHandled) {
@@ -341,8 +342,6 @@
         getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW,
                 Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText));
 
-        mAppTransitionManager = LauncherAppTransitionManager.newInstance(this);
-
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.onCreate(savedInstanceState);
         }
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/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 8d4f2ef..e1e1f83 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -46,6 +46,7 @@
 
 import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.ItemInfo;
+import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.LauncherAppState;
@@ -69,6 +70,8 @@
 import java.util.Arrays;
 import java.util.List;
 
+import static com.android.launcher3.ItemInfoWithIcon.FLAG_ICON_BADGED;
+
 public class DragView extends View {
     private static final ColorMatrix sTempMatrix1 = new ColorMatrix();
     private static final ColorMatrix sTempMatrix2 = new ColorMatrix();
@@ -364,7 +367,10 @@
     private Drawable getBadge(ItemInfo info, LauncherAppState appState, Object obj) {
         int iconSize = appState.getInvariantDeviceProfile().iconBitmapSize;
         if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
-            if (info.id == ItemInfo.NO_ID || !(obj instanceof ShortcutInfoCompat)) {
+            boolean iconBadged = (info instanceof ItemInfoWithIcon)
+                    && (((ItemInfoWithIcon) info).runtimeStatusFlags & FLAG_ICON_BADGED) > 0;
+            if ((info.id == ItemInfo.NO_ID && !iconBadged)
+                    || !(obj instanceof ShortcutInfoCompat)) {
                 // The item is not yet added on home screen.
                 return new FixedSizeEmptyDrawable(iconSize);
             }
diff --git a/src/com/android/launcher3/logging/UserEventDispatcher.java b/src/com/android/launcher3/logging/UserEventDispatcher.java
index 90355bd..bf870cc 100644
--- a/src/com/android/launcher3/logging/UserEventDispatcher.java
+++ b/src/com/android/launcher3/logging/UserEventDispatcher.java
@@ -16,6 +16,14 @@
 
 package com.android.launcher3.logging;
 
+import static com.android.launcher3.logging.LoggerUtils.newCommandAction;
+import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
+import static com.android.launcher3.logging.LoggerUtils.newDropTarget;
+import static com.android.launcher3.logging.LoggerUtils.newItemTarget;
+import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent;
+import static com.android.launcher3.logging.LoggerUtils.newTarget;
+import static com.android.launcher3.logging.LoggerUtils.newTouchAction;
+
 import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
@@ -38,20 +46,13 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.LauncherEvent;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.InstantAppResolver;
 import com.android.launcher3.util.LogConfig;
 
 import java.util.Locale;
 import java.util.UUID;
 
-import static com.android.launcher3.logging.LoggerUtils.newCommandAction;
-import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
-import static com.android.launcher3.logging.LoggerUtils.newDropTarget;
-import static com.android.launcher3.logging.LoggerUtils.newItemTarget;
-import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent;
-import static com.android.launcher3.logging.LoggerUtils.newTarget;
-import static com.android.launcher3.logging.LoggerUtils.newTouchAction;
-
 /**
  * Manages the creation of {@link LauncherEvent}.
  * To debug this class, execute following command before side loading a new apk.
@@ -162,14 +163,15 @@
         dispatchUserEvent(event, intent);
     }
 
-    public void logTaskLaunch(int action, int direction, ComponentName componentName){
-        LauncherEvent event = newLauncherEvent(newTouchAction(action), // TAP or SWIPE
+    public void logTaskLaunchOrDismiss(int action, int direction, ComponentKey componentKey) {
+        LauncherEvent event = newLauncherEvent(newTouchAction(action), // TAP or SWIPE or FLING
                 newTarget(Target.Type.ITEM));
         if (action == Action.Touch.SWIPE || action == Action.Touch.FLING) {
+            // Direction DOWN means the task was launched, UP means it was dismissed.
             event.action.dir = direction;
         }
         event.srcTarget[0].itemType = LauncherLogProto.ItemType.TASK;
-        fillComponentInfo(event.srcTarget[0], componentName);
+        fillComponentInfo(event.srcTarget[0], componentKey.componentName);
         dispatchUserEvent(event, null);
     }
 
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 454fc7d..b522b55 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -517,6 +517,12 @@
     }
 
     @Override
+    protected void closeComplete() {
+        super.closeComplete();
+        mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible());
+    }
+
+    @Override
     public boolean onTouch(View v, MotionEvent ev) {
         // Touched a shortcut, update where it was touched so we can drag from there on long click.
         switch (ev.getAction()) {
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 9726704..f1195ed 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -124,6 +124,10 @@
         return mLauncher.getAllAppsController().getShiftRange();
     }
 
+    /**
+     * Returns the state to go to from fromState given the drag direction. If there is no state in
+     * that direction, returns fromState.
+     */
     protected abstract LauncherState getTargetState(LauncherState fromState,
             boolean isDragTowardPositive);
 
@@ -246,24 +250,24 @@
     }
 
     protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
-        if (targetState != mFromState) {
-            // Transition complete. log the action
-            mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
-                    getDirectionForLog(),
-                    mStartContainerType,
-                    mFromState.containerType,
-                    mToState.containerType,
-                    mLauncher.getWorkspace().getCurrentPage());
-        }
         clearState();
         boolean shouldGoToTargetState = true;
         if (mPendingAnimation != null) {
             boolean reachedTarget = mToState == targetState;
-            mPendingAnimation.finish(reachedTarget);
+            mPendingAnimation.finish(reachedTarget, logAction);
             mPendingAnimation = null;
             shouldGoToTargetState = !reachedTarget;
         }
         if (shouldGoToTargetState) {
+            if (targetState != mFromState) {
+                // Transition complete. log the action
+                mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
+                        getDirectionForLog(),
+                        mStartContainerType,
+                        mFromState.containerType,
+                        mToState.containerType,
+                        mLauncher.getWorkspace().getCurrentPage());
+            }
             mLauncher.getStateManager().goToState(targetState, false /* animated */);
         }
     }
diff --git a/src/com/android/launcher3/util/PendingAnimation.java b/src/com/android/launcher3/util/PendingAnimation.java
index 4116d56..617a38b 100644
--- a/src/com/android/launcher3/util/PendingAnimation.java
+++ b/src/com/android/launcher3/util/PendingAnimation.java
@@ -32,7 +32,7 @@
 @TargetApi(Build.VERSION_CODES.O)
 public class PendingAnimation {
 
-    private final ArrayList<Consumer<Boolean>> mEndListeners = new ArrayList<>();
+    private final ArrayList<Consumer<OnEndListener>> mEndListeners = new ArrayList<>();
 
     public final AnimatorSet anim;
 
@@ -40,14 +40,24 @@
         this.anim = anim;
     }
 
-    public void finish(boolean isSuccess) {
-        for (Consumer<Boolean> listeners : mEndListeners) {
-            listeners.accept(isSuccess);
+    public void finish(boolean isSuccess, int logAction) {
+        for (Consumer<OnEndListener> listeners : mEndListeners) {
+            listeners.accept(new OnEndListener(isSuccess, logAction));
         }
         mEndListeners.clear();
     }
 
-    public void addEndListener(Consumer<Boolean> listener) {
+    public void addEndListener(Consumer<OnEndListener> listener) {
         mEndListeners.add(listener);
     }
+
+    public static class OnEndListener {
+        public boolean isSuccess;
+        public int logAction;
+
+        public OnEndListener(boolean isSuccess, int logAction) {
+            this.isSuccess = isSuccess;
+            this.logAction = logAction;
+        }
+    }
 }
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
index c97c3cc..d1cddc1 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
@@ -54,7 +54,12 @@
 
     @Override
     protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
-        return fromState == ALL_APPS ? NORMAL : ALL_APPS;
+        if (fromState == NORMAL && isDragTowardPositive) {
+            return ALL_APPS;
+        } else if (fromState == ALL_APPS && !isDragTowardPositive) {
+            return NORMAL;
+        }
+        return fromState;
     }
 
     @Override