Merge "Prepares SecondaryDropTarget to inherit from" into ub-launcher3-edmonton
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index ad11bd3..88cd376 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -102,6 +102,13 @@
      */
     boolean deferStartingActivity(int downHitTarget);
 
+    boolean supportsLongSwipe(T activity);
+
+    /**
+     * Must return a non-null controller is supportsLongSwipe was true.
+     */
+    LongSwipeHelper getLongSwipeController(T activity, RemoteAnimationTargetSet targetSet);
+
     class LauncherActivityControllerHelper implements ActivityControlHelper<Launcher> {
 
         @Override
@@ -164,11 +171,13 @@
         @Override
         public AnimationFactory prepareRecentsUI(Launcher activity, boolean activityVisible,
                 Consumer<AnimatorPlaybackController> callback) {
-            LauncherState startState = activity.getStateManager().getState();
+            final LauncherState startState = activity.getStateManager().getState();
+
+            LauncherState resetState = startState;
             if (startState.disableRestore) {
-                startState = activity.getStateManager().getRestState();
+                resetState = activity.getStateManager().getRestState();
             }
-            activity.getStateManager().setRestState(startState);
+            activity.getStateManager().setRestState(resetState);
 
             if (!activityVisible) {
                 // Since the launcher is not visible, we can safely reset the scroll position.
@@ -180,11 +189,21 @@
                 activity.getAppsView().getContentView().setVisibility(View.GONE);
             }
 
-            return (transitionLength) ->
-                    createActivityController(activity, activityVisible, transitionLength, callback);
+            return new AnimationFactory() {
+                @Override
+                public void createActivityController(long transitionLength) {
+                    createActivityControllerInternal(activity, activityVisible, transitionLength,
+                            callback);
+                }
+
+                @Override
+                public void onTransitionCancelled() {
+                    activity.getStateManager().goToState(startState, false /* animate */);
+                }
+            };
         }
 
-        private void createActivityController(Launcher activity, boolean wasVisible,
+        private void createActivityControllerInternal(Launcher activity, boolean wasVisible,
                 long transitionLength, Consumer<AnimatorPlaybackController> callback) {
             if (wasVisible) {
                 DeviceProfile dp = activity.getDeviceProfile();
@@ -272,6 +291,20 @@
         public boolean shouldMinimizeSplitScreen() {
             return true;
         }
+
+        @Override
+        public boolean supportsLongSwipe(Launcher activity) {
+            return !activity.getDeviceProfile().isVerticalBarLayout();
+        }
+
+        @Override
+        public LongSwipeHelper getLongSwipeController(Launcher activity,
+                RemoteAnimationTargetSet targetSet) {
+            if (activity.getDeviceProfile().isVerticalBarLayout()) {
+                return null;
+            }
+            return new LongSwipeHelper(activity, targetSet);
+        }
     }
 
     class FallbackActivityControllerHelper implements ActivityControlHelper<RecentsActivity> {
@@ -419,6 +452,17 @@
             // TODO: Remove this once b/77875376 is fixed
             return false;
         }
+
+        @Override
+        public boolean supportsLongSwipe(RecentsActivity activity) {
+            return false;
+        }
+
+        @Override
+        public LongSwipeHelper getLongSwipeController(RecentsActivity activity,
+                RemoteAnimationTargetSet targetSet) {
+            return null;
+        }
     }
 
     interface LayoutListener {
@@ -445,5 +489,7 @@
         default void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) { }
 
         void createActivityController(long transitionLength);
+
+        default void onTransitionCancelled() { }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/LongSwipeHelper.java b/quickstep/src/com/android/quickstep/LongSwipeHelper.java
new file mode 100644
index 0000000..4ce18b3
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/LongSwipeHelper.java
@@ -0,0 +1,162 @@
+/*
+ * 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 static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.quickstep.WindowTransformSwipeHandler.MAX_SWIPE_DURATION;
+import static com.android.systemui.shared.recents.utilities.Utilities.getNextFrameNumber;
+import static com.android.systemui.shared.recents.utilities.Utilities.getSurface;
+
+import android.animation.ValueAnimator;
+import android.view.Surface;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.AllAppsTransitionController;
+import com.android.launcher3.allapps.DiscoveryBounce;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.quickstep.util.RemoteAnimationTargetSet;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.TransactionCompat;
+
+/**
+ * Utility class to handle long swipe from an app.
+ * This assumes the presence of Launcher activity as long swipe is not supported on the
+ * fallback activity.
+ */
+public class LongSwipeHelper {
+
+    private static final float MIN_PROGRESS_TO_ALL_APPS = 0.35f;
+    private static final float SWIPE_DURATION_MULTIPLIER =
+            Math.min(1 / MIN_PROGRESS_TO_ALL_APPS, 1 / (1 - MIN_PROGRESS_TO_ALL_APPS));
+
+    private final Launcher mLauncher;
+    private final RemoteAnimationTargetSet mTargetSet;
+
+    private float mMaxSwipeDistance = 1;
+    private AnimatorPlaybackController mAnimator;
+
+    LongSwipeHelper(Launcher launcher, RemoteAnimationTargetSet targetSet) {
+        mLauncher = launcher;
+        mTargetSet = targetSet;
+        init();
+    }
+
+    private void init() {
+        setTargetAlpha(0, true);
+
+        // Init animations
+        AllAppsTransitionController controller = mLauncher.getAllAppsController();
+        // TODO: Scale it down so that we can reach all-apps in screen space
+        mMaxSwipeDistance = Math.max(1, controller.getProgress() * controller.getShiftRange());
+        mAnimator = mLauncher.getStateManager()
+                .createAnimationToNewWorkspace(ALL_APPS, Math.round(2 * mMaxSwipeDistance));
+        mAnimator.dispatchOnStart();
+    }
+
+    public void onMove(float displacement) {
+        mAnimator.setPlayFraction(displacement / mMaxSwipeDistance);
+    }
+
+    public void destroy() {
+        // TODO: We can probably also hide the task view
+        setTargetAlpha(1, false);
+
+        mLauncher.getStateManager().goToState(OVERVIEW, false);
+    }
+
+    public void end(float velocity, boolean isFling, Runnable callback) {
+        long duration = MAX_SWIPE_DURATION;
+
+        final float currentFraction = mAnimator.getProgressFraction();
+        final boolean toAllApps;
+        float endProgress;
+
+        if (!isFling) {
+            toAllApps = currentFraction > MIN_PROGRESS_TO_ALL_APPS;
+            endProgress = toAllApps ? 1 : 0;
+
+            long expectedDuration = Math.abs(Math.round((endProgress - currentFraction)
+                    * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
+            duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
+        } else {
+            toAllApps = velocity < 0;
+            endProgress = toAllApps ? 1 : 0;
+
+            float minFlingVelocity = mLauncher.getResources()
+                    .getDimension(R.dimen.quickstep_fling_min_velocity);
+            if (Math.abs(velocity) > minFlingVelocity && mMaxSwipeDistance > 0) {
+                float distanceToTravel = (endProgress - currentFraction) * mMaxSwipeDistance;
+
+                // we want the page's snap velocity to approximately match the velocity at
+                // which the user flings, so we scale the duration by a value near to the
+                // derivative of the scroll interpolator at zero, ie. 2.
+                long baseDuration = Math.round(1000 * Math.abs(distanceToTravel / velocity));
+                duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
+            }
+        }
+
+        mAnimator.setEndAction(() -> onSwipeAnimationComplete(toAllApps, isFling, callback));
+        ValueAnimator animator = mAnimator.getAnimationPlayer();
+        animator.setDuration(duration).setInterpolator(DEACCEL);
+        animator.setFloatValues(currentFraction, endProgress);
+        animator.start();
+    }
+
+    private void setTargetAlpha(float alpha, boolean defer) {
+        final Surface surface = getSurface(mLauncher.getDragLayer());
+        final long frameNumber = defer && surface != null ? getNextFrameNumber(surface) : -1;
+        if (defer) {
+            if (frameNumber == -1) {
+                defer = false;
+            } else {
+                mLauncher.getDragLayer().invalidate();
+            }
+        }
+
+        TransactionCompat transaction = new TransactionCompat();
+        for (RemoteAnimationTargetCompat app : mTargetSet.apps) {
+            if (!(app.isNotInRecents
+                    || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME)) {
+                transaction.setAlpha(app.leash, alpha);
+                if (defer) {
+                    transaction.deferTransactionUntil(app.leash, surface, frameNumber);
+                }
+            }
+        }
+        transaction.apply();
+    }
+
+    private void onSwipeAnimationComplete(boolean toAllApps, boolean isFling, Runnable callback) {
+        mLauncher.getStateManager().goToState(toAllApps ? ALL_APPS : OVERVIEW, false);
+        if (!toAllApps) {
+            DiscoveryBounce.showForOverviewIfNeeded(mLauncher);
+        }
+
+        mLauncher.getUserEventDispatcher().logStateChangeAction(
+                isFling ? Touch.FLING : Touch.SWIPE, Direction.UP,
+                ContainerType.NAVBAR, ContainerType.APP,
+                toAllApps ? ContainerType.ALLAPPS : ContainerType.TASKSWITCHER,
+                0);
+
+        callback.run();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/MultiStateCallback.java b/quickstep/src/com/android/quickstep/MultiStateCallback.java
index 7a74176..bda3d06 100644
--- a/quickstep/src/com/android/quickstep/MultiStateCallback.java
+++ b/quickstep/src/com/android/quickstep/MultiStateCallback.java
@@ -59,4 +59,8 @@
     public int getState() {
         return mState;
     }
+
+    public boolean hasStates(int stateMask) {
+        return (mState & stateMask) == stateMask;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index b8be6b8..881b09b 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -31,7 +31,6 @@
 import android.annotation.TargetApi;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
-import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -52,9 +51,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.logging.UserEventDispatcher;
@@ -103,16 +100,28 @@
     private static final int STATE_HANDLER_INVALIDATED = 1 << 7;
     private static final int STATE_GESTURE_STARTED = 1 << 8;
     private static final int STATE_GESTURE_CANCELLED = 1 << 9;
+    private static final int STATE_GESTURE_COMPLETED = 1 << 10;
 
     // States for quick switch/scrub
-    private static final int STATE_SWITCH_TO_SCREENSHOT_COMPLETE = 1 << 10;
-    private static final int STATE_QUICK_SCRUB_START = 1 << 11;
-    private static final int STATE_QUICK_SCRUB_END = 1 << 12;
+    private static final int STATE_CURRENT_TASK_FINISHED = 1 << 11;
+    private static final int STATE_QUICK_SCRUB_START = 1 << 12;
+    private static final int STATE_QUICK_SCRUB_END = 1 << 13;
+
+    private static final int STATE_CAPTURE_SCREENSHOT = 1 << 14;
+    private static final int STATE_SCREENSHOT_CAPTURED = 1 << 15;
 
     private static final int LAUNCHER_UI_STATES =
             STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_ACTIVITY_MULTIPLIER_COMPLETE
             | STATE_LAUNCHER_STARTED;
 
+    private static final int LONG_SWIPE_ENTER_STATE =
+            STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED
+                    | STATE_APP_CONTROLLER_RECEIVED;
+
+    private static final int LONG_SWIPE_START_STATE =
+            STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED
+                    | STATE_APP_CONTROLLER_RECEIVED | STATE_SCREENSHOT_CAPTURED;
+
     // For debugging, keep in sync with above states
     private static final String[] STATES = new String[] {
             "STATE_LAUNCHER_PRESENT",
@@ -125,14 +134,16 @@
             "STATE_HANDLER_INVALIDATED",
             "STATE_GESTURE_STARTED",
             "STATE_GESTURE_CANCELLED",
-            "STATE_SWITCH_TO_SCREENSHOT_COMPLETE",
-            "STATE_QUICK_SWITCH",
+            "STATE_GESTURE_COMPLETED",
+            "STATE_CURRENT_TASK_FINISHED",
             "STATE_QUICK_SCRUB_START",
-            "STATE_QUICK_SCRUB_END"
+            "STATE_QUICK_SCRUB_END",
+            "STATE_CAPTURE_SCREENSHOT",
+            "STATE_SCREENSHOT_CAPTURED",
     };
 
-    private static final long MAX_SWIPE_DURATION = 350;
-    private static final long MIN_SWIPE_DURATION = 80;
+    public static final long MAX_SWIPE_DURATION = 350;
+    public static final long MIN_SWIPE_DURATION = 80;
 
     private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
     private static final float SWIPE_DURATION_MULTIPLIER =
@@ -151,7 +162,7 @@
     // visible.
     private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
 
-    private final MainThreadExecutor mMainExecutor = new MainThreadExecutor();
+    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
 
     private final Context mContext;
     private final int mRunningTaskId;
@@ -171,7 +182,6 @@
 
     private boolean mWasLauncherAlreadyVisible;
 
-    private float mCurrentDisplacement;
     private boolean mGestureStarted;
     private int mLogAction = Touch.SWIPE;
     private float mCurrentQuickScrubProgress;
@@ -185,6 +195,11 @@
     private final long mTouchTimeMs;
     private long mLauncherFrameDrawnTime;
 
+    private boolean mBgLongSwipeMode = false;
+    private boolean mUiLongSwipeMode = false;
+    private float mLongSwipeDisplacement = 0;
+    private LongSwipeHelper mLongSwipeController;
+
     WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context, long touchTimeMs,
             ActivityControlHelper<T> controller) {
         mContext = context;
@@ -194,10 +209,10 @@
         mActivityInitListener = mActivityControlHelper
                 .createActivityInitListener(this::onActivityInit);
 
+        initStateCallbacks();
         // Register the input consumer on the UI thread, to ensure that it runs after any pending
         // unregister calls
-        mMainExecutor.execute(mInputConsumer::registerInputConsumer);
-        initStateCallbacks();
+        executeOnUiThread(mInputConsumer::registerInputConsumer);
     }
 
     private void initStateCallbacks() {
@@ -226,12 +241,18 @@
                 this::resumeLastTask);
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
                         | STATE_ACTIVITY_MULTIPLIER_COMPLETE
-                        | STATE_SCALED_CONTROLLER_RECENTS,
+                        | STATE_CAPTURE_SCREENSHOT,
                 this::switchToScreenshot);
+
+        mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
+                        | STATE_SCALED_CONTROLLER_RECENTS,
+                this::finishCurrentTransitionToHome);
+
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
                         | STATE_ACTIVITY_MULTIPLIER_COMPLETE
                         | STATE_SCALED_CONTROLLER_RECENTS
-                        | STATE_SWITCH_TO_SCREENSHOT_COMPLETE,
+                        | STATE_CURRENT_TASK_FINISHED
+                        | STATE_GESTURE_COMPLETED,
                 this::setupLauncherUiAfterSwipeUpAnimation);
 
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_APP,
@@ -240,21 +261,34 @@
         mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
                 this::invalidateHandlerWithLauncher);
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED
+                | STATE_SCALED_CONTROLLER_APP,
+                this::notifyTransitionCancelled);
 
         mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START,
                 this::onQuickScrubStart);
         mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START
                 | STATE_SCALED_CONTROLLER_RECENTS, this::onFinishedTransitionToQuickScrub);
-        mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_SWITCH_TO_SCREENSHOT_COMPLETE
+        mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_CURRENT_TASK_FINISHED
                 | STATE_QUICK_SCRUB_END, this::switchToFinalAppAfterQuickScrub);
+
+        mStateCallback.addCallback(LONG_SWIPE_ENTER_STATE, this::checkLongSwipeCanEnter);
+        mStateCallback.addCallback(LONG_SWIPE_START_STATE, this::checkLongSwipeCanStart);
+    }
+
+    private void executeOnUiThread(Runnable action) {
+        if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
+            action.run();
+        } else {
+            postAsyncCallback(mMainThreadHandler, action);
+        }
     }
 
     private void setStateOnUiThread(int stateFlag) {
-        Handler handler = mMainExecutor.getHandler();
-        if (Looper.myLooper() == handler.getLooper()) {
+        if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
             mStateCallback.setState(stateFlag);
         } else {
-            postAsyncCallback(handler, () -> mStateCallback.setState(stateFlag));
+            postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag));
         }
     }
 
@@ -321,7 +355,7 @@
         if (mActivity != activity) {
             return;
         }
-        if ((mStateCallback.getState() & STATE_HANDLER_INVALIDATED) != 0) {
+        if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
             return;
         }
 
@@ -416,11 +450,26 @@
 
     @WorkerThread
     public void updateDisplacement(float displacement) {
-        mCurrentDisplacement = displacement;
+        // We are moving in the negative x/y direction
+        displacement = -displacement;
+        if (displacement > mTransitionDragLength) {
+            mCurrentShift.updateValue(1);
 
-        float translation = Utilities.boundToRange(-mCurrentDisplacement, 0, mTransitionDragLength);
-        float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
-        mCurrentShift.updateValue(shift);
+            if (!mBgLongSwipeMode) {
+                mBgLongSwipeMode = true;
+                executeOnUiThread(this::onLongSwipeEnabledUi);
+            }
+            mLongSwipeDisplacement = displacement - mTransitionDragLength;
+            executeOnUiThread(this::onLongSwipeDisplacementUpdated);
+        } else {
+            if (mBgLongSwipeMode) {
+                mBgLongSwipeMode = false;
+                executeOnUiThread(this::onLongSwipeDisabledUi);
+            }
+            float translation = Math.max(displacement, 0);
+            float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
+            mCurrentShift.updateValue(shift);
+        }
     }
 
     /**
@@ -448,20 +497,18 @@
                 float interpolated = interpolator.getInterpolation(shift);
                 mClipAnimationHelper.applyTransform(
                         mRecentsAnimationWrapper.targetSet, interpolated);
+
+                // TODO: This logic is spartanic!
+                boolean passedThreshold = shift > 0.12f;
+                mRecentsAnimationWrapper.setAnimationTargetsBehindSystemBars(!passedThreshold);
+                if (mActivityControlHelper.shouldMinimizeSplitScreen()) {
+                    mRecentsAnimationWrapper
+                            .setSplitScreenMinimizedForTransaction(passedThreshold);
+                }
             }
         }
 
-        if (mRecentsAnimationWrapper.controller != null) {
-            // TODO: This logic is spartanic!
-            boolean passedThreshold = shift > 0.12f;
-            mRecentsAnimationWrapper.setAnimationTargetsBehindSystemBars(!passedThreshold);
-            if (mActivityControlHelper.shouldMinimizeSplitScreen()) {
-                mRecentsAnimationWrapper
-                        .setSplitScreenMinimizedForTransaction(passedThreshold);
-            }
-        }
-
-        mMainExecutor.execute(this::updateFinalShiftUi);
+        executeOnUiThread(this::updateFinalShiftUi);
     }
 
     private void updateFinalShiftUi() {
@@ -539,10 +586,21 @@
 
     @WorkerThread
     public void onGestureEnded(float endVelocity) {
-        Resources res = mContext.getResources();
-        float flingThreshold = res.getDimension(R.dimen.quickstep_fling_threshold_velocity);
+        float flingThreshold = mContext.getResources()
+                .getDimension(R.dimen.quickstep_fling_threshold_velocity);
         boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold;
+        setStateOnUiThread(STATE_GESTURE_COMPLETED);
 
+        mLogAction = isFling ? Touch.FLING : Touch.SWIPE;
+
+        if (mBgLongSwipeMode) {
+            executeOnUiThread(() -> onLongSwipeGestureFinishUi(endVelocity, isFling));
+        } else {
+            handleNormalGestureEnd(endVelocity, isFling);
+        }
+    }
+
+    private void handleNormalGestureEnd(float endVelocity, boolean isFling) {
         long duration = MAX_SWIPE_DURATION;
         final float endShift;
         if (!isFling) {
@@ -550,10 +608,10 @@
             long expectedDuration = Math.abs(Math.round((endShift - mCurrentShift.value)
                     * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
             duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
-            mLogAction = Touch.SWIPE;
         } else {
             endShift = endVelocity < 0 ? 1 : 0;
-            float minFlingVelocity = res.getDimension(R.dimen.quickstep_fling_min_velocity);
+            float minFlingVelocity = mContext.getResources()
+                    .getDimension(R.dimen.quickstep_fling_min_velocity);
             if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
                 float distanceToTravel = (endShift - mCurrentShift.value) * mTransitionDragLength;
 
@@ -563,7 +621,6 @@
                 long baseDuration = Math.round(1000 * Math.abs(distanceToTravel / endVelocity));
                 duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
             }
-            mLogAction = Touch.FLING;
         }
 
         animateToProgress(endShift, duration, DEACCEL);
@@ -593,8 +650,9 @@
         anim.addListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationSuccess(Animator animator) {
-                setStateOnUiThread(mIsGoingToHome ?
-                        STATE_SCALED_CONTROLLER_RECENTS : STATE_SCALED_CONTROLLER_APP);
+                setStateOnUiThread(mIsGoingToHome
+                        ? (STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT)
+                        : STATE_SCALED_CONTROLLER_APP);
             }
         });
         anim.start();
@@ -633,6 +691,10 @@
         mRecentsView.setFirstTaskIconScaledDown(false /* isScaledDown */, false /* animate */);
     }
 
+    private void notifyTransitionCancelled() {
+        mAnimationFactory.onTransitionCancelled();
+    }
+
     private void resetStateForAnimationCancel() {
         boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted;
         mActivityControlHelper.onTransitionCancelled(mActivity, wasVisible);
@@ -646,13 +708,6 @@
 
     private void switchToScreenshot() {
         boolean finishTransitionPosted = false;
-        final Runnable finishTransitionRunnable = () -> {
-            synchronized (mRecentsAnimationWrapper) {
-                mRecentsAnimationWrapper.finish(true /* toHome */,
-                        () -> setStateOnUiThread(STATE_SWITCH_TO_SCREENSHOT_COMPLETE));
-            }
-        };
-
         synchronized (mRecentsAnimationWrapper) {
             if (mRecentsAnimationWrapper.controller != null) {
                 // Update the screenshot of the task
@@ -667,7 +722,7 @@
 
                         @Override
                         public void onPostDraw(Canvas canvas) {
-                            finishTransitionRunnable.run();
+                            setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
                             detach();
                         }
                     }.attach();
@@ -675,8 +730,15 @@
             }
         }
         if (!finishTransitionPosted) {
-            // If we haven't posted the transition end runnable, run it now
-            finishTransitionRunnable.run();
+            // If we haven't posted a draw callback, set the state immediately.
+            setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+        }
+    }
+
+    private void finishCurrentTransitionToHome() {
+        synchronized (mRecentsAnimationWrapper) {
+            mRecentsAnimationWrapper.finish(true /* toHome */,
+                    () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
         }
     }
 
@@ -773,4 +835,64 @@
     public void setGestureEndCallback(Runnable gestureEndCallback) {
         mGestureEndCallback = gestureEndCallback;
     }
+
+    // Handling long swipe
+    private void onLongSwipeEnabledUi() {
+        mUiLongSwipeMode = true;
+        checkLongSwipeCanEnter();
+        checkLongSwipeCanStart();
+    }
+
+    private void onLongSwipeDisabledUi() {
+        mUiLongSwipeMode = false;
+
+        if (mLongSwipeController != null) {
+            mLongSwipeController.destroy();
+
+            // Rebuild animations
+            buildAnimationController();
+        }
+    }
+
+    private void onLongSwipeDisplacementUpdated() {
+        if (!mUiLongSwipeMode || mLongSwipeController == null) {
+            return;
+        }
+
+        mLongSwipeController.onMove(mLongSwipeDisplacement);
+    }
+
+    private void checkLongSwipeCanEnter() {
+        if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_ENTER_STATE)
+                || !mActivityControlHelper.supportsLongSwipe(mActivity)) {
+            return;
+        }
+
+        // We are entering long swipe mode, make sure the screen shot is captured.
+        mStateCallback.setState(STATE_CAPTURE_SCREENSHOT);
+
+    }
+
+    private void checkLongSwipeCanStart() {
+        if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_START_STATE)
+                || !mActivityControlHelper.supportsLongSwipe(mActivity)) {
+            return;
+        }
+
+        mLongSwipeController = mActivityControlHelper.getLongSwipeController(
+                mActivity, mRecentsAnimationWrapper.targetSet);
+        onLongSwipeDisplacementUpdated();
+    }
+
+    private void onLongSwipeGestureFinishUi(float velocity, boolean isFling) {
+        if (!mUiLongSwipeMode || mLongSwipeController == null) {
+            handleNormalGestureEnd(velocity, isFling);
+            return;
+        }
+
+        finishCurrentTransitionToHome();
+        mLongSwipeController.end(velocity, isFling,
+                () -> setStateOnUiThread(STATE_HANDLER_INVALIDATED));
+
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index 592166d..c724930 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -16,6 +16,8 @@
 
 package com.android.quickstep.views;
 
+import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
+
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
@@ -206,7 +208,8 @@
                 // Rotate the screenshot if not in multi-window mode
                 rotate = FeatureFlags.OVERVIEW_USE_SCREENSHOT_ORIENTATION &&
                         configuration.orientation != mThumbnailData.orientation &&
-                        !mActivity.isInMultiWindowModeCompat();
+                        !mActivity.isInMultiWindowModeCompat() &&
+                        mThumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
                 // Scale the screenshot to always fit the width of the card.
                 thumbnailScale = rotate
                         ? getMeasuredWidth() / thumbnailHeight